UNPKG

alepha

Version:

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

795 lines (672 loc) 23.6 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, CryptoProvider } from "alepha/security"; import { BadRequestError, ConflictError, HttpError } from "alepha/server"; import { describe, it } from "vitest"; import { AlephaApiUsers, RealmProvider, RegistrationService, SessionService, UserNotifications, UserService, } from "../index.ts"; const setup = async (realmSettings?: Record<string, unknown>) => { const alepha = Alepha.create(); alepha.with(AlephaOrmPostgres); alepha.with(AlephaSecurity); alepha.with(AlephaEmail); alepha.with(AlephaApiVerification); alepha.with(AlephaApiUsers); alepha.with(UserNotifications); await alepha.start(); const emailProvider = alepha.inject(MemoryEmailProvider); emailProvider.records = []; const realmProvider = alepha.inject(RealmProvider); // Configure realm settings if provided (applies to default realm) if (realmSettings) { realmProvider.register("default", { settings: realmSettings as never, }); } return { alepha, registrationService: alepha.inject(RegistrationService), userService: alepha.inject(UserService), sessionService: alepha.inject(SessionService), cryptoProvider: alepha.inject(CryptoProvider), dateTimeProvider: alepha.inject(DateTimeProvider), emailProvider, realmProvider, }; }; // Helper to extract code from email const extractCode = (emailBody: string): string => { const match = emailBody.match(/(\d{6})/); if (!match) throw new Error("Code not found in email"); return match[1]; }; describe("alepha/api/users - RegistrationService", () => { describe("Phase 1: createRegistrationIntent", () => { it("should create a registration intent with valid data", async ({ expect, }) => { const { registrationService } = await setup(); const result = await registrationService.createRegistrationIntent({ email: "newuser@example.com", password: "SecurePassword123!", username: "newuser", firstName: "New", lastName: "User", }); expect(result.intentId).toBeDefined(); expect(result.expiresAt).toBeDefined(); expect(result.expectCaptcha).toBe(false); expect(result.expectEmailVerification).toBe(false); expect(result.expectPhoneVerification).toBe(false); }); it("should create intent with email verification required when configured", async ({ expect, }) => { const { registrationService, emailProvider, realmProvider } = await setup(); // Register realm with email verification required realmProvider.register("verify-email-realm", { features: { notifications: true, }, settings: { verifyEmailRequired: true, } as never, }); const result = await registrationService.createRegistrationIntent( { email: "verify@example.com", password: "SecurePassword123!", }, "verify-email-realm", ); expect(result.intentId).toBeDefined(); expect(result.expectEmailVerification).toBe(true); expect(result.expectPhoneVerification).toBe(false); expect(result.expectCaptcha).toBe(false); // Verify email was sent await expect.poll(() => emailProvider.records.length).toBe(1); expect(emailProvider.records[0].to).toBe("verify@example.com"); expect(emailProvider.records[0].subject).toBe( "Verify your email address", ); }); it("should reject registration when disabled in realm settings", async ({ expect, }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("no-registration-realm", { settings: { registrationAllowed: false, } as never, }); await expect( registrationService.createRegistrationIntent( { email: "test@example.com", password: "SecurePassword123!", }, "no-registration-realm", ), ).rejects.toThrowError(BadRequestError); }); it("should reject when required username is missing", async ({ expect, }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("username-required-realm", { settings: { username: "required", } as never, }); await expect( registrationService.createRegistrationIntent( { email: "test@example.com", password: "SecurePassword123!", // username is missing }, "username-required-realm", ), ).rejects.toThrowError(BadRequestError); }); it("should reject when required email is missing", async ({ expect }) => { const { registrationService } = await setup(); // Default realm requires email await expect( registrationService.createRegistrationIntent({ username: "testuser", password: "SecurePassword123!", // email is missing }), ).rejects.toThrowError(BadRequestError); }); it("should reject when required phone is missing", async ({ expect }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("phone-required-realm", { settings: { phoneNumber: "required", email: "optional", } as never, }); await expect( registrationService.createRegistrationIntent( { email: "test@example.com", password: "SecurePassword123!", // phoneNumber is missing }, "phone-required-realm", ), ).rejects.toThrowError(BadRequestError); }); it("should reject duplicate username", async ({ expect }) => { const { registrationService, userService } = await setup(); // Create existing user await userService.users().create({ username: "existinguser", email: "existing@example.com", roles: ["user"], }); await expect( registrationService.createRegistrationIntent({ username: "existinguser", email: "new@example.com", password: "SecurePassword123!", }), ).rejects.toThrowError(ConflictError); }); it("should reject duplicate email", async ({ expect }) => { const { registrationService, userService } = await setup(); // Create existing user await userService.users().create({ username: "existinguser", email: "existing@example.com", roles: ["user"], }); await expect( registrationService.createRegistrationIntent({ username: "newuser", email: "existing@example.com", password: "SecurePassword123!", }), ).rejects.toThrowError(ConflictError); }); it("should reject duplicate phone number", async ({ expect }) => { const { registrationService, userService, realmProvider } = await setup(); realmProvider.register("phone-realm", { settings: { phoneNumber: "optional", email: "optional", } as never, }); // Create existing user with phone in the same realm await userService.users("phone-realm").create({ realm: "phone-realm", username: "existinguser", email: "existing@example.com", phoneNumber: "+1234567890", roles: ["user"], }); await expect( registrationService.createRegistrationIntent( { username: "newuser", email: "new@example.com", phoneNumber: "+1234567890", password: "SecurePassword123!", }, "phone-realm", ), ).rejects.toThrowError(ConflictError); }); it("should set correct expiration time (10 minutes)", async ({ expect, }) => { const { registrationService, dateTimeProvider } = await setup(); const before = dateTimeProvider.now(); const result = await registrationService.createRegistrationIntent({ email: "expiry@example.com", password: "SecurePassword123!", }); const expiresAt = dateTimeProvider.of(result.expiresAt); const expectedExpiry = before.add(10, "minutes"); // Should expire approximately 10 minutes from now expect(expiresAt.diff(expectedExpiry, "seconds")).toBeLessThan(5); }); }); describe("Phase 2: completeRegistration", () => { it("should complete registration without verification requirements", async ({ expect, }) => { const { registrationService, userService } = await setup(); // Create intent const intent = await registrationService.createRegistrationIntent({ email: "complete@example.com", password: "SecurePassword123!", username: "completeuser", firstName: "Complete", lastName: "User", }); // Complete registration const user = await registrationService.completeRegistration({ intentId: intent.intentId, }); expect(user.id).toBeDefined(); expect(user.email).toBe("complete@example.com"); expect(user.username).toBe("completeuser"); expect(user.firstName).toBe("Complete"); expect(user.lastName).toBe("User"); expect(user.roles).toContain("user"); expect(user.enabled).toBe(true); }); it("should complete registration with valid email verification code", async ({ expect, }) => { const { registrationService, emailProvider, realmProvider } = await setup(); realmProvider.register("email-verify-realm", { features: { notifications: true, }, settings: { verifyEmailRequired: true, } as never, }); // Create intent (sends verification email) const intent = await registrationService.createRegistrationIntent( { email: "emailverify@example.com", password: "SecurePassword123!", }, "email-verify-realm", ); expect(intent.expectEmailVerification).toBe(true); // Extract code from email await expect.poll(() => emailProvider.records.length).toBe(1); const emailCode = extractCode(emailProvider.records[0].body); // Complete registration with code const user = await registrationService.completeRegistration({ intentId: intent.intentId, emailCode, }); expect(user.email).toBe("emailverify@example.com"); expect(user.emailVerified).toBe(true); }); it("should reject expired intent (410 Gone)", async ({ expect }) => { const { registrationService, dateTimeProvider } = await setup(); // Create intent const intent = await registrationService.createRegistrationIntent({ email: "expired@example.com", password: "SecurePassword123!", }); // Travel forward 11 minutes (intent expires at 10) dateTimeProvider.travel(11, "minutes"); // Attempt to complete await expect( registrationService.completeRegistration({ intentId: intent.intentId, }), ).rejects.toThrow(HttpError); }); it("should reject invalid intent ID", async ({ expect }) => { const { registrationService } = await setup(); await expect( registrationService.completeRegistration({ intentId: "550e8400-e29b-41d4-a716-446655440000", }), ).rejects.toThrow(HttpError); }); it("should reject when email code is required but not provided", async ({ expect, }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("email-required-realm", { features: { notifications: true, }, settings: { verifyEmailRequired: true, } as never, }); const intent = await registrationService.createRegistrationIntent( { email: "needscode@example.com", password: "SecurePassword123!", }, "email-required-realm", ); await expect( registrationService.completeRegistration({ intentId: intent.intentId, // emailCode not provided }), ).rejects.toThrowError(BadRequestError); }); it("should reject invalid email verification code", async ({ expect }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("email-verify-realm", { features: { notifications: true, }, settings: { verifyEmailRequired: true, } as never, }); const intent = await registrationService.createRegistrationIntent( { email: "wrongcode@example.com", password: "SecurePassword123!", }, "email-verify-realm", ); await expect( registrationService.completeRegistration({ intentId: intent.intentId, emailCode: "000000", // Wrong code }), ).rejects.toThrowError(BadRequestError); }); it("should not allow intent reuse after successful registration", async ({ expect, }) => { const { registrationService } = await setup(); const intent = await registrationService.createRegistrationIntent({ email: "onetime@example.com", password: "SecurePassword123!", }); // First completion should succeed await registrationService.completeRegistration({ intentId: intent.intentId, }); // Second attempt should fail (intent deleted) await expect( registrationService.completeRegistration({ intentId: intent.intentId, }), ).rejects.toThrow(HttpError); }); it("should detect race condition when email is taken during verification", async ({ expect, }) => { const { registrationService, userService, realmProvider } = await setup(); realmProvider.register("race-realm", { settings: { verifyEmailRequired: false, } as never, }); // Create intent const intent = await registrationService.createRegistrationIntent( { email: "race@example.com", password: "SecurePassword123!", }, "race-realm", ); // Simulate another user registering with same email while verification pending await userService.users("race-realm").create({ realm: "race-realm", email: "race@example.com", username: "racewinner", roles: ["user"], }); // Attempt to complete should fail await expect( registrationService.completeRegistration({ intentId: intent.intentId, }), ).rejects.toThrowError(ConflictError); }); it("should create credentials identity with hashed password", async ({ expect, }) => { const { registrationService, sessionService, realmProvider } = await setup(); const intent = await registrationService.createRegistrationIntent({ email: "withpassword@example.com", password: "SecurePassword123!", }); const user = await registrationService.completeRegistration({ intentId: intent.intentId, }); // Verify login works with the password const loggedInUser = await sessionService.login( "credentials", "withpassword@example.com", "SecurePassword123!", ); expect(loggedInUser?.id).toBe(user.id); }); it("should allow login with username after registration", async ({ expect, }) => { const { registrationService, sessionService } = await setup({ username: "optional", }); const intent = await registrationService.createRegistrationIntent({ email: "logintest@example.com", username: "loginuser", password: "SecurePassword123!", }); const user = await registrationService.completeRegistration({ intentId: intent.intentId, }); // Verify login works with username const loggedInUser = await sessionService.login( "credentials", "loginuser", "SecurePassword123!", ); expect(loggedInUser?.id).toBe(user.id); }); }); describe("Full registration flow integration", () => { it("should complete full registration flow without verification", async ({ expect, }) => { const { registrationService, sessionService } = await setup({ username: "optional", }); // Phase 1: Create intent const intent = await registrationService.createRegistrationIntent({ email: "fullflow@example.com", username: "fullflowuser", password: "SecurePassword123!", firstName: "Full", lastName: "Flow", }); expect(intent.expectEmailVerification).toBe(false); expect(intent.expectPhoneVerification).toBe(false); // Phase 2: Complete registration const user = await registrationService.completeRegistration({ intentId: intent.intentId, }); expect(user.email).toBe("fullflow@example.com"); expect(user.username).toBe("fullflowuser"); // Verify user can login const session = await sessionService.login( "credentials", "fullflowuser", "SecurePassword123!", ); expect(session?.id).toBe(user.id); }); it("should complete full registration flow with email verification", async ({ expect, }) => { const { registrationService, sessionService, emailProvider, realmProvider, } = await setup(); realmProvider.register("full-verify-realm", { features: { notifications: true, }, settings: { verifyEmailRequired: true, } as never, }); // Phase 1: Create intent const intent = await registrationService.createRegistrationIntent( { email: "fullverify@example.com", password: "SecurePassword123!", }, "full-verify-realm", ); expect(intent.expectEmailVerification).toBe(true); // Extract verification code await expect.poll(() => emailProvider.records.length).toBe(1); const emailCode = extractCode(emailProvider.records[0].body); // Phase 2: Complete registration with code const user = await registrationService.completeRegistration({ intentId: intent.intentId, emailCode, }); expect(user.email).toBe("fullverify@example.com"); expect(user.emailVerified).toBe(true); // Verify user can login const session = await sessionService.login( "credentials", "fullverify@example.com", "SecurePassword123!", "full-verify-realm", ); expect(session?.id).toBe(user.id); }); }); describe("Password policy enforcement during registration", () => { it("should reject registration when password violates realm policy", async ({ expect, }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("strict-policy-realm", { settings: { passwordPolicy: { minLength: 10, requireUppercase: true, requireLowercase: false, requireNumbers: false, requireSpecialCharacters: false, }, } as never, }); await expect( registrationService.createRegistrationIntent( { email: "weakpass@example.com", password: "shortpw!", }, "strict-policy-realm", ), ).rejects.toThrowError(BadRequestError); }); it("should accept registration when password meets realm policy", async ({ expect, }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("strict-policy-realm", { settings: { passwordPolicy: { minLength: 10, requireUppercase: true, requireLowercase: false, requireNumbers: false, requireSpecialCharacters: false, }, } as never, }); const result = await registrationService.createRegistrationIntent( { email: "strongpass@example.com", password: "StrongPass123", }, "strict-policy-realm", ); expect(result.intentId).toBeDefined(); }); }); describe("Registration rate limiting", () => { it("should rate limit registration attempts by IP", async ({ expect }) => { const { alepha, registrationService } = await setup(); await alepha.fork(async () => { alepha.store.set("alepha.http.request", { ip: "10.0.0.1" } as never); // Make 10 registration attempts (they may fail for duplicate email, that's fine) for (let i = 0; i < 10; i++) { await registrationService .createRegistrationIntent({ email: `ratelimit-${i}@example.com`, password: "SecurePassword123!", }) .catch(() => {}); } // 11th attempt should be rate limited await expect( registrationService.createRegistrationIntent({ email: "ratelimit-overflow@example.com", password: "SecurePassword123!", }), ).rejects.toThrowError(BadRequestError); }); }); }); describe("Default roles from realm settings", () => { it("should assign defaultRoles from realm settings to new users", async ({ expect, }) => { const { registrationService, realmProvider } = await setup(); realmProvider.register("custom-roles-realm", { settings: { defaultRoles: ["member", "viewer"], } as never, }); const intent = await registrationService.createRegistrationIntent( { email: "roleuser@example.com", password: "SecurePassword123!", }, "custom-roles-realm", ); const user = await registrationService.completeRegistration({ intentId: intent.intentId, }); expect(user.roles).toEqual(["member", "viewer"]); }); }); describe("Cross-realm email uniqueness", () => { it("should allow same email in different realms", async ({ expect }) => { const { registrationService, userService, realmProvider } = await setup(); realmProvider.register("realm-a"); realmProvider.register("realm-b"); // Create a user with this email in realm-a directly await userService.users("realm-a").create({ realm: "realm-a", email: "shared@example.com", roles: ["user"], }); // Register a new user with the same email in realm-b via registration flow const intent = await registrationService.createRegistrationIntent( { email: "shared@example.com", password: "SecurePassword123!", }, "realm-b", ); const user = await registrationService.completeRegistration({ intentId: intent.intentId, }); expect(user.email).toBe("shared@example.com"); }); }); });