process.env.DATABASE_PATH = ':memory:'; process.env.THEMES_DIR = './themes'; process.env.FRONTEND_DIST = './frontend/dist'; import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import * as express from 'express'; import request from 'supertest'; import { CookieAccessInfo } from 'cookiejar'; import { AppModule } from '../../backend/src/app.module'; import { GlobalExceptionFilter } from '../../backend/src/common/filters/global-exception.filter'; import { CsrfMiddleware } from '../../backend/src/common/middleware/csrf.middleware'; import { ConfigService } from '@nestjs/config'; import { initDb } from './db-helper'; import { SettingsService } from '../../backend/src/modules/settings/settings.module'; describe('POST /api/v1/auth/register (player self-registration)', () => { let app: INestApplication; let settings: SettingsService; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = moduleRef.createNestApplication(); app.use(cookieParser()); app.use(express.json({ limit: '1mb' })); const csrfMw = new CsrfMiddleware(app.get(ConfigService)); app.use((req: any, res: any, next: any) => csrfMw.use(req, res, next)); const httpAdapterHost = app.get(HttpAdapterHost); app.useGlobalFilters(new GlobalExceptionFilter(httpAdapterHost)); await app.init(); await initDb(app); settings = app.get(SettingsService); await settings.set('registrationsEnabled', 'true'); }); afterAll(async () => { await app.close(); }); async function primeCsrf(agent: any): Promise { const res = await agent.get('/api/v1/auth/csrf'); const cookies: any = agent.jar.getCookies(CookieAccessInfo.All); return cookies.find((c: any) => c.name === 'csrf').value; } it('rejects without CSRF token', async () => { const server = app.getHttpServer(); const agent = request.agent(server); await agent.get('/api/v1/auth/csrf'); await agent .post('/api/v1/auth/register') .send({ username: 'alice', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }) .expect(403); }); it('registers a new player and returns a session', async () => { const server = app.getHttpServer(); const agent = request.agent(server); const csrf = await primeCsrf(agent); const res = await agent .post('/api/v1/auth/register') .set('X-CSRF-Token', csrf) .send({ username: 'alice', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }) .expect(201); expect(res.body.accessToken).toBeDefined(); expect(res.body.user.username).toBe('alice'); expect(res.body.user.role).toBe('player'); const cookies: any = agent.jar.getCookies(CookieAccessInfo.All); expect(cookies.find((c: any) => c.name === 'rt')).toBeDefined(); }); it('rejects mismatched password confirmation', async () => { const server = app.getHttpServer(); const agent = request.agent(server); const csrf = await primeCsrf(agent); const res = await agent .post('/api/v1/auth/register') .set('X-CSRF-Token', csrf) .send({ username: 'bob', password: 'Sup3rSecret!Pass', passwordConfirm: 'OtherP4ss!' }); expect(res.status).toBe(400); expect(res.body.code).toBe('VALIDATION_FAILED'); }); it('rejects weak password', async () => { const server = app.getHttpServer(); const agent = request.agent(server); const csrf = await primeCsrf(agent); const res = await agent .post('/api/v1/auth/register') .set('X-CSRF-Token', csrf) .send({ username: 'weak', password: 'short', passwordConfirm: 'short' }); expect(res.status).toBe(400); expect(res.body.code).toBe('WEAK_PASSWORD'); }); it('rejects duplicate username', async () => { const server = app.getHttpServer(); const agent = request.agent(server); const csrf = await primeCsrf(agent); await agent .post('/api/v1/auth/register') .set('X-CSRF-Token', csrf) .send({ username: 'carol', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }) .expect(201); const csrf2 = await primeCsrf(agent); const res = await agent .post('/api/v1/auth/register') .set('X-CSRF-Token', csrf2) .send({ username: 'carol', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }); expect(res.status).toBe(409); expect(res.body.code).toBe('USERNAME_TAKEN'); }); it('rejects when registrations are disabled', async () => { await settings.set('registrationsEnabled', 'false'); const server = app.getHttpServer(); const agent = request.agent(server); const csrf = await primeCsrf(agent); const res = await agent .post('/api/v1/auth/register') .set('X-CSRF-Token', csrf) .send({ username: 'dave', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }); expect(res.status).toBe(403); expect(res.body.code).toBe('REGISTRATIONS_DISABLED'); await settings.set('registrationsEnabled', 'true'); }); it('rejects when per-IP registration rate limit exceeded', async () => { const { RegistrationRateLimitService } = await import('../../backend/src/common/services/registration-rate-limit.service'); const rl = app.get(RegistrationRateLimitService); const now = Date.now(); for (let i = 0; i < 10; i++) { rl.record('::ffff:127.0.0.1', now + i); rl.record('127.0.0.1', now + i); } const server = app.getHttpServer(); const agent = request.agent(server); const csrf = await primeCsrf(agent); const res = await agent .post('/api/v1/auth/register') .set('X-CSRF-Token', csrf) .send({ username: 'eve', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }); expect(res.status).toBe(429); expect(res.body.code).toBe('RATE_LIMITED'); }); });