- 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>
272 lines
9.4 KiB
TypeScript
272 lines
9.4 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
import * as request from 'supertest';
|
|
import { AppModule } from '../src/app.module';
|
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
import { User } from '../src/modules/users/entities/user.entity';
|
|
import { Store } from '../src/modules/stores/entities/store.entity';
|
|
import { StoreUser, StoreUserRole } from '../src/modules/stores/entities/store-user.entity';
|
|
import { Video, VideoStatus } from '../src/modules/videos/entities/video.entity';
|
|
import { InventoryItem } from '../src/modules/inventory/entities/inventory-item.entity';
|
|
import { ValidationRequest, ValidationRequestStatus } from '../src/modules/validations/entities/validation-request.entity';
|
|
import { ValidationResponse } from '../src/modules/validations/entities/validation-response.entity';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { Repository } from 'typeorm';
|
|
|
|
describe('ValidationsController (e2e)', () => {
|
|
let app: INestApplication;
|
|
let jwtService: JwtService;
|
|
let userRepository: Repository<User>;
|
|
let storeRepository: Repository<Store>;
|
|
let storeUserRepository: Repository<StoreUser>;
|
|
let videoRepository: Repository<Video>;
|
|
let inventoryRepository: Repository<InventoryItem>;
|
|
let validationRequestRepository: Repository<ValidationRequest>;
|
|
let validationResponseRepository: Repository<ValidationResponse>;
|
|
|
|
let testUser: User;
|
|
let testStore: Store;
|
|
let testVideo: Video;
|
|
let testItems: InventoryItem[];
|
|
let testValidationRequest: ValidationRequest;
|
|
let authToken: string;
|
|
|
|
beforeAll(async () => {
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
}).compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
|
await app.init();
|
|
|
|
jwtService = moduleFixture.get<JwtService>(JwtService);
|
|
userRepository = moduleFixture.get(getRepositoryToken(User));
|
|
storeRepository = moduleFixture.get(getRepositoryToken(Store));
|
|
storeUserRepository = moduleFixture.get(getRepositoryToken(StoreUser));
|
|
videoRepository = moduleFixture.get(getRepositoryToken(Video));
|
|
inventoryRepository = moduleFixture.get(getRepositoryToken(InventoryItem));
|
|
validationRequestRepository = moduleFixture.get(getRepositoryToken(ValidationRequest));
|
|
validationResponseRepository = moduleFixture.get(getRepositoryToken(ValidationResponse));
|
|
|
|
// Create test user
|
|
testUser = await userRepository.save({
|
|
phone: '+529876543210',
|
|
name: 'Test Validation User',
|
|
});
|
|
|
|
// Create test store
|
|
testStore = await storeRepository.save({
|
|
name: 'Test Validation Store',
|
|
ownerId: testUser.id,
|
|
});
|
|
|
|
// Create store-user relationship
|
|
await storeUserRepository.save({
|
|
storeId: testStore.id,
|
|
userId: testUser.id,
|
|
role: StoreUserRole.OWNER,
|
|
});
|
|
|
|
// Create test video
|
|
testVideo = await videoRepository.save({
|
|
storeId: testStore.id,
|
|
uploadedById: testUser.id,
|
|
fileName: 'test-validation.mp4',
|
|
status: VideoStatus.COMPLETED,
|
|
});
|
|
|
|
// Create test inventory items
|
|
testItems = await inventoryRepository.save([
|
|
{
|
|
storeId: testStore.id,
|
|
detectedByVideoId: testVideo.id,
|
|
name: 'Validation Item 1',
|
|
quantity: 5,
|
|
detectionConfidence: 0.65,
|
|
},
|
|
{
|
|
storeId: testStore.id,
|
|
detectedByVideoId: testVideo.id,
|
|
name: 'Validation Item 2',
|
|
quantity: 10,
|
|
detectionConfidence: 0.85,
|
|
},
|
|
{
|
|
storeId: testStore.id,
|
|
detectedByVideoId: testVideo.id,
|
|
name: 'Validation Item 3',
|
|
quantity: 3,
|
|
detectionConfidence: 0.55,
|
|
},
|
|
]);
|
|
|
|
// Create test validation request
|
|
const expiresAt = new Date();
|
|
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
|
|
testValidationRequest = await validationRequestRepository.save({
|
|
videoId: testVideo.id,
|
|
userId: testUser.id,
|
|
storeId: testStore.id,
|
|
totalItems: 3,
|
|
triggerReason: 'LOW_CONFIDENCE_DETECTIONS',
|
|
probabilityScore: 0.35,
|
|
expiresAt,
|
|
});
|
|
|
|
// Generate auth token
|
|
authToken = jwtService.sign({ sub: testUser.id, phone: testUser.phone });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Cleanup
|
|
await validationResponseRepository.delete({ requestId: testValidationRequest.id });
|
|
await validationRequestRepository.delete({ id: testValidationRequest.id });
|
|
for (const item of testItems) {
|
|
await inventoryRepository.delete({ id: item.id });
|
|
}
|
|
await videoRepository.delete({ id: testVideo.id });
|
|
await storeUserRepository.delete({ storeId: testStore.id });
|
|
await storeRepository.delete({ id: testStore.id });
|
|
await userRepository.delete({ id: testUser.id });
|
|
await app.close();
|
|
});
|
|
|
|
describe('/validations/:requestId/items (GET)', () => {
|
|
it('should return validation items', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get(`/validations/${testValidationRequest.id}/items`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.request).toBeDefined();
|
|
expect(response.body.request.id).toBe(testValidationRequest.id);
|
|
expect(response.body.items).toBeDefined();
|
|
expect(Array.isArray(response.body.items)).toBe(true);
|
|
});
|
|
|
|
it('should return 404 for non-existent request', async () => {
|
|
await request(app.getHttpServer())
|
|
.get('/validations/00000000-0000-0000-0000-000000000000/items')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.expect(404);
|
|
});
|
|
|
|
it('should reject without auth', async () => {
|
|
await request(app.getHttpServer())
|
|
.get(`/validations/${testValidationRequest.id}/items`)
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
describe('/validations/:requestId/submit (POST)', () => {
|
|
it('should submit validation responses', async () => {
|
|
// First, create a fresh validation request for this test
|
|
const expiresAt = new Date();
|
|
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
|
|
const freshRequest = await validationRequestRepository.save({
|
|
videoId: testVideo.id,
|
|
userId: testUser.id,
|
|
storeId: testStore.id,
|
|
totalItems: 3,
|
|
triggerReason: 'TEST',
|
|
probabilityScore: 0.5,
|
|
expiresAt,
|
|
});
|
|
|
|
const response = await request(app.getHttpServer())
|
|
.post(`/validations/${freshRequest.id}/submit`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
responses: [
|
|
{ inventoryItemId: testItems[0].id, isCorrect: true },
|
|
{ inventoryItemId: testItems[1].id, isCorrect: false, correctedQuantity: 12 },
|
|
{ inventoryItemId: testItems[2].id, isCorrect: true },
|
|
],
|
|
})
|
|
.expect(201);
|
|
|
|
expect(response.body.message).toBe('Validacion completada exitosamente');
|
|
expect(response.body.itemsValidated).toBe(3);
|
|
expect(response.body.creditsRewarded).toBeGreaterThanOrEqual(0);
|
|
|
|
// Cleanup
|
|
await validationResponseRepository.delete({ requestId: freshRequest.id });
|
|
await validationRequestRepository.delete({ id: freshRequest.id });
|
|
});
|
|
|
|
it('should reject invalid item IDs', async () => {
|
|
await request(app.getHttpServer())
|
|
.post(`/validations/${testValidationRequest.id}/submit`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
responses: [
|
|
{ inventoryItemId: 'invalid-uuid', isCorrect: true },
|
|
],
|
|
})
|
|
.expect(400);
|
|
});
|
|
});
|
|
|
|
describe('/validations/:requestId/skip (POST)', () => {
|
|
it('should skip validation', async () => {
|
|
// Create fresh request for skip test
|
|
const expiresAt = new Date();
|
|
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
|
|
const skipRequest = await validationRequestRepository.save({
|
|
videoId: testVideo.id,
|
|
userId: testUser.id,
|
|
storeId: testStore.id,
|
|
totalItems: 3,
|
|
triggerReason: 'TEST_SKIP',
|
|
probabilityScore: 0.5,
|
|
expiresAt,
|
|
});
|
|
|
|
const response = await request(app.getHttpServer())
|
|
.post(`/validations/${skipRequest.id}/skip`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.expect(201);
|
|
|
|
expect(response.body.message).toBe('Validacion omitida');
|
|
|
|
// Verify status changed
|
|
const updatedRequest = await validationRequestRepository.findOne({
|
|
where: { id: skipRequest.id },
|
|
});
|
|
expect(updatedRequest?.status).toBe(ValidationRequestStatus.SKIPPED);
|
|
|
|
// Cleanup
|
|
await validationRequestRepository.delete({ id: skipRequest.id });
|
|
});
|
|
|
|
it('should reject skip for already processed request', async () => {
|
|
// Create and complete a request
|
|
const expiresAt = new Date();
|
|
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
|
|
const completedRequest = await validationRequestRepository.save({
|
|
videoId: testVideo.id,
|
|
userId: testUser.id,
|
|
storeId: testStore.id,
|
|
totalItems: 3,
|
|
triggerReason: 'TEST_COMPLETED',
|
|
probabilityScore: 0.5,
|
|
status: ValidationRequestStatus.COMPLETED,
|
|
expiresAt,
|
|
});
|
|
|
|
await request(app.getHttpServer())
|
|
.post(`/validations/${completedRequest.id}/skip`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.expect(400);
|
|
|
|
// Cleanup
|
|
await validationRequestRepository.delete({ id: completedRequest.id });
|
|
});
|
|
});
|
|
});
|