import { mock, test, expect } from "bun:test"; import * as TE from "fp-ts/lib/TaskEither"; import { constVoid, pipe } from "fp-ts/lib/function"; import type { EmailFromInstruction, EmailToInstruction } from "../src/job"; import { perform, type EmailJobDependencies } from "../src/email"; const from: EmailFromInstruction = { send_port: 465, email: "test@localhost", username: "test", password: "password", server: "localhost", }; const to: EmailToInstruction = { read_port: 993, email: "test@localhost", username: "test", password: "password", server: "localhost", }; const getMocks = () => { const lock = { path: "INBOX", release: mock(() => constVoid()), }; const imap = { fetchAll: mock(() => Promise.resolve([])), connect: mock(() => Promise.resolve()), getMailboxLock: mock(() => Promise.resolve(lock)), messageDelete: mock(() => Promise.resolve(true)), logout: mock(() => Promise.resolve()), }; const mockDependencies: Partial = { getImapImpl: () => TE.right(imap), getSendImpl: mock(() => (email: any) => TE.right(email)), matchesEmailImpl: mock(() => () => true), }; return { lock, imap, mockDependencies }; }; test("retries until message is in inbox", async () => { const { imap, mockDependencies } = getMocks(); const retry = { retries: 3, interval: 400 }; const emailJob = { from, to, readRetry: retry }; let attempts = 0; const messageInInbox = { uid: 1 } as any; imap.fetchAll = mock(() => { attempts++; if (attempts === 3) { return Promise.resolve([messageInInbox] as any); } return Promise.resolve([]); }); mockDependencies.matchesEmailImpl = mock( (_: any) => (message: any) => message.uid === 1, ); await pipe( perform(emailJob, mockDependencies), TE.map((x) => { expect(x).toBeTruthy(); expect(attempts).toBe(3); }), TE.mapLeft(() => expect(false).toBeTruthy()), )(); }); test("failure to send message goes left", async () => { const { mockDependencies } = getMocks(); const emailJob = { from, to, readRetry: { retries: 1, interval: 1 } }; mockDependencies.getSendImpl = mock(() => () => TE.left(new Error("fail"))); await pipe( perform(emailJob, mockDependencies), TE.map(() => expect(false).toBeTruthy()), TE.mapLeft((e) => { expect(e.message).toBe("fail"); }), )(); }); test("goes left when message not ever received", async () => { const { imap, mockDependencies } = getMocks(); const emailJob = { from, to, readRetry: { retries: 3, interval: 1 } }; imap.fetchAll = mock(() => Promise.resolve([])); expect( await pipe( perform(emailJob, mockDependencies), TE.map(() => expect(false).toBeTruthy()), TE.mapLeft((e) => { expect(e.message).toBe("Email message not found"); }), )(), ); }); test("releases lock on left", async () => { const { lock, imap, mockDependencies } = getMocks(); const emailJob = { from, to, readRetry: { retries: 1, interval: 1 } }; imap.fetchAll = mock(() => Promise.resolve([])); await pipe( perform(emailJob, mockDependencies), TE.map(() => expect(false).toBeTruthy()), TE.mapLeft(() => { expect(imap.getMailboxLock).toHaveBeenCalledTimes(1); expect(lock.release).toHaveBeenCalledTimes(1); expect(imap.logout).toHaveBeenCalledTimes(1); }), )(); }); test("releases lock on right", async () => { const { lock, imap, mockDependencies } = getMocks(); const emailJob = { from, to, readRetry: { retries: 1, interval: 1 } }; mockDependencies.findEmailUidInInboxImpl = () => TE.right(1); await pipe( perform(emailJob, mockDependencies), TE.map(() => { expect(imap.getMailboxLock).toHaveBeenCalledTimes(1); expect(lock.release).toHaveBeenCalledTimes(1); expect(imap.logout).toHaveBeenCalledTimes(1); }), TE.mapLeft(() => expect(false).toBeTruthy()), )(); }); test("cleans up sent messages from inbox", async () => { const { imap, mockDependencies } = getMocks(); const emailJob = { from, to, readRetry: { retries: 1, interval: 1 } }; mockDependencies.findEmailUidInInboxImpl = () => TE.right(1); await pipe( perform(emailJob, mockDependencies), TE.map(() => { expect(imap.messageDelete).toHaveBeenCalledTimes(1); expect(imap.messageDelete).toHaveBeenCalledWith([1], { uid: true }); }), TE.mapLeft(() => expect(false).toBeTruthy()), )(); });