Download packages/api/src/__tests__/verification.integration.test.ts from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/__tests__/verification.integration.test.ts
- Command line
-
hf download hf://spaces/Atmint/cpns/packages/api/src/__tests__/verification.integration.test.ts
-
curl -L -o verification.integration.test.ts https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/__tests__/verification.integration.test.ts
11.3 kB
| import { describe, it, expect, beforeAll, afterAll, mock } from "bun:test"; | |
| import { drizzle } from "drizzle-orm/pglite"; | |
| import { eq } from "drizzle-orm"; | |
| import * as schema from "../../../db/src/schema"; | |
| import { getTestPGlite, closeTestPGlite } from "./test-setup"; | |
| // ββ Mocks (must be declared before dynamic imports) ββββββββββββββββββββββββββ | |
| mock.module("@pram/env/server", () => ({ | |
| env: { | |
| DATABASE_URL: "postgres://localhost:5432/test", | |
| BETTER_AUTH_SECRET: "a".repeat(32), | |
| BETTER_AUTH_URL: "http://localhost:3000", | |
| CORS_ORIGIN: "http://localhost:5173", | |
| API_KEY_ENCRYPTION_KEY: "z".repeat(32), | |
| REDIS_URL: "redis://localhost:6379", | |
| SMTP_HOST: "localhost", | |
| SMTP_USER: "test", | |
| SMTP_PASS: "test", | |
| SMTP_FROM: "test@test.com", | |
| FREE_CREDITS_ENABLED: false, | |
| DEFAULT_SIGNUP_CREDIT_TOKENS: 50_000, | |
| // CLOUDFLARE_TURNSTILE_SECRET_KEY intentionally omitted β Turnstile disabled | |
| }, | |
| })); | |
| const mockCheckRateLimit = mock(async () => {}); | |
| mock.module("../lib/rate-limit", () => ({ | |
| checkRateLimit: mockCheckRateLimit, | |
| checkRateLimitAllowed: mock(async () => true), | |
| })); | |
| const mockSendOtpEmail = mock(async (_opts: unknown) => {}); | |
| mock.module("../lib/email", () => ({ | |
| sendOtpEmail: mockSendOtpEmail, | |
| })); | |
| mock.module("../lib/turnstile", () => ({ | |
| validateTurnstileToken: mock(async () => {}), | |
| })); | |
| // ββ Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const TEST_IP = "203.0.113.42"; | |
| const EMAIL_UNVERIFIED = "unverified@example.com"; | |
| const EMAIL_VERIFIED = "verified@example.com"; | |
| const EMAIL_NONEXISTENT = "ghost@example.com"; | |
| describe("verification router", () => { | |
| let db: ReturnType<typeof drizzle>; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| let verificationRouter: any; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| let caller: any; | |
| beforeAll(async () => { | |
| const pg = await getTestPGlite(); | |
| db = drizzle(pg, { schema }); | |
| mock.module("@pram/db", () => ({ | |
| __esModule: true, | |
| ...schema, | |
| db, | |
| })); | |
| const mod = await import("../routers/verification"); | |
| verificationRouter = mod.verificationRouter; | |
| caller = verificationRouter.createCaller({ session: null, auth: null, ip: TEST_IP }); | |
| await db.insert(schema.user).values([ | |
| { id: "v-user-1", name: "Unverified User", email: EMAIL_UNVERIFIED, emailVerified: false }, | |
| { id: "v-user-2", name: "Verified User", email: EMAIL_VERIFIED, emailVerified: true }, | |
| ]); | |
| // Seed account row so resetPassword can update the password hash | |
| await db.insert(schema.account).values({ | |
| id: "v-account-1", | |
| accountId: "v-user-1", | |
| providerId: "credential", | |
| userId: "v-user-1", | |
| password: "hashed_old_password", | |
| }); | |
| }); | |
| afterAll(async () => { | |
| await closeTestPGlite(); | |
| }); | |
| // ββ sendVerificationOtp βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("sendVerificationOtp", () => { | |
| it("sends email and returns generic success for unverified user", async () => { | |
| mockSendOtpEmail.mockClear(); | |
| const result = await caller.sendVerificationOtp({ email: EMAIL_UNVERIFIED }); | |
| expect(result.success).toBe(true); | |
| expect(mockSendOtpEmail).toHaveBeenCalledTimes(1); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const callArg = (mockSendOtpEmail.mock.calls[0] as any[])[0] as { to: string; type: string; otp: string }; | |
| expect(callArg.to).toBe(EMAIL_UNVERIFIED); | |
| expect(callArg.type).toBe("email-verification"); | |
| expect(callArg.otp).toMatch(/^\d{6}$/); | |
| }); | |
| it("silently returns generic response for non-existent email (no enumeration)", async () => { | |
| mockSendOtpEmail.mockClear(); | |
| const result = await caller.sendVerificationOtp({ email: EMAIL_NONEXISTENT }); | |
| expect(result.success).toBe(true); | |
| expect(mockSendOtpEmail).not.toHaveBeenCalled(); | |
| }); | |
| it("silently returns generic response for already-verified email", async () => { | |
| mockSendOtpEmail.mockClear(); | |
| const result = await caller.sendVerificationOtp({ email: EMAIL_VERIFIED }); | |
| expect(result.success).toBe(true); | |
| expect(mockSendOtpEmail).not.toHaveBeenCalled(); | |
| }); | |
| it("checks per-email and per-IP rate limits with correct keys", async () => { | |
| mockCheckRateLimit.mockClear(); | |
| await caller.sendVerificationOtp({ email: EMAIL_UNVERIFIED }); | |
| const keys = mockCheckRateLimit.mock.calls.map((c: unknown[]) => (c[0] as { key: string }).key); | |
| expect(keys).toContain(`otp-send:email:${EMAIL_UNVERIFIED}`); | |
| expect(keys).toContain(`otp-send:ip:${TEST_IP}:email`); | |
| }); | |
| it("passes strict:true to checkRateLimit", async () => { | |
| mockCheckRateLimit.mockClear(); | |
| await caller.sendVerificationOtp({ email: EMAIL_UNVERIFIED }); | |
| const allStrict = mockCheckRateLimit.mock.calls.every( | |
| (c: unknown[]) => (c[0] as { strict?: boolean }).strict === true, | |
| ); | |
| expect(allStrict).toBe(true); | |
| }); | |
| }); | |
| // ββ verifyEmailOtp ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("verifyEmailOtp", () => { | |
| it("verifies email and returns success with correct OTP", async () => { | |
| // Seed a fresh OTP directly into the verification table | |
| const otp = "123456"; | |
| await db.delete(schema.verification).where( | |
| eq(schema.verification.identifier, `email-verification:${EMAIL_UNVERIFIED}`), | |
| ); | |
| await db.insert(schema.verification).values({ | |
| id: "v-otp-1", | |
| identifier: `email-verification:${EMAIL_UNVERIFIED}`, | |
| value: otp, | |
| expiresAt: new Date(Date.now() + 5 * 60 * 1000), | |
| }); | |
| const result = await caller.verifyEmailOtp({ email: EMAIL_UNVERIFIED, otp }); | |
| expect(result.success).toBe(true); | |
| // emailVerified should now be true in the DB | |
| const [updated] = await db | |
| .select({ emailVerified: schema.user.emailVerified }) | |
| .from(schema.user) | |
| .where(eq(schema.user.email, EMAIL_UNVERIFIED)); | |
| expect(updated?.emailVerified).toBe(true); | |
| // OTP record should be deleted after use | |
| const remaining = await db | |
| .select() | |
| .from(schema.verification) | |
| .where(eq(schema.verification.id, "v-otp-1")); | |
| expect(remaining).toHaveLength(0); | |
| }); | |
| it("throws BAD_REQUEST for wrong OTP", async () => { | |
| await expect( | |
| caller.verifyEmailOtp({ email: EMAIL_UNVERIFIED, otp: "000000" }), | |
| ).rejects.toMatchObject({ code: "BAD_REQUEST" }); | |
| }); | |
| it("throws BAD_REQUEST for expired OTP", async () => { | |
| await db.delete(schema.verification).where( | |
| eq(schema.verification.identifier, `email-verification:${EMAIL_VERIFIED}`), | |
| ); | |
| await db.insert(schema.verification).values({ | |
| id: "v-otp-expired", | |
| identifier: `email-verification:${EMAIL_VERIFIED}`, | |
| value: "654321", | |
| expiresAt: new Date(Date.now() - 1000), // already expired | |
| }); | |
| await expect( | |
| caller.verifyEmailOtp({ email: EMAIL_VERIFIED, otp: "654321" }), | |
| ).rejects.toMatchObject({ code: "BAD_REQUEST" }); | |
| }); | |
| }); | |
| // ββ sendPasswordResetOtp ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("sendPasswordResetOtp", () => { | |
| it("sends reset email for registered user", async () => { | |
| mockSendOtpEmail.mockClear(); | |
| const result = await caller.sendPasswordResetOtp({ email: EMAIL_UNVERIFIED }); | |
| expect(result.success).toBe(true); | |
| expect(mockSendOtpEmail).toHaveBeenCalledTimes(1); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const callArg = (mockSendOtpEmail.mock.calls[0] as any[])[0] as { to: string; type: string }; | |
| expect(callArg.to).toBe(EMAIL_UNVERIFIED); | |
| expect(callArg.type).toBe("forget-password"); | |
| }); | |
| it("silently returns generic response for non-existent email", async () => { | |
| mockSendOtpEmail.mockClear(); | |
| const result = await caller.sendPasswordResetOtp({ email: EMAIL_NONEXISTENT }); | |
| expect(result.success).toBe(true); | |
| expect(mockSendOtpEmail).not.toHaveBeenCalled(); | |
| }); | |
| it("checks per-email and per-IP rate limits for reset", async () => { | |
| mockCheckRateLimit.mockClear(); | |
| await caller.sendPasswordResetOtp({ email: EMAIL_UNVERIFIED }); | |
| const keys = mockCheckRateLimit.mock.calls.map((c: unknown[]) => (c[0] as { key: string }).key); | |
| expect(keys).toContain(`otp-send:email:${EMAIL_UNVERIFIED}`); | |
| expect(keys).toContain(`otp-send:ip:${TEST_IP}:reset`); | |
| }); | |
| }); | |
| // ββ resetPassword βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("resetPassword", () => { | |
| it("resets password with valid OTP", async () => { | |
| const otp = "789012"; | |
| await db.delete(schema.verification).where( | |
| eq(schema.verification.identifier, `forget-password:${EMAIL_UNVERIFIED}`), | |
| ); | |
| await db.insert(schema.verification).values({ | |
| id: "v-reset-1", | |
| identifier: `forget-password:${EMAIL_UNVERIFIED}`, | |
| value: otp, | |
| expiresAt: new Date(Date.now() + 5 * 60 * 1000), | |
| }); | |
| const result = await caller.resetPassword({ | |
| email: EMAIL_UNVERIFIED, | |
| otp, | |
| newPassword: "NewPass123", | |
| }); | |
| expect(result.success).toBe(true); | |
| // OTP record should be deleted | |
| const remaining = await db | |
| .select() | |
| .from(schema.verification) | |
| .where(eq(schema.verification.id, "v-reset-1")); | |
| expect(remaining).toHaveLength(0); | |
| }); | |
| it("throws BAD_REQUEST for invalid OTP", async () => { | |
| await expect( | |
| caller.resetPassword({ email: EMAIL_UNVERIFIED, otp: "000000", newPassword: "NewPass123" }), | |
| ).rejects.toMatchObject({ code: "BAD_REQUEST" }); | |
| }); | |
| }); | |
| // ββ rate limit propagation βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe("rate limit enforcement", () => { | |
| it("throws TOO_MANY_REQUESTS when checkRateLimit throws", async () => { | |
| const { TRPCError } = await import("@trpc/server"); | |
| mockCheckRateLimit.mockImplementation(async () => { | |
| throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: "Too many requests. Please try again later." }); | |
| }); | |
| await expect( | |
| caller.sendVerificationOtp({ email: EMAIL_UNVERIFIED }), | |
| ).rejects.toMatchObject({ code: "TOO_MANY_REQUESTS" }); | |
| // Restore no-op | |
| mockCheckRateLimit.mockImplementation(async () => {}); | |
| }); | |
| }); | |
| }); | |