@bsv/sdk 2.0.3 → 2.0.4

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 (31) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/Peer.js +35 -33
  3. package/dist/cjs/src/auth/Peer.js.map +1 -1
  4. package/dist/cjs/src/auth/clients/AuthFetch.js +31 -5
  5. package/dist/cjs/src/auth/clients/AuthFetch.js.map +1 -1
  6. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.test.js +77 -0
  7. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.test.js.map +1 -1
  8. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js +4 -1
  9. package/dist/cjs/src/auth/transports/SimplifiedFetchTransport.js.map +1 -1
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/src/auth/Peer.js +35 -33
  12. package/dist/esm/src/auth/Peer.js.map +1 -1
  13. package/dist/esm/src/auth/clients/AuthFetch.js +31 -5
  14. package/dist/esm/src/auth/clients/AuthFetch.js.map +1 -1
  15. package/dist/esm/src/auth/clients/__tests__/AuthFetch.test.js +77 -0
  16. package/dist/esm/src/auth/clients/__tests__/AuthFetch.test.js.map +1 -1
  17. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js +4 -1
  18. package/dist/esm/src/auth/transports/SimplifiedFetchTransport.js.map +1 -1
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/src/auth/Peer.d.ts.map +1 -1
  21. package/dist/types/src/auth/clients/AuthFetch.d.ts.map +1 -1
  22. package/dist/types/src/auth/transports/SimplifiedFetchTransport.d.ts.map +1 -1
  23. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  24. package/dist/umd/bundle.js +1 -1
  25. package/dist/umd/bundle.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/auth/Peer.ts +49 -47
  28. package/src/auth/__tests/Peer.test.ts +35 -30
  29. package/src/auth/clients/AuthFetch.ts +46 -18
  30. package/src/auth/clients/__tests__/AuthFetch.test.ts +97 -0
  31. package/src/auth/transports/SimplifiedFetchTransport.ts +24 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
package/src/auth/Peer.ts CHANGED
@@ -136,7 +136,7 @@ export class Peer {
136
136
  }
137
137
 
138
138
  if (peerSession.certificatesRequired === true &&
139
- peerSession.certificatesValidated !== true) {
139
+ peerSession.certificatesValidated !== true) {
140
140
  throw new Error(
141
141
  'Cannot send general message before certificate validation is complete'
142
142
  )
@@ -450,33 +450,28 @@ export class Peer {
450
450
  )
451
451
  }
452
452
 
453
- try {
454
- switch (message.messageType) {
455
- case 'initialRequest':
456
- await this.processInitialRequest(message)
457
- break
458
- case 'initialResponse':
459
- await this.processInitialResponse(message)
460
- break
461
- case 'certificateRequest':
462
- await this.processCertificateRequest(message)
463
- break
464
- case 'certificateResponse':
465
- await this.processCertificateResponse(message)
466
- break
467
- case 'general':
468
- await this.processGeneralMessage(message)
469
- break
470
- default:
471
- throw new Error(
472
- `Unknown message type of ${String(message.messageType)} from ${String(
473
- message.identityKey
474
- )}`
475
- )
476
- }
477
- } catch (err) {
478
- // Swallow protocol violations so transport does not crash the process
479
- // (Message is intentionally rejected)
453
+ switch (message.messageType) {
454
+ case 'initialRequest':
455
+ await this.processInitialRequest(message)
456
+ break
457
+ case 'initialResponse':
458
+ await this.processInitialResponse(message)
459
+ break
460
+ case 'certificateRequest':
461
+ await this.processCertificateRequest(message)
462
+ break
463
+ case 'certificateResponse':
464
+ await this.processCertificateResponse(message)
465
+ break
466
+ case 'general':
467
+ await this.processGeneralMessage(message)
468
+ break
469
+ default:
470
+ throw new Error(
471
+ `Unknown message type of ${String(message.messageType)} from ${String(
472
+ message.identityKey
473
+ )}`
474
+ )
480
475
  }
481
476
  }
482
477
 
@@ -675,10 +670,16 @@ export class Peer {
675
670
  message.identityKey,
676
671
  this.originator
677
672
  )
678
- await this.sendCertificateResponse(
679
- message.identityKey,
680
- verifiableCertificates
681
- )
673
+ // Only send if we actually have certificates to provide.
674
+ // An empty certificateResponse has no value and can race with a
675
+ // subsequent certificateRequest that shares the same initialNonce,
676
+ // causing the server to mis-route non-general handle responses.
677
+ if (verifiableCertificates.length > 0) {
678
+ await this.sendCertificateResponse(
679
+ message.identityKey,
680
+ verifiableCertificates
681
+ )
682
+ }
682
683
  }
683
684
  }
684
685
  }
@@ -818,21 +819,23 @@ export class Peer {
818
819
  )
819
820
  }
820
821
 
821
- // We also handle optional validation if there's a requestedCertificates field
822
- await validateCertificates(
823
- this.wallet,
824
- message,
825
- message.requestedCertificates,
826
- this.originator
827
- )
822
+ // Validate certificates only if they were actually provided
823
+ if (Array.isArray(message.certificates) && message.certificates.length > 0) {
824
+ await validateCertificates(
825
+ this.wallet,
826
+ message,
827
+ message.requestedCertificates,
828
+ this.originator
829
+ )
828
830
 
829
- peerSession.certificatesValidated = true
830
- peerSession.lastUpdate = Date.now()
831
- this.sessionManager.updateSession(peerSession)
831
+ peerSession.certificatesValidated = true
832
+ peerSession.lastUpdate = Date.now()
833
+ this.sessionManager.updateSession(peerSession)
832
834
 
833
- // Resolve any promises waiting for certificate validation
834
- if (peerSession.sessionNonce != null) {
835
- this.resolveCertificateValidation(peerSession.sessionNonce)
835
+ // Resolve any promises waiting for certificate validation
836
+ if (peerSession.sessionNonce != null) {
837
+ this.resolveCertificateValidation(peerSession.sessionNonce)
838
+ }
836
839
  }
837
840
 
838
841
  // Notify any listeners
@@ -886,8 +889,7 @@ export class Peer {
886
889
  if (promise != null) {
887
890
  this.certificateValidationPromises.delete(sessionNonce)
888
891
  reject(new Error(
889
- `Timeout waiting for certificate validation from peer ${
890
- peerSession.peerIdentityKey ?? 'unknown'
892
+ `Timeout waiting for certificate validation from peer ${peerSession.peerIdentityKey ?? 'unknown'
891
893
  }`
892
894
  ))
893
895
  }
@@ -40,13 +40,18 @@ class LocalTransport implements Transport {
40
40
  }
41
41
 
42
42
  async onData(
43
- callback: (message: AuthMessage) => void
43
+ callback: (message: AuthMessage) => Promise<void>
44
44
  ): Promise<void> {
45
- this.onDataCallback = callback
45
+ this.onDataCallback = (m) => {
46
+ void (callback(m) as Promise<void>).catch(() => {
47
+ // Match real transport behaviour: catch errors from handleIncomingMessage
48
+ // to prevent unhandled promise rejections in tests.
49
+ })
50
+ }
46
51
  }
47
52
  }
48
53
 
49
- function waitForNextGeneralMessage (
54
+ function waitForNextGeneralMessage(
50
55
  peer: Peer,
51
56
  handler?: (senderPublicKey: string, payload: number[]) => void
52
57
  ): Promise<void> {
@@ -385,8 +390,8 @@ describe('Peer class mutual authentication and certificate exchange', () => {
385
390
  describe('propagateTransportError', () => {
386
391
  const createPeerInstance = (): Peer => {
387
392
  const transport: Transport = {
388
- send: jest.fn(async (_message: AuthMessage) => {}),
389
- onData: async () => {}
393
+ send: jest.fn(async (_message: AuthMessage) => { }),
394
+ onData: async () => { }
390
395
  }
391
396
  return new Peer({} as WalletInterface, transport)
392
397
  }
@@ -409,7 +414,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
409
414
  it('preserves existing details when appending peer identity', () => {
410
415
  const peer = createPeerInstance()
411
416
  const originalError = new Error('existing details')
412
- ;(originalError as any).details = { status: 503 }
417
+ ; (originalError as any).details = { status: 503 }
413
418
 
414
419
  let thrown: Error | undefined
415
420
  try {
@@ -745,14 +750,14 @@ describe('Peer class mutual authentication and certificate exchange', () => {
745
750
  test('Should trigger "Failed to send message to peer" error with network failure', async () => {
746
751
  // Create a mock fetch that always fails
747
752
  const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Network connection failed'))
748
-
753
+
749
754
  // Create a transport that will fail
750
755
  const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
751
-
756
+
752
757
  // Create a peer with the failing transport
753
758
  const wallet = new CompletedProtoWallet(privKey)
754
759
  const peer = new Peer(wallet, transport)
755
-
760
+
756
761
  // Register a dummy onData callback (required before sending)
757
762
  await transport.onData(async (message) => {
758
763
  // This won't be called due to network failure
@@ -777,12 +782,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
777
782
  }, 100)
778
783
  })
779
784
  })
780
-
785
+
781
786
  const transport = new SimplifiedFetchTransport('http://localhost:9999', timeoutFetch)
782
787
  const wallet = new CompletedProtoWallet(privKey)
783
788
  const peer = new Peer(wallet, transport)
784
-
785
- await transport.onData(async (message) => {})
789
+
790
+ await transport.onData(async (message) => { })
786
791
 
787
792
  try {
788
793
  await peer.toPeer([5, 6, 7, 8], '03def789abc123')
@@ -800,12 +805,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
800
805
  errno: -3008,
801
806
  message: 'getaddrinfo ENOTFOUND nonexistent.domain'
802
807
  })
803
-
808
+
804
809
  const transport = new SimplifiedFetchTransport('http://nonexistent.domain:3000', dnsFetch)
805
810
  const wallet = new CompletedProtoWallet(privKey)
806
811
  const peer = new Peer(wallet, transport)
807
-
808
- await transport.onData(async (message) => {})
812
+
813
+ await transport.onData(async (message) => { })
809
814
 
810
815
  try {
811
816
  await peer.toPeer([9, 10, 11, 12], '03xyz987fed654')
@@ -819,12 +824,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
819
824
  test('Should trigger error during certificate request send', async () => {
820
825
  // Create a failing fetch
821
826
  const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Connection reset by peer'))
822
-
827
+
823
828
  const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
824
829
  const wallet = new CompletedProtoWallet(privKey)
825
830
  const peer = new Peer(wallet, transport)
826
-
827
- await transport.onData(async (message) => {})
831
+
832
+ await transport.onData(async (message) => { })
828
833
 
829
834
  try {
830
835
  // Try to send a certificate request - this should also trigger the error
@@ -842,12 +847,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
842
847
  test('Should trigger error during certificate response send', async () => {
843
848
  // Create a failing fetch
844
849
  const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Socket hang up'))
845
-
850
+
846
851
  const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
847
852
  const wallet = new CompletedProtoWallet(privKey)
848
853
  const peer = new Peer(wallet, transport)
849
-
850
- await transport.onData(async (message) => {})
854
+
855
+ await transport.onData(async (message) => { })
851
856
 
852
857
  try {
853
858
  // Try to send a certificate response - this should also trigger the error
@@ -864,12 +869,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
864
869
  const customError = new Error('Custom transport error')
865
870
  customError.stack = 'Custom stack trace'
866
871
  const failingFetch = (jest.fn() as any).mockRejectedValue(customError)
867
-
872
+
868
873
  const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
869
874
  const wallet = new CompletedProtoWallet(privKey)
870
875
  const peer = new Peer(wallet, transport)
871
-
872
- await transport.onData(async (message) => {})
876
+
877
+ await transport.onData(async (message) => { })
873
878
 
874
879
  try {
875
880
  await peer.toPeer([13, 14, 15, 16], '03peer123456')
@@ -886,12 +891,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
886
891
  test('Should handle non-Error transport failures', async () => {
887
892
  // Create a fetch that throws a non-Error object
888
893
  const failingFetch = (jest.fn() as any).mockRejectedValue('String error message')
889
-
894
+
890
895
  const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
891
896
  const wallet = new CompletedProtoWallet(privKey)
892
897
  const peer = new Peer(wallet, transport)
893
-
894
- await transport.onData(async (message) => {})
898
+
899
+ await transport.onData(async (message) => { })
895
900
 
896
901
  try {
897
902
  await peer.toPeer([17, 18, 19, 20], '03peer789abc')
@@ -906,12 +911,12 @@ describe('Peer class mutual authentication and certificate exchange', () => {
906
911
  test('Should handle undefined peer identity gracefully', async () => {
907
912
  // Create a failing fetch
908
913
  const failingFetch = (jest.fn() as any).mockRejectedValue('Network failure')
909
-
914
+
910
915
  const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
911
916
  const wallet = new CompletedProtoWallet(privKey)
912
917
  const peer = new Peer(wallet, transport)
913
-
914
- await transport.onData(async (message) => {})
918
+
919
+ await transport.onData(async (message) => { })
915
920
 
916
921
  try {
917
922
  // Try to send to an undefined peer (this might happen in some edge cases)
@@ -137,7 +137,9 @@ export class AuthFetch {
137
137
  verifier,
138
138
  this.originator
139
139
  )
140
- await this.peers[baseURL].peer.sendCertificateResponse(verifier, certificatesToInclude)
140
+ if (certificatesToInclude.length > 0) {
141
+ await this.peers[baseURL].peer.sendCertificateResponse(verifier, certificatesToInclude)
142
+ }
141
143
  } finally {
142
144
  // Give the backend 500 ms to process the certificates we just sent, before releasing the queue entry
143
145
  await new Promise(resolve => setTimeout(resolve, 500))
@@ -263,7 +265,15 @@ export class AuthFetch {
263
265
 
264
266
  // Send the request, now that all listeners are set up
265
267
  await peerToUse.peer.toPeer(writer.toArray(), peerToUse.identityKey).catch(async error => {
266
- if (error.message.includes('Session not found for nonce')) {
268
+ const isStaleSession =
269
+ error.message.includes('Session not found for nonce') ||
270
+ (error.message.includes('without valid BSV authentication') &&
271
+ peerToUse.identityKey != null &&
272
+ (error as any).details?.status === 401)
273
+ if (isStaleSession) {
274
+ // Stale session: server no longer recognises the session nonce
275
+ // (e.g. after a server restart). Clear the cached peer so a fresh
276
+ // handshake is performed on retry.
267
277
  delete this.peers[baseURL]
268
278
  config.retryCounter ??= 3
269
279
  const response = await this.fetch(url, config)
@@ -322,20 +332,38 @@ export class AuthFetch {
322
332
  }
323
333
 
324
334
  // Return a promise that resolves when certificates are received
335
+ const CERTIFICATE_REQUEST_TIMEOUT_MS = 30000
325
336
  return await new Promise<VerifiableCertificate[]>((async (resolve, reject) => {
337
+ let settled = false
338
+
339
+ const cleanup = (): void => {
340
+ settled = true
341
+ clearTimeout(timer)
342
+ peerToUse.peer.stopListeningForCertificatesReceived(callbackId)
343
+ }
344
+
326
345
  // Set up the listener before making the request
327
346
  const callbackId = peerToUse.peer.listenForCertificatesReceived((_senderPublicKey: string, certs: VerifiableCertificate[]) => {
328
- peerToUse.peer.stopListeningForCertificatesReceived(callbackId)
347
+ if (settled) return
348
+ cleanup()
329
349
  this.certificatesReceived.push(...certs)
330
350
  resolve(certs)
331
351
  })
332
352
 
353
+ const timer = setTimeout(() => {
354
+ if (settled) return
355
+ cleanup()
356
+ reject(new Error(`sendCertificateRequest timed out after ${CERTIFICATE_REQUEST_TIMEOUT_MS}ms waiting for certificate response from ${baseURL}`))
357
+ }, CERTIFICATE_REQUEST_TIMEOUT_MS)
358
+
333
359
  try {
334
360
  // Initiate the certificate request
335
361
  await peerToUse.peer.requestCertificates(certificatesToRequest, peerToUse.identityKey)
336
362
  } catch (err) {
337
- peerToUse.peer.stopListeningForCertificatesReceived(callbackId)
338
- reject(err)
363
+ if (!settled) {
364
+ cleanup()
365
+ reject(err)
366
+ }
339
367
  }
340
368
  }) as Function)
341
369
  }
@@ -594,7 +622,7 @@ export class AuthFetch {
594
622
  }
595
623
  }
596
624
 
597
- private isPaymentContextCompatible (
625
+ private isPaymentContextCompatible(
598
626
  context: PaymentRetryContext,
599
627
  satoshisRequired: number,
600
628
  serverIdentityKey: string,
@@ -607,7 +635,7 @@ export class AuthFetch {
607
635
  )
608
636
  }
609
637
 
610
- private async createPaymentContext (
638
+ private async createPaymentContext(
611
639
  url: string,
612
640
  config: SimplifiedFetchRequestOptions,
613
641
  satoshisRequired: number,
@@ -652,7 +680,7 @@ export class AuthFetch {
652
680
  }
653
681
  }
654
682
 
655
- private getMaxPaymentAttempts (config: SimplifiedFetchRequestOptions): number {
683
+ private getMaxPaymentAttempts(config: SimplifiedFetchRequestOptions): number {
656
684
  const attempts = typeof config.paymentRetryAttempts === 'number' ? config.paymentRetryAttempts : undefined
657
685
  if (typeof attempts === 'number' && attempts > 0) {
658
686
  return Math.floor(attempts)
@@ -660,7 +688,7 @@ export class AuthFetch {
660
688
  return 3
661
689
  }
662
690
 
663
- private buildPaymentRequestSummary (
691
+ private buildPaymentRequestSummary(
664
692
  url: string,
665
693
  config: SimplifiedFetchRequestOptions
666
694
  ): PaymentRetryContext['requestSummary'] {
@@ -677,7 +705,7 @@ export class AuthFetch {
677
705
  }
678
706
  }
679
707
 
680
- private describeRequestBodyForLogging (body: any): { type: string, byteLength: number } {
708
+ private describeRequestBodyForLogging(body: any): { type: string, byteLength: number } {
681
709
  if (body == null) {
682
710
  return { type: 'none', byteLength: 0 }
683
711
  }
@@ -733,7 +761,7 @@ export class AuthFetch {
733
761
  return { type: typeof body, byteLength: 0 }
734
762
  }
735
763
 
736
- private composePaymentLogDetails (url: string, context: PaymentRetryContext): Record<string, any> {
764
+ private composePaymentLogDetails(url: string, context: PaymentRetryContext): Record<string, any> {
737
765
  return {
738
766
  url,
739
767
  request: context.requestSummary,
@@ -753,7 +781,7 @@ export class AuthFetch {
753
781
  }
754
782
  }
755
783
 
756
- private logPaymentAttempt (
784
+ private logPaymentAttempt(
757
785
  level: 'info' | 'warn' | 'error',
758
786
  message: string,
759
787
  details: Record<string, any>
@@ -772,7 +800,7 @@ export class AuthFetch {
772
800
  }
773
801
  }
774
802
 
775
- private createPaymentErrorEntry (attempt: number, error: unknown): PaymentErrorLogEntry {
803
+ private createPaymentErrorEntry(attempt: number, error: unknown): PaymentErrorLogEntry {
776
804
  const entry: PaymentErrorLogEntry = {
777
805
  attempt,
778
806
  timestamp: new Date().toISOString(),
@@ -790,20 +818,20 @@ export class AuthFetch {
790
818
  return entry
791
819
  }
792
820
 
793
- private getPaymentRetryDelay (attempt: number): number {
821
+ private getPaymentRetryDelay(attempt: number): number {
794
822
  const baseDelay = 250
795
823
  const multiplier = Math.min(attempt, 5)
796
824
  return baseDelay * multiplier
797
825
  }
798
826
 
799
- private async wait (ms: number): Promise<void> {
827
+ private async wait(ms: number): Promise<void> {
800
828
  if (ms <= 0) {
801
829
  return
802
830
  }
803
831
  await new Promise(resolve => setTimeout(resolve, ms))
804
832
  }
805
833
 
806
- private buildPaymentFailureError (
834
+ private buildPaymentFailureError(
807
835
  url: string,
808
836
  context: PaymentRetryContext,
809
837
  lastError: unknown
@@ -828,10 +856,10 @@ export class AuthFetch {
828
856
  errors: context.errors
829
857
  }
830
858
 
831
- ;(error as any).details = failureDetails
859
+ ; (error as any).details = failureDetails
832
860
 
833
861
  if (lastError instanceof Error) {
834
- ;(error as any).cause = lastError
862
+ ; (error as any).cause = lastError
835
863
  }
836
864
 
837
865
  return error
@@ -260,3 +260,100 @@ describe('AuthFetch payment handling', () => {
260
260
  expect(paymentContext.errors).toHaveLength(2)
261
261
  })
262
262
  })
263
+
264
+ describe('AuthFetch certificate request handling', () => {
265
+ const requestedCertificates = {
266
+ certifiers: ['certifier-key'],
267
+ types: { 'certificate-type': ['firstName'] }
268
+ }
269
+
270
+ test('sendCertificateRequest resolves and cleans up listener when certificates are received', async () => {
271
+ const wallet = createWalletStub()
272
+ const authFetch = new AuthFetch(wallet as any)
273
+
274
+ let certificatesListener: ((senderPublicKey: string, certs: any[]) => void) | undefined
275
+ const stopListeningForCertificatesReceived = jest.fn()
276
+ const requestCertificates = jest.fn(async () => {
277
+ certificatesListener?.('server-identity', [{ serialNumber: 'abc123' } as any])
278
+ })
279
+
280
+ const peerStub = {
281
+ listenForCertificatesReceived: jest.fn((listener: (senderPublicKey: string, certs: any[]) => void) => {
282
+ certificatesListener = listener
283
+ return 7
284
+ }),
285
+ stopListeningForCertificatesReceived,
286
+ requestCertificates
287
+ }
288
+
289
+ ; (authFetch as any).peers['https://api.example.com'] = {
290
+ peer: peerStub,
291
+ pendingCertificateRequests: []
292
+ }
293
+
294
+ const certs = await authFetch.sendCertificateRequest('https://api.example.com', requestedCertificates as any)
295
+ expect(certs).toHaveLength(1)
296
+ expect(requestCertificates).toHaveBeenCalledWith(requestedCertificates, undefined)
297
+ expect(stopListeningForCertificatesReceived).toHaveBeenCalledWith(7)
298
+ })
299
+
300
+ test('sendCertificateRequest rejects and cleans up listener when requestCertificates throws', async () => {
301
+ const wallet = createWalletStub()
302
+ const authFetch = new AuthFetch(wallet as any)
303
+ const requestError = new Error('request failed')
304
+
305
+ const stopListeningForCertificatesReceived = jest.fn()
306
+ const requestCertificates = jest.fn(async () => {
307
+ throw requestError
308
+ })
309
+
310
+ const peerStub = {
311
+ listenForCertificatesReceived: jest.fn(() => 9),
312
+ stopListeningForCertificatesReceived,
313
+ requestCertificates
314
+ }
315
+
316
+ ; (authFetch as any).peers['https://api.example.com'] = {
317
+ peer: peerStub,
318
+ pendingCertificateRequests: []
319
+ }
320
+
321
+ await expect(
322
+ authFetch.sendCertificateRequest('https://api.example.com', requestedCertificates as any)
323
+ ).rejects.toThrow('request failed')
324
+ expect(stopListeningForCertificatesReceived).toHaveBeenCalledWith(9)
325
+ })
326
+
327
+ test('sendCertificateRequest times out and cleans up listener when no certificate response arrives', async () => {
328
+ jest.useFakeTimers()
329
+ try {
330
+ const wallet = createWalletStub()
331
+ const authFetch = new AuthFetch(wallet as any)
332
+
333
+ const stopListeningForCertificatesReceived = jest.fn()
334
+ const requestCertificates = jest.fn(async () => {})
335
+
336
+ const peerStub = {
337
+ listenForCertificatesReceived: jest.fn(() => 11),
338
+ stopListeningForCertificatesReceived,
339
+ requestCertificates
340
+ }
341
+
342
+ ; (authFetch as any).peers['https://api.example.com'] = {
343
+ peer: peerStub,
344
+ pendingCertificateRequests: []
345
+ }
346
+
347
+ const pending = authFetch.sendCertificateRequest('https://api.example.com', requestedCertificates as any)
348
+ const timeoutAssertion = expect(pending).rejects.toThrow(
349
+ 'sendCertificateRequest timed out after 30000ms waiting for certificate response from https://api.example.com'
350
+ )
351
+ await jest.advanceTimersByTimeAsync(30000)
352
+
353
+ await timeoutAssertion
354
+ expect(stopListeningForCertificatesReceived).toHaveBeenCalledWith(11)
355
+ } finally {
356
+ jest.useRealTimers()
357
+ }
358
+ })
359
+ })