miinventario-v2/apps/backend/test/stores.e2e-spec.ts
rckrdmrd 1a53b5c4d3 [MIINVENTARIO] feat: Initial commit - Sistema de inventario con análisis de video IA
- 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>
2026-01-13 02:25:48 -06:00

324 lines
11 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, JwtService } from '@nestjs/jwt';
import * as request from 'supertest';
import { DataSource } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { AuthModule } from '../src/modules/auth/auth.module';
import { UsersModule } from '../src/modules/users/users.module';
import { StoresModule } from '../src/modules/stores/stores.module';
import { CreditsModule } from '../src/modules/credits/credits.module';
import { User } from '../src/modules/users/entities/user.entity';
import { Store } from '../src/modules/stores/entities/store.entity';
import { StoreUser } from '../src/modules/stores/entities/store-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('StoresController (e2e)', () => {
let app: INestApplication;
let dataSource: DataSource;
let jwtService: JwtService;
let accessToken: string;
let userId: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: () => ({
type: 'postgres',
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT || '5433'),
username: process.env.DATABASE_USER || 'postgres',
password: process.env.DATABASE_PASSWORD || 'postgres',
database: process.env.DATABASE_NAME || 'miinventario_test',
entities: [User, Store, StoreUser, Otp, RefreshToken, CreditBalance, CreditTransaction, CreditPackage],
synchronize: true,
dropSchema: true,
}),
inject: [ConfigService],
}),
JwtModule.register({
secret: 'test-secret',
signOptions: { expiresIn: '15m' },
}),
AuthModule,
UsersModule,
StoresModule,
CreditsModule,
],
})
.overrideProvider(ConfigService)
.useValue({
get: (key: string, defaultValue?: any) => {
const config: Record<string, any> = {
NODE_ENV: 'development',
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);
jwtService = moduleFixture.get<JwtService>(JwtService);
});
afterAll(async () => {
if (dataSource?.isInitialized) {
await dataSource.destroy();
}
await app.close();
});
beforeEach(async () => {
// Clean up
await dataSource.query('DELETE FROM store_users');
await dataSource.query('DELETE FROM stores');
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');
// Create test user
const passwordHash = await bcrypt.hash('testpassword123', 10);
const result = await dataSource.query(
`INSERT INTO users (id, phone, name, "passwordHash", role, "isActive")
VALUES (uuid_generate_v4(), '5512345678', 'Test User', $1, 'USER', true)
RETURNING id`,
[passwordHash],
);
userId = result[0].id;
// Create credit balance
await dataSource.query(
`INSERT INTO credit_balances (id, "userId", balance, "totalPurchased", "totalConsumed", "totalFromReferrals")
VALUES (uuid_generate_v4(), $1, 100, 100, 0, 0)`,
[userId],
);
// Generate token
accessToken = jwtService.sign({ sub: userId });
});
describe('POST /stores', () => {
it('should create a new store', async () => {
const response = await request(app.getHttpServer())
.post('/stores')
.set('Authorization', `Bearer ${accessToken}`)
.send({
name: 'Mi Tiendita',
giro: 'Abarrotes',
address: 'Calle Principal 123',
})
.expect(201);
expect(response.body).toMatchObject({
name: 'Mi Tiendita',
giro: 'Abarrotes',
address: 'Calle Principal 123',
ownerId: userId,
});
expect(response.body.id).toBeDefined();
});
it('should fail without authentication', async () => {
await request(app.getHttpServer())
.post('/stores')
.send({ name: 'Mi Tiendita' })
.expect(401);
});
it('should fail with missing name', async () => {
await request(app.getHttpServer())
.post('/stores')
.set('Authorization', `Bearer ${accessToken}`)
.send({ giro: 'Abarrotes' })
.expect(400);
});
});
describe('GET /stores', () => {
beforeEach(async () => {
// Create test stores
await dataSource.query(
`INSERT INTO stores (id, "ownerId", name, giro, address, "isActive")
VALUES
(uuid_generate_v4(), $1, 'Store 1', 'Abarrotes', 'Address 1', true),
(uuid_generate_v4(), $1, 'Store 2', 'Farmacia', 'Address 2', true)`,
[userId],
);
});
it('should return all stores for the user', async () => {
const response = await request(app.getHttpServer())
.get('/stores')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body).toHaveLength(2);
expect(response.body[0]).toHaveProperty('name');
expect(response.body[0]).toHaveProperty('giro');
});
it('should fail without authentication', async () => {
await request(app.getHttpServer())
.get('/stores')
.expect(401);
});
});
describe('GET /stores/:id', () => {
let storeId: string;
beforeEach(async () => {
const result = await dataSource.query(
`INSERT INTO stores (id, "ownerId", name, giro, address, "isActive")
VALUES (uuid_generate_v4(), $1, 'Test Store', 'Abarrotes', 'Test Address', true)
RETURNING id`,
[userId],
);
storeId = result[0].id;
});
it('should return a specific store', async () => {
const response = await request(app.getHttpServer())
.get(`/stores/${storeId}`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(response.body).toMatchObject({
id: storeId,
name: 'Test Store',
giro: 'Abarrotes',
});
});
it('should fail for non-existent store', async () => {
await request(app.getHttpServer())
.get('/stores/00000000-0000-0000-0000-000000000000')
.set('Authorization', `Bearer ${accessToken}`)
.expect(404);
});
it('should fail for store owned by another user', async () => {
// Create another user and store
const otherUserResult = await dataSource.query(
`INSERT INTO users (id, phone, name, role, "isActive")
VALUES (uuid_generate_v4(), '9999999999', 'Other User', 'USER', true)
RETURNING id`,
);
const otherUserId = otherUserResult[0].id;
const otherStoreResult = await dataSource.query(
`INSERT INTO stores (id, "ownerId", name, giro, address, "isActive")
VALUES (uuid_generate_v4(), $1, 'Other Store', 'Farmacia', 'Other Address', true)
RETURNING id`,
[otherUserId],
);
const otherStoreId = otherStoreResult[0].id;
await request(app.getHttpServer())
.get(`/stores/${otherStoreId}`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(404);
});
});
describe('PATCH /stores/:id', () => {
let storeId: string;
beforeEach(async () => {
const result = await dataSource.query(
`INSERT INTO stores (id, "ownerId", name, giro, address, "isActive")
VALUES (uuid_generate_v4(), $1, 'Test Store', 'Abarrotes', 'Test Address', true)
RETURNING id`,
[userId],
);
storeId = result[0].id;
});
it('should update a store', async () => {
const response = await request(app.getHttpServer())
.patch(`/stores/${storeId}`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ name: 'Updated Store Name' })
.expect(200);
expect(response.body.name).toBe('Updated Store Name');
});
it('should fail for non-existent store', async () => {
await request(app.getHttpServer())
.patch('/stores/00000000-0000-0000-0000-000000000000')
.set('Authorization', `Bearer ${accessToken}`)
.send({ name: 'Updated' })
.expect(404);
});
});
describe('DELETE /stores/:id', () => {
let storeId: string;
beforeEach(async () => {
const result = await dataSource.query(
`INSERT INTO stores (id, "ownerId", name, giro, address, "isActive")
VALUES (uuid_generate_v4(), $1, 'Test Store', 'Abarrotes', 'Test Address', true)
RETURNING id`,
[userId],
);
storeId = result[0].id;
});
it('should delete a store', async () => {
await request(app.getHttpServer())
.delete(`/stores/${storeId}`)
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
// Verify store is deleted
const stores = await dataSource.query(
'SELECT * FROM stores WHERE id = $1 AND "isActive" = true',
[storeId],
);
expect(stores).toHaveLength(0);
});
it('should fail for non-existent store', async () => {
await request(app.getHttpServer())
.delete('/stores/00000000-0000-0000-0000-000000000000')
.set('Authorization', `Bearer ${accessToken}`)
.expect(404);
});
});
});