import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import * as request from 'supertest'; import { DataSource } from 'typeorm'; import { AuthModule } from '../src/auth/auth.module'; import { UserProfile } from '../src/entities/user-profile.entity'; import { UserIdentity } from '../src/entities/user-identity.entity'; describe('AuthController (e2e)', () => { let app: INestApplication; let dataSource: DataSource; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'postgres', url: process.env.TEST_DATABASE_URL || 'postgresql://user:password@localhost:5432/meteor_test', entities: [UserProfile, UserIdentity], synchronize: true, // Use synchronize for tests dropSchema: true, // Clean slate for each test run logging: false, }), AuthModule, ], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); dataSource = moduleFixture.get(DataSource); await app.init(); }); afterAll(async () => { await dataSource.destroy(); await app.close(); }); afterEach(async () => { // Clean up database after each test await dataSource.query( 'TRUNCATE TABLE user_identities, user_profiles CASCADE', ); }); describe('/api/v1/auth/register-email (POST)', () => { const validRegistrationData = { email: 'test@example.com', password: 'Password123', displayName: 'Test User', }; it('should register a new user successfully', () => { return request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send(validRegistrationData) .expect(201) .expect((res) => { expect(res.body).toHaveProperty( 'message', 'User registered successfully', ); expect(res.body).toHaveProperty('userId'); expect(typeof res.body.userId).toBe('string'); expect(res.body).not.toHaveProperty('password'); expect(res.body).not.toHaveProperty('passwordHash'); }); }); it('should create records in both user_profiles and user_identities tables', async () => { await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send(validRegistrationData) .expect(201); // Check user_profiles table const userProfiles = await dataSource.query( 'SELECT * FROM user_profiles', ); expect(userProfiles).toHaveLength(1); expect(userProfiles[0].display_name).toBe('Test User'); // Check user_identities table const userIdentities = await dataSource.query( 'SELECT * FROM user_identities', ); expect(userIdentities).toHaveLength(1); expect(userIdentities[0].email).toBe('test@example.com'); expect(userIdentities[0].provider).toBe('email'); expect(userIdentities[0].provider_id).toBe('test@example.com'); expect(userIdentities[0].password_hash).toBeTruthy(); expect(userIdentities[0].password_hash).not.toBe('Password123'); // Should be hashed // Check foreign key relationship expect(userIdentities[0].user_profile_id).toBe(userProfiles[0].id); }); it('should return 409 Conflict when email already exists', async () => { // First registration await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send(validRegistrationData) .expect(201); // Second registration with same email return request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send(validRegistrationData) .expect(409) .expect((res) => { expect(res.body.message).toBe('Email is already registered'); }); }); it('should return 400 Bad Request for invalid email format', () => { return request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ ...validRegistrationData, email: 'invalid-email', }) .expect(400) .expect((res) => { expect(res.body.message).toContain( 'Please provide a valid email address', ); }); }); it('should return 400 Bad Request for password too short', () => { return request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ ...validRegistrationData, password: '123', }) .expect(400) .expect((res) => { expect(res.body.message).toContain( 'Password must be at least 8 characters long', ); }); }); it('should return 400 Bad Request for password without complexity requirements', () => { return request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ ...validRegistrationData, password: 'password', }) .expect(400) .expect((res) => { expect(res.body.message).toContain( 'Password must contain at least one lowercase letter, one uppercase letter, and one number', ); }); }); it('should return 400 Bad Request for missing required fields', () => { return request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ email: 'test@example.com', // Missing password and displayName }) .expect(400); }); it('should reject extra fields not in DTO', () => { return request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ ...validRegistrationData, extraField: 'should not be accepted', }) .expect(400); }); it('should handle database transaction rollback on error gracefully', async () => { // This test would ideally simulate a database error during transaction // For simplicity, we'll test that the endpoint handles unexpected errors // Mock a scenario where the service might fail after validation passes const invalidData = { email: 'test@example.com', password: 'Password123', displayName: 'Test User', }; const response = await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send(invalidData); // Should either succeed (201) or fail gracefully with proper error handling expect([201, 400, 409, 500]).toContain(response.status); }); }); describe('/api/v1/auth/login-email (POST)', () => { const validLoginData = { email: 'test@example.com', password: 'Password123', }; beforeEach(async () => { // Register a user first for login tests await request(app.getHttpServer()) .post('/api/v1/auth/register-email') .send({ email: 'test@example.com', password: 'Password123', displayName: 'Test User', }) .expect(201); }); it('should login successfully with valid credentials', () => { return request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send(validLoginData) .expect(200) .expect((res) => { expect(res.body).toHaveProperty('message', 'Login successful'); expect(res.body).toHaveProperty('userId'); expect(res.body).toHaveProperty('accessToken'); expect(res.body).toHaveProperty('refreshToken'); expect(typeof res.body.userId).toBe('string'); expect(typeof res.body.accessToken).toBe('string'); expect(typeof res.body.refreshToken).toBe('string'); expect(res.body.accessToken).not.toBe(res.body.refreshToken); }); }); it('should return 401 Unauthorized for non-existent user', () => { return request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'nonexistent@example.com', password: 'Password123', }) .expect(401) .expect((res) => { expect(res.body.message).toBe('Invalid credentials'); }); }); it('should return 401 Unauthorized for incorrect password', () => { return request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'test@example.com', password: 'WrongPassword123', }) .expect(401) .expect((res) => { expect(res.body.message).toBe('Invalid credentials'); }); }); it('should return 400 Bad Request for invalid email format', () => { return request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'invalid-email', password: 'Password123', }) .expect(400) .expect((res) => { expect(res.body.message).toContain( 'Please provide a valid email address', ); }); }); it('should return 400 Bad Request for missing password', () => { return request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ email: 'test@example.com', }) .expect(400) .expect((res) => { expect(res.body.message).toContain('Password is required'); }); }); it('should return 400 Bad Request for missing email', () => { return request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send({ password: 'Password123', }) .expect(400) .expect((res) => { expect(res.body.message).toContain('Email is required'); }); }); it('should generate valid JWT tokens that can be decoded', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/auth/login-email') .send(validLoginData) .expect(200); const { accessToken, refreshToken } = response.body; // Basic JWT format validation (should have 3 parts separated by dots) expect(accessToken.split('.')).toHaveLength(3); expect(refreshToken.split('.')).toHaveLength(3); // Tokens should be different expect(accessToken).not.toBe(refreshToken); }); }); });