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 { InventoryModule } from '../src/modules/inventory/inventory.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 { InventoryItem } from '../src/modules/inventory/entities/inventory-item.entity'; import { Video } from '../src/modules/videos/entities/video.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('InventoryController (e2e)', () => { let app: INestApplication; let dataSource: DataSource; let jwtService: JwtService; let accessToken: string; let userId: string; let storeId: 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, InventoryItem, Video, Otp, RefreshToken, CreditBalance, CreditTransaction, CreditPackage ], synchronize: true, dropSchema: true, }), inject: [ConfigService], }), JwtModule.register({ secret: 'test-secret', signOptions: { expiresIn: '15m' }, }), AuthModule, UsersModule, StoresModule, InventoryModule, CreditsModule, ], }) .overrideProvider(ConfigService) .useValue({ get: (key: string, defaultValue?: any) => { const config: Record = { 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); jwtService = moduleFixture.get(JwtService); }); afterAll(async () => { if (dataSource?.isInitialized) { await dataSource.destroy(); } await app.close(); }); beforeEach(async () => { // Clean up - order matters due to foreign keys await dataSource.query('DELETE FROM inventory_items'); await dataSource.query('DELETE FROM videos'); await dataSource.query('DELETE FROM store_users'); await dataSource.query('DELETE FROM stores'); await dataSource.query('DELETE FROM credit_transactions'); await dataSource.query('DELETE FROM credit_balances'); await dataSource.query('DELETE FROM refresh_tokens'); await dataSource.query('DELETE FROM otps'); await dataSource.query('DELETE FROM users'); // Create test user const passwordHash = await bcrypt.hash('testpassword123', 10); const userResult = 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 = userResult[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], ); // Create test store const storeResult = 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 = storeResult[0].id; // Create inventory items await dataSource.query( `INSERT INTO inventory_items (id, "storeId", name, category, quantity, price, "minStock") VALUES (uuid_generate_v4(), $1, 'Coca Cola 600ml', 'Bebidas', 50, 18.00, 10), (uuid_generate_v4(), $1, 'Sabritas Original', 'Snacks', 25, 15.50, 5), (uuid_generate_v4(), $1, 'Pan Bimbo', 'Panaderia', 3, 45.00, 5)`, [storeId], ); // Generate token accessToken = jwtService.sign({ sub: userId }); }); describe('GET /stores/:storeId/inventory', () => { it('should return inventory items for the store', async () => { const response = await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory`) .set('Authorization', `Bearer ${accessToken}`) .expect(200); expect(response.body.items).toHaveLength(3); expect(response.body.total).toBe(3); expect(response.body.page).toBe(1); expect(response.body).toHaveProperty('hasMore'); }); it('should support pagination', async () => { const response = await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory?page=1&limit=2`) .set('Authorization', `Bearer ${accessToken}`) .expect(200); expect(response.body.items).toHaveLength(2); expect(response.body.limit).toBe(2); }); it('should fail without authentication', async () => { await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory`) .expect(401); }); 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, "isActive") VALUES (uuid_generate_v4(), $1, 'Other Store', 'Farmacia', true) RETURNING id`, [otherUserId], ); const otherStoreId = otherStoreResult[0].id; // The API returns 403 Forbidden when accessing stores you don't own await request(app.getHttpServer()) .get(`/stores/${otherStoreId}/inventory`) .set('Authorization', `Bearer ${accessToken}`) .expect(403); }); }); describe('GET /stores/:storeId/inventory/statistics', () => { it('should return inventory statistics', async () => { const response = await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory/statistics`) .set('Authorization', `Bearer ${accessToken}`) .expect(200); expect(response.body).toHaveProperty('totalItems'); expect(response.body).toHaveProperty('totalValue'); expect(response.body).toHaveProperty('lowStockCount'); }); }); describe('GET /stores/:storeId/inventory/low-stock', () => { it('should return low stock items', async () => { const response = await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory/low-stock`) .set('Authorization', `Bearer ${accessToken}`) .expect(200); // Pan Bimbo has quantity 3 and minStock 5, so it should be low stock expect(response.body).toBeInstanceOf(Array); const lowStockItem = response.body.find((item: any) => item.name === 'Pan Bimbo'); expect(lowStockItem).toBeDefined(); }); }); describe('GET /stores/:storeId/inventory/categories', () => { it('should return categories', async () => { const response = await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory/categories`) .set('Authorization', `Bearer ${accessToken}`) .expect(200); expect(response.body).toBeInstanceOf(Array); expect(response.body.length).toBeGreaterThan(0); }); }); describe('GET /stores/:storeId/inventory/:itemId', () => { let itemId: string; beforeEach(async () => { const result = await dataSource.query( `SELECT id FROM inventory_items WHERE "storeId" = $1 LIMIT 1`, [storeId], ); itemId = result[0].id; }); it('should return a specific inventory item', async () => { const response = await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory/${itemId}`) .set('Authorization', `Bearer ${accessToken}`) .expect(200); expect(response.body).toHaveProperty('id', itemId); expect(response.body).toHaveProperty('name'); expect(response.body).toHaveProperty('quantity'); }); it('should fail for non-existent item', async () => { await request(app.getHttpServer()) .get(`/stores/${storeId}/inventory/00000000-0000-0000-0000-000000000000`) .set('Authorization', `Bearer ${accessToken}`) .expect(404); }); }); describe('PATCH /stores/:storeId/inventory/:itemId', () => { let itemId: string; beforeEach(async () => { const result = await dataSource.query( `SELECT id FROM inventory_items WHERE "storeId" = $1 LIMIT 1`, [storeId], ); itemId = result[0].id; }); it('should update inventory item', async () => { const response = await request(app.getHttpServer()) .patch(`/stores/${storeId}/inventory/${itemId}`) .set('Authorization', `Bearer ${accessToken}`) .send({ quantity: 100, price: 20.00 }) .expect(200); expect(response.body.quantity).toBe(100); expect(parseFloat(response.body.price)).toBe(20.00); }); it('should fail for non-existent item', async () => { await request(app.getHttpServer()) .patch(`/stores/${storeId}/inventory/00000000-0000-0000-0000-000000000000`) .set('Authorization', `Bearer ${accessToken}`) .send({ quantity: 100 }) .expect(404); }); }); describe('DELETE /stores/:storeId/inventory/:itemId', () => { let itemId: string; beforeEach(async () => { const result = await dataSource.query( `SELECT id FROM inventory_items WHERE "storeId" = $1 LIMIT 1`, [storeId], ); itemId = result[0].id; }); it('should delete inventory item', async () => { await request(app.getHttpServer()) .delete(`/stores/${storeId}/inventory/${itemId}`) .set('Authorization', `Bearer ${accessToken}`) .expect(200); // Verify item is deleted const items = await dataSource.query( 'SELECT * FROM inventory_items WHERE id = $1', [itemId], ); expect(items).toHaveLength(0); }); it('should fail for non-existent item', async () => { await request(app.getHttpServer()) .delete(`/stores/${storeId}/inventory/00000000-0000-0000-0000-000000000000`) .set('Authorization', `Bearer ${accessToken}`) .expect(404); }); }); });