applesauce-relay 0.0.0-next-20250808173123 → 0.0.0-next-20250815164532

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,654 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
+ import { getSeenRelays } from "applesauce-core/helpers";
4
+ import { WS } from "vitest-websocket-mock";
5
+ import { Relay } from "../relay.js";
6
+ import { filter } from "rxjs/operators";
7
+ import { firstValueFrom, of, Subject, throwError, timer } from "rxjs";
8
+ const defaultMockInfo = {
9
+ name: "Test Relay",
10
+ description: "Test Relay Description",
11
+ pubkey: "testpubkey",
12
+ contact: "test@example.com",
13
+ supported_nips: [1, 2, 3],
14
+ software: "test-software",
15
+ version: "1.0.0",
16
+ };
17
+ let server;
18
+ let relay;
19
+ beforeEach(async () => {
20
+ // Mock empty information document
21
+ vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of(null));
22
+ // Create mock relay
23
+ server = new WS("wss://test", { jsonProtocol: true });
24
+ // Create relay
25
+ relay = new Relay("wss://test");
26
+ relay.keepAlive = 0;
27
+ });
28
+ // Wait for server to close to prevent memory leaks
29
+ afterEach(async () => {
30
+ await WS.clean();
31
+ vi.clearAllTimers();
32
+ vi.useRealTimers();
33
+ });
34
+ const mockEvent = {
35
+ kind: 1,
36
+ id: "00007641c9c3e65a71843933a44a18060c7c267a4f9169efa3735ece45c8f621",
37
+ pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
38
+ created_at: 1743712795,
39
+ tags: [["nonce", "13835058055282167643", "16"]],
40
+ content: "This is just stupid: https://codestr.fiatjaf.com/",
41
+ sig: "5a57b5a12bba4b7cf0121077b1421cf4df402c5c221376c076204fc4f7519e28ce6508f26ddc132c406ccfe6e62cc6db857b96c788565cdca9674fe9a0710ac2",
42
+ };
43
+ describe("req", () => {
44
+ it("should trigger connection to relay", async () => {
45
+ subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
46
+ // Wait for connection
47
+ await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
48
+ expect(relay.connected).toBe(true);
49
+ });
50
+ it("should send expected messages to relay", async () => {
51
+ subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
52
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
53
+ });
54
+ it("should not close the REQ when EOSE is received", async () => {
55
+ // Create subscription that completes after first EOSE
56
+ const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
57
+ // Verify REQ was sent
58
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
59
+ // Send EOSE to complete subscription
60
+ server.send(["EVENT", "sub1", mockEvent]);
61
+ server.send(["EOSE", "sub1"]);
62
+ // Verify the subscription did not complete
63
+ expect(sub.receivedComplete()).toBe(false);
64
+ expect(sub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
65
+ });
66
+ it("should send CLOSE when unsubscribed", async () => {
67
+ // Create subscription that completes after first EOSE
68
+ const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
69
+ // Verify REQ was sent
70
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
71
+ // Complete the subscription
72
+ sub.unsubscribe();
73
+ // Verify CLOSE was sent
74
+ await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
75
+ });
76
+ it("should emit nostr event and EOSE", async () => {
77
+ const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
78
+ await server.connected;
79
+ // Send EVENT message
80
+ server.send(["EVENT", "sub1", mockEvent]);
81
+ // Send EOSE message
82
+ server.send(["EOSE", "sub1"]);
83
+ expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
84
+ });
85
+ it("should ignore EVENT and EOSE messages that do not match subscription id", async () => {
86
+ const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
87
+ await server.connected;
88
+ // Send EVENT message with wrong subscription id
89
+ server.send(["EVENT", "wrong_sub", mockEvent]);
90
+ // Send EOSE message with wrong subscription id
91
+ server.send(["EOSE", "wrong_sub"]);
92
+ // Send EVENT message with correct subscription id
93
+ server.send(["EVENT", "sub1", mockEvent]);
94
+ // Send EOSE message with correct subscription id
95
+ server.send(["EOSE", "sub1"]);
96
+ expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
97
+ });
98
+ it("should mark events with their source relay", async () => {
99
+ const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
100
+ await server.connected;
101
+ // Send EVENT message
102
+ server.send(["EVENT", "sub1", mockEvent]);
103
+ // Get the received event
104
+ const receivedEvent = spy.getValues()[0];
105
+ // Verify the event was marked as seen from this relay
106
+ expect(getSeenRelays(receivedEvent)).toContain("wss://test");
107
+ });
108
+ it("should error subscription when CLOSED message is received", async () => {
109
+ const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
110
+ await server.connected;
111
+ // Send CLOSED message for the subscription
112
+ server.send(["CLOSED", "sub1", "reason"]);
113
+ // Verify the subscription completed
114
+ expect(spy.receivedError()).toBe(true);
115
+ });
116
+ it("should not send multiple REQ messages for multiple subscriptions", async () => {
117
+ const sub = relay.req([{ kinds: [1] }], "sub1");
118
+ sub.subscribe();
119
+ sub.subscribe();
120
+ // Wait for all messages to be sent
121
+ await new Promise((resolve) => setTimeout(resolve, 10));
122
+ expect(server.messages).toEqual([["REQ", "sub1", { kinds: [1] }]]);
123
+ });
124
+ it("should wait for authentication if relay responds with auth-required", async () => {
125
+ // First subscription to trigger auth-required
126
+ const firstSub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
127
+ await server.nextMessage;
128
+ // Send CLOSED message with auth-required reason
129
+ server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
130
+ // wait for complete
131
+ await firstSub.onError();
132
+ await server.nextMessage;
133
+ // Create a second subscription that should wait for auth
134
+ const secondSub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub2"), { expectErrors: true });
135
+ // Verify no REQ message was sent yet (waiting for auth)
136
+ expect(server).not.toHaveReceivedMessages(["REQ", "sub2", { kinds: [1] }]);
137
+ // Simulate successful authentication
138
+ relay.authenticated$.next(true);
139
+ // Now the REQ should be sent
140
+ await expect(server).toReceiveMessage(["REQ", "sub2", { kinds: [1] }]);
141
+ // Send EVENT and EOSE to complete the subscription
142
+ server.send(["EVENT", "sub2", mockEvent]);
143
+ server.send(["EOSE", "sub2"]);
144
+ // Verify the second subscription received the event and EOSE
145
+ expect(secondSub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
146
+ });
147
+ it("should open connection and wait for authentication if relay info document has limitations.auth_required = true", async () => {
148
+ // Mock the fetchInformationDocument method to return a document with auth_required = true
149
+ vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of({
150
+ name: "Auth Required Relay",
151
+ description: "A relay that requires authentication",
152
+ pubkey: "",
153
+ contact: "",
154
+ supported_nips: [1, 2, 4],
155
+ software: "",
156
+ version: "",
157
+ limitation: {
158
+ auth_required: true,
159
+ },
160
+ }));
161
+ // Create a subscription that should wait for auth
162
+ const sub = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
163
+ // Wait 10ms to ensure the information document is fetched
164
+ await new Promise((resolve) => setTimeout(resolve, 10));
165
+ // Wait for connection
166
+ await server.connected;
167
+ // Verify no REQ message was sent yet (waiting for auth)
168
+ expect(server).not.toHaveReceivedMessages(["REQ", "sub1", { kinds: [1] }]);
169
+ // Send AUTH challenge
170
+ server.send(["AUTH", "challenge"]);
171
+ // Send auth response
172
+ subscribeSpyTo(relay.auth(mockEvent));
173
+ // Verify the auth event was sent
174
+ await expect(server.nextMessage).resolves.toEqual(["AUTH", mockEvent]);
175
+ // Accept auth
176
+ server.send(["OK", mockEvent.id, true, ""]);
177
+ // Simulate successful authentication
178
+ expect(relay.authenticated).toBe(true);
179
+ // Now the REQ should be sent
180
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
181
+ // Send EVENT and EOSE to complete the subscription
182
+ server.send(["EVENT", "sub1", mockEvent]);
183
+ server.send(["EOSE", "sub1"]);
184
+ // Verify the subscription received the event and EOSE
185
+ expect(sub.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
186
+ });
187
+ it("should throw error if relay closes connection with error", async () => {
188
+ const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
189
+ await server.connected;
190
+ // Send CLOSE message with error
191
+ server.error({
192
+ reason: "error message",
193
+ code: 1000,
194
+ wasClean: false,
195
+ });
196
+ // Verify the subscription completed with an error
197
+ expect(spy.receivedError()).toBe(true);
198
+ });
199
+ it("should not return EOSE while waiting for the relay to be ready", async () => {
200
+ vi.useFakeTimers();
201
+ // @ts-expect-error
202
+ relay.ready$.next(false);
203
+ const spy = subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"), { expectErrors: true });
204
+ // Fast-forward time by 20 seconds
205
+ await vi.advanceTimersByTimeAsync(20000);
206
+ expect(spy.receivedComplete()).toBe(false);
207
+ expect(spy.receivedError()).toBe(false);
208
+ expect(spy.receivedNext()).toBe(false);
209
+ });
210
+ it("should wait when relay isn't ready", async () => {
211
+ // @ts-expect-error
212
+ relay.ready$.next(false);
213
+ subscribeSpyTo(relay.req([{ kinds: [1] }], "sub1"));
214
+ // Wait 10ms to ensure the relay didn't receive anything
215
+ await new Promise((resolve) => setTimeout(resolve, 10));
216
+ expect(server.messages.length).toBe(0);
217
+ // @ts-expect-error
218
+ relay.ready$.next(true);
219
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
220
+ });
221
+ it("should wait for filters if filters are provided as an observable", async () => {
222
+ const filters = new Subject();
223
+ subscribeSpyTo(relay.req(filters, "sub1"));
224
+ // Wait 10sm and ensure no messages were sent yet
225
+ await new Promise((resolve) => setTimeout(resolve, 10));
226
+ expect(server.messagesToConsume.pendingItems.length).toBe(0);
227
+ // Send REQ message with filters
228
+ filters.next([{ kinds: [1] }]);
229
+ // Wait for the REQ message to be sent
230
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
231
+ });
232
+ it("should update filters if filters are provided as an observable", async () => {
233
+ const filters = new Subject();
234
+ subscribeSpyTo(relay.req(filters, "sub1"));
235
+ // Send REQ message with filters
236
+ filters.next([{ kinds: [1] }]);
237
+ // Should send REQ message with new filters
238
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
239
+ // Send REQ message with filters
240
+ filters.next([{ kinds: [2] }]);
241
+ // Should send new REQ message with new filters
242
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [2] }]);
243
+ // It should not send CLOSE message
244
+ await expect(server.messages).not.toContain(["CLOSE", "sub1"]);
245
+ });
246
+ it("should complete if filters are provided as an observable that completes", async () => {
247
+ const filters = new Subject();
248
+ const sub = subscribeSpyTo(relay.req(filters, "sub1"));
249
+ // Send REQ message with filters
250
+ filters.next([{ kinds: [1] }]);
251
+ // Complete the observable
252
+ filters.complete();
253
+ await sub.onComplete();
254
+ expect(sub.receivedComplete()).toBe(true);
255
+ });
256
+ });
257
+ describe("event", () => {
258
+ it("should wait for authentication if relay responds with auth-required", async () => {
259
+ // First event to trigger auth-required
260
+ const firstSpy = subscribeSpyTo(relay.event(mockEvent));
261
+ await expect(server).toReceiveMessage(["EVENT", mockEvent]);
262
+ // Send OK with auth-required message
263
+ server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
264
+ await firstSpy.onComplete();
265
+ // Create a second event that should wait for auth
266
+ const secondSpy = subscribeSpyTo(relay.event(mockEvent));
267
+ // Verify no EVENT message was sent yet (waiting for auth)
268
+ expect(server).not.toHaveReceivedMessages(["EVENT", mockEvent]);
269
+ // Simulate successful authentication
270
+ relay.authenticated$.next(true);
271
+ // Now the EVENT should be sent
272
+ await expect(server).toReceiveMessage(["EVENT", mockEvent]);
273
+ // Send OK response to complete the event
274
+ server.send(["OK", mockEvent.id, true, ""]);
275
+ // Verify the second event completed successfully
276
+ await secondSpy.onComplete();
277
+ expect(secondSpy.receivedComplete()).toBe(true);
278
+ });
279
+ it("should wait for authentication if relay info document has limitations.auth_required = true", async () => {
280
+ // Mock the fetchInformationDocument method to return a document with auth_required = true
281
+ vi.spyOn(Relay, "fetchInformationDocument").mockImplementation(() => of({
282
+ name: "Auth Required Relay",
283
+ description: "A relay that requires authentication",
284
+ pubkey: "",
285
+ contact: "",
286
+ supported_nips: [1, 2, 4],
287
+ software: "",
288
+ version: "",
289
+ limitation: {
290
+ auth_required: true,
291
+ },
292
+ }));
293
+ // Create a subscription that should wait for auth
294
+ const sub = subscribeSpyTo(relay.event(mockEvent));
295
+ // Wait 10ms to ensure the information document is fetched
296
+ await new Promise((resolve) => setTimeout(resolve, 10));
297
+ // Verify no REQ message was sent yet (waiting for auth)
298
+ expect(server).not.toHaveReceivedMessages(["EVENT", mockEvent]);
299
+ // Simulate successful authentication
300
+ relay.authenticated$.next(true);
301
+ // Now the REQ should be sent
302
+ await expect(server).toReceiveMessage(["EVENT", mockEvent]);
303
+ // Send EVENT and EOSE to complete the subscription
304
+ server.send(["OK", mockEvent.id, true, ""]);
305
+ // Verify the subscription completed
306
+ await sub.onComplete();
307
+ expect(sub.receivedComplete()).toBe(true);
308
+ });
309
+ it("should trigger connection to relay", async () => {
310
+ subscribeSpyTo(relay.event(mockEvent));
311
+ // Wait for connection
312
+ await firstValueFrom(relay.connected$.pipe(filter(Boolean)));
313
+ expect(relay.connected).toBe(true);
314
+ });
315
+ it("observable should complete when matching OK response received", async () => {
316
+ const spy = subscribeSpyTo(relay.event(mockEvent));
317
+ // Verify EVENT message was sent
318
+ expect(await server.nextMessage).toEqual(["EVENT", mockEvent]);
319
+ // Send matching OK response
320
+ server.send(["OK", mockEvent.id, true, ""]);
321
+ await spy.onComplete();
322
+ expect(spy.receivedComplete()).toBe(true);
323
+ });
324
+ it("should ignore OK responses for different events", async () => {
325
+ const spy = subscribeSpyTo(relay.event(mockEvent));
326
+ await server.connected;
327
+ // Send non-matching OK response
328
+ server.send(["OK", "different_id", true, ""]);
329
+ expect(spy.receivedComplete()).toBe(false);
330
+ // Send matching OK response
331
+ server.send(["OK", mockEvent.id, true, ""]);
332
+ expect(spy.receivedComplete()).toBe(true);
333
+ });
334
+ it("should send EVENT message to relay", async () => {
335
+ relay.event(mockEvent).subscribe();
336
+ expect(await server.nextMessage).toEqual(["EVENT", mockEvent]);
337
+ });
338
+ it("should complete with error if no OK received within 10s", async () => {
339
+ vi.useFakeTimers();
340
+ const spy = subscribeSpyTo(relay.event(mockEvent));
341
+ // Fast-forward time by 10 seconds
342
+ await vi.advanceTimersByTimeAsync(10000);
343
+ expect(spy.receivedComplete()).toBe(true);
344
+ expect(spy.getLastValue()).toEqual({ ok: false, from: "wss://test", message: "Timeout" });
345
+ });
346
+ it("should throw error if relay closes connection with error", async () => {
347
+ const spy = subscribeSpyTo(relay.event(mockEvent), { expectErrors: true });
348
+ await server.connected;
349
+ // Send CLOSE message with error
350
+ server.error({
351
+ reason: "error message",
352
+ code: 1000,
353
+ wasClean: false,
354
+ });
355
+ // Verify the subscription completed with an error
356
+ expect(spy.receivedError()).toBe(true);
357
+ });
358
+ it("should not throw a timeout error while waiting for the relay to be ready", async () => {
359
+ vi.useFakeTimers();
360
+ // @ts-expect-error
361
+ relay.ready$.next(false);
362
+ const spy = subscribeSpyTo(relay.event(mockEvent), { expectErrors: true });
363
+ // Fast-forward time by 20 seconds
364
+ await vi.advanceTimersByTimeAsync(20000);
365
+ expect(spy.receivedComplete()).toBe(false);
366
+ expect(spy.receivedError()).toBe(false);
367
+ });
368
+ it("should wait when relay isn't ready", async () => {
369
+ // @ts-expect-error
370
+ relay.ready$.next(false);
371
+ subscribeSpyTo(relay.event(mockEvent));
372
+ // Wait 10ms to ensure the relay didn't receive anything
373
+ await new Promise((resolve) => setTimeout(resolve, 10));
374
+ expect(server.messages.length).toBe(0);
375
+ // @ts-expect-error
376
+ relay.ready$.next(true);
377
+ await expect(server).toReceiveMessage(["EVENT", mockEvent]);
378
+ });
379
+ });
380
+ describe("notices$", () => {
381
+ it("should not trigger connection to relay", async () => {
382
+ subscribeSpyTo(relay.notices$);
383
+ expect(relay.connected).toBe(false);
384
+ });
385
+ it("should accumulate notices in notices$ state", async () => {
386
+ subscribeSpyTo(relay.req({ kinds: [1] }));
387
+ // Send multiple NOTICE messages
388
+ server.send(["NOTICE", "Notice 1"]);
389
+ server.send(["NOTICE", "Notice 2"]);
390
+ server.send(["NOTICE", "Notice 3"]);
391
+ // Verify the notices state contains all messages
392
+ expect(relay.notices$.value).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
393
+ });
394
+ it("should ignore non-NOTICE messages", async () => {
395
+ subscribeSpyTo(relay.req({ kinds: [1] }));
396
+ server.send(["NOTICE", "Important notice"]);
397
+ server.send(["OTHER", "other message"]);
398
+ // Verify only NOTICE messages are in the state
399
+ expect(relay.notices$.value).toEqual(["Important notice"]);
400
+ });
401
+ });
402
+ describe("notice$", () => {
403
+ it("should not trigger connection to relay", async () => {
404
+ subscribeSpyTo(relay.notice$);
405
+ await new Promise((resolve) => setTimeout(resolve, 10));
406
+ expect(relay.connected).toBe(false);
407
+ });
408
+ it("should emit NOTICE messages when they are received", async () => {
409
+ const spy = subscribeSpyTo(relay.notice$);
410
+ // Start connection
411
+ subscribeSpyTo(relay.req({ kinds: [1] }));
412
+ // Send multiple NOTICE messages
413
+ server.send(["NOTICE", "Notice 1"]);
414
+ server.send(["NOTICE", "Notice 2"]);
415
+ server.send(["NOTICE", "Notice 3"]);
416
+ // Verify the notices state contains all messages
417
+ expect(spy.getValues()).toEqual(["Notice 1", "Notice 2", "Notice 3"]);
418
+ });
419
+ it("should ignore non-NOTICE messages", async () => {
420
+ const spy = subscribeSpyTo(relay.notice$);
421
+ // Start connection
422
+ subscribeSpyTo(relay.req({ kinds: [1] }));
423
+ server.send(["NOTICE", "Important notice"]);
424
+ server.send(["OTHER", "other message"]);
425
+ // Verify only NOTICE messages are in the state
426
+ expect(spy.getValues()).toEqual(["Important notice"]);
427
+ });
428
+ });
429
+ describe("message$", () => {
430
+ it("should not trigger connection to relay", async () => {
431
+ subscribeSpyTo(relay.message$);
432
+ await new Promise((resolve) => setTimeout(resolve, 10));
433
+ expect(relay.connected).toBe(false);
434
+ });
435
+ it("should emit all messages when they are received", async () => {
436
+ const spy = subscribeSpyTo(relay.message$);
437
+ // Start connection
438
+ subscribeSpyTo(relay.req({ kinds: [1] }));
439
+ // Send multiple NOTICE messages
440
+ server.send(["NOTICE", "Notice 1"]);
441
+ server.send(["EVENT", "sub1", mockEvent]);
442
+ server.send(["EOSE", "sub1"]);
443
+ // Verify the notices state contains all messages
444
+ expect(spy.getValues()).toEqual([
445
+ ["NOTICE", "Notice 1"],
446
+ ["EVENT", "sub1", mockEvent],
447
+ ["EOSE", "sub1"],
448
+ ]);
449
+ });
450
+ });
451
+ describe("challenge$", () => {
452
+ it("should not trigger connection to relay", async () => {
453
+ subscribeSpyTo(relay.challenge$);
454
+ await new Promise((resolve) => setTimeout(resolve, 10));
455
+ expect(relay.connected).toBe(false);
456
+ });
457
+ it("should set challenge$ when AUTH message received", async () => {
458
+ subscribeSpyTo(relay.req({ kinds: [1] }));
459
+ // Send AUTH message with challenge string
460
+ server.send(["AUTH", "challenge-string-123"]);
461
+ // Verify challenge$ was set
462
+ expect(relay.challenge$.value).toBe("challenge-string-123");
463
+ });
464
+ it("should ignore non-AUTH messages", async () => {
465
+ subscribeSpyTo(relay.req({ kinds: [1] }));
466
+ server.send(["NOTICE", "Not a challenge"]);
467
+ server.send(["OTHER", "other message"]);
468
+ // Verify challenge$ remains null
469
+ expect(relay.challenge$.value).toBe(null);
470
+ });
471
+ });
472
+ describe("information$", () => {
473
+ it("should fetch information document when information$ is subscribed to", async () => {
474
+ // Mock the fetchInformationDocument method
475
+ const mockInfo = { ...defaultMockInfo, limitation: { auth_required: false } };
476
+ vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(of(mockInfo));
477
+ // Subscribe to information$
478
+ const sub = subscribeSpyTo(relay.information$);
479
+ // Verify fetchInformationDocument was called with the relay URL
480
+ expect(Relay.fetchInformationDocument).toHaveBeenCalledWith(relay.url);
481
+ // Verify the information was emitted
482
+ expect(sub.getLastValue()).toEqual(mockInfo);
483
+ });
484
+ it("should return null when fetchInformationDocument fails", async () => {
485
+ // Mock the fetchInformationDocument method to throw an error
486
+ vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(throwError(() => new Error("Failed to fetch")));
487
+ // Subscribe to information$
488
+ const sub = subscribeSpyTo(relay.information$);
489
+ // Verify fetchInformationDocument was called
490
+ expect(Relay.fetchInformationDocument).toHaveBeenCalled();
491
+ // Verify null was emitted
492
+ expect(sub.getLastValue()).toBeNull();
493
+ });
494
+ it("should cache the information document", async () => {
495
+ // Mock the fetchInformationDocument method
496
+ const mockInfo = { ...defaultMockInfo, limitation: { auth_required: true } };
497
+ vi.spyOn(Relay, "fetchInformationDocument").mockReturnValue(of(mockInfo));
498
+ // Subscribe to information$ multiple times
499
+ const sub1 = subscribeSpyTo(relay.information$);
500
+ const sub2 = subscribeSpyTo(relay.information$);
501
+ // Verify fetchInformationDocument was called only once
502
+ expect(Relay.fetchInformationDocument).toHaveBeenCalledTimes(1);
503
+ // Verify both subscriptions received the same information
504
+ expect(sub1.getLastValue()).toEqual(mockInfo);
505
+ expect(sub2.getLastValue()).toEqual(mockInfo);
506
+ // Verify the internal state was updated
507
+ expect(relay.information).toEqual(mockInfo);
508
+ });
509
+ });
510
+ describe("createReconnectTimer", () => {
511
+ it("should create a reconnect timer when relay closes with error", async () => {
512
+ const reconnectTimer = vi.fn().mockReturnValue(timer(1000));
513
+ vi.spyOn(Relay, "createReconnectTimer").mockReturnValue(reconnectTimer);
514
+ relay = new Relay("wss://test");
515
+ const spy = subscribeSpyTo(relay.req([{ kinds: [1] }]), { expectErrors: true });
516
+ // Send CLOSE message with error
517
+ server.error({
518
+ reason: "error message",
519
+ code: 1000,
520
+ wasClean: false,
521
+ });
522
+ // Verify the subscription errored
523
+ expect(spy.receivedError()).toBe(true);
524
+ expect(reconnectTimer).toHaveBeenCalledWith(expect.any(Error), 0);
525
+ });
526
+ it("should set ready$ to false until the reconnect timer completes", async () => {
527
+ vi.useFakeTimers();
528
+ const reconnectTimer = vi.fn().mockReturnValue(timer(1000));
529
+ vi.spyOn(Relay, "createReconnectTimer").mockReturnValue(reconnectTimer);
530
+ relay = new Relay("wss://test");
531
+ subscribeSpyTo(relay.req([{ kinds: [1] }]), { expectErrors: true });
532
+ // Send CLOSE message with error
533
+ server.error({
534
+ reason: "error message",
535
+ code: 1000,
536
+ wasClean: false,
537
+ });
538
+ // @ts-expect-error
539
+ expect(relay.ready$.value).toBe(false);
540
+ // Fast-forward time by 10ms
541
+ await vi.advanceTimersByTimeAsync(5000);
542
+ // @ts-expect-error
543
+ expect(relay.ready$.value).toBe(true);
544
+ });
545
+ });
546
+ describe("publish", () => {
547
+ it("should retry when auth-required is received and authentication is completed", async () => {
548
+ // First attempt to publish
549
+ const spy = subscribeSpyTo(relay.publish(mockEvent));
550
+ // Verify EVENT was sent
551
+ await expect(server).toReceiveMessage(["EVENT", mockEvent]);
552
+ // Send auth-required response
553
+ server.send(["AUTH", "challenge-string"]);
554
+ server.send(["OK", mockEvent.id, false, "auth-required: need to authenticate"]);
555
+ // Send auth event
556
+ const authEvent = { ...mockEvent, id: "auth-id" };
557
+ subscribeSpyTo(relay.auth(authEvent));
558
+ // Verify AUTH was sent
559
+ await expect(server).toReceiveMessage(["AUTH", authEvent]);
560
+ // Send successful auth response
561
+ server.send(["OK", authEvent.id, true, ""]);
562
+ // Wait for the event to be sent again
563
+ await expect(server).toReceiveMessage(["EVENT", mockEvent]);
564
+ // Send successful response for the retried event
565
+ server.send(["OK", mockEvent.id, true, ""]);
566
+ // Verify the final result is successful
567
+ expect(spy.getLastValue()).toEqual({ ok: true, message: "", from: "wss://test" });
568
+ });
569
+ it("should error after max retries", async () => {
570
+ const spy = subscribeSpyTo(relay.publish(mockEvent, { retries: 0 }), { expectErrors: true });
571
+ // Close with error
572
+ server.error({ reason: "error message", code: 1000, wasClean: false });
573
+ // Verify the subscription errored
574
+ expect(spy.receivedError()).toBe(true);
575
+ });
576
+ });
577
+ describe("request", () => {
578
+ it("should retry when auth-required is received and authentication is completed", async () => {
579
+ // First attempt to request
580
+ const spy = subscribeSpyTo(relay.request({ kinds: [1] }, { id: "sub1" }));
581
+ // Verify REQ was sent
582
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
583
+ // Send auth-required response
584
+ server.send(["AUTH", "challenge-string"]);
585
+ server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
586
+ // Wait for subscription to close
587
+ await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
588
+ // Send auth event
589
+ const authEvent = { ...mockEvent, id: "auth-id" };
590
+ const authSpy = subscribeSpyTo(relay.auth(authEvent));
591
+ // Verify AUTH was sent
592
+ await expect(server).toReceiveMessage(["AUTH", authEvent]);
593
+ server.send(["OK", authEvent.id, true, ""]);
594
+ // Wait for auth to complete
595
+ await authSpy.onComplete();
596
+ // Wait for retry
597
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
598
+ // Send response
599
+ server.send(["EVENT", "sub1", mockEvent]);
600
+ server.send(["EOSE", "sub1"]);
601
+ // Verify the final result is successful
602
+ expect(spy.getLastValue()).toEqual(expect.objectContaining(mockEvent));
603
+ expect(spy.receivedComplete()).toBe(true);
604
+ });
605
+ });
606
+ describe("subscription", () => {
607
+ it("should retry when auth-required is received and authentication is completed", async () => {
608
+ // First attempt to request
609
+ const spy = subscribeSpyTo(relay.subscription({ kinds: [1] }, { id: "sub1" }));
610
+ // Verify REQ was sent
611
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
612
+ // Send auth-required response
613
+ server.send(["AUTH", "challenge-string"]);
614
+ server.send(["CLOSED", "sub1", "auth-required: need to authenticate"]);
615
+ // Wait for subscription to close
616
+ await expect(server).toReceiveMessage(["CLOSE", "sub1"]);
617
+ // Send auth event
618
+ const authEvent = { ...mockEvent, id: "auth-id" };
619
+ const authSpy = subscribeSpyTo(relay.auth(authEvent));
620
+ // Verify AUTH was sent
621
+ await expect(server).toReceiveMessage(["AUTH", authEvent]);
622
+ server.send(["OK", authEvent.id, true, ""]);
623
+ // Wait for auth to complete
624
+ await authSpy.onComplete();
625
+ // Wait for retry
626
+ await expect(server).toReceiveMessage(["REQ", "sub1", { kinds: [1] }]);
627
+ // Send response
628
+ server.send(["EVENT", "sub1", mockEvent]);
629
+ server.send(["EOSE", "sub1"]);
630
+ // Verify the final result is successful
631
+ expect(spy.getValues()).toEqual([expect.objectContaining(mockEvent), "EOSE"]);
632
+ expect(spy.receivedComplete()).toBe(false);
633
+ });
634
+ });
635
+ // describe("keepAlive", () => {
636
+ // it("should close the socket connection after keepAlive timeout", async () => {
637
+ // vi.useFakeTimers();
638
+ // // Set a short keepAlive timeout for testing
639
+ // relay.keepAlive = 100; // 100ms for quick testing
640
+ // // Subscribe to the relay to ensure it is active
641
+ // const sub = subscribeSpyTo(relay.req([{ kinds: [1] }]));
642
+ // // Wait for connection
643
+ // await server.connected;
644
+ // // Close the subscription
645
+ // sub.unsubscribe();
646
+ // // Fast-forward time by 10ms
647
+ // await vi.advanceTimersByTimeAsync(10);
648
+ // // should still be connected
649
+ // expect(relay.connected).toBe(true);
650
+ // // Wait for the keepAlive timeout to elapse
651
+ // await vi.advanceTimersByTimeAsync(150);
652
+ // expect(relay.connected).toBe(false);
653
+ // });
654
+ // });
@@ -0,0 +1,7 @@
1
+ import Nostr from "nostr-typedef";
2
+ export interface ClientSpy {
3
+ __createClientSpy: true;
4
+ next: () => Promise<Nostr.ToClientMessage.Any>;
5
+ dispose: () => void;
6
+ }
7
+ export declare function createClientSpy(addOnMessageListener: (listener: (message: Nostr.ToClientMessage.Any) => void) => void): ClientSpy;