@sockethub/platform-xmpp 5.0.0-alpha.10

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.
@@ -0,0 +1,525 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import sinon from "sinon";
3
+
4
+ import XMPP from "./index.js";
5
+
6
+ const actor = {
7
+ type: "person",
8
+ id: "testingham@jabber.net",
9
+ name: "testing ham",
10
+ };
11
+
12
+ const credentials = {
13
+ actor: actor,
14
+ object: {
15
+ type: "credentials",
16
+ userAddress: "testingham@jabber.net",
17
+ password: "foobar",
18
+ resource: "home",
19
+ },
20
+ };
21
+
22
+ const target = {
23
+ mrfoobar: {
24
+ type: "person",
25
+ id: "mrfoobar@jabber.net",
26
+ name: "Mr FooBar",
27
+ },
28
+ partyroom: {
29
+ type: "room",
30
+ id: "partyroom@jabber.net",
31
+ },
32
+ roomuser: {
33
+ type: "room",
34
+ id: "partyroom@jabber.net/ms tut",
35
+ },
36
+ };
37
+
38
+ const job = {
39
+ connect: {
40
+ context: "xmpp",
41
+ type: "connect",
42
+ actor: {
43
+ id: "slvrbckt@jabber.net/Home",
44
+ type: "person",
45
+ name: "Nick Jennings",
46
+ userName: "slvrbckt",
47
+ },
48
+ },
49
+ join: {
50
+ actor: actor,
51
+ object: {
52
+ type: "update",
53
+ name: "Frank",
54
+ },
55
+ target: target.partyroom,
56
+ },
57
+ leave: {
58
+ actor: actor,
59
+ target: target.partyroom,
60
+ },
61
+ send: {
62
+ chat: {
63
+ actor: actor,
64
+ object: {
65
+ type: "message",
66
+ id: "hc-1234abcd",
67
+ content: "hello",
68
+ },
69
+ target: target.mrfoobar,
70
+ },
71
+ groupchat: {
72
+ actor: actor,
73
+ object: {
74
+ type: "message",
75
+ id: "hc-1234abcd",
76
+ content: "hi all",
77
+ },
78
+ target: target.roomuser,
79
+ },
80
+ correction: {
81
+ actor: actor,
82
+ object: {
83
+ type: "message",
84
+ id: "hc-1234abcd",
85
+ content: "hi yall",
86
+ "xmpp:replace": { id: "hc-234bcde" },
87
+ },
88
+ target: target.roomuser,
89
+ },
90
+ },
91
+ update: {
92
+ presenceOnline: {
93
+ actor: actor,
94
+ object: {
95
+ type: "presence",
96
+ presence: "online",
97
+ content: "ready to chat",
98
+ },
99
+ },
100
+ presenceUnavailable: {
101
+ actor: actor,
102
+ object: {
103
+ type: "presence",
104
+ presence: "away",
105
+ content: "eating popcorn",
106
+ },
107
+ },
108
+ presenceOffline: {
109
+ actor: actor,
110
+ object: {
111
+ type: "presence",
112
+ presence: "offline",
113
+ content: "",
114
+ },
115
+ },
116
+ },
117
+ "request-friend": {
118
+ actor: actor,
119
+ target: target.mrfoobar,
120
+ },
121
+ "remove-friend": {
122
+ actor: actor,
123
+ target: target.mrfoobar,
124
+ },
125
+ "make-friend": {
126
+ actor: actor,
127
+ target: target.mrfoobar,
128
+ },
129
+ query: {
130
+ actor: actor,
131
+ target: target.partyroom,
132
+ object: {
133
+ type: "attendance",
134
+ },
135
+ },
136
+ };
137
+
138
+ describe("XMPP", () => {
139
+ let clientFake, xmlFake, clientObjectFake, xp;
140
+
141
+ beforeEach(() => {
142
+ clientObjectFake = {
143
+ on: sinon.fake(),
144
+ start: sinon.fake.resolves(),
145
+ send: sinon.fake.resolves(),
146
+ join: sinon.fake.resolves(),
147
+ stop: sinon.fake.resolves()
148
+ };
149
+ clientFake = sinon.fake.returns(clientObjectFake);
150
+
151
+ // Mock XML object with chainable .c() method for building stanzas
152
+ const mockXmlElement = {
153
+ name: "presence",
154
+ attrs: {},
155
+ children: [],
156
+ c: sinon.fake.returns({
157
+ name: "x",
158
+ attrs: { xmlns: "http://jabber.org/protocol/muc" },
159
+ parent: null
160
+ }),
161
+ getChild: sinon.fake((name, xmlns) => {
162
+ if (name === "x" && xmlns === "http://jabber.org/protocol/muc") {
163
+ return { attrs: { xmlns: "http://jabber.org/protocol/muc" } };
164
+ }
165
+ return null;
166
+ })
167
+ };
168
+
169
+ // Create a smart fake that returns complex object for presence, simple for others
170
+ xmlFake = sinon.fake((elementName) => {
171
+ if (elementName === "presence") {
172
+ return mockXmlElement;
173
+ }
174
+ return undefined; // Default return for other elements
175
+ });
176
+
177
+ class TestXMPP extends XMPP {
178
+ createClient() {
179
+ this.__clientConstructor = clientFake;
180
+ }
181
+ createXml() {
182
+ this.__xml = xmlFake;
183
+ }
184
+ }
185
+
186
+ xp = new TestXMPP({
187
+ id: actor,
188
+ debug: sinon.fake(),
189
+ sendToClient: sinon.fake(),
190
+ });
191
+ });
192
+
193
+ afterEach(() => {
194
+ sinon.restore();
195
+ });
196
+
197
+ describe("Successful initialization", () => {
198
+ it("works as intended", (done) => {
199
+ xp.connect(job.connect, credentials, () => {
200
+ sinon.assert.calledOnce(clientFake);
201
+ expect(xp.__client.on).toBeDefined();
202
+ expect(xp.__client.start).toBeDefined();
203
+ expect(xp.__client.send).toBeDefined();
204
+ expect(xp.__client.send.callCount).toEqual(0);
205
+ sinon.assert.calledOnce(clientObjectFake.start);
206
+ sinon.assert.notCalled(xp.sendToClient);
207
+ done();
208
+ });
209
+ });
210
+ });
211
+
212
+ describe("Bad initialization", () => {
213
+ it("returns the existing __client object", (done) => {
214
+ const dummyClient = {
215
+ foo: "bar",
216
+ socket: {
217
+ writable: true
218
+ },
219
+ status: "online"
220
+ };
221
+ xp.__client = dummyClient
222
+ xp.connect(job.connect, credentials, (d) => {
223
+ console.log('result: ', d);
224
+ expect(xp.__client).toEqual(dummyClient);
225
+ sinon.assert.notCalled(clientFake);
226
+ sinon.assert.notCalled(xp.sendToClient);
227
+ // sinon.assert.calledOnce(clientFake);
228
+ // sinon.assert.calledOnce(xp.sendToClient);
229
+ done();
230
+ });
231
+ });
232
+
233
+ it("deletes the __client property on failed connect", (done) => {
234
+ clientObjectFake.start = sinon.fake.rejects("foo");
235
+ xp.connect(job.connect, credentials, () => {
236
+ expect(xp.__client).toBeUndefined();
237
+ sinon.assert.notCalled(xp.sendToClient);
238
+ done();
239
+ });
240
+ });
241
+ });
242
+
243
+ describe("Platform functionality", () => {
244
+ beforeEach((done) => {
245
+ xp.connect(job.join, credentials, () => done());
246
+ });
247
+
248
+ describe("#join", () => {
249
+ it("calls xmpp.js correctly", (done) => {
250
+ expect(xp.__client.send).toBeInstanceOf(Function);
251
+ xp.join(job.join, () => {
252
+ sinon.assert.calledOnce(xp.__client.send);
253
+
254
+ // Verify MUC <x> element was created with correct namespace
255
+ sinon.assert.calledWith(xmlFake, "x", { xmlns: "http://jabber.org/protocol/muc" });
256
+
257
+ // Verify presence stanza was created with correct attributes
258
+ sinon.assert.calledWith(xmlFake, "presence", {
259
+ from: "testingham@jabber.net",
260
+ to: "partyroom@jabber.net/testing ham",
261
+ });
262
+
263
+ done();
264
+ });
265
+ });
266
+ });
267
+
268
+ describe("#leave", () => {
269
+ it("calls xmpp.js correctly", (done) => {
270
+ expect(xp.__client).toBeDefined();
271
+ expect(xp.__client.send).toBeInstanceOf(Function);
272
+ xp.leave(job.leave, () => {
273
+ sinon.assert.calledOnce(xp.__client.send);
274
+ sinon.assert.calledWith(xmlFake, "presence", {
275
+ from: "testingham@jabber.net",
276
+ to: "partyroom@jabber.net/testing ham",
277
+ type: "unavailable",
278
+ });
279
+ done();
280
+ });
281
+ });
282
+ });
283
+
284
+ describe("#send", () => {
285
+ it("calls xmpp.js correctly", (done) => {
286
+ expect(xp.__client).toBeDefined();
287
+ expect(xp.__client.send).toBeInstanceOf(Function);
288
+ xp.send(job.send.chat, () => {
289
+ sinon.assert.calledOnce(xp.__client.send);
290
+ expect(xmlFake.getCall(0).args).toEqual([
291
+ "body",
292
+ {},
293
+ job.send.chat.object.content,
294
+ ]);
295
+ expect(xmlFake.getCall(1).args).toEqual([
296
+ "message",
297
+ {
298
+ type: "chat",
299
+ to: job.send.chat.target.id,
300
+ id: job.send.chat.object.id,
301
+ },
302
+ undefined,
303
+ undefined,
304
+ ]);
305
+ done();
306
+ });
307
+ });
308
+
309
+ it("calls xmpp.js correctly for a groupchat", (done) => {
310
+ xp.send(job.send.groupchat, () => {
311
+ sinon.assert.calledOnce(xp.__client.send);
312
+ expect(xmlFake.getCall(0).args).toEqual([
313
+ "body",
314
+ {},
315
+ job.send.groupchat.object.content,
316
+ ]);
317
+ expect(xmlFake.getCall(1).args).toEqual([
318
+ "message",
319
+ {
320
+ type: "groupchat",
321
+ to: job.send.groupchat.target.id,
322
+ id: job.send.groupchat.object.id,
323
+ },
324
+ undefined,
325
+ undefined,
326
+ ]);
327
+ done();
328
+ });
329
+ });
330
+
331
+ it("calls xmpp.js correctly for a message correction", (done) => {
332
+ xp.send(job.send.correction, () => {
333
+ sinon.assert.calledOnce(xp.__client.send);
334
+ expect(xmlFake.getCall(0).args).toEqual([
335
+ "body",
336
+ {},
337
+ job.send.correction.object.content,
338
+ ]);
339
+ expect(xmlFake.getCall(1).args).toEqual([
340
+ "replace",
341
+ {
342
+ id: job.send.correction.object["xmpp:replace"].id,
343
+ xmlns: "urn:xmpp:message-correct:0",
344
+ },
345
+ ]);
346
+ expect(xmlFake.getCall(2).args).toEqual([
347
+ "message",
348
+ {
349
+ type: "groupchat",
350
+ to: job.send.correction.target.id,
351
+ id: job.send.correction.object.id,
352
+ },
353
+ undefined,
354
+ undefined,
355
+ ]);
356
+ done();
357
+ });
358
+ });
359
+ });
360
+
361
+ describe("#update", () => {
362
+ it("calls xml() correctly for available", (done) => {
363
+ xp.update(job.update.presenceOnline, () => {
364
+ sinon.assert.calledOnce(xp.__client.send);
365
+ expect(xmlFake.getCall(0).args).toEqual([
366
+ "presence",
367
+ {},
368
+ {},
369
+ { status: "ready to chat" },
370
+ ]);
371
+ done();
372
+ });
373
+ });
374
+ it("calls xml() correctly for unavailable", (done) => {
375
+ xp.update(job.update.presenceUnavailable, () => {
376
+ sinon.assert.calledOnce(xp.__client.send);
377
+ expect(xmlFake.getCall(0).args).toEqual([
378
+ "presence",
379
+ {},
380
+ { show: "away" },
381
+ { status: "eating popcorn" },
382
+ ]);
383
+ done();
384
+ });
385
+ });
386
+ it("calls xml() correctly for offline", (done) => {
387
+ xp.update(job.update.presenceOffline, () => {
388
+ sinon.assert.calledOnce(xp.__client.send);
389
+ expect(xmlFake.getCall(0).args).toEqual([
390
+ "presence",
391
+ { type: "unavailable" },
392
+ {},
393
+ {},
394
+ ]);
395
+ done();
396
+ });
397
+ });
398
+ });
399
+
400
+ describe("#request-friend", () => {
401
+ it("calls xmpp.js correctly", (done) => {
402
+ xp["request-friend"](job["request-friend"], () => {
403
+ sinon.assert.calledOnce(xp.__client.send);
404
+ expect(xmlFake.getCall(0).args).toEqual([
405
+ "presence",
406
+ {
407
+ type: "subscribe",
408
+ to: job["request-friend"].target["id"],
409
+ },
410
+ ]);
411
+ done();
412
+ });
413
+ });
414
+ });
415
+
416
+ describe("#remove-friend", () => {
417
+ it("calls xmpp.js correctly", (done) => {
418
+ xp["remove-friend"](job["remove-friend"], () => {
419
+ sinon.assert.calledOnce(xp.__client.send);
420
+ expect(xmlFake.getCall(0).args).toEqual([
421
+ "presence",
422
+ {
423
+ type: "unsubscribe",
424
+ to: job["remove-friend"].target["id"],
425
+ },
426
+ ]);
427
+ done();
428
+ });
429
+ });
430
+ });
431
+
432
+ describe("#make-friend", () => {
433
+ it("calls xmpp.js correctly", (done) => {
434
+ xp["remove-friend"](job["remove-friend"], () => {
435
+ sinon.assert.calledOnce(xp.__client.send);
436
+ expect(xmlFake.getCall(0).args).toEqual([
437
+ "presence",
438
+ {
439
+ type: "unsubscribe",
440
+ to: job["make-friend"].target["id"],
441
+ },
442
+ ]);
443
+ done();
444
+ });
445
+ });
446
+ });
447
+
448
+ describe("#query", () => {
449
+ it("calls xmpp.js correctly", (done) => {
450
+ xp.query(job.query, () => {
451
+ sinon.assert.calledOnce(xp.__client.send);
452
+ expect(xmlFake.getCall(0).args).toEqual([
453
+ "query",
454
+ { xmlns: "http://jabber.org/protocol/disco#items" },
455
+ ]);
456
+ expect(xmlFake.getCall(1).args).toEqual([
457
+ "iq",
458
+ {
459
+ id: "muc_id",
460
+ type: "get",
461
+ from: "testingham@jabber.net",
462
+ to: "partyroom@jabber.net",
463
+ },
464
+ undefined,
465
+ ]);
466
+ done();
467
+ });
468
+ });
469
+ });
470
+
471
+ describe("#disconnect", () => {
472
+ it("calls cleanup", (done) => {
473
+ let cleanupCalled = false;
474
+ xp.cleanup = (done) => {
475
+ cleanupCalled = true;
476
+ done();
477
+ }
478
+ xp.disconnect(job, () => {
479
+ expect(cleanupCalled).toEqual(true);
480
+ done()
481
+ });
482
+ })
483
+ });
484
+
485
+ describe("#cleanup", () => {
486
+ it("calls client.stop", (done) => {
487
+ expect(xp.config.initialized).toEqual(true);
488
+ xp.cleanup(() => {
489
+ expect(xp.config.initialized).toEqual(false);
490
+ sinon.assert.calledOnce(xp.__client.stop);
491
+ done()
492
+ });
493
+ })
494
+ });
495
+
496
+ describe("#join", () => {
497
+ it("creates correct MUC presence stanza with namespace", (done) => {
498
+ const joinJob = {
499
+ actor: {
500
+ id: "testingham@jabber.net",
501
+ name: "Testing Ham"
502
+ },
503
+ target: {
504
+ id: "testroom@conference.jabber.net"
505
+ }
506
+ };
507
+
508
+ xp.join(joinJob, () => {
509
+ sinon.assert.calledOnce(xp.__client.send);
510
+
511
+ // Verify MUC <x> element was created with correct namespace
512
+ sinon.assert.calledWith(xmlFake, "x", { xmlns: "http://jabber.org/protocol/muc" });
513
+
514
+ // Verify presence stanza was created with correct attributes
515
+ sinon.assert.calledWith(xmlFake, "presence", {
516
+ from: "testingham@jabber.net",
517
+ to: "testroom@conference.jabber.net/Testing Ham",
518
+ });
519
+
520
+ done();
521
+ });
522
+ });
523
+ });
524
+ });
525
+ });
package/src/schema.js ADDED
@@ -0,0 +1,61 @@
1
+ import PackageJSON from "../package.json" with { type: "json" };
2
+
3
+ export const PlatformSchema = {
4
+ name: "xmpp",
5
+ version: PackageJSON.version,
6
+ messages: {
7
+ required: ["type"],
8
+ properties: {
9
+ type: {
10
+ enum: [
11
+ "connect",
12
+ "join",
13
+ "leave",
14
+ "send",
15
+ "update",
16
+ "request-friend",
17
+ "remove-friend",
18
+ "make-friend",
19
+ "query",
20
+ "disconnect",
21
+ ],
22
+ },
23
+ },
24
+ },
25
+ credentials: {
26
+ required: ["object"],
27
+ properties: {
28
+ // TODO platforms shouldn't have to define the actor property if
29
+ // they don't want to, just credential specifics
30
+ actor: {
31
+ type: "object",
32
+ required: ["id"],
33
+ },
34
+ object: {
35
+ type: "object",
36
+ required: ["type", "userAddress", "password", "resource"],
37
+ additionalProperties: false,
38
+ properties: {
39
+ type: {
40
+ type: "string",
41
+ },
42
+ userAddress: {
43
+ type: "string",
44
+ },
45
+ password: {
46
+ type: "string",
47
+ },
48
+ server: {
49
+ type: "string",
50
+ },
51
+ port: {
52
+ type: "number",
53
+ },
54
+ resource: {
55
+ type: "string",
56
+ },
57
+ },
58
+ },
59
+ },
60
+ },
61
+ };
package/src/utils.js ADDED
@@ -0,0 +1,21 @@
1
+ export const utils = {
2
+ buildXmppCredentials: (credentials) => {
3
+ const [username, server] = credentials.object.userAddress.split("@");
4
+ const xmpp_creds = {
5
+ service: credentials.object.server
6
+ ? credentials.object.server
7
+ : server
8
+ ? server
9
+ : undefined,
10
+ username: username,
11
+ password: credentials.object.password,
12
+ };
13
+ if (credentials.object.port) {
14
+ xmpp_creds.service = `${xmpp_creds.service}:${credentials.object.port}`;
15
+ }
16
+ if (credentials.object.resource) {
17
+ xmpp_creds.resource = credentials.object.resource;
18
+ }
19
+ return xmpp_creds;
20
+ },
21
+ };
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { utils } from "./utils.js";
4
+
5
+ describe("Utils", () => {
6
+ describe("buildXmppCredentials", () => {
7
+ it("returns correct credential object used for xmpp.js connect", () => {
8
+ expect(
9
+ utils.buildXmppCredentials({
10
+ object: {
11
+ userAddress: "barney@dinosaur.com.au",
12
+ password: "bar",
13
+ resource: "Home",
14
+ },
15
+ }),
16
+ ).toEqual({
17
+ password: "bar",
18
+ service: "dinosaur.com.au",
19
+ username: "barney",
20
+ resource: "Home",
21
+ });
22
+ });
23
+ });
24
+ it("allows overriding server value", () => {
25
+ expect(
26
+ utils.buildXmppCredentials({
27
+ object: {
28
+ userAddress: "barney@dinosaur.com.au",
29
+ server: "foo",
30
+ password: "bar",
31
+ resource: "Home",
32
+ },
33
+ }),
34
+ ).toEqual({
35
+ password: "bar",
36
+ service: "foo",
37
+ username: "barney",
38
+ resource: "Home",
39
+ });
40
+ });
41
+ it("allows a custom port", () => {
42
+ expect(
43
+ utils.buildXmppCredentials({
44
+ object: {
45
+ userAddress: "barney@dinosaur.com.au",
46
+ port: 123,
47
+ password: "bar",
48
+ resource: "Home",
49
+ },
50
+ }),
51
+ ).toEqual({
52
+ password: "bar",
53
+ service: "dinosaur.com.au:123",
54
+ username: "barney",
55
+ resource: "Home",
56
+ });
57
+ });
58
+ });