Files
Curio/src/app/api/auth/register/__tests__/route.test.ts
T
arnaudne 4e38b5a791 feat: initial commit — Curio mastery tutor
Full Next.js App Router application with:
- AI-graded lesson checkpoints (BKT/Elo mastery tracking)
- Auth.js v5: credentials, Google, GitHub, generic OIDC
- Anonymous-session-first with migrate-on-signin
- Admin panel: users, blueprints, reports, site settings
- Password reset + email verification (nodemailer/SMTP)
- Site config: require_auth + signups_enabled flags
- server-only guards on all DB/generation/verification modules
- PostgreSQL 16 + pgvector, Redis cache, Drizzle ORM
- 271 unit tests (Vitest), golden-eval harness, Playwright e2e stubs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:25:43 +02:00

123 lines
4.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NextRequest } from 'next/server';
const { mockSelect, mockInsert } = vi.hoisted(() => ({
mockSelect: vi.fn(),
mockInsert: vi.fn(),
}));
vi.mock('@/lib/db', () => ({ db: { select: mockSelect, insert: mockInsert } }));
vi.mock('@/lib/site-config', () => ({ isSignupsEnabled: vi.fn().mockResolvedValue(true) }));
vi.mock('bcryptjs', () => ({
default: { hash: vi.fn().mockResolvedValue('hashed_pw'), compare: vi.fn() },
}));
import { POST } from '../route';
import bcrypt from 'bcryptjs';
const VALID_BODY = { email: 'user@example.com', name: 'Test User', password: 'securepassword123' };
function makeRequest(body: unknown) {
return new NextRequest('http://localhost/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
function setupDefault({ existingUser = false } = {}) {
mockSelect.mockImplementation((fields: Record<string, unknown> | undefined) => {
if (fields && 'total' in fields) {
// count() query — first user, so total = 0
return { from: vi.fn().mockResolvedValue([{ total: 0 }]) };
}
// email duplicate check
const rows = existingUser ? [{ id: 'existing' }] : [];
return { from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue(rows) }) }) };
});
mockInsert.mockReturnValue({
values: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([{ id: 'new-user-uuid' }]) }),
});
}
describe('POST /api/auth/register', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(bcrypt.hash).mockResolvedValue('hashed_pw' as never);
setupDefault();
});
it('returns 400 on bad JSON', async () => {
const req = new NextRequest('http://localhost/api/auth/register', {
method: 'POST',
body: '{not json',
headers: { 'Content-Type': 'application/json' },
});
const res = await POST(req);
expect(res.status).toBe(400);
});
it('returns 400 when email missing', async () => {
const res = await POST(makeRequest({ name: 'Test', password: 'password123' }));
expect(res.status).toBe(400);
});
it('returns 400 when email invalid', async () => {
const res = await POST(makeRequest({ ...VALID_BODY, email: 'not-an-email' }));
expect(res.status).toBe(400);
});
it('returns 400 when name missing', async () => {
const res = await POST(makeRequest({ email: 'a@b.com', password: 'password123' }));
expect(res.status).toBe(400);
});
it('returns 400 when name empty', async () => {
const res = await POST(makeRequest({ ...VALID_BODY, name: '' }));
expect(res.status).toBe(400);
});
it('returns 400 when password too short', async () => {
const res = await POST(makeRequest({ ...VALID_BODY, password: 'short' }));
expect(res.status).toBe(400);
});
it('returns 400 when password over 128 chars', async () => {
const res = await POST(makeRequest({ ...VALID_BODY, password: 'a'.repeat(129) }));
expect(res.status).toBe(400);
});
it('returns 409 when email already registered', async () => {
setupDefault({ existingUser: true });
const res = await POST(makeRequest(VALID_BODY));
expect(res.status).toBe(409);
const body = await res.json();
expect(body.error).toMatch(/already registered/i);
});
it('returns 201 with userId on success', async () => {
const res = await POST(makeRequest(VALID_BODY));
expect(res.status).toBe(201);
const body = await res.json();
expect(body.userId).toBe('new-user-uuid');
});
it('hashes password with cost 12', async () => {
await POST(makeRequest(VALID_BODY));
expect(bcrypt.hash).toHaveBeenCalledWith(VALID_BODY.password, 12);
});
it('stores hashed password not plaintext', async () => {
await POST(makeRequest(VALID_BODY));
const valuesMock = vi.mocked(mockInsert).mock.results[0]?.value?.values;
expect(valuesMock).toHaveBeenCalledWith(expect.objectContaining({ passwordHash: 'hashed_pw' }));
});
it('normalizes email to lowercase', async () => {
await POST(makeRequest({ ...VALID_BODY, email: 'USER@EXAMPLE.COM' }));
const valuesMock = vi.mocked(mockInsert).mock.results[0]?.value?.values;
expect(valuesMock).toHaveBeenCalledWith(expect.objectContaining({ email: 'user@example.com' }));
});
});