import { INestApplication, ValidationPipe } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtModule } from '@nestjs/jwt'; import * as request from 'supertest'; import { DataSource } from 'typeorm'; import { AuthModule } from '../src/modules/auth/auth.module'; import { UsersModule } from '../src/modules/users/users.module'; import { CreditsModule } from '../src/modules/credits/credits.module'; import { User } from '../src/modules/users/entities/user.entity'; import { Otp } from '../src/modules/auth/entities/otp.entity'; import { RefreshToken } from '../src/modules/auth/entities/refresh-token.entity'; import { CreditBalance } from '../src/modules/credits/entities/credit-balance.entity'; import { CreditTransaction } from '../src/modules/credits/entities/credit-transaction.entity'; import { CreditPackage } from '../src/modules/credits/entities/credit-package.entity'; describe('AuthController (e2e)', () => { let app: INestApplication; let dataSource: DataSource; const testPhone = '5512345678'; const testName = 'Test User'; const testPassword = 'testpassword123'; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env.test', }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ type: 'postgres', host: configService.get('DATABASE_HOST', 'localhost'), port: configService.get('DATABASE_PORT', 5433), username: configService.get('DATABASE_USER', 'postgres'), password: configService.get('DATABASE_PASSWORD', 'postgres'), database: configService.get('DATABASE_NAME', 'miinventario_test'), entities: [User, Otp, RefreshToken, CreditBalance, CreditTransaction, CreditPackage], synchronize: true, dropSchema: true, }), inject: [ConfigService], }), JwtModule.register({ secret: 'test-secret', signOptions: { expiresIn: '15m' }, }), AuthModule, UsersModule, CreditsModule, ], }) .overrideProvider(ConfigService) .useValue({ get: (key: string, defaultValue?: any) => { const config: Record = { NODE_ENV: 'development', // Use development mode for test OTP DATABASE_HOST: process.env.DATABASE_HOST || 'localhost', DATABASE_PORT: parseInt(process.env.DATABASE_PORT || '5433'), DATABASE_NAME: process.env.DATABASE_NAME || 'miinventario_test', DATABASE_USER: process.env.DATABASE_USER || 'postgres', DATABASE_PASSWORD: process.env.DATABASE_PASSWORD || 'postgres', JWT_SECRET: 'test-secret', JWT_EXPIRES_IN: '15m', JWT_REFRESH_SECRET: 'test-refresh-secret', JWT_REFRESH_EXPIRES_IN: '7d', }; return config[key] ?? defaultValue; }, }) .compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.init(); dataSource = moduleFixture.get(DataSource); }); afterAll(async () => { if (dataSource?.isInitialized) { await dataSource.destroy(); } await app.close(); }); beforeEach(async () => { // Clean up test data before each test await dataSource.query('DELETE FROM refresh_tokens'); await dataSource.query('DELETE FROM otps'); await dataSource.query('DELETE FROM credit_transactions'); await dataSource.query('DELETE FROM credit_balances'); await dataSource.query('DELETE FROM users'); }); describe('POST /auth/register', () => { it('should initiate registration and send OTP', async () => { const response = await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone, name: testName }) .expect(201); expect(response.body).toMatchObject({ message: 'Codigo de verificacion enviado', expiresIn: 300, }); }); it('should fail with invalid phone format', async () => { await request(app.getHttpServer()) .post('/auth/register') .send({ phone: '123', name: testName }) .expect(400); }); it('should fail with missing name', async () => { await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone }) .expect(400); }); it('should fail if phone already registered', async () => { // Create existing user await dataSource.query( `INSERT INTO users (id, phone, name, role, "isActive") VALUES (uuid_generate_v4(), $1, $2, 'USER', true)`, [testPhone, testName], ); await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone, name: testName }) .expect(400); }); }); describe('POST /auth/verify-otp', () => { it('should verify OTP and create account (dev mode with 123456)', async () => { // First initiate registration await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone, name: testName }) .expect(201); // Verify with test OTP (123456 works in dev mode) const response = await request(app.getHttpServer()) .post('/auth/verify-otp') .send({ phone: testPhone, name: testName, otp: '123456', password: testPassword, }) .expect(200); expect(response.body).toHaveProperty('user'); expect(response.body).toHaveProperty('accessToken'); expect(response.body).toHaveProperty('refreshToken'); expect(response.body.user.phone).toBe(testPhone); expect(response.body.user.name).toBe(testName); }); it('should fail with invalid OTP', async () => { await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone, name: testName }) .expect(201); await request(app.getHttpServer()) .post('/auth/verify-otp') .send({ phone: testPhone, name: testName, otp: '000000', password: testPassword, }) .expect(400); }); it('should fail with short password', async () => { await request(app.getHttpServer()) .post('/auth/verify-otp') .send({ phone: testPhone, name: testName, otp: '123456', password: '123', }) .expect(400); }); }); describe('POST /auth/login', () => { beforeEach(async () => { // Create a user with password await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone, name: testName }); await request(app.getHttpServer()) .post('/auth/verify-otp') .send({ phone: testPhone, name: testName, otp: '123456', password: testPassword, }); }); it('should login with valid credentials', async () => { const response = await request(app.getHttpServer()) .post('/auth/login') .send({ phone: testPhone, password: testPassword }) .expect(200); expect(response.body).toHaveProperty('user'); expect(response.body).toHaveProperty('accessToken'); expect(response.body).toHaveProperty('refreshToken'); expect(response.body.user.phone).toBe(testPhone); }); it('should fail with invalid password', async () => { await request(app.getHttpServer()) .post('/auth/login') .send({ phone: testPhone, password: 'wrongpassword' }) .expect(401); }); it('should fail with non-existent phone', async () => { await request(app.getHttpServer()) .post('/auth/login') .send({ phone: '9999999999', password: testPassword }) .expect(401); }); }); describe('POST /auth/refresh', () => { let refreshToken: string; beforeEach(async () => { await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone, name: testName }); const verifyResponse = await request(app.getHttpServer()) .post('/auth/verify-otp') .send({ phone: testPhone, name: testName, otp: '123456', password: testPassword, }); refreshToken = verifyResponse.body.refreshToken; }); it('should refresh tokens with valid refresh token', async () => { const response = await request(app.getHttpServer()) .post('/auth/refresh') .send({ refreshToken }) .expect(200); expect(response.body).toHaveProperty('accessToken'); expect(response.body).toHaveProperty('refreshToken'); expect(response.body.expiresIn).toBe(900); // Verify new tokens are valid JWTs expect(response.body.accessToken.split('.')).toHaveLength(3); expect(response.body.refreshToken.split('.')).toHaveLength(3); }); it('should fail with invalid refresh token', async () => { await request(app.getHttpServer()) .post('/auth/refresh') .send({ refreshToken: 'invalid-token' }) .expect(401); }); }); describe('POST /auth/logout', () => { let refreshToken: string; beforeEach(async () => { await request(app.getHttpServer()) .post('/auth/register') .send({ phone: testPhone, name: testName }); const verifyResponse = await request(app.getHttpServer()) .post('/auth/verify-otp') .send({ phone: testPhone, name: testName, otp: '123456', password: testPassword, }); refreshToken = verifyResponse.body.refreshToken; }); it('should logout and invalidate refresh token', async () => { await request(app.getHttpServer()) .post('/auth/logout') .send({ refreshToken }) .expect(200); // Refresh should fail after logout await request(app.getHttpServer()) .post('/auth/refresh') .send({ refreshToken }) .expect(401); }); }); });