UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

398 lines (333 loc) 11.4 kB
import { Alepha } from "alepha"; import { AlephaApiVerification } from "alepha/api/verifications"; import { DateTimeProvider } from "alepha/datetime"; import { AlephaEmail, MemoryEmailProvider } from "alepha/email"; import { AlephaOrmPostgres } from "alepha/orm/postgres"; import { AlephaSecurity, currentUserAtom } from "alepha/security"; import { BadRequestError } from "alepha/server"; import { describe, it } from "vitest"; import { AlephaApiUsers, RealmProvider, UserController, UserNotifications, UserService, } from "../index.ts"; const setup = async () => { const alepha = Alepha.create({ env: { LOG_LEVEL: "error" }, }); alepha.with(AlephaOrmPostgres); alepha.with(AlephaSecurity); alepha.with(AlephaEmail); alepha.with(AlephaApiVerification); alepha.with(AlephaApiUsers); alepha.with(UserNotifications); await alepha.start(); // Enable notifications for the default realm const realmProvider = alepha.inject(RealmProvider); realmProvider.register("default", { features: { notifications: true, }, }); const emailProvider = alepha.inject(MemoryEmailProvider); emailProvider.records = []; return { alepha, userService: alepha.inject(UserService), dateTimeProvider: alepha.inject(DateTimeProvider), emailProvider, actions: alepha.inject(UserController), }; }; // Helper to extract code from email const extractCode = (emailBody: string): string => { // Code is displayed in the email body as a 6-digit number const match = emailBody.match(/(\d{6})/); if (!match) throw new Error("Code not found in email"); return match[1]; }; describe("alepha/api/users - Email Verification", () => { it("should successfully request email verification and send email", async ({ expect, }) => { const { userService, emailProvider, actions } = await setup(); // Create a test user const user = await userService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], emailVerified: false, }); // Request email verification const result = await actions.requestEmailVerification({ body: { email: "test@example.com", }, }); expect(result.success).toBe(true); expect(result.message).toContain("verification code has been sent"); // Verify email was sent via verification service await expect.poll(() => emailProvider.records.length).toBe(1); const email = emailProvider.records[0]; expect(email.to).toBe("test@example.com"); expect(email.subject).toBe("Verify your email address"); // Check for 6-digit code in email expect(email.body).toMatch(/\d{6}/); expect(email.body).toContain("5 minutes"); // Default code verification expiration }); it("should not reveal if email does not exist", async ({ expect }) => { const { emailProvider, actions } = await setup(); // Request email verification for non-existent email const result = await actions.requestEmailVerification({ body: { email: "nonexistent@example.com", }, }); // Should return success to prevent email enumeration expect(result.success).toBe(true); expect(result.message).toContain("verification code has been sent"); // But no email should be sent expect(emailProvider.records).toHaveLength(0); }); it("should not send email if already verified", async ({ expect }) => { const { userService, emailProvider, actions } = await setup(); // Create a user with already verified email await userService.users().create({ username: "verifieduser", email: "verified@example.com", roles: ["user"], emailVerified: true, }); // Request email verification const result = await actions.requestEmailVerification({ body: { email: "verified@example.com", }, }); // Should return success but not send email expect(result.success).toBe(true); expect(emailProvider.records).toHaveLength(0); }); it("should successfully verify email with valid token", async ({ expect, }) => { const { userService, emailProvider, actions } = await setup(); // Create a test user const user = await userService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], emailVerified: false, }); // Request email verification await userService.requestEmailVerification("test@example.com"); // Extract code from email await expect.poll(() => emailProvider.records.length).toBe(1); const token = extractCode(emailProvider.records[0].body); // Verify email const result = await actions.verifyEmail({ body: { email: "test@example.com", token, }, }); expect(result.success).toBe(true); expect(result.message).toContain("Email has been verified successfully"); // Check that emailVerified is now true const updatedUser = await userService.users().getOne({ where: { id: { eq: user.id } }, }); expect(updatedUser.emailVerified).toBe(true); }); it("should reject invalid verification token", async ({ expect }) => { const { userService, actions } = await setup(); // Create a test user await userService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], emailVerified: false, }); // Attempt to verify with invalid token await expect( actions.verifyEmail({ body: { email: "test@example.com", token: "550e8400-e29b-41d4-a716-446655440000", }, }), ).rejects.toThrowError(BadRequestError); }); it("should reject expired verification token", async ({ expect }) => { const { userService, dateTimeProvider, emailProvider, actions } = await setup(); // Create a test user await userService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], emailVerified: false, }); // Request email verification await userService.requestEmailVerification("test@example.com"); // Extract code await expect.poll(() => emailProvider.records.length).toBe(1); const token = extractCode(emailProvider.records[0].body); // Travel forward in time to expire the token (default expiration is 5 minutes for code verification) dateTimeProvider.travel(6, "minutes"); // Attempt to verify with expired token await expect( actions.verifyEmail({ body: { email: "test@example.com", token, }, }), ).rejects.toThrowError(BadRequestError); }); it("should not allow token reuse after successful verification", async ({ expect, }) => { const { userService, emailProvider, actions } = await setup(); // Create a test user await userService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], emailVerified: false, }); // Request email verification await userService.requestEmailVerification("test@example.com"); // Extract code await expect.poll(() => emailProvider.records.length).toBe(1); const token = extractCode(emailProvider.records[0].body); // Verify email await actions.verifyEmail({ body: { email: "test@example.com", token, }, }); // Attempt to use the same token again should fail await expect( actions.verifyEmail({ body: { email: "test@example.com", token, }, }), ).rejects.toThrowError(BadRequestError); }); it("should check email verification status", async ({ expect }) => { const { alepha, userService, actions } = await setup(); // Create a test user with unverified email await userService.users().create({ username: "unverifieduser", email: "unverified@example.com", roles: ["user"], emailVerified: false, }); // Create a test user with verified email await userService.users().create({ username: "verifieduser", email: "verified@example.com", roles: ["user"], emailVerified: true, }); // checkEmailVerification requires $secure() authentication await alepha.context.run(async () => { alepha.store.set(currentUserAtom, { id: "admin-1", name: "Admin", email: "admin@example.com", realm: "default", roles: ["admin"], }); // Check unverified email const unverifiedResult = await actions.checkEmailVerification({ query: { email: "unverified@example.com" }, }); expect(unverifiedResult.verified).toBe(false); // Check verified email const verifiedResult = await actions.checkEmailVerification({ query: { email: "verified@example.com" }, }); expect(verifiedResult.verified).toBe(true); // Check non-existent email const nonExistentResult = await actions.checkEmailVerification({ query: { email: "nonexistent@example.com" }, }); expect(nonExistentResult.verified).toBe(false); }); }); it("should respect rate limiting on verification requests", async ({ expect, }) => { const { userService, dateTimeProvider, emailProvider, actions } = await setup(); // Create a test user await userService.users().create({ username: "testuser", email: "test@example.com", roles: ["user"], emailVerified: false, }); // Request email verification multiple times within cooldown period await actions.requestEmailVerification({ body: { email: "test@example.com", }, }); await expect.poll(() => emailProvider.records.length).toBe(1); // Second request should be silently ignored (cooldown) await actions.requestEmailVerification({ body: { email: "test@example.com", }, }); // Still only 1 email expect(emailProvider.records.length).toBe(1); // Wait for cooldown to pass (90 seconds default) dateTimeProvider.travel(91, "seconds"); // Now should work await actions.requestEmailVerification({ body: { email: "test@example.com", }, }); await expect.poll(() => emailProvider.records.length).toBe(2); }); it("should correctly identify verified vs unverified users using isEmailVerified", async ({ expect, }) => { const { userService } = await setup(); // Create users with different verification statuses await userService.users().create({ username: "unverifieduser", email: "unverified@example.com", roles: ["user"], emailVerified: false, }); await userService.users().create({ username: "verifieduser", email: "verified@example.com", roles: ["user"], emailVerified: true, }); // Check verification status const unverifiedStatus = await userService.isEmailVerified( "unverified@example.com", ); expect(unverifiedStatus).toBe(false); const verifiedStatus = await userService.isEmailVerified( "verified@example.com", ); expect(verifiedStatus).toBe(true); const nonExistentStatus = await userService.isEmailVerified( "nonexistent@example.com", ); expect(nonExistentStatus).toBe(false); }); });