11/* eslint-disable @typescript-eslint/no-unsafe-assignment */
2- import { describe , it , expect , beforeEach , vi } from 'vitest' ;
2+ import { describe , it , test , expect , beforeEach , vi } from 'vitest' ;
33import { Test } from '@nestjs/testing' ;
44import { NotFoundException , BadRequestException } from '@nestjs/common' ;
55import { ConfigService } from '@nestjs/config' ;
@@ -254,9 +254,9 @@ describe('EmailService', () => {
254254 it ( 'when opening a conversation by an id that does not exist, then the user is told it was not found' , async ( ) => {
255255 provider . getThread . mockResolvedValue ( [ ] ) ;
256256
257- await expect (
258- service . getThread ( userEmail , 'nonexistent' ) ,
259- ) . rejects . toThrow ( NotFoundException ) ;
257+ await expect ( service . getThread ( userEmail , 'nonexistent' ) ) . rejects . toThrow (
258+ NotFoundException ,
259+ ) ;
260260 } ) ;
261261
262262 it ( 'when a conversation contains end-to-end encrypted messages, then the encrypted previews and wrapped keys are attached for the client to decrypt' , async ( ) => {
@@ -349,7 +349,11 @@ describe('EmailService', () => {
349349
350350 await service . sendEmail ( userEmail , dto ) ;
351351
352- expect ( provider . sendEmail ) . toHaveBeenCalledWith ( userEmail , dto , undefined ) ;
352+ expect ( provider . sendEmail ) . toHaveBeenCalledWith (
353+ userEmail ,
354+ dto ,
355+ undefined ,
356+ ) ;
353357 } ) ;
354358
355359 it ( 'when replying to an existing email, then the reply is delivered into the same conversation' , async ( ) => {
@@ -367,7 +371,11 @@ describe('EmailService', () => {
367371 userEmail ,
368372 'parent-id' ,
369373 ) ;
370- expect ( provider . sendEmail ) . toHaveBeenCalledWith ( userEmail , dto , threading ) ;
374+ expect ( provider . sendEmail ) . toHaveBeenCalledWith (
375+ userEmail ,
376+ dto ,
377+ threading ,
378+ ) ;
371379 } ) ;
372380
373381 it ( 'when replying to an email that no longer exists, then the user is told the original was not found' , async ( ) => {
@@ -521,10 +529,7 @@ describe('EmailService', () => {
521529 expect ( smtp . sendRaw ) . toHaveBeenCalledWith (
522530 expect . objectContaining ( {
523531 inReplyTo : '<parent@example.com>' ,
524- references : [
525- '<grandparent@example.com>' ,
526- '<parent@example.com>' ,
527- ] ,
532+ references : [ '<grandparent@example.com>' , '<parent@example.com>' ] ,
528533 } ) ,
529534 ) ;
530535 expect ( provider . saveToSent ) . toHaveBeenCalledWith (
@@ -570,12 +575,104 @@ describe('EmailService', () => {
570575 describe ( 'saveDraft' , ( ) => {
571576 it ( 'when called, then delegates to provider' , async ( ) => {
572577 const dto = newDraftEmailDto ( ) ;
573- provider . saveDraft . mockResolvedValue ( { id : 'draft-id' } ) ;
578+ const savedDraft = newEmail ( { isDraft : true } ) ;
579+ provider . saveDraft . mockResolvedValue ( savedDraft ) ;
574580
575581 const result = await service . saveDraft ( userEmail , dto ) ;
576582
577583 expect ( provider . saveDraft ) . toHaveBeenCalledWith ( userEmail , dto ) ;
578- expect ( result ) . toEqual ( { id : 'draft-id' } ) ;
584+ expect ( result ) . toBe ( savedDraft ) ;
585+ } ) ;
586+
587+ it ( 'when DTO has encryption block, then serializes it into textBody and clears htmlBody' , async ( ) => {
588+ const encryption = newEncryptionBlock ( ) ;
589+ const dto = newDraftEmailDto ( {
590+ encryption,
591+ htmlBody : '<p>original</p>' ,
592+ } ) ;
593+ provider . saveDraft . mockResolvedValue ( newEmail ( { isDraft : true } ) ) ;
594+
595+ await service . saveDraft ( userEmail , dto ) ;
596+
597+ const expectedBundle = Buffer . from ( JSON . stringify ( encryption ) ) . toString (
598+ 'base64' ,
599+ ) ;
600+ expect ( provider . saveDraft ) . toHaveBeenCalledWith (
601+ userEmail ,
602+ expect . objectContaining ( {
603+ textBody : `INTERNXT-ENCRYPTED-EMAIL-v1\n${ expectedBundle } ` ,
604+ htmlBody : undefined ,
605+ } ) ,
606+ ) ;
607+ } ) ;
608+ } ) ;
609+
610+ describe ( 'updateDraft' , ( ) => {
611+ it ( 'when called, then delegates to provider' , async ( ) => {
612+ const dto = newDraftEmailDto ( ) ;
613+ const updatedDraft = newEmail ( { isDraft : true } ) ;
614+ provider . updateDraft . mockResolvedValue ( updatedDraft ) ;
615+
616+ const result = await service . updateDraft ( userEmail , 'draft-id' , dto ) ;
617+
618+ expect ( provider . updateDraft ) . toHaveBeenCalledWith (
619+ userEmail ,
620+ 'draft-id' ,
621+ dto ,
622+ ) ;
623+ expect ( result ) . toBe ( updatedDraft ) ;
624+ } ) ;
625+
626+ it ( 'when DTO has encryption block, then serializes it into textBody and clears htmlBody' , async ( ) => {
627+ const encryption = newEncryptionBlock ( ) ;
628+ const dto = newDraftEmailDto ( {
629+ encryption,
630+ htmlBody : '<p>original</p>' ,
631+ } ) ;
632+ provider . updateDraft . mockResolvedValue ( newEmail ( { isDraft : true } ) ) ;
633+
634+ await service . updateDraft ( userEmail , 'draft-id' , dto ) ;
635+
636+ const expectedBundle = Buffer . from ( JSON . stringify ( encryption ) ) . toString (
637+ 'base64' ,
638+ ) ;
639+ expect ( provider . updateDraft ) . toHaveBeenCalledWith (
640+ userEmail ,
641+ 'draft-id' ,
642+ expect . objectContaining ( {
643+ textBody : `INTERNXT-ENCRYPTED-EMAIL-v1\n${ expectedBundle } ` ,
644+ htmlBody : undefined ,
645+ } ) ,
646+ ) ;
647+ } ) ;
648+
649+ test ( 'When the user tries to update a draft that does not exist, then they are told it was not found' , async ( ) => {
650+ provider . updateDraft . mockResolvedValue ( null ) ;
651+
652+ await expect (
653+ service . updateDraft ( userEmail , 'missing-draft' , newDraftEmailDto ( ) ) ,
654+ ) . rejects . toThrow ( NotFoundException ) ;
655+ } ) ;
656+ } ) ;
657+
658+ describe ( 'Discard Draft' , ( ) => {
659+ test ( 'When the user discards an existing draft, then it is removed from their mailbox' , async ( ) => {
660+ const draft = newEmail ( { isDraft : true } ) ;
661+ provider . getDraft . mockResolvedValue ( draft ) ;
662+ provider . discardDraft . mockResolvedValue ( undefined ) ;
663+
664+ await service . discardDraft ( userEmail , draft . id ) ;
665+
666+ expect ( provider . discardDraft ) . toHaveBeenCalledWith ( userEmail , draft . id ) ;
667+ } ) ;
668+
669+ test ( 'When the user tries to discard a draft that does not exist, then they are told it was not found' , async ( ) => {
670+ provider . getDraft . mockResolvedValue ( null ) ;
671+
672+ await expect (
673+ service . discardDraft ( userEmail , 'missing-draft' ) ,
674+ ) . rejects . toThrow ( NotFoundException ) ;
675+ expect ( provider . discardDraft ) . not . toHaveBeenCalled ( ) ;
579676 } ) ;
580677 } ) ;
581678
0 commit comments