import { describe, it, expect, beforeEach, vi } from 'vitest'; type RateLimitResult = { allowed: boolean; remaining: number; resetAt: number; }; const mockCheckRateLimit = vi.fn<[], Promise>(); const mockGetClientIp = vi.fn<[], string>(); vi.mock('../../../apps/website/lib/rate-limit', () => ({ checkRateLimit: (...args: unknown[]) => mockCheckRateLimit(...(args as [])), getClientIp: (..._args: unknown[]) => mockGetClientIp(), })); async function getPostHandler() { const routeModule = (await import( '../../../apps/website/app/api/signup/route' )) as { POST: (request: Request) => Promise }; return routeModule.POST; } function createJsonRequest(body: unknown): Request { return new Request('http://localhost/api/signup', { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify(body), }); } describe('/api/signup POST', () => { beforeEach(() => { vi.resetModules(); mockCheckRateLimit.mockReset(); mockGetClientIp.mockReset(); mockGetClientIp.mockReturnValue('127.0.0.1'); mockCheckRateLimit.mockResolvedValue({ allowed: true, remaining: 4, resetAt: Date.now() + 60 * 60 * 1000, }); }); it('accepts a valid email within rate limits and returns success payload', async () => { const POST = await getPostHandler(); const response = await POST( createJsonRequest({ email: 'user@example.com', }), ); expect(response.status).toBeGreaterThanOrEqual(200); expect(response.status).toBeLessThan(300); const data = (await response.json()) as { message: unknown; ok: unknown }; expect(data).toHaveProperty('message'); expect(typeof data.message).toBe('string'); expect(data).toHaveProperty('ok', true); }); it('rejects an invalid email with 400 and error message', async () => { const POST = await getPostHandler(); const response = await POST( createJsonRequest({ email: 'not-an-email', }), ); expect(response.status).toBe(400); const data = (await response.json()) as { error: unknown }; expect(typeof data.error).toBe('string'); expect(data.error.toLowerCase()).toContain('email'); }); it('rejects disposable email domains with 400 and error message', async () => { const POST = await getPostHandler(); const response = await POST( createJsonRequest({ email: 'foo@mailinator.com', }), ); expect(response.status).toBe(400); const data = (await response.json()) as { error: unknown }; expect(typeof data.error).toBe('string'); }); it('returns 409 and friendly message when email is already subscribed', async () => { const POST = await getPostHandler(); const email = 'duplicate@example.com'; const first = await POST(createJsonRequest({ email })); expect(first.status).toBeGreaterThanOrEqual(200); expect(first.status).toBeLessThan(300); const second = await POST(createJsonRequest({ email })); expect(second.status).toBe(409); const data = (await second.json()) as { error: unknown }; expect(typeof data.error).toBe('string'); expect(data.error.toLowerCase()).toContain('already'); }); it('returns 429 with retryAfter when rate limit is exceeded', async () => { mockCheckRateLimit.mockResolvedValueOnce({ allowed: false, remaining: 0, resetAt: Date.now() + 30_000, }); const POST = await getPostHandler(); const response = await POST( createJsonRequest({ email: 'limited@example.com', }), ); expect(response.status).toBe(429); const data = (await response.json()) as { error: unknown; retryAfter?: unknown }; expect(typeof data.error).toBe('string'); expect(data).toHaveProperty('retryAfter'); }); });