@sockethub/server 5.0.0-alpha.4 → 5.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +54 -60
  2. package/bin/sockethub +4 -3
  3. package/package.json +42 -54
  4. package/res/socket.io.js +4908 -0
  5. package/res/sockethub-client.js +602 -0
  6. package/res/sockethub-client.min.js +19 -0
  7. package/sockethub.config.example.json +2 -3
  8. package/src/bootstrap/init.d.ts +16 -13
  9. package/src/bootstrap/init.test.ts +211 -0
  10. package/src/bootstrap/init.ts +152 -76
  11. package/src/bootstrap/load-platforms.ts +151 -0
  12. package/src/config.test.ts +27 -22
  13. package/src/config.ts +82 -86
  14. package/src/defaults.json +24 -16
  15. package/src/index.ts +61 -22
  16. package/src/janitor.test.ts +191 -169
  17. package/src/janitor.ts +141 -118
  18. package/src/listener.ts +148 -58
  19. package/src/middleware/create-activity-object.test.ts +28 -8
  20. package/src/middleware/create-activity-object.ts +16 -10
  21. package/src/middleware/expand-activity-stream.test.data.ts +331 -345
  22. package/src/middleware/expand-activity-stream.test.ts +65 -66
  23. package/src/middleware/expand-activity-stream.ts +26 -21
  24. package/src/middleware/store-credentials.test.ts +74 -60
  25. package/src/middleware/store-credentials.ts +14 -8
  26. package/src/middleware/validate.test.data.ts +240 -242
  27. package/src/middleware/validate.test.ts +39 -78
  28. package/src/middleware/validate.ts +62 -36
  29. package/src/middleware.test.ts +168 -138
  30. package/src/middleware.ts +57 -55
  31. package/src/platform-instance.test.ts +508 -214
  32. package/src/platform-instance.ts +324 -231
  33. package/src/platform.test.ts +375 -0
  34. package/src/platform.ts +306 -117
  35. package/src/process-manager.ts +75 -51
  36. package/src/routes.test.ts +43 -89
  37. package/src/routes.ts +40 -78
  38. package/src/sentry.test.ts +106 -0
  39. package/src/sentry.ts +19 -0
  40. package/src/sockethub.ts +190 -129
  41. package/src/util.ts +5 -0
  42. package/coverage/tmp/coverage-39338-1663949520416-0.json +0 -1
  43. package/dist/bootstrap/init.d.ts +0 -18
  44. package/dist/bootstrap/init.js +0 -64
  45. package/dist/bootstrap/init.js.map +0 -1
  46. package/dist/bootstrap/platforms.js +0 -75
  47. package/dist/config.d.ts +0 -12
  48. package/dist/config.js +0 -107
  49. package/dist/config.js.map +0 -1
  50. package/dist/defaults.json +0 -28
  51. package/dist/index.d.ts +0 -1
  52. package/dist/index.js +0 -29
  53. package/dist/index.js.map +0 -1
  54. package/dist/janitor.d.ts +0 -30
  55. package/dist/janitor.js +0 -120
  56. package/dist/janitor.js.map +0 -1
  57. package/dist/listener.d.ts +0 -31
  58. package/dist/listener.js +0 -94
  59. package/dist/listener.js.map +0 -1
  60. package/dist/middleware/create-activity-object.d.ts +0 -8
  61. package/dist/middleware/create-activity-object.js +0 -19
  62. package/dist/middleware/create-activity-object.js.map +0 -1
  63. package/dist/middleware/expand-activity-stream.d.ts +0 -3
  64. package/dist/middleware/expand-activity-stream.js +0 -36
  65. package/dist/middleware/expand-activity-stream.js.map +0 -1
  66. package/dist/middleware/expand-activity-stream.test.data.d.ts +0 -480
  67. package/dist/middleware/expand-activity-stream.test.data.js +0 -360
  68. package/dist/middleware/expand-activity-stream.test.data.js.map +0 -1
  69. package/dist/middleware/store-credentials.d.ts +0 -3
  70. package/dist/middleware/store-credentials.js +0 -9
  71. package/dist/middleware/store-credentials.js.map +0 -1
  72. package/dist/middleware/validate.d.ts +0 -2
  73. package/dist/middleware/validate.js +0 -56
  74. package/dist/middleware/validate.js.map +0 -1
  75. package/dist/middleware/validate.test.data.d.ts +0 -532
  76. package/dist/middleware/validate.test.data.js +0 -263
  77. package/dist/middleware/validate.test.data.js.map +0 -1
  78. package/dist/middleware.d.ts +0 -21
  79. package/dist/middleware.js +0 -56
  80. package/dist/middleware.js.map +0 -1
  81. package/dist/platform-instance.d.ts +0 -78
  82. package/dist/platform-instance.js +0 -226
  83. package/dist/platform-instance.js.map +0 -1
  84. package/dist/platform.d.ts +0 -6
  85. package/dist/platform.js +0 -176
  86. package/dist/platform.js.map +0 -1
  87. package/dist/process-manager.d.ts +0 -11
  88. package/dist/process-manager.js +0 -82
  89. package/dist/process-manager.js.map +0 -1
  90. package/dist/routes.d.ts +0 -13
  91. package/dist/routes.js +0 -83
  92. package/dist/routes.js.map +0 -1
  93. package/dist/sockethub.d.ts +0 -18
  94. package/dist/sockethub.js +0 -112
  95. package/dist/sockethub.js.map +0 -1
  96. package/src/bootstrap/platforms.js +0 -75
  97. package/test/init-suite.js +0 -41
  98. package/test/sockethub-suite.js +0 -25
  99. package/tsconfig.json +0 -18
  100. package/views/examples/dummy.ejs +0 -95
  101. package/views/examples/feeds.ejs +0 -90
  102. package/views/examples/irc.ejs +0 -239
  103. package/views/examples/shared.js +0 -72
  104. package/views/examples/xmpp.ejs +0 -217
  105. package/views/index.ejs +0 -17
@@ -1,237 +1,531 @@
1
- import proxyquire from 'proxyquire';
2
- import { expect } from 'chai';
3
- import * as sinon from 'sinon';
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import * as sinon from "sinon";
3
+ import { __dirname } from "./util.js";
4
+ const FORK_PATH = __dirname + "/platform.js";
4
5
 
5
- proxyquire.noPreserveCache();
6
- proxyquire.noCallThru();
7
-
8
- const FORK_PATH = __dirname + '/platform.js';
6
+ import PlatformInstance, { platformInstances } from "./platform-instance.js";
9
7
 
10
8
  describe("PlatformInstance", () => {
11
- let pi, sandbox, forkFake, socketMock, getSocketFake, PlatformInstance, platformInstances;
12
-
13
- beforeEach(() => {
14
- sandbox = sinon.createSandbox();
15
- forkFake = sandbox.fake();
16
- socketMock = {
17
- emit: sandbox.spy()
18
- };
19
- getSocketFake = sinon.fake.resolves(socketMock);
20
-
21
- const PlatformInstanceMod = proxyquire('./platform-instance', {
22
- '@sockethub/data-layer': {
23
- JobQueue: sandbox.stub().returns({
24
- shutdown: sandbox.stub(),
25
- on: sandbox.stub(),
26
- getJob: sandbox.stub(),
27
- initResultEvents: sandbox.stub()
28
- })
29
- },
30
- 'child_process': {
31
- fork: forkFake,
32
- ChildProcess: sandbox.stub()
33
- },
34
- './listener': {
35
- 'io': {
36
- 'in': sandbox.stub().returns({
37
- fetchSockets: () => {
38
- return [socketMock];
39
- }
40
- })
41
- },
42
- getSocket: getSocketFake
43
- }
44
- });
45
- PlatformInstance = PlatformInstanceMod.default;
46
- platformInstances = PlatformInstanceMod.platformInstances;
47
- });
48
-
49
- afterEach(() => {
50
- sinon.restore();
51
- });
52
-
53
- describe('private instance per-actor', () => {
54
- it("is set as non-global when an actor is provided", async () => {
55
- const pi = new PlatformInstance({
56
- identifier: 'id',
57
- platform: 'name',
58
- parentId: 'parentId',
59
- actor: 'actor string'
60
- });
61
- expect(pi.global).to.be.equal(false);
62
- sandbox.assert.calledWith(forkFake, FORK_PATH, ['parentId', 'name', 'id']);
63
- await pi.shutdown();
64
- });
65
- });
9
+ let pi, sandbox, forkFake, socketMock, getSocketFake;
66
10
 
67
- describe('PlatformInstance objects', () => {
68
11
  beforeEach(() => {
69
- pi = new PlatformInstance({
70
- identifier: 'platform identifier',
71
- platform: 'a platform name',
72
- parentId: 'the parentId'
73
- });
74
- platformInstances.set(pi.id, pi);
75
-
76
- pi.process = {
77
- on: sandbox.spy(),
78
- removeListener: sandbox.spy(),
79
- removeAllListeners: sandbox.spy(),
80
- unref: sandbox.spy(),
81
- kill: sandbox.spy(),
82
- };
12
+ sandbox = sinon.createSandbox();
13
+ socketMock = {
14
+ emit: sandbox.spy(),
15
+ };
16
+ getSocketFake = sinon.fake.resolves(socketMock);
17
+ forkFake = sandbox.fake();
83
18
  });
84
19
 
85
- afterEach(async () => {
86
- await pi.shutdown();
87
- });
20
+ function getTestPlatformInstanceClass() {
21
+ class TestPlatformInstance extends PlatformInstance {
22
+ createQueue() {
23
+ this.JobQueue = sandbox.stub().returns({
24
+ shutdown: sandbox.stub(),
25
+ on: sandbox.stub(),
26
+ getJob: sandbox.stub(),
27
+ initResultEvents: sandbox.stub(),
28
+ });
29
+ }
88
30
 
89
- it('has expected properties', () => {
90
- expect(typeof PlatformInstance).to.be.equal('function');
91
- });
31
+ initProcess(parentId, name, id, env) {
32
+ this.process = forkFake(FORK_PATH, [parentId, name, id], env);
33
+ }
92
34
 
93
- it('should have a platformInstances Map', () => {
94
- expect(platformInstances instanceof Map).to.be.equal(true);
95
- });
35
+ createGetSocket() {
36
+ this.getSocket = getSocketFake;
37
+ }
38
+ }
39
+ return TestPlatformInstance;
40
+ }
96
41
 
97
- it("has certain accessible properties", () => {
98
- expect(pi.id).to.be.equal('platform identifier');
99
- expect(pi.name).to.be.equal('a platform name');
100
- expect(pi.parentId).to.be.equal('the parentId');
101
- expect(pi.flaggedForTermination).to.be.equal(false);
102
- expect(pi.global).to.be.equal(true);
103
- expect(forkFake.calledWith(FORK_PATH, [
104
- 'the parentId', 'a platform name', 'platform identifier'
105
- ])).to.be.ok;
42
+ afterEach(() => {
43
+ sinon.restore();
106
44
  });
107
45
 
108
- describe('registerSession', () => {
109
- beforeEach(() => {
110
- pi.callbackFunction = sandbox.fake();
111
- });
112
-
113
- it('adds a close and message handler when a session is registered', () => {
114
- pi.registerSession('my session id');
115
- expect(pi.callbackFunction.callCount).to.equal(2);
116
- sandbox.assert.calledWith(pi.callbackFunction, 'close', 'my session id');
117
- sandbox.assert.calledWith(pi.callbackFunction, 'message', 'my session id');
118
- expect(pi.sessions.has('my session id')).to.be.equal(true);
119
- });
120
-
121
- it('is able to generate failure reports', () => {
122
- pi.registerSession('my session id');
123
- expect(pi.sessions.has('my session id')).to.be.equal(true);
124
- pi.reportError('my session id', 'an error message');
125
- pi.sendToClient = sandbox.stub();
126
- pi.shutdown = sandbox.stub();
127
- expect(pi.sessions.size).to.be.equal(0);
128
- });
46
+ describe("private instance per-actor", () => {
47
+ it("is set as non-global when an actor is provided", async () => {
48
+ const TestPlatformInstance = getTestPlatformInstanceClass();
49
+ const pi = new TestPlatformInstance({
50
+ identifier: "id",
51
+ platform: "name",
52
+ parentId: "parentId",
53
+ actor: "actor string",
54
+ });
55
+ expect(pi.global).toEqual(false);
56
+ sandbox.assert.calledWith(forkFake, FORK_PATH, [
57
+ "parentId",
58
+ "name",
59
+ "id",
60
+ ]);
61
+ await pi.shutdown();
62
+ });
129
63
  });
130
64
 
131
- it('initializes the job queue', () => {
132
- expect(pi.jobQueue).to.be.undefined;
133
- pi.initQueue('a secret');
134
- expect(pi.jobQueue).to.be.ok;
135
- });
65
+ describe("PlatformInstance objects", () => {
66
+ beforeEach(() => {
67
+ const TestPlatformInstance = getTestPlatformInstanceClass();
68
+ pi = new TestPlatformInstance({
69
+ identifier: "platform identifier",
70
+ platform: "a platform name",
71
+ parentId: "the parentId",
72
+ });
73
+ platformInstances.set(pi.id, pi);
136
74
 
137
- it("cleans up its references when shutdown", async () => {
138
- pi.initQueue('a secret');
139
- expect(pi.jobQueue).to.be.ok;
140
- expect(platformInstances.has('platform identifier')).to.be.true;
141
- await pi.shutdown();
142
- expect(pi.jobQueue).not.to.be.ok;
143
- expect(platformInstances.has('platform identifier')).to.be.false;
144
- });
75
+ pi.process = {
76
+ on: sandbox.spy(),
77
+ removeListener: sandbox.spy(),
78
+ removeAllListeners: sandbox.spy(),
79
+ unref: sandbox.spy(),
80
+ kill: sandbox.spy(),
81
+ };
82
+ });
145
83
 
146
- it("updates its identifier when changed", () => {
147
- pi.updateIdentifier('foo bar');
148
- expect(pi.id).to.be.equal('foo bar');
149
- expect(platformInstances.has('platform identifier')).to.be.false;
150
- expect(platformInstances.has('foo bar')).to.be.true;
151
- });
84
+ afterEach(async () => {
85
+ await pi.shutdown();
86
+ });
152
87
 
153
- it('sends messages to client using socket session id', async () => {
154
- await pi.sendToClient('my session id',
155
- {foo: 'this is a message object', sessionSecret: 'private data'});
156
- expect(getSocketFake.callCount).to.equal(1);
157
- sandbox.assert.calledOnce(getSocketFake);
158
- sandbox.assert.calledWith(getSocketFake, 'my session id');
159
- sandbox.assert.calledOnce(socketMock.emit);
160
- sandbox.assert.calledWith(
161
- socketMock.emit, 'message', {foo:'this is a message object', context: 'a platform name'});
162
- });
88
+ it("has expected properties", () => {
89
+ const TestPlatformInstance = getTestPlatformInstanceClass();
90
+ expect(typeof TestPlatformInstance).toEqual("function");
91
+ });
163
92
 
164
- it('broadcasts to peers', async () => {
165
- pi.sessions.add('other peer');
166
- pi.sessions.add('another peer');
167
- await pi.broadcastToSharedPeers('myself', {foo: 'bar'});
168
- expect(getSocketFake.callCount).to.equal(2);
169
- sandbox.assert.calledWith(getSocketFake, 'other peer');
170
- });
93
+ it("should have a platformInstances Map", () => {
94
+ expect(platformInstances instanceof Map).toEqual(true);
95
+ });
96
+
97
+ it("has certain accessible properties", () => {
98
+ expect(pi.id).toEqual("platform identifier");
99
+ expect(pi.name).toEqual("a platform name");
100
+ expect(pi.parentId).toEqual("the parentId");
101
+ expect(pi.flaggedForTermination).toEqual(false);
102
+ expect(pi.global).toEqual(true);
103
+ expect(
104
+ forkFake.calledWith(FORK_PATH, [
105
+ "the parentId",
106
+ "a platform name",
107
+ "platform identifier",
108
+ ]),
109
+ ).toEqual(true);
110
+ });
111
+
112
+ describe("registerSession", () => {
113
+ beforeEach(() => {
114
+ pi.callbackFunction = sandbox.fake();
115
+ });
116
+
117
+ it("adds a close and message handler when a session is registered", () => {
118
+ pi.registerSession("my session id");
119
+ expect(pi.callbackFunction.callCount).toEqual(2);
120
+ sandbox.assert.calledWith(
121
+ pi.callbackFunction,
122
+ "close",
123
+ "my session id",
124
+ );
125
+ sandbox.assert.calledWith(
126
+ pi.callbackFunction,
127
+ "message",
128
+ "my session id",
129
+ );
130
+ expect(pi.sessions.has("my session id")).toEqual(true);
131
+ });
132
+
133
+ it("is able to generate failure reports", async () => {
134
+ pi.registerSession("my session id");
135
+ expect(pi.sessions.has("my session id")).toEqual(true);
136
+ pi.sendToClient = sandbox.stub();
137
+ pi.shutdown = sandbox.stub();
138
+ await pi.reportError("my session id", "an error message");
139
+ expect(pi.sessions.size).toEqual(0);
140
+ });
141
+ });
142
+
143
+ it("initializes the job queue", () => {
144
+ expect(pi.queue).toBeUndefined();
145
+ pi.initQueue("a secret");
146
+ expect(pi.queue).toBeDefined();
147
+ });
148
+
149
+ it("cleans up its references when shutdown", async () => {
150
+ pi.initQueue("a secret");
151
+ expect(pi.queue).toBeDefined();
152
+ expect(platformInstances.has("platform identifier")).toBeTrue();
153
+ await pi.shutdown();
154
+ expect(pi.queue).toBeUndefined();
155
+ expect(platformInstances.has("platform identifier")).toBeFalse();
156
+ });
157
+
158
+ it("updates its identifier when changed", () => {
159
+ pi.updateIdentifier("foo bar");
160
+ expect(pi.id).toEqual("foo bar");
161
+ expect(platformInstances.has("platform identifier")).toBeFalse();
162
+ expect(platformInstances.has("foo bar")).toBeTrue();
163
+ });
164
+
165
+ it("sends messages to client using socket session id", async () => {
166
+ await pi.sendToClient("my session id", {
167
+ foo: "this is a message object",
168
+ sessionSecret: "private data",
169
+ });
170
+ expect(getSocketFake.callCount).toEqual(1);
171
+ sandbox.assert.calledOnce(getSocketFake);
172
+ sandbox.assert.calledWith(getSocketFake, "my session id");
173
+ sandbox.assert.calledOnce(socketMock.emit);
174
+ sandbox.assert.calledWith(socketMock.emit, "message", {
175
+ foo: "this is a message object",
176
+ context: "a platform name",
177
+ });
178
+ });
179
+
180
+ it("broadcasts to peers", async () => {
181
+ pi.sessions.add("other peer");
182
+ pi.sessions.add("another peer");
183
+ await pi.broadcastToSharedPeers("myself", { foo: "bar" });
184
+ expect(getSocketFake.callCount).toEqual(2);
185
+ sandbox.assert.calledWith(getSocketFake, "other peer");
186
+ });
187
+
188
+ describe("handleJobResult", () => {
189
+ beforeEach(() => {
190
+ pi.sendToClient = sandbox.fake();
191
+ pi.broadcastToSharedPeers = sandbox.fake();
192
+ pi.config = { persist: false };
193
+ });
194
+
195
+ it("broadcasts to peers when handling a completed job", async () => {
196
+ pi.sessions.add("other peer");
197
+ await pi.handleJobResult(
198
+ "completed",
199
+ { msg: { foo: "bar" } },
200
+ undefined,
201
+ );
202
+ expect(pi.sendToClient.callCount).toEqual(1);
203
+ expect(pi.broadcastToSharedPeers.callCount).toEqual(1);
204
+ });
205
+
206
+ it("appends completed result message when present", async () => {
207
+ await pi.handleJobResult(
208
+ "completed",
209
+ { sessionId: "a session id", msg: { foo: "bar" } },
210
+ "a good result message",
211
+ );
212
+ expect(pi.broadcastToSharedPeers.callCount).toEqual(1);
213
+ sandbox.assert.calledWith(pi.sendToClient, "a session id", {
214
+ foo: "bar",
215
+ });
216
+ });
217
+
218
+ it("appends failed result message when present", async () => {
219
+ await pi.handleJobResult(
220
+ "failed",
221
+ { sessionId: "a session id", msg: { foo: "bar" } },
222
+ "a bad result message",
223
+ );
224
+ expect(pi.broadcastToSharedPeers.callCount).toEqual(1);
225
+ sandbox.assert.calledWith(pi.sendToClient, "a session id", {
226
+ foo: "bar",
227
+ error: "a bad result message",
228
+ });
229
+ });
230
+ });
231
+
232
+ describe("callbackFunction", () => {
233
+ beforeEach(() => {
234
+ pi.reportError = sandbox.fake();
235
+ pi.sendToClient = sandbox.fake();
236
+ pi.updateIdentifier = sandbox.fake();
237
+ });
238
+
239
+ it("close events from platform thread are reported", async () => {
240
+ // Mock process as connected and not flagged for termination
241
+ pi.process.connected = true;
242
+ pi.flaggedForTermination = false;
243
+
244
+ const close = pi.callbackFunction("close", "my session id");
245
+ await close("error msg");
246
+ sandbox.assert.calledWith(
247
+ pi.reportError,
248
+ "my session id",
249
+ "Error: session thread closed unexpectedly: error msg",
250
+ );
251
+ });
252
+
253
+ it("close events skip error reporting when process disconnected", async () => {
254
+ // Mock process as disconnected
255
+ pi.process.connected = false;
256
+ pi.flaggedForTermination = false;
257
+ pi.shutdown = sandbox.stub();
258
+
259
+ const close = pi.callbackFunction("close", "my session id");
260
+ await close("error msg");
261
+
262
+ // Should NOT attempt to report error
263
+ sandbox.assert.notCalled(pi.reportError);
264
+ // Should call shutdown
265
+ sandbox.assert.called(pi.shutdown);
266
+ });
267
+
268
+ it("close events skip error reporting when flagged for termination", async () => {
269
+ // Mock process as flagged for termination
270
+ pi.process.connected = true;
271
+ pi.flaggedForTermination = true;
272
+ pi.shutdown = sandbox.stub();
273
+
274
+ const close = pi.callbackFunction("close", "my session id");
275
+ await close("error msg");
276
+
277
+ // Should NOT attempt to report error
278
+ sandbox.assert.notCalled(pi.reportError);
279
+ // Should call shutdown
280
+ sandbox.assert.called(pi.shutdown);
281
+ });
282
+
283
+ it("message events from platform thread are route based on command: error", () => {
284
+ const message = pi.callbackFunction("message", "my session id");
285
+ message(["error", "error message"]);
286
+ sandbox.assert.calledWith(
287
+ pi.reportError,
288
+ "my session id",
289
+ "error message",
290
+ );
291
+ });
292
+
293
+ it("message events from platform thread are route based on command: updateActor", () => {
294
+ const message = pi.callbackFunction("message", "my session id");
295
+ message(["updateActor", undefined, { foo: "bar" }]);
296
+ sandbox.assert.calledWith(pi.updateIdentifier, { foo: "bar" });
297
+ });
171
298
 
172
- describe('handleJobResult', () => {
173
- beforeEach(() => {
174
- pi.sendToClient = sandbox.fake();
175
- pi.broadcastToSharedPeers = sandbox.fake();
176
- });
177
-
178
- it('broadcasts to peers when handling a completed job', async () => {
179
- pi.sessions.add('other peer');
180
- await pi.handleJobResult('completed', {msg: {foo: 'bar'}},
181
- undefined);
182
- expect(pi.sendToClient.callCount).to.equal(1);
183
- expect(pi.broadcastToSharedPeers.callCount).to.equal(1);
184
- });
185
-
186
- it('appends completed result message when present', async () => {
187
- await pi.handleJobResult('completed', {sessionId: 'a session id', msg: {foo: 'bar'}},
188
- 'a good result message');
189
- expect(pi.broadcastToSharedPeers.callCount).to.equal(1);
190
- sandbox.assert.calledWith(pi.sendToClient, 'a session id',
191
- {foo: 'bar'});
192
- });
193
-
194
- it('appends failed result message when present', async () => {
195
- await pi.handleJobResult('failed', {sessionId: 'a session id', msg: {foo: 'bar'}},
196
- 'a bad result message');
197
- expect(pi.broadcastToSharedPeers.callCount).to.equal(1);
198
- sandbox.assert.calledWith(pi.sendToClient, 'a session id',
199
- {foo: 'bar', error: 'a bad result message'});
200
- });
299
+ it("message events from platform thread are route based on command: else", () => {
300
+ const message = pi.callbackFunction("message", "my session id");
301
+ message(["blah", { foo: "bar" }]);
302
+ sandbox.assert.calledWith(pi.sendToClient, "my session id", {
303
+ foo: "bar",
304
+ });
305
+ });
306
+ });
201
307
  });
202
308
 
203
- describe('callbackFunction', () => {
204
- beforeEach(() => {
205
- pi.reportError = sandbox.fake();
206
- pi.sendToClient = sandbox.fake();
207
- pi.updateIdentifier = sandbox.fake();
208
- });
209
-
210
- it('close events from platform thread are reported', () => {
211
- const close = pi.callbackFunction('close', 'my session id');
212
- close('error msg');
213
- sandbox.assert.calledWith(pi.reportError,
214
- 'my session id', 'Error: session thread closed unexpectedly: error msg');
215
- });
216
-
217
- it('message events from platform thread are route based on command: error', () => {
218
- const message = pi.callbackFunction('message', 'my session id');
219
- message(['error', 'error message']);
220
- sandbox.assert.calledWith(pi.reportError, 'my session id', 'error message');
221
- });
222
-
223
- it('message events from platform thread are route based on command: updateActor', () => {
224
- const message = pi.callbackFunction('message', 'my session id');
225
- message(['updateActor', undefined, {foo: 'bar'}]);
226
- sandbox.assert.calledWith(pi.updateIdentifier, {foo:'bar'});
227
- });
228
-
229
- it('message events from platform thread are route based on command: else', () => {
230
- const message = pi.callbackFunction('message', 'my session id');
231
- message(['blah', {foo: 'bar'}]);
232
- sandbox.assert.calledWith(pi.sendToClient,
233
- 'my session id', {foo:'bar'});
234
- });
309
+ describe("credential failure handling", () => {
310
+ let queueMock: any;
311
+ let processMock: any;
312
+
313
+ beforeEach(() => {
314
+ queueMock = {
315
+ pause: sandbox.stub().resolves(),
316
+ resume: sandbox.stub().resolves(),
317
+ shutdown: sandbox.stub().resolves(),
318
+ on: sandbox.stub(),
319
+ getJob: sandbox.stub(),
320
+ initResultEvents: sandbox.stub(),
321
+ };
322
+
323
+ processMock = {
324
+ removeAllListeners: sandbox.stub(),
325
+ unref: sandbox.stub(),
326
+ kill: sandbox.stub(),
327
+ };
328
+ });
329
+
330
+ describe("POSITIVE: Platform initialized - credential failure should NOT terminate", () => {
331
+ it("should keep platform alive when credential job fails on initialized platform", async () => {
332
+ const TestPlatformInstance = getTestPlatformInstanceClass();
333
+ pi = new TestPlatformInstance({
334
+ identifier: "test-platform-id",
335
+ platform: "xmpp",
336
+ parentId: "test-parent",
337
+ actor: "testuser@localhost",
338
+ });
339
+
340
+ // Override queue with our mock
341
+ pi.queue = queueMock;
342
+ pi.process = processMock;
343
+
344
+ // Setup: Platform is already initialized
345
+ pi.config = {
346
+ persist: true,
347
+ initialized: true,
348
+ requireCredentials: ["connect"],
349
+ };
350
+ pi.flaggedForTermination = false;
351
+
352
+ const job = {
353
+ sessionId: "session123",
354
+ msg: {
355
+ type: "connect",
356
+ context: "xmpp",
357
+ actor: { id: "testuser@localhost", type: "person" },
358
+ },
359
+ title: "xmpp-1",
360
+ sessionSecret: "secret",
361
+ };
362
+
363
+ const errorResult = "credentials mismatch for testuser@localhost";
364
+
365
+ pi.sendToClient = sandbox.stub();
366
+
367
+ // Simulate job failure
368
+ await pi.handleJobResult("failed", job, errorResult);
369
+
370
+ // ASSERTIONS
371
+ // 1. Platform should NOT be flagged for termination
372
+ expect(pi.flaggedForTermination).toBe(false);
373
+
374
+ // 2. Queue should NOT be paused
375
+ sinon.assert.notCalled(queueMock.pause);
376
+
377
+ // 3. Platform should remain initialized
378
+ expect(pi.config.initialized).toBe(true);
379
+
380
+ // 4. Error should still be sent to client
381
+ sinon.assert.called(pi.sendToClient);
382
+ });
383
+
384
+ it("should allow subsequent jobs after non-fatal credential error", async () => {
385
+ const TestPlatformInstance = getTestPlatformInstanceClass();
386
+ pi = new TestPlatformInstance({
387
+ identifier: "test-platform-id",
388
+ platform: "xmpp",
389
+ parentId: "test-parent",
390
+ actor: "testuser@localhost",
391
+ });
392
+
393
+ pi.queue = queueMock;
394
+ pi.process = processMock;
395
+
396
+ pi.config = {
397
+ persist: true,
398
+ initialized: true,
399
+ requireCredentials: ["connect"],
400
+ };
401
+ pi.flaggedForTermination = false;
402
+ pi.sendToClient = sandbox.stub();
403
+
404
+ const failedJob = {
405
+ sessionId: "session123",
406
+ msg: {
407
+ type: "connect",
408
+ context: "xmpp",
409
+ actor: { id: "testuser@localhost", type: "person" },
410
+ },
411
+ title: "xmpp-1",
412
+ sessionSecret: "secret",
413
+ };
414
+
415
+ // First job fails with credential error
416
+ await pi.handleJobResult("failed", failedJob, "credential error");
417
+
418
+ // Platform should still be operational
419
+ expect(pi.flaggedForTermination).toBe(false);
420
+ expect(pi.config.initialized).toBe(true);
421
+
422
+ // Second job succeeds
423
+ const successJob = {
424
+ sessionId: "session456",
425
+ msg: {
426
+ type: "send",
427
+ context: "xmpp",
428
+ actor: { id: "testuser@localhost", type: "person" },
429
+ },
430
+ title: "xmpp-2",
431
+ sessionSecret: "secret",
432
+ };
433
+
434
+ await pi.handleJobResult("completed", successJob, undefined);
435
+
436
+ // Platform should still be alive
437
+ expect(pi.flaggedForTermination).toBe(false);
438
+ expect(pi.config.initialized).toBe(true);
439
+ });
440
+ });
441
+
442
+ describe("NEGATIVE: Platform NOT initialized - credential failure SHOULD terminate", () => {
443
+ it("should terminate platform when credential job fails on uninitialized platform", async () => {
444
+ const TestPlatformInstance = getTestPlatformInstanceClass();
445
+ pi = new TestPlatformInstance({
446
+ identifier: "test-platform-id",
447
+ platform: "xmpp",
448
+ parentId: "test-parent",
449
+ actor: "testuser@localhost",
450
+ });
451
+
452
+ pi.queue = queueMock;
453
+ pi.process = processMock;
454
+
455
+ // Setup: Platform is NOT initialized
456
+ pi.config = {
457
+ persist: true,
458
+ initialized: false,
459
+ requireCredentials: ["connect"],
460
+ };
461
+ pi.flaggedForTermination = false;
462
+ pi.sendToClient = sandbox.stub();
463
+
464
+ const job = {
465
+ sessionId: "session123",
466
+ msg: {
467
+ type: "connect",
468
+ context: "xmpp",
469
+ actor: { id: "testuser@localhost", type: "person" },
470
+ },
471
+ title: "xmpp-1",
472
+ sessionSecret: "secret",
473
+ };
474
+
475
+ const errorResult = "invalid credentials for testuser@localhost";
476
+
477
+ // Simulate job failure on uninitialized platform
478
+ await pi.handleJobResult("failed", job, errorResult);
479
+
480
+ // ASSERTIONS
481
+ // 1. Platform SHOULD be flagged for termination
482
+ expect(pi.flaggedForTermination).toBe(true);
483
+
484
+ // 2. Queue SHOULD be paused
485
+ sinon.assert.calledOnce(queueMock.pause);
486
+
487
+ // 3. Platform should remain uninitialized
488
+ expect(pi.config.initialized).toBe(false);
489
+
490
+ // 4. Error should still be sent to client
491
+ sinon.assert.called(pi.sendToClient);
492
+ });
493
+
494
+ it("should pause queue when credential initialization fails", async () => {
495
+ const TestPlatformInstance = getTestPlatformInstanceClass();
496
+ pi = new TestPlatformInstance({
497
+ identifier: "test-platform-id",
498
+ platform: "xmpp",
499
+ parentId: "test-parent",
500
+ actor: "testuser@localhost",
501
+ });
502
+
503
+ pi.queue = queueMock;
504
+ pi.process = processMock;
505
+
506
+ pi.config = {
507
+ persist: true,
508
+ initialized: false,
509
+ requireCredentials: ["connect"],
510
+ };
511
+ pi.sendToClient = sandbox.stub();
512
+
513
+ const job = {
514
+ sessionId: "session123",
515
+ msg: {
516
+ type: "connect",
517
+ context: "xmpp",
518
+ actor: { id: "testuser@localhost", type: "person" },
519
+ },
520
+ title: "xmpp-1",
521
+ sessionSecret: "secret",
522
+ };
523
+
524
+ await pi.handleJobResult("failed", job, "connection failed");
525
+
526
+ // Queue must be paused
527
+ sinon.assert.calledOnce(queueMock.pause);
528
+ });
529
+ });
235
530
  });
236
- });
237
531
  });