From 3186fa3ca1d6e67eefa224afdb055b496061bb66 Mon Sep 17 00:00:00 2001 From: Satyam0000000 Date: Mon, 15 Jun 2026 15:15:59 +0530 Subject: [PATCH 1/2] Add mailinator provider --- src/__tests__/providers/mailinator.test.ts | 174 +++++++++++++++++++++ src/providers/mailinator.ts | 74 +++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/__tests__/providers/mailinator.test.ts create mode 100644 src/providers/mailinator.ts diff --git a/src/__tests__/providers/mailinator.test.ts b/src/__tests__/providers/mailinator.test.ts new file mode 100644 index 0000000..b595a1b --- /dev/null +++ b/src/__tests__/providers/mailinator.test.ts @@ -0,0 +1,174 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mailinatorProvider } from "../../providers/mailinator"; + +// This replaces the real fetch with a fake one +// No real API calls are made — zero cost, zero internet needed +vi.stubGlobal("fetch", vi.fn()); + +describe("mailinatorProvider", () => { + + // Reset the fake fetch before each test + beforeEach(() => { + vi.resetAllMocks(); + }); + + // ✅ Test 1 — basic structure + it("returns correct domain", () => { + const provider = mailinatorProvider(); + expect(provider.domain).toBe("mailinator.com"); + }); + + // ✅ Test 2 — happy path, email arrives and OTP extracted + it("extracts email body successfully", async () => { + const fetchMock = vi.fn() + // first fetch call → inbox list + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + msgs: [{ id: "msg-abc-123", subject: "Your OTP Code" }], + }), + }) + // second fetch call → full message content + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + parts: [{ body: "Your verification code is 847291. It expires in 10 minutes." }], + }), + }); + + vi.stubGlobal("fetch", fetchMock); + + const provider = mailinatorProvider(); + const result = await provider.extractContent({ + email: "testuser@mailinator.com", + prompt: "get the 6 digit verification code", + }); + + // result should contain the OTP + expect(result).toContain("847291"); + + // fetch should have been called exactly twice + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + // ✅ Test 3 — works with API key in headers + it("sends API key in headers when provided", async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + msgs: [{ id: "msg-456", subject: "Reset Password" }], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + parts: [{ body: "Click here to reset your password: https://example.com/reset/abc" }], + }), + }); + + vi.stubGlobal("fetch", fetchMock); + + const provider = mailinatorProvider({ apiKey: "my-test-api-key" }); + await provider.extractContent({ + email: "testuser@mailinator.com", + prompt: "get the reset link", + }); + + // check first call had Authorization header + const firstCallHeaders = fetchMock.mock.calls[0][1].headers; + expect(firstCallHeaders).toEqual({ + Authorization: "Bearer my-test-api-key", + }); + }); + + // ✅ Test 4 — no API key means no Authorization header + it("sends no headers when no API key provided", async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + msgs: [{ id: "msg-789" }], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + parts: [{ body: "Welcome! Your code is 123456" }], + }), + }); + + vi.stubGlobal("fetch", fetchMock); + + const provider = mailinatorProvider(); // no API key + await provider.extractContent({ + email: "test@mailinator.com", + prompt: "get the code", + }); + + const firstCallHeaders = fetchMock.mock.calls[0][1].headers; + expect(firstCallHeaders).toEqual({}); + }); + + // ✅ Test 5 — inbox is empty + it("throws when inbox has no emails", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => ({ msgs: [] }), // empty inbox + })); + + const provider = mailinatorProvider(); + + await expect( + provider.extractContent({ + email: "nobody@mailinator.com", + prompt: "get the code", + }) + ).rejects.toThrow("No emails found for nobody@mailinator.com"); + }); + + // ✅ Test 6 — API returns error + it("throws when inbox API call fails", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce({ + ok: false, + status: 429, // too many requests + })); + + const provider = mailinatorProvider(); + + await expect( + provider.extractContent({ + email: "test@mailinator.com", + prompt: "get the code", + }) + ).rejects.toThrow("HTTP 429"); + }); + + // ✅ Test 7 — email body is empty + it("throws when email body is empty", async () => { + vi.stubGlobal("fetch", vi.fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + msgs: [{ id: "msg-empty" }], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + parts: [{ body: "" }], // empty body + }), + }) + ); + + const provider = mailinatorProvider(); + + await expect( + provider.extractContent({ + email: "test@mailinator.com", + prompt: "get the code", + }) + ).rejects.toThrow("Email body is empty"); + }); + +}); \ No newline at end of file diff --git a/src/providers/mailinator.ts b/src/providers/mailinator.ts new file mode 100644 index 0000000..ed20a02 --- /dev/null +++ b/src/providers/mailinator.ts @@ -0,0 +1,74 @@ +import type { EmailProvider } from "../config"; + +type MailinatorInbox = { + msgs?: Array<{ + id: string; + }>; +}; + +type MailinatorMessage = { + parts?: Array<{ + body?: string; + }>; +}; + +export function mailinatorProvider(options: { + apiKey?: string; +} = {}): EmailProvider { + return { + domain: "mailinator.com", + + extractContent: async ({ email, prompt }) => { + // extract username from email + // "test.user.123@mailinator.com" → "test.user.123" + const username = email.split("@")[0]; + + // Step 1 — fetch inbox + const inboxResponse = await fetch( + `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${username}`, + { + headers: options.apiKey + ? { Authorization: `Bearer ${options.apiKey}` } + : {}, + } + ); + + if (!inboxResponse.ok) { + throw new Error( + `[mailinator] Failed to fetch inbox for ${email}: HTTP ${inboxResponse.status}` + ); + } + + const inbox = (await inboxResponse.json()) as MailinatorInbox; + + // Step 2 — check emails exist + if (!inbox.msgs || inbox.msgs.length === 0) { + throw new Error( + `[mailinator] No emails found for ${email}` + ); + } + + // Step 3 — get latest email content + const latestEmail = inbox.msgs[0]; + const messageResponse = await fetch( + `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${username}/messages/${latestEmail.id}`, + { + headers: options.apiKey + ? { Authorization: `Bearer ${options.apiKey}` } + : {}, + } + ); + + const message = (await messageResponse.json()) as MailinatorMessage; + const emailBody = message.parts?.[0]?.body ?? ""; + + if (!emailBody) { + throw new Error( + `[mailinator] Email body is empty for ${email}` + ); + } + + return emailBody; + }, + }; +} \ No newline at end of file From 7d2dac73e6b33f056a1cbf8464999116e57369d7 Mon Sep 17 00:00:00 2001 From: Satyam0000000 Date: Mon, 15 Jun 2026 20:34:57 +0530 Subject: [PATCH 2/2] feature: add Mailinator email provider --- src/__tests__/providers/mailinator.test.ts | 63 ++++++++++++---------- src/providers/mailinator.ts | 39 +++++++++----- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/src/__tests__/providers/mailinator.test.ts b/src/__tests__/providers/mailinator.test.ts index b595a1b..fccf7b4 100644 --- a/src/__tests__/providers/mailinator.test.ts +++ b/src/__tests__/providers/mailinator.test.ts @@ -1,34 +1,26 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { mailinatorProvider } from "../../providers/mailinator"; -// This replaces the real fetch with a fake one -// No real API calls are made — zero cost, zero internet needed vi.stubGlobal("fetch", vi.fn()); describe("mailinatorProvider", () => { - - // Reset the fake fetch before each test beforeEach(() => { vi.resetAllMocks(); }); - // ✅ Test 1 — basic structure it("returns correct domain", () => { const provider = mailinatorProvider(); expect(provider.domain).toBe("mailinator.com"); }); - // ✅ Test 2 — happy path, email arrives and OTP extracted it("extracts email body successfully", async () => { const fetchMock = vi.fn() - // first fetch call → inbox list .mockResolvedValueOnce({ ok: true, json: async () => ({ - msgs: [{ id: "msg-abc-123", subject: "Your OTP Code" }], + msgs: [{ id: "msg-abc-123" }], }), }) - // second fetch call → full message content .mockResolvedValueOnce({ ok: true, json: async () => ({ @@ -44,20 +36,17 @@ describe("mailinatorProvider", () => { prompt: "get the 6 digit verification code", }); - // result should contain the OTP expect(result).toContain("847291"); - - // fetch should have been called exactly twice + expect(fetchMock).toHaveBeenCalledTimes(2); }); - // ✅ Test 3 — works with API key in headers it("sends API key in headers when provided", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ - msgs: [{ id: "msg-456", subject: "Reset Password" }], + msgs: [{ id: "msg-456" }], }), }) .mockResolvedValueOnce({ @@ -75,14 +64,12 @@ describe("mailinatorProvider", () => { prompt: "get the reset link", }); - // check first call had Authorization header - const firstCallHeaders = fetchMock.mock.calls[0][1].headers; - expect(firstCallHeaders).toEqual({ + const inboxRequestHeaders = fetchMock.mock.calls[0]?.[1]?.headers; + expect(inboxRequestHeaders).toEqual({ Authorization: "Bearer my-test-api-key", }); }); - // ✅ Test 4 — no API key means no Authorization header it("sends no headers when no API key provided", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce({ @@ -100,21 +87,20 @@ describe("mailinatorProvider", () => { vi.stubGlobal("fetch", fetchMock); - const provider = mailinatorProvider(); // no API key + const provider = mailinatorProvider(); await provider.extractContent({ email: "test@mailinator.com", prompt: "get the code", }); - const firstCallHeaders = fetchMock.mock.calls[0][1].headers; - expect(firstCallHeaders).toEqual({}); + const inboxRequestHeaders = fetchMock.mock.calls[0]?.[1]?.headers; + expect(inboxRequestHeaders ?? {}).toEqual({}); }); - // ✅ Test 5 — inbox is empty it("throws when inbox has no emails", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce({ ok: true, - json: async () => ({ msgs: [] }), // empty inbox + json: async () => ({ msgs: [] }), })); const provider = mailinatorProvider(); @@ -127,11 +113,10 @@ describe("mailinatorProvider", () => { ).rejects.toThrow("No emails found for nobody@mailinator.com"); }); - // ✅ Test 6 — API returns error it("throws when inbox API call fails", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce({ ok: false, - status: 429, // too many requests + status: 429, })); const provider = mailinatorProvider(); @@ -144,7 +129,30 @@ describe("mailinatorProvider", () => { ).rejects.toThrow("HTTP 429"); }); - // ✅ Test 7 — email body is empty + it("throws when message API call fails", async () => { + vi.stubGlobal("fetch", vi.fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + msgs: [{ id: "msg-123" }], + }), + }) + .mockResolvedValueOnce({ + ok: false, + status: 500, + }) + ); + + const provider = mailinatorProvider(); + + await expect( + provider.extractContent({ + email: "test@mailinator.com", + prompt: "get the code", + }) + ).rejects.toThrow("HTTP 500"); + }); + it("throws when email body is empty", async () => { vi.stubGlobal("fetch", vi.fn() .mockResolvedValueOnce({ @@ -156,7 +164,7 @@ describe("mailinatorProvider", () => { .mockResolvedValueOnce({ ok: true, json: async () => ({ - parts: [{ body: "" }], // empty body + parts: [{ body: "" }], }), }) ); @@ -170,5 +178,4 @@ describe("mailinatorProvider", () => { }) ).rejects.toThrow("Email body is empty"); }); - }); \ No newline at end of file diff --git a/src/providers/mailinator.ts b/src/providers/mailinator.ts index ed20a02..5b7b60f 100644 --- a/src/providers/mailinator.ts +++ b/src/providers/mailinator.ts @@ -12,24 +12,35 @@ type MailinatorMessage = { }>; }; +/** + * Mailinator provider for retrieving email content from Mailinator inboxes. + * + * @param options - Configuration options for the Mailinator provider + * @returns An EmailProvider instance + */ export function mailinatorProvider(options: { apiKey?: string; } = {}): EmailProvider { return { domain: "mailinator.com", - extractContent: async ({ email, prompt }) => { - // extract username from email - // "test.user.123@mailinator.com" → "test.user.123" - const username = email.split("@")[0]; + extractContent: async ({ email }) => { + const headers: Record = {}; + + if (options.apiKey) { + headers.Authorization = `Bearer ${options.apiKey}`; + } + + const [username] = email.split("@"); + + if (!username) { + throw new Error(`[mailinator] Invalid email: ${email}`); + } - // Step 1 — fetch inbox const inboxResponse = await fetch( `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${username}`, { - headers: options.apiKey - ? { Authorization: `Bearer ${options.apiKey}` } - : {}, + headers, } ); @@ -41,24 +52,26 @@ export function mailinatorProvider(options: { const inbox = (await inboxResponse.json()) as MailinatorInbox; - // Step 2 — check emails exist if (!inbox.msgs || inbox.msgs.length === 0) { throw new Error( `[mailinator] No emails found for ${email}` ); } - // Step 3 — get latest email content const latestEmail = inbox.msgs[0]; const messageResponse = await fetch( `https://mailinator.com/api/v2/domains/mailinator.com/inboxes/${username}/messages/${latestEmail.id}`, { - headers: options.apiKey - ? { Authorization: `Bearer ${options.apiKey}` } - : {}, + headers, } ); + if (!messageResponse.ok) { + throw new Error( + `[mailinator] Failed to fetch message ${latestEmail.id}: HTTP ${messageResponse.status}` + ); + } + const message = (await messageResponse.json()) as MailinatorMessage; const emailBody = message.parts?.[0]?.body ?? "";