/** * @ai-summary Integration tests for auth API endpoints * @ai-context Tests complete request/response cycle with mocked Auth0 */ import request from 'supertest'; import { buildApp } from '../../../../app'; import { pool } from '../../../../core/config/database'; import { auth0ManagementClient } from '../../../../core/auth/auth0-management.client'; import fastifyPlugin from 'fastify-plugin'; import { FastifyInstance } from 'fastify'; // Mock Auth0 Management client jest.mock('../../../../core/auth/auth0-management.client'); const mockAuth0Client = jest.mocked(auth0ManagementClient); // Mock auth plugin for protected routes jest.mock('../../../../core/plugins/auth.plugin', () => { return { default: fastifyPlugin(async function (fastify) { fastify.decorate('authenticate', async function (request, _reply) { // JWT sub is still auth0|xxx format request.user = { sub: 'auth0|test-user-123' }; }); }, { name: 'auth-plugin' }), }; }); describe('Auth Integration Tests', () => { let app: FastifyInstance; beforeAll(async () => { app = await buildApp(); await app.ready(); // Ensure user_profiles table exists (should be created by migrations) // We don't need to run migration here as it should already exist }); afterAll(async () => { await app.close(); await pool.end(); }); beforeEach(async () => { jest.clearAllMocks(); // Clean up test data before each test await pool.query('DELETE FROM user_profiles WHERE auth0_sub = $1', [ 'auth0|test-user-123', ]); await pool.query('DELETE FROM user_profiles WHERE email = $1', [ 'newuser@example.com', ]); }); describe('POST /api/auth/signup', () => { it('should create a new user successfully', async () => { const email = 'newuser@example.com'; const password = 'Password123'; const auth0UserId = 'auth0|new-user-456'; mockAuth0Client.createUser.mockResolvedValue(auth0UserId); const response = await request(app.server) .post('/api/auth/signup') .send({ email, password }) .expect(201); expect(response.body).toMatchObject({ userId: auth0UserId, email, message: expect.stringContaining('check your email'), }); expect(mockAuth0Client.createUser).toHaveBeenCalledWith({ email, password }); // Verify user was created in local database const userResult = await pool.query( 'SELECT * FROM user_profiles WHERE auth0_sub = $1', [auth0UserId] ); expect(userResult.rows).toHaveLength(1); expect(userResult.rows[0].email).toBe(email); expect(userResult.rows[0].email_verified).toBe(false); }); it('should reject signup with invalid email', async () => { const response = await request(app.server) .post('/api/auth/signup') .send({ email: 'invalid-email', password: 'Password123' }) .expect(400); expect(response.body.message).toContain('validation'); }); it('should reject signup with weak password', async () => { const response = await request(app.server) .post('/api/auth/signup') .send({ email: 'test@example.com', password: 'weak' }) .expect(400); expect(response.body.message).toContain('validation'); }); it('should reject signup when email already exists', async () => { const email = 'existing@example.com'; const password = 'Password123'; mockAuth0Client.createUser.mockRejectedValue( new Error('User already exists') ); const response = await request(app.server) .post('/api/auth/signup') .send({ email, password }) .expect(409); expect(response.body.error).toBe('Conflict'); expect(response.body.message).toContain('already exists'); }); }); describe('GET /api/auth/verify-status', () => { beforeEach(async () => { // Create a test user profile await pool.query( 'INSERT INTO user_profiles (auth0_sub, email, email_verified) VALUES ($1, $2, $3)', ['auth0|test-user-123', 'test@example.com', false] ); }); it('should return verification status', async () => { const email = 'test@example.com'; mockAuth0Client.getUser.mockResolvedValue({ userId: 'auth0|test-user-123', email, emailVerified: false, }); const response = await request(app.server) .get('/api/auth/verify-status') .expect(200); expect(response.body).toMatchObject({ emailVerified: false, email, }); expect(mockAuth0Client.getUser).toHaveBeenCalledWith('auth0|test-user-123'); }); it('should update local database when verification status changes', async () => { const email = 'test@example.com'; mockAuth0Client.getUser.mockResolvedValue({ userId: 'auth0|test-user-123', email, emailVerified: true, }); const response = await request(app.server) .get('/api/auth/verify-status') .expect(200); expect(response.body.emailVerified).toBe(true); // Verify local database was updated const userResult = await pool.query( 'SELECT email_verified FROM user_profiles WHERE auth0_sub = $1', ['auth0|test-user-123'] ); expect(userResult.rows[0].email_verified).toBe(true); }); }); describe('POST /api/auth/resend-verification', () => { beforeEach(async () => { // Create a test user profile await pool.query( 'INSERT INTO user_profiles (auth0_sub, email, email_verified) VALUES ($1, $2, $3)', ['auth0|test-user-123', 'test@example.com', false] ); }); it('should resend verification email when user is not verified', async () => { mockAuth0Client.checkEmailVerified.mockResolvedValue(false); mockAuth0Client.resendVerificationEmail.mockResolvedValue(undefined); const response = await request(app.server) .post('/api/auth/resend-verification') .expect(200); expect(response.body.message).toContain('Verification email sent'); expect(mockAuth0Client.checkEmailVerified).toHaveBeenCalledWith( 'auth0|test-user-123' ); expect(mockAuth0Client.resendVerificationEmail).toHaveBeenCalledWith( 'auth0|test-user-123' ); }); it('should skip sending email when user is already verified', async () => { mockAuth0Client.checkEmailVerified.mockResolvedValue(true); const response = await request(app.server) .post('/api/auth/resend-verification') .expect(200); expect(response.body.message).toContain('already verified'); expect(mockAuth0Client.resendVerificationEmail).not.toHaveBeenCalled(); }); }); });