@@ -401,3 +401,234 @@ describe("ResendTransport - Config Validation", () => {
401401 assert . equal ( transport . config . retries , 5 ) ;
402402 } ) ;
403403} ) ;
404+
405+ // Note: Idempotency key tests are split into separate describe blocks
406+ // because Deno runs tests within the same describe block concurrently,
407+ // which causes globalThis.fetch mocking to interfere between tests.
408+
409+ describe ( "ResendTransport - Idempotency Key Provided" , ( ) => {
410+ it ( "should use provided idempotency key in HTTP header" , async ( ) => {
411+ const originalFetch = globalThis . fetch ;
412+ const captured : { headers : Headers | null } = { headers : null } ;
413+
414+ try {
415+ // Mock fetch to capture headers
416+ globalThis . fetch = ( _url , init ) => {
417+ captured . headers = new Headers ( init ?. headers ) ;
418+ return Promise . resolve (
419+ new Response (
420+ JSON . stringify ( { id : "test-message-id" } ) ,
421+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
422+ ) ,
423+ ) ;
424+ } ;
425+
426+ const transport = new ResendTransport ( { apiKey : "test-key" } ) ;
427+
428+ const testKey = `test-key-${ Date . now ( ) } ` ;
429+ const message : Message = {
430+ sender : { address : "sender@example.com" } ,
431+ recipients : [ { address : "recipient@example.com" } ] ,
432+ ccRecipients : [ ] ,
433+ bccRecipients : [ ] ,
434+ replyRecipients : [ ] ,
435+ subject : "Test Subject" ,
436+ content : { text : "Test content" } ,
437+ attachments : [ ] ,
438+ priority : "normal" ,
439+ tags : [ ] ,
440+ headers : new Headers ( ) ,
441+ idempotencyKey : testKey ,
442+ } ;
443+
444+ await transport . send ( message ) ;
445+
446+ assert . ok ( captured . headers !== null ) ;
447+ assert . equal ( captured . headers . get ( "Idempotency-Key" ) , testKey ) ;
448+ } finally {
449+ globalThis . fetch = originalFetch ;
450+ }
451+ } ) ;
452+ } ) ;
453+
454+ describe ( "ResendTransport - Idempotency Key Auto-generated" , ( ) => {
455+ it ( "should generate idempotency key when not provided" , async ( ) => {
456+ const originalFetch = globalThis . fetch ;
457+ const captured : { headers : Headers | null } = { headers : null } ;
458+
459+ try {
460+ globalThis . fetch = ( _url , init ) => {
461+ captured . headers = new Headers ( init ?. headers ) ;
462+ return Promise . resolve (
463+ new Response (
464+ JSON . stringify ( { id : "test-message-id" } ) ,
465+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
466+ ) ,
467+ ) ;
468+ } ;
469+
470+ const transport = new ResendTransport ( { apiKey : "test-key" } ) ;
471+
472+ const message : Message = {
473+ sender : { address : "sender@example.com" } ,
474+ recipients : [ { address : "recipient@example.com" } ] ,
475+ ccRecipients : [ ] ,
476+ bccRecipients : [ ] ,
477+ replyRecipients : [ ] ,
478+ subject : "Test Subject" ,
479+ content : { text : "Test content" } ,
480+ attachments : [ ] ,
481+ priority : "normal" ,
482+ tags : [ ] ,
483+ headers : new Headers ( ) ,
484+ } ;
485+
486+ await transport . send ( message ) ;
487+
488+ assert . ok ( captured . headers !== null ) ;
489+ const idempotencyKey = captured . headers . get ( "Idempotency-Key" ) ;
490+ assert . ok ( idempotencyKey , "Idempotency-Key header should be present" ) ;
491+ assert . ok (
492+ idempotencyKey . length > 10 ,
493+ "Auto-generated key should have reasonable length" ,
494+ ) ;
495+ } finally {
496+ globalThis . fetch = originalFetch ;
497+ }
498+ } ) ;
499+ } ) ;
500+
501+ describe ( "ResendTransport - Idempotency Key Retry" , ( ) => {
502+ it ( "should allow retry with same idempotency key" , async ( ) => {
503+ const originalFetch = globalThis . fetch ;
504+ const capturedKeys : string [ ] = [ ] ;
505+
506+ try {
507+ let callCount = 0 ;
508+ globalThis . fetch = ( _url , init ) => {
509+ const headers = new Headers ( init ?. headers ) ;
510+ const key = headers . get ( "Idempotency-Key" ) ;
511+ if ( key ) capturedKeys . push ( key ) ;
512+
513+ callCount ++ ;
514+ if ( callCount === 1 ) {
515+ // First call fails
516+ return Promise . resolve (
517+ new Response (
518+ JSON . stringify ( { message : "Server error" } ) ,
519+ { status : 500 , headers : { "Content-Type" : "application/json" } } ,
520+ ) ,
521+ ) ;
522+ }
523+ // Second call succeeds
524+ return Promise . resolve (
525+ new Response (
526+ JSON . stringify ( { id : "test-message-id" } ) ,
527+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
528+ ) ,
529+ ) ;
530+ } ;
531+
532+ const transport = new ResendTransport ( { apiKey : "test-key" , retries : 0 } ) ;
533+
534+ const idempotencyKey = `retry-key-${ Date . now ( ) } ` ;
535+ const message : Message = {
536+ sender : { address : "sender@example.com" } ,
537+ recipients : [ { address : "recipient@example.com" } ] ,
538+ ccRecipients : [ ] ,
539+ bccRecipients : [ ] ,
540+ replyRecipients : [ ] ,
541+ subject : "Test Subject" ,
542+ content : { text : "Test content" } ,
543+ attachments : [ ] ,
544+ priority : "normal" ,
545+ tags : [ ] ,
546+ headers : new Headers ( ) ,
547+ idempotencyKey,
548+ } ;
549+
550+ // First attempt fails
551+ const receipt1 = await transport . send ( message ) ;
552+ assert . equal ( receipt1 . successful , false ) ;
553+
554+ // Retry with same message (same idempotency key)
555+ const receipt2 = await transport . send ( message ) ;
556+ assert . equal ( receipt2 . successful , true ) ;
557+
558+ // Both calls should use the same key
559+ assert . equal ( capturedKeys . length , 2 ) ;
560+ assert . equal ( capturedKeys [ 0 ] , idempotencyKey ) ;
561+ assert . equal ( capturedKeys [ 1 ] , idempotencyKey ) ;
562+ } finally {
563+ globalThis . fetch = originalFetch ;
564+ }
565+ } ) ;
566+ } ) ;
567+
568+ describe ( "ResendTransport - Idempotency Key Batch" , ( ) => {
569+ it ( "should use provided idempotency key in batch API" , async ( ) => {
570+ const originalFetch = globalThis . fetch ;
571+ const captured : { headers : Headers | null } = { headers : null } ;
572+
573+ try {
574+ globalThis . fetch = ( url , init ) => {
575+ if ( typeof url === "string" && url . includes ( "/emails/batch" ) ) {
576+ captured . headers = new Headers ( init ?. headers ) ;
577+ return Promise . resolve (
578+ new Response (
579+ JSON . stringify ( {
580+ data : [ { id : "message-1" } , { id : "message-2" } ] ,
581+ } ) ,
582+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
583+ ) ,
584+ ) ;
585+ }
586+ return Promise . reject ( new Error ( "Unexpected URL called" ) ) ;
587+ } ;
588+
589+ const transport = new ResendTransport ( { apiKey : "test-key" } ) ;
590+
591+ const batchKey = `batch-key-${ Date . now ( ) } ` ;
592+ const messages : Message [ ] = [
593+ {
594+ sender : { address : "sender@example.com" } ,
595+ recipients : [ { address : "recipient1@example.com" } ] ,
596+ ccRecipients : [ ] ,
597+ bccRecipients : [ ] ,
598+ replyRecipients : [ ] ,
599+ subject : "Test Subject 1" ,
600+ content : { text : "Test content 1" } ,
601+ attachments : [ ] ,
602+ priority : "normal" ,
603+ tags : [ ] ,
604+ headers : new Headers ( ) ,
605+ idempotencyKey : batchKey , // First message's key is used for batch
606+ } ,
607+ {
608+ sender : { address : "sender@example.com" } ,
609+ recipients : [ { address : "recipient2@example.com" } ] ,
610+ ccRecipients : [ ] ,
611+ bccRecipients : [ ] ,
612+ replyRecipients : [ ] ,
613+ subject : "Test Subject 2" ,
614+ content : { text : "Test content 2" } ,
615+ attachments : [ ] ,
616+ priority : "normal" ,
617+ tags : [ ] ,
618+ headers : new Headers ( ) ,
619+ } ,
620+ ] ;
621+
622+ const receipts = [ ] ;
623+ for await ( const receipt of transport . sendMany ( messages ) ) {
624+ receipts . push ( receipt ) ;
625+ }
626+
627+ assert . ok ( captured . headers !== null ) ;
628+ assert . equal ( captured . headers . get ( "Idempotency-Key" ) , batchKey ) ;
629+ assert . equal ( receipts . length , 2 ) ;
630+ } finally {
631+ globalThis . fetch = originalFetch ;
632+ }
633+ } ) ;
634+ } ) ;
0 commit comments