test: tests unitaires socketio-adapter (8 tests, bun test)
- Injection socket factory pour testabilité (2e param constructeur) - Tests : état initial, connect, login_error, connect_error, reconnecting, reconnexion, logoff, onStateChange - Script test ajouté dans package.json
This commit is contained in:
@@ -13,7 +13,8 @@
|
|||||||
"build:linux-x64": "electron-builder --linux --x64",
|
"build:linux-x64": "electron-builder --linux --x64",
|
||||||
"build:linux-arm64": "electron-builder --linux --arm64",
|
"build:linux-arm64": "electron-builder --linux --arm64",
|
||||||
"dist": "electron-builder",
|
"dist": "electron-builder",
|
||||||
"postinstall": "electron-builder install-app-deps"
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"electron",
|
"electron",
|
||||||
|
|||||||
@@ -8,11 +8,28 @@
|
|||||||
const io = require('socket.io-client');
|
const io = require('socket.io-client');
|
||||||
|
|
||||||
class SocketIOAdapter {
|
class SocketIOAdapter {
|
||||||
constructor(serverUrl) {
|
constructor(serverUrl, socketFactory = null) {
|
||||||
this.serverUrl = serverUrl;
|
this.serverUrl = serverUrl;
|
||||||
|
this._socketFactory = socketFactory || io;
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this._state = 'disconnected'; // disconnected, connecting, connected, error
|
this._state = 'disconnected'; // disconnected, connecting, connected, reconnecting, error
|
||||||
this._eventHandlers = new Map();
|
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) {
|
connect(accessCode, password, terminal) {
|
||||||
return new Promise((resolve, reject) => {
|
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 },
|
auth: { access_code: accessCode, password, terminal },
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
@@ -39,7 +56,7 @@ class SocketIOAdapter {
|
|||||||
this.socket.once('login_ok', (data) => {
|
this.socket.once('login_ok', (data) => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
this._state = 'connected';
|
this._setState('connected');
|
||||||
resolve(data);
|
resolve(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,7 +64,7 @@ class SocketIOAdapter {
|
|||||||
this.socket.once('login_error', (data) => {
|
this.socket.once('login_error', (data) => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
this._state = 'error';
|
this._setState('error');
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
reject(new Error(data.message || 'Authentification refusee'));
|
reject(new Error(data.message || 'Authentification refusee'));
|
||||||
});
|
});
|
||||||
@@ -56,23 +73,41 @@ class SocketIOAdapter {
|
|||||||
this.socket.on('connect_error', (err) => {
|
this.socket.on('connect_error', (err) => {
|
||||||
if (!settled) {
|
if (!settled) {
|
||||||
settled = true;
|
settled = true;
|
||||||
this._state = 'error';
|
this._setState('error');
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
reject(new Error(err.message || 'Connexion refusee'));
|
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)
|
// Timeout de connexion initiale (15s)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled) {
|
||||||
settled = true;
|
settled = true;
|
||||||
this._state = 'error';
|
this._setState('error');
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
reject(new Error('Timeout de connexion au serveur'));
|
reject(new Error('Timeout de connexion au serveur'));
|
||||||
}
|
}
|
||||||
}, 15000);
|
}, 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
|
// Restaurer les handlers enregistres avant connect
|
||||||
this._eventHandlers.forEach((handler, event) => {
|
this._eventHandlers.forEach((handler, event) => {
|
||||||
this.socket.on(event, handler);
|
this.socket.on(event, handler);
|
||||||
@@ -87,13 +122,13 @@ class SocketIOAdapter {
|
|||||||
logoff() {
|
logoff() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!this.socket || !this.socket.connected) {
|
if (!this.socket || !this.socket.connected) {
|
||||||
this._state = 'disconnected';
|
this._setState('disconnected');
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket.once('logout_ok', () => {
|
this.socket.once('logout_ok', () => {
|
||||||
this._state = 'disconnected';
|
this._setState('disconnected');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,7 +139,7 @@ class SocketIOAdapter {
|
|||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
this._state = 'disconnected';
|
this._setState('disconnected');
|
||||||
resolve();
|
resolve();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
@@ -117,7 +152,7 @@ class SocketIOAdapter {
|
|||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
this._state = 'disconnected';
|
this._setState('disconnected');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
131
socketio-adapter.test.js
Normal file
131
socketio-adapter.test.js
Normal file
@@ -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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user