Files
fluxer/fluxer_api/src/api/auth/tests/Registration.test.ts
T

377 lines
13 KiB
TypeScript

// 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<string, unknown> {
return {
email: createUniqueEmail(prefix),
username: createUniqueUsername(prefix),
global_name: 'Bootstrap Admin',
date_of_birth: '2000-01-01',
consent: true,
};
}
function bootstrapRegistrationBodyWithDnsEmail(prefix: string): Record<string, unknown> {
return {
...bootstrapRegistrationBody(prefix),
email: `${prefix}-${randomUUID()}@gmail.com`,
};
}
async function withBootstrapAdminConfig(
configOverrides: {
selfHosted: boolean;
testModeEnabled: boolean;
},
callback: () => Promise<void>,
): Promise<void> {
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<string>): Promise<void> {
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: '[email protected]',
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: '[email protected]',
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<string, unknown>;
}> = [
{
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: '[email protected]',
username: '',
global_name: 'Test User',
password: 'a-strong-password',
date_of_birth: '2000-01-01',
consent: true,
},
},
{
name: 'missing password',
body: {
email: '[email protected]',
username: 'itest',
global_name: 'Test User',
password: '',
date_of_birth: '2000-01-01',
consent: true,
},
},
{
name: 'missing date of birth',
body: {
email: '[email protected]',
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<LoginSuccessResponse>(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 = '[email protected]';
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<LoginSuccessResponse>(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: '[email protected]',
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('[email protected]');
});
});