// SPDX-License-Identifier: AGPL-3.0-or-later import {randomUUID} from 'node:crypto'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; import {createUserID} from '../../BrandedTypes'; import {getConfig} from '../../Config'; import {getInstanceConfigRepository, getUserRepository} from '../../middleware/ServiceSingletons'; import {torExitListCache} from '../../middleware/TorExitListCache'; import type {ApiTestHarness} from '../../test/ApiTestHarness'; import {createBuilder, createBuilderWithoutAuth} from '../../test/TestRequestBuilder'; import { createAuthHarness, createUniqueEmail, createUniqueUsername, fetchMe, type LoginSuccessResponse, registerUser, titleCaseEmail, type UserMeResponse, } from './AuthTestUtils'; function bootstrapRegistrationBody(prefix: string): Record { return { email: createUniqueEmail(prefix), username: createUniqueUsername(prefix), global_name: 'Bootstrap Admin', date_of_birth: '2000-01-01', consent: true, }; } function bootstrapRegistrationBodyWithDnsEmail(prefix: string): Record { return { ...bootstrapRegistrationBody(prefix), email: `${prefix}-${randomUUID()}@gmail.com`, }; } async function withBootstrapAdminConfig( configOverrides: { selfHosted: boolean; testModeEnabled: boolean; }, callback: () => Promise, ): Promise { const config = getConfig(); const originalSelfHosted = config.instance.selfHosted; const originalTestModeEnabled = config.dev.testModeEnabled; try { config.instance.selfHosted = configOverrides.selfHosted; config.dev.testModeEnabled = configOverrides.testModeEnabled; await callback(); } finally { config.instance.selfHosted = originalSelfHosted; config.dev.testModeEnabled = originalTestModeEnabled; } } async function expectUserACLs(userId: string, expectedACLs: Array): Promise { const user = await getUserRepository().findUniqueAssert(createUserID(BigInt(userId))); expect([...user.acls].sort()).toEqual([...expectedACLs].sort()); } describe('Auth registration', () => { let harness: ApiTestHarness; beforeAll(async () => { harness = await createAuthHarness(); }); beforeEach(async () => { await harness.reset(); }); afterAll(async () => { await harness?.shutdown(); }); it('returns token and user_id', async () => { const email = createUniqueEmail('register'); const reg = await registerUser(harness, { email, username: createUniqueUsername('register'), global_name: 'Register User', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }); expect(reg.token.length).toBeGreaterThan(0); expect(reg.user_id.length).toBeGreaterThan(0); }); it('grants wildcard admin ACL to first accepted local dev registration', async () => { await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: false}, async () => { const first = await registerUser(harness, bootstrapRegistrationBodyWithDnsEmail('localdevadminone')); const second = await registerUser(harness, bootstrapRegistrationBodyWithDnsEmail('localdevadmintwo')); await expectUserACLs(first.user_id, [AdminACLs.WILDCARD]); await expectUserACLs(second.user_id, []); await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true); }); }); it('does not grant local dev bootstrap admin ACL in test mode', async () => { await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => { const account = await registerUser(harness, bootstrapRegistrationBody('testmodeadmin')); await expectUserACLs(account.user_id, []); await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(false); }); }); it('grants wildcard admin ACL to first accepted self-hosted registration', async () => { await withBootstrapAdminConfig({selfHosted: true, testModeEnabled: true}, async () => { const first = await registerUser(harness, bootstrapRegistrationBody('selfhostadminone')); const second = await registerUser(harness, bootstrapRegistrationBody('selfhostadmintwo')); await expectUserACLs(first.user_id, [AdminACLs.WILDCARD]); await expectUserACLs(second.user_id, []); await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true); }); }); it('grants wildcard admin ACL to first accepted unconfigured setup registration', async () => { await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => { await getInstanceConfigRepository().setAppPublicConfig({setup: {configured: false}}); const first = await registerUser(harness, bootstrapRegistrationBody('setupopenadminone')); const second = await registerUser(harness, bootstrapRegistrationBody('setupopenadmintwo')); await expectUserACLs(first.user_id, [AdminACLs.WILDCARD]); await expectUserACLs(second.user_id, []); await expect(getInstanceConfigRepository().isAdminBootstrapped()).resolves.toBe(true); }); }); it('allows setup-open sessions to fetch instance config when bootstrap marker is stale', async () => { await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => { const instanceConfigRepository = getInstanceConfigRepository(); await instanceConfigRepository.setAppPublicConfig({setup: {configured: false}}); await instanceConfigRepository.markAdminBootstrapped(); const account = await registerUser(harness, bootstrapRegistrationBody('stalesetupmarker')); await expectUserACLs(account.user_id, []); await createBuilder(harness, account.token).post('/admin/instance-config/get').body({}).execute(); }); }); it('repairs setup completer admin ACL when bootstrap marker is stale', async () => { await withBootstrapAdminConfig({selfHosted: false, testModeEnabled: true}, async () => { const instanceConfigRepository = getInstanceConfigRepository(); await instanceConfigRepository.setAppPublicConfig({setup: {configured: false}}); await instanceConfigRepository.markAdminBootstrapped(); const account = await registerUser(harness, bootstrapRegistrationBody('stalesetupcomplete')); await expectUserACLs(account.user_id, []); await createBuilder(harness, account.token) .post('/admin/instance-config/update') .body({app_public: {setup: {configured: true}}}) .execute(); await expectUserACLs(account.user_id, [AdminACLs.WILDCARD]); await createBuilder(harness, account.token).post('/admin/instance-config/get').body({}).execute(); }); }); it('allows emoji global name', async () => { const globalName = '🌻 Sunflower'; const reg = await registerUser(harness, { email: createUniqueEmail('global-name-emoji'), username: createUniqueUsername('globalnameemoji'), global_name: globalName, password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }); const me = (await fetchMe(harness, reg.token)).json as UserMeResponse; expect(me.global_name).toBe(globalName); }); it('derives username from display name when username is omitted', async () => { const reg = await registerUser(harness, { email: createUniqueEmail('derived-username'), password: 'a-strong-password', global_name: 'Magic Tester', date_of_birth: '2000-01-01', consent: true, }); const me = (await fetchMe(harness, reg.token)).json as UserMeResponse; expect(me.username).toBe('Magic_Tester'); }); it('rejects invalid registration payloads', async () => { await createBuilderWithoutAuth(harness) .post('/auth/register') .body({ email: 'not-an-email', username: 'itest', global_name: 'Test User', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }) .expect(400) .execute(); await createBuilderWithoutAuth(harness) .post('/auth/register') .body({ email: createUniqueEmail('weak-password'), username: 'itest', global_name: 'Test User', password: 'weak', date_of_birth: '2000-01-01', consent: true, }) .expect(400) .execute(); await registerUser(harness, { email: 'integration-duplicate-email@example.com', username: createUniqueUsername('firstuser'), global_name: 'Test User', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }); const duplicateJson = await createBuilderWithoutAuth<{ code: string; errors: Array<{ path: string; message: string; }>; }>(harness) .post('/auth/register') .body({ email: 'integration-duplicate-email@example.com', username: createUniqueUsername('seconduser'), global_name: 'Test User', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }) .expect(400) .execute(); expect(duplicateJson.code).toBe('INVALID_FORM_BODY'); const emailError = duplicateJson.errors.find((e) => e.path === 'email'); expect(emailError?.message).toBe('Email is already in use.'); const missingFieldsCases: Array<{ name: string; body: Record; }> = [ { name: 'missing email', body: { email: '', username: 'itest', global_name: 'Test User', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }, }, { name: 'missing username', body: { email: 'integration-missing-username@example.com', username: '', global_name: 'Test User', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }, }, { name: 'missing password', body: { email: 'integration-missing-password@example.com', username: 'itest', global_name: 'Test User', password: '', date_of_birth: '2000-01-01', consent: true, }, }, { name: 'missing date of birth', body: { email: 'integration-missing-dob@example.com', username: 'itest', global_name: 'Test User', password: 'a-strong-password', date_of_birth: '', consent: true, }, }, ]; for (const testCase of missingFieldsCases) { await createBuilderWithoutAuth(harness).post('/auth/register').body(testCase.body).expect(400).execute(); } }); it('allows login after registration', async () => { const email = createUniqueEmail('login'); const password = 'a-strong-password'; const reg = await registerUser(harness, { email, username: createUniqueUsername('loginuser'), global_name: 'Login User', password, date_of_birth: '2000-01-01', consent: true, }); const login = await createBuilderWithoutAuth(harness) .post('/auth/login') .body({email, password}) .execute(); expect('mfa' in login).toBe(false); expect(login.token.length).toBeGreaterThan(0); expect(login.user_id).toBe(reg.user_id); }); it('blocks any request from a Tor exit at the edge', async () => { torExitListCache.seedForTesting(['127.0.0.1']); try { await createBuilderWithoutAuth(harness) .post('/auth/register') .body({ email: createUniqueEmail('tor-register'), username: createUniqueUsername('torregister'), global_name: 'Tor Register', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }) .expect(403, 'GLOBAL_IP_BANNED') .execute(); } finally { torExitListCache.clearForTesting(); } }); it('treats email as case-insensitive across auth flows', async () => { const baseEmail = 'Integration-Test-Case-Email@Example.COM'; const password = 'a-strong-password'; await registerUser(harness, { email: baseEmail, username: createUniqueUsername('caseuser'), global_name: 'Test User', password, date_of_birth: '2000-01-01', consent: true, }); const loginEmails = [baseEmail.toLowerCase(), baseEmail.toUpperCase(), titleCaseEmail(baseEmail)]; for (const email of loginEmails) { const login = await createBuilderWithoutAuth(harness) .post('/auth/login') .body({email, password}) .execute(); expect(login.token.length).toBeGreaterThan(0); } const duplicateJson = await createBuilderWithoutAuth<{ code: string; errors: Array<{ path: string; message: string; }>; }>(harness) .post('/auth/register') .body({ email: baseEmail.toUpperCase(), username: createUniqueUsername('caseuser2'), global_name: 'Test User', password: 'another-strong-password', date_of_birth: '2000-01-01', consent: true, }) .expect(400) .execute(); expect(duplicateJson.code).toBe('INVALID_FORM_BODY'); const emailError = duplicateJson.errors.find((e) => e.path === 'email'); expect(emailError?.message).toBe('Email is already in use.'); await createBuilderWithoutAuth(harness) .post('/auth/forgot') .body({email: baseEmail.toUpperCase()}) .expect(204) .execute(); const caseEmailUser = await registerUser(harness, { email: 'integration-case-store-email@example.com', username: createUniqueUsername('caseemailstored'), global_name: 'Stored Email', password: 'a-strong-password', date_of_birth: '2000-01-01', consent: true, }); const me = (await fetchMe(harness, caseEmailUser.token)).json as UserMeResponse; expect(me.email).toBe('integration-case-store-email@example.com'); }); });