- Backend NestJS con módulos de autenticación, inventario, créditos - Frontend React con dashboard y componentes UI - Base de datos PostgreSQL con migraciones - Tests E2E configurados - Configuración de Docker y deployment Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
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<string, any> = {
|
|
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>(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);
|
|
});
|
|
});
|
|
});
|