diff --git a/package.json b/package.json index b916c8f..0ba5b53 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "build:linux-x64": "electron-builder --linux --x64", "build:linux-arm64": "electron-builder --linux --arm64", "dist": "electron-builder", - "postinstall": "electron-builder install-app-deps" + "postinstall": "electron-builder install-app-deps", + "test": "bun test" }, "keywords": [ "electron", diff --git a/socketio-adapter.js b/socketio-adapter.js index 2ff8015..792b9aa 100644 --- a/socketio-adapter.js +++ b/socketio-adapter.js @@ -8,11 +8,28 @@ const io = require('socket.io-client'); class SocketIOAdapter { - constructor(serverUrl) { + constructor(serverUrl, socketFactory = null) { this.serverUrl = serverUrl; + this._socketFactory = socketFactory || io; this.socket = null; - this._state = 'disconnected'; // disconnected, connecting, connected, error + this._state = 'disconnected'; // disconnected, connecting, connected, reconnecting, error this._eventHandlers = new Map(); + this._onStateChange = null; // callback pour notifier main.js des changements d'etat + } + + /** + * Enregistrer un callback de changement d'etat. + * @param {function(string)} callback - recoit le nouvel etat + */ + onStateChange(callback) { + this._onStateChange = callback; + } + + _setState(state) { + this._state = state; + if (this._onStateChange) { + this._onStateChange(state); + } } /** @@ -22,9 +39,9 @@ class SocketIOAdapter { */ connect(accessCode, password, terminal) { return new Promise((resolve, reject) => { - this._state = 'connecting'; + this._setState('connecting'); - this.socket = io(this.serverUrl, { + this.socket = this._socketFactory(this.serverUrl, { auth: { access_code: accessCode, password, terminal }, transports: ['websocket'], reconnection: true, @@ -39,7 +56,7 @@ class SocketIOAdapter { this.socket.once('login_ok', (data) => { if (settled) return; settled = true; - this._state = 'connected'; + this._setState('connected'); resolve(data); }); @@ -47,7 +64,7 @@ class SocketIOAdapter { this.socket.once('login_error', (data) => { if (settled) return; settled = true; - this._state = 'error'; + this._setState('error'); this.socket.disconnect(); reject(new Error(data.message || 'Authentification refusee')); }); @@ -56,23 +73,41 @@ class SocketIOAdapter { this.socket.on('connect_error', (err) => { if (!settled) { settled = true; - this._state = 'error'; + this._setState('error'); this.socket.disconnect(); reject(new Error(err.message || 'Connexion refusee')); } - // Apres login reussi : reconnexion auto geree par socket.io + // Apres login reussi : reconnexion auto, on passe en 'reconnecting' }); // Timeout de connexion initiale (15s) setTimeout(() => { if (!settled) { settled = true; - this._state = 'error'; + this._setState('error'); this.socket.disconnect(); reject(new Error('Timeout de connexion au serveur')); } }, 15000); + // === Handlers de reconnexion (actifs apres le premier login) === + + // Deconnexion temporaire → passer en 'reconnecting' + this.socket.on('disconnect', (reason) => { + if (this._state === 'connected') { + // Deconnexion temporaire, socket.io va retenter + this._setState('reconnecting'); + } + }); + + // Reconnexion reussie → le serveur recoit un nouveau connect avec les memes auth + // et emet 'login_ok' (reprise de session) + this.socket.on('login_ok', (data) => { + if (settled && this._state === 'reconnecting') { + this._setState('connected'); + } + }); + // Restaurer les handlers enregistres avant connect this._eventHandlers.forEach((handler, event) => { this.socket.on(event, handler); @@ -87,13 +122,13 @@ class SocketIOAdapter { logoff() { return new Promise((resolve) => { if (!this.socket || !this.socket.connected) { - this._state = 'disconnected'; + this._setState('disconnected'); resolve(); return; } this.socket.once('logout_ok', () => { - this._state = 'disconnected'; + this._setState('disconnected'); resolve(); }); @@ -104,7 +139,7 @@ class SocketIOAdapter { if (this.socket) { this.socket.disconnect(); } - this._state = 'disconnected'; + this._setState('disconnected'); resolve(); }, 5000); }); @@ -117,7 +152,7 @@ class SocketIOAdapter { if (this.socket) { this.socket.disconnect(); } - this._state = 'disconnected'; + this._setState('disconnected'); } /** diff --git a/socketio-adapter.test.js b/socketio-adapter.test.js new file mode 100644 index 0000000..9838eb1 --- /dev/null +++ b/socketio-adapter.test.js @@ -0,0 +1,131 @@ +import { describe, test, expect, mock, beforeEach } from "bun:test"; + +const SocketIOAdapter = require("./socketio-adapter.js"); + +// EventEmitter minimal pour simuler un socket.io +function createFakeSocket() { + const listeners = {}; + + const socket = { + on(event, fn) { (listeners[event] ??= []).push(fn); }, + once(event, fn) { + const wrapper = (...args) => { + socket.off(event, wrapper); + fn(...args); + }; + (listeners[event] ??= []).push(wrapper); + }, + off(event, fn) { + if (fn) { + listeners[event] = (listeners[event] || []).filter(f => f !== fn); + } else { + delete listeners[event]; + } + }, + emit: mock(() => {}), + disconnect: mock(() => {}), + connected: true, + + // Helper test : déclencher un event côté "serveur" + _fire(event, ...args) { + (listeners[event] || []).slice().forEach(fn => fn(...args)); + }, + }; + return socket; +} + +describe("SocketIOAdapter", () => { + let adapter, socket; + + beforeEach(() => { + socket = null; + const factory = (url, opts) => { + socket = createFakeSocket(); + return socket; + }; + adapter = new SocketIOAdapter("http://localhost:8004", factory); + }); + + test("état initial = disconnected", () => { + expect(adapter.state).toBe("disconnected"); + }); + + test("connect() passe en connecting puis connected sur login_ok", async () => { + const states = []; + adapter.onStateChange((s) => states.push(s)); + + const p = adapter.connect("1234", "pass", "2001"); + socket._fire("login_ok", { accessCode: "1234", firstName: "Test", lastName: "User", connList: [] }); + + const result = await p; + + expect(states).toEqual(["connecting", "connected"]); + expect(adapter.state).toBe("connected"); + expect(result.accessCode).toBe("1234"); + }); + + test("connect() rejette sur login_error", async () => { + const p = adapter.connect("1234", "wrong", "2001"); + socket._fire("login_error", { message: "Mot de passe incorrect" }); + + expect(p).rejects.toThrow("Mot de passe incorrect"); + expect(adapter.state).toBe("error"); + }); + + test("connect() rejette sur connect_error", async () => { + const p = adapter.connect("1234", "pass", "2001"); + socket._fire("connect_error", new Error("Connection refused")); + + expect(p).rejects.toThrow("Connection refused"); + expect(adapter.state).toBe("error"); + }); + + test("déconnexion temporaire passe en reconnecting", async () => { + const p = adapter.connect("1234", "pass", "2001"); + socket._fire("login_ok", { accessCode: "1234" }); + await p; + + socket._fire("disconnect", "transport close"); + + expect(adapter.state).toBe("reconnecting"); + }); + + test("reconnexion après déconnexion repasse en connected", async () => { + const p = adapter.connect("1234", "pass", "2001"); + socket._fire("login_ok", { accessCode: "1234" }); + await p; + + socket._fire("disconnect", "transport close"); + expect(adapter.state).toBe("reconnecting"); + + socket._fire("login_ok", { accessCode: "1234" }); + expect(adapter.state).toBe("connected"); + }); + + test("logoff() émet logout et passe en disconnected sur logout_ok", async () => { + const p = adapter.connect("1234", "pass", "2001"); + socket._fire("login_ok", { accessCode: "1234" }); + await p; + + const logoffP = adapter.logoff(); + socket._fire("logout_ok"); + await logoffP; + + expect(socket.emit).toHaveBeenCalledWith("logout"); + expect(adapter.state).toBe("disconnected"); + }); + + test("onStateChange est appelé à chaque transition", async () => { + const states = []; + adapter.onStateChange((s) => states.push(s)); + + const p = adapter.connect("1234", "pass", "2001"); + socket._fire("login_ok", { accessCode: "1234" }); + await p; + + socket._fire("disconnect", "transport close"); + socket._fire("login_ok", { accessCode: "1234" }); + + expect(states).toEqual(["connecting", "connected", "reconnecting", "connected"]); + }); +});