import { afterEach, beforeEach, describe, expect, it, } from "@suaveplan/testing/runner"; import { MailpitFixture } from "./mailpit-fixture.js"; function makeContainerFake(smtpPort = 1025, apiPort = 8025) { const stopped: string[] = []; const portMap: Record = { 1025: smtpPort, 8025: apiPort, }; const fakeStarted = { getMappedPort: (port: number) => portMap[port] ?? port, stop: async () => { stopped.push("stopped"); }, }; const fakeContainer = { withExposedPorts: function () { return this; }, withWaitStrategy: function () { return this; }, start: async () => fakeStarted, }; return { fakeContainer, fakeStarted, stopped, factory: (_img: string) => fakeContainer as never, }; } describe("MailpitFixture", () => { beforeEach(() => { delete process.env["MAILPIT_SMTP_URL"]; delete process.env["MAILPIT_API_URL"]; }); afterEach(() => { delete process.env["MAILPIT_SMTP_URL"]; delete process.env["MAILPIT_API_URL"]; }); it("name property returns mailpit", { timeout: 5000 }, () => { const fixture = new MailpitFixture(); expect(fixture.name).toBe("mailpit"); }); it("passthrough mode: returns config from both env vars", { timeout: 30000, }, async () => { process.env["MAILPIT_SMTP_URL"] = "smtp://localhost:1025"; process.env["MAILPIT_API_URL"] = "http://localhost:8025"; const fixture = new MailpitFixture(); const config = await fixture.start(); expect(config.smtpUrl).toBe("smtp://localhost:1025"); expect(config.apiUrl).toBe("http://localhost:8025"); }); it("passthrough mode: stop is a no-op", { timeout: 30000 }, async () => { process.env["MAILPIT_SMTP_URL"] = "smtp://localhost:1025"; process.env["MAILPIT_API_URL"] = "http://localhost:8025"; const fixture = new MailpitFixture(); await fixture.start(); await expect(fixture.stop()).resolves.toBeUndefined(); }); it("partial passthrough (only SMTP set) falls through to container mode", { timeout: 30000, }, async () => { process.env["MAILPIT_SMTP_URL"] = "smtp://localhost:1025"; // MAILPIT_API_URL not set const { factory } = makeContainerFake(11025, 18025); const fixture = new MailpitFixture({ containerFactory: factory }); const config = await fixture.start(); // Should have used the container, not env var expect(config.smtpUrl).toBe("smtp://localhost:11025"); expect(config.apiUrl).toBe("http://localhost:18025"); }); it("container mode happy path: returns correct URLs", { timeout: 30000, }, async () => { const { factory } = makeContainerFake(11025, 18025); const fixture = new MailpitFixture({ containerFactory: factory }); const config = await fixture.start(); expect(config.smtpUrl).toBe("smtp://localhost:11025"); expect(config.apiUrl).toBe("http://localhost:18025"); }); it("container mode: stop calls container.stop()", { timeout: 30000, }, async () => { const { factory, stopped } = makeContainerFake(11025, 18025); const fixture = new MailpitFixture({ containerFactory: factory }); await fixture.start(); await fixture.stop(); expect(stopped).toContain("stopped"); }); it("healthCheck returns true via injected fetchFn", { timeout: 30000, }, async () => { const { factory } = makeContainerFake(11025, 18025); const fixture = new MailpitFixture({ containerFactory: factory, fetchFn: async () => ({ ok: true }) as Response, }); await fixture.start(); const healthy = await fixture.healthCheck(); expect(healthy).toBe(true); }); it("healthCheck returns false via injected fetchFn (ok: false)", { timeout: 30000, }, async () => { const { factory } = makeContainerFake(11025, 18025); const fixture = new MailpitFixture({ containerFactory: factory, fetchFn: async () => ({ ok: false }) as Response, }); await fixture.start(); const healthy = await fixture.healthCheck(); expect(healthy).toBe(false); }); it("healthCheck returns false when fetch throws", { timeout: 30000, }, async () => { const { factory } = makeContainerFake(11025, 18025); const fixture = new MailpitFixture({ containerFactory: factory, fetchFn: async () => { throw new Error("ECONNREFUSED"); }, }); await fixture.start(); const healthy = await fixture.healthCheck(); expect(healthy).toBe(false); }); it("healthCheck returns false when no container", { timeout: 30000, }, async () => { const fixture = new MailpitFixture(); const healthy = await fixture.healthCheck(); expect(healthy).toBe(false); }); it("stop when no container is a no-op", { timeout: 30000 }, async () => { const fixture = new MailpitFixture(); await expect(fixture.stop()).resolves.toBeUndefined(); }); it("resolveContainerFactory: falls back to GenericContainer import", { timeout: 60000, }, async () => { class Exposed extends MailpitFixture { resolveFactory() { return this.resolveContainerFactory(); } } const exposed = new Exposed(); const factory = await exposed.resolveFactory(); expect(typeof factory).toBe("function"); const container = factory("axllent/mailpit:latest"); expect(container).toBeDefined(); }); });