@sockethub/server 5.0.0-alpha.3 → 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 (127) hide show
  1. package/README.md +54 -60
  2. package/bin/sockethub +4 -3
  3. package/package.json +42 -60
  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 +20 -7
  9. package/src/bootstrap/init.test.ts +211 -0
  10. package/src/bootstrap/init.ts +152 -75
  11. package/src/bootstrap/load-platforms.ts +151 -0
  12. package/src/config.test.ts +27 -22
  13. package/src/config.ts +82 -78
  14. package/src/defaults.json +24 -16
  15. package/src/index.ts +67 -27
  16. package/src/janitor.test.ts +211 -0
  17. package/src/janitor.ts +145 -77
  18. package/src/listener.ts +151 -57
  19. package/src/middleware/create-activity-object.test.ts +28 -8
  20. package/src/middleware/create-activity-object.ts +17 -8
  21. package/src/middleware/expand-activity-stream.test.data.ts +332 -346
  22. package/src/middleware/expand-activity-stream.test.ts +65 -66
  23. package/src/middleware/expand-activity-stream.ts +29 -19
  24. package/src/middleware/store-credentials.test.ts +74 -62
  25. package/src/middleware/store-credentials.ts +15 -15
  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 +63 -39
  29. package/src/middleware.test.ts +168 -138
  30. package/src/middleware.ts +62 -43
  31. package/src/platform-instance.test.ts +507 -213
  32. package/src/platform-instance.ts +337 -219
  33. package/src/platform.test.ts +375 -0
  34. package/src/platform.ts +306 -139
  35. package/src/process-manager.ts +75 -51
  36. package/src/routes.test.ts +43 -89
  37. package/src/routes.ts +40 -77
  38. package/src/sentry.test.ts +106 -0
  39. package/src/sentry.ts +19 -0
  40. package/src/sockethub.ts +186 -153
  41. package/src/util.ts +5 -0
  42. package/coverage/tmp/coverage-93126-1649152190997-0.json +0 -1
  43. package/dist/bootstrap/init.d.ts +0 -18
  44. package/dist/bootstrap/init.js +0 -63
  45. package/dist/bootstrap/init.js.map +0 -1
  46. package/dist/bootstrap/platforms.js +0 -75
  47. package/dist/common.d.ts +0 -3
  48. package/dist/common.js +0 -20
  49. package/dist/common.js.map +0 -1
  50. package/dist/config.d.ts +0 -6
  51. package/dist/config.js +0 -102
  52. package/dist/config.js.map +0 -1
  53. package/dist/crypto.d.ts +0 -10
  54. package/dist/crypto.js +0 -38
  55. package/dist/crypto.js.map +0 -1
  56. package/dist/defaults.json +0 -28
  57. package/dist/index.d.ts +0 -2
  58. package/dist/index.js +0 -25
  59. package/dist/index.js.map +0 -1
  60. package/dist/janitor.d.ts +0 -15
  61. package/dist/janitor.js +0 -89
  62. package/dist/janitor.js.map +0 -1
  63. package/dist/listener.d.ts +0 -28
  64. package/dist/listener.js +0 -91
  65. package/dist/listener.js.map +0 -1
  66. package/dist/middleware/create-activity-object.d.ts +0 -6
  67. package/dist/middleware/create-activity-object.js +0 -19
  68. package/dist/middleware/create-activity-object.js.map +0 -1
  69. package/dist/middleware/expand-activity-stream.d.ts +0 -2
  70. package/dist/middleware/expand-activity-stream.js +0 -33
  71. package/dist/middleware/expand-activity-stream.js.map +0 -1
  72. package/dist/middleware/expand-activity-stream.test.data.d.ts +0 -480
  73. package/dist/middleware/expand-activity-stream.test.data.js +0 -360
  74. package/dist/middleware/expand-activity-stream.test.data.js.map +0 -1
  75. package/dist/middleware/store-credentials.d.ts +0 -3
  76. package/dist/middleware/store-credentials.js +0 -19
  77. package/dist/middleware/store-credentials.js.map +0 -1
  78. package/dist/middleware/validate.d.ts +0 -2
  79. package/dist/middleware/validate.js +0 -58
  80. package/dist/middleware/validate.js.map +0 -1
  81. package/dist/middleware/validate.test.data.d.ts +0 -532
  82. package/dist/middleware/validate.test.data.js +0 -263
  83. package/dist/middleware/validate.test.data.js.map +0 -1
  84. package/dist/middleware.d.ts +0 -10
  85. package/dist/middleware.js +0 -54
  86. package/dist/middleware.js.map +0 -1
  87. package/dist/platform-instance.d.ts +0 -77
  88. package/dist/platform-instance.js +0 -211
  89. package/dist/platform-instance.js.map +0 -1
  90. package/dist/platform.d.ts +0 -6
  91. package/dist/platform.js +0 -187
  92. package/dist/platform.js.map +0 -1
  93. package/dist/process-manager.d.ts +0 -11
  94. package/dist/process-manager.js +0 -78
  95. package/dist/process-manager.js.map +0 -1
  96. package/dist/routes.d.ts +0 -13
  97. package/dist/routes.js +0 -83
  98. package/dist/routes.js.map +0 -1
  99. package/dist/sockethub.d.ts +0 -39
  100. package/dist/sockethub.js +0 -119
  101. package/dist/sockethub.js.map +0 -1
  102. package/dist/store.d.ts +0 -5
  103. package/dist/store.js +0 -17
  104. package/dist/store.js.map +0 -1
  105. package/src/bootstrap/platforms.js +0 -75
  106. package/src/common.test.ts +0 -54
  107. package/src/common.ts +0 -14
  108. package/src/config.d.ts +0 -2
  109. package/src/crypto.d.ts +0 -5
  110. package/src/crypto.test.ts +0 -41
  111. package/src/crypto.ts +0 -41
  112. package/src/janitor.d.ts +0 -8
  113. package/src/middleware/validate.d.ts +0 -1
  114. package/src/middleware.d.ts +0 -21
  115. package/src/sockethub.d.ts +0 -1
  116. package/src/store.test.ts +0 -28
  117. package/src/store.ts +0 -17
  118. package/test/init-suite.js +0 -41
  119. package/test/queue.functional.test.js +0 -0
  120. package/test/sockethub-suite.js +0 -25
  121. package/tsconfig.json +0 -18
  122. package/views/examples/dummy.ejs +0 -93
  123. package/views/examples/feeds.ejs +0 -90
  124. package/views/examples/irc.ejs +0 -239
  125. package/views/examples/shared.js +0 -72
  126. package/views/examples/xmpp.ejs +0 -191
  127. 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
- 'bull': sandbox.stub().returns({
23
- on: sandbox.stub()
24
- }),
25
- './store': {
26
- redisConfig: {
27
- createClient: () => {}
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", () => {
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
- pi.destroy();
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
 
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
+ }
30
+
31
+ initProcess(parentId, name, id, env) {
32
+ this.process = forkFake(FORK_PATH, [parentId, name, id], env);
33
+ }
34
+
35
+ createGetSocket() {
36
+ this.getSocket = getSocketFake;
37
+ }
38
+ }
39
+ return TestPlatformInstance;
40
+ }
41
+
85
42
  afterEach(() => {
86
- pi.destroy();
43
+ sinon.restore();
87
44
  });
88
45
 
89
- it('has expected properties', () => {
90
- expect(typeof PlatformInstance).to.be.equal('function');
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
+ });
91
63
  });
92
64
 
93
- it('should have a platformInstances Map', () => {
94
- expect(platformInstances instanceof Map).to.be.equal(true);
95
- });
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);
96
74
 
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;
106
- });
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
+ });
107
83
 
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.destroy = sandbox.stub();
127
- expect(pi.sessions.size).to.be.equal(0);
128
- });
129
- });
84
+ afterEach(async () => {
85
+ await pi.shutdown();
86
+ });
130
87
 
131
- it('initializes the job queue', () => {
132
- expect(pi.queue).to.be.undefined;
133
- pi.initQueue('a secret');
134
- expect(pi.queue).to.be.ok;
135
- });
88
+ it("has expected properties", () => {
89
+ const TestPlatformInstance = getTestPlatformInstanceClass();
90
+ expect(typeof TestPlatformInstance).toEqual("function");
91
+ });
136
92
 
137
- it("cleans up its references when destroyed", async () => {
138
- pi.initQueue('a secret');
139
- expect(pi.queue).to.be.ok;
140
- expect(platformInstances.has('platform identifier')).to.be.true;
141
- await pi.destroy();
142
- expect(pi.queue).not.to.be.ok;
143
- expect(platformInstances.has('platform identifier')).to.be.false;
144
- });
93
+ it("should have a platformInstances Map", () => {
94
+ expect(platformInstances instanceof Map).toEqual(true);
95
+ });
145
96
 
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
- });
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
+ });
152
111
 
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
- });
112
+ describe("registerSession", () => {
113
+ beforeEach(() => {
114
+ pi.callbackFunction = sandbox.fake();
115
+ });
163
116
 
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
- });
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
+ });
171
292
 
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
- });
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
+ });
298
+
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
  });