- 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>
433 lines
14 KiB
TypeScript
433 lines
14 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, UserRole } 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 { IaProvider } from '../src/modules/admin/entities/ia-provider.entity';
|
|
import { Promotion, PromotionType } from '../src/modules/admin/entities/promotion.entity';
|
|
import { CreditPackage } from '../src/modules/credits/entities/credit-package.entity';
|
|
import { ProductSubmission } from '../src/modules/feedback/entities/product-submission.entity';
|
|
import { Referral, ReferralStatus } from '../src/modules/referrals/entities/referral.entity';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { Repository } from 'typeorm';
|
|
|
|
describe('AdminController (e2e)', () => {
|
|
let app: INestApplication;
|
|
let jwtService: JwtService;
|
|
let userRepository: Repository<User>;
|
|
let storeRepository: Repository<Store>;
|
|
let storeUserRepository: Repository<StoreUser>;
|
|
let providerRepository: Repository<IaProvider>;
|
|
let promotionRepository: Repository<Promotion>;
|
|
let packageRepository: Repository<CreditPackage>;
|
|
let productSubmissionRepository: Repository<ProductSubmission>;
|
|
let referralRepository: Repository<Referral>;
|
|
|
|
let superAdminUser: User;
|
|
let adminUser: User;
|
|
let moderatorUser: User;
|
|
let viewerUser: User;
|
|
let regularUser: User;
|
|
let testStore: Store;
|
|
let testProvider: IaProvider;
|
|
let testPackage: CreditPackage;
|
|
|
|
let superAdminToken: string;
|
|
let adminToken: string;
|
|
let moderatorToken: string;
|
|
let viewerToken: string;
|
|
let regularToken: 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));
|
|
providerRepository = moduleFixture.get(getRepositoryToken(IaProvider));
|
|
promotionRepository = moduleFixture.get(getRepositoryToken(Promotion));
|
|
packageRepository = moduleFixture.get(getRepositoryToken(CreditPackage));
|
|
productSubmissionRepository = moduleFixture.get(getRepositoryToken(ProductSubmission));
|
|
referralRepository = moduleFixture.get(getRepositoryToken(Referral));
|
|
|
|
// Create test users with different roles
|
|
superAdminUser = await userRepository.save({
|
|
phone: '+521111111111',
|
|
name: 'Super Admin Test',
|
|
role: UserRole.SUPER_ADMIN,
|
|
});
|
|
|
|
adminUser = await userRepository.save({
|
|
phone: '+522222222222',
|
|
name: 'Admin Test',
|
|
role: UserRole.ADMIN,
|
|
});
|
|
|
|
moderatorUser = await userRepository.save({
|
|
phone: '+523333333333',
|
|
name: 'Moderator Test',
|
|
role: UserRole.MODERATOR,
|
|
});
|
|
|
|
viewerUser = await userRepository.save({
|
|
phone: '+524444444444',
|
|
name: 'Viewer Test',
|
|
role: UserRole.VIEWER,
|
|
});
|
|
|
|
regularUser = await userRepository.save({
|
|
phone: '+525555555555',
|
|
name: 'Regular User Test',
|
|
role: UserRole.USER,
|
|
});
|
|
|
|
// Create test store
|
|
testStore = await storeRepository.save({
|
|
name: 'Test Admin Store',
|
|
ownerId: regularUser.id,
|
|
});
|
|
|
|
await storeUserRepository.save({
|
|
storeId: testStore.id,
|
|
userId: regularUser.id,
|
|
role: StoreUserRole.OWNER,
|
|
});
|
|
|
|
// Create test provider
|
|
testProvider = await providerRepository.save({
|
|
name: 'Test Provider',
|
|
code: 'test-provider',
|
|
costPerFrame: 0.01,
|
|
costPerToken: 0.00001,
|
|
isActive: true,
|
|
isDefault: false,
|
|
});
|
|
|
|
// Create test package
|
|
testPackage = await packageRepository.save({
|
|
name: 'Test Package',
|
|
credits: 100,
|
|
priceMXN: 99.00,
|
|
isActive: true,
|
|
});
|
|
|
|
// Generate tokens
|
|
superAdminToken = jwtService.sign({ sub: superAdminUser.id, phone: superAdminUser.phone, role: superAdminUser.role });
|
|
adminToken = jwtService.sign({ sub: adminUser.id, phone: adminUser.phone, role: adminUser.role });
|
|
moderatorToken = jwtService.sign({ sub: moderatorUser.id, phone: moderatorUser.phone, role: moderatorUser.role });
|
|
viewerToken = jwtService.sign({ sub: viewerUser.id, phone: viewerUser.phone, role: viewerUser.role });
|
|
regularToken = jwtService.sign({ sub: regularUser.id, phone: regularUser.phone, role: regularUser.role });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Cleanup
|
|
await promotionRepository.delete({});
|
|
await providerRepository.delete({ id: testProvider.id });
|
|
await storeUserRepository.delete({ storeId: testStore.id });
|
|
await storeRepository.delete({ id: testStore.id });
|
|
await userRepository.delete({ id: superAdminUser.id });
|
|
await userRepository.delete({ id: adminUser.id });
|
|
await userRepository.delete({ id: moderatorUser.id });
|
|
await userRepository.delete({ id: viewerUser.id });
|
|
await userRepository.delete({ id: regularUser.id });
|
|
await app.close();
|
|
});
|
|
|
|
describe('/admin/dashboard (GET)', () => {
|
|
it('should return dashboard metrics for VIEWER+', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/admin/dashboard')
|
|
.set('Authorization', `Bearer ${viewerToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.metrics).toBeDefined();
|
|
expect(response.body.metrics.users).toBeDefined();
|
|
expect(response.body.metrics.stores).toBeDefined();
|
|
expect(response.body.metrics.revenue).toBeDefined();
|
|
});
|
|
|
|
it('should reject regular users', async () => {
|
|
await request(app.getHttpServer())
|
|
.get('/admin/dashboard')
|
|
.set('Authorization', `Bearer ${regularToken}`)
|
|
.expect(403);
|
|
});
|
|
|
|
it('should reject without auth', async () => {
|
|
await request(app.getHttpServer())
|
|
.get('/admin/dashboard')
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
describe('/admin/providers (GET)', () => {
|
|
it('should return providers for ADMIN+', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/admin/providers')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.providers).toBeDefined();
|
|
expect(Array.isArray(response.body.providers)).toBe(true);
|
|
});
|
|
|
|
it('should reject MODERATOR', async () => {
|
|
await request(app.getHttpServer())
|
|
.get('/admin/providers')
|
|
.set('Authorization', `Bearer ${moderatorToken}`)
|
|
.expect(403);
|
|
});
|
|
});
|
|
|
|
describe('/admin/providers/:id (PATCH)', () => {
|
|
it('should update provider for SUPER_ADMIN only', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.patch(`/admin/providers/${testProvider.id}`)
|
|
.set('Authorization', `Bearer ${superAdminToken}`)
|
|
.send({ costPerFrame: 0.02 })
|
|
.expect(200);
|
|
|
|
expect(response.body.message).toBe('Proveedor actualizado exitosamente');
|
|
expect(Number(response.body.provider.costPerFrame)).toBe(0.02);
|
|
});
|
|
|
|
it('should reject ADMIN', async () => {
|
|
await request(app.getHttpServer())
|
|
.patch(`/admin/providers/${testProvider.id}`)
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.send({ costPerFrame: 0.03 })
|
|
.expect(403);
|
|
});
|
|
});
|
|
|
|
describe('/admin/packages (POST)', () => {
|
|
let createdPackageId: string;
|
|
|
|
it('should create package for ADMIN+', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post('/admin/packages')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.send({
|
|
name: 'New Admin Package',
|
|
credits: 500,
|
|
priceMxn: 299.00,
|
|
})
|
|
.expect(201);
|
|
|
|
expect(response.body.message).toBe('Paquete creado exitosamente');
|
|
expect(response.body.package.name).toBe('New Admin Package');
|
|
createdPackageId = response.body.package.id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (createdPackageId) {
|
|
await packageRepository.delete({ id: createdPackageId });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('/admin/promotions (POST)', () => {
|
|
let createdPromotionId: string;
|
|
|
|
it('should create promotion for ADMIN+', async () => {
|
|
const startsAt = new Date();
|
|
const endsAt = new Date();
|
|
endsAt.setMonth(endsAt.getMonth() + 1);
|
|
|
|
const response = await request(app.getHttpServer())
|
|
.post('/admin/promotions')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.send({
|
|
name: 'Test Promo',
|
|
code: 'TESTPROMO2024',
|
|
type: PromotionType.PERCENTAGE,
|
|
value: 20,
|
|
startsAt: startsAt.toISOString(),
|
|
endsAt: endsAt.toISOString(),
|
|
})
|
|
.expect(201);
|
|
|
|
expect(response.body.message).toBe('Promocion creada exitosamente');
|
|
expect(response.body.promotion.code).toBe('TESTPROMO2024');
|
|
createdPromotionId = response.body.promotion.id;
|
|
});
|
|
|
|
it('should reject duplicate promotion code', async () => {
|
|
const startsAt = new Date();
|
|
const endsAt = new Date();
|
|
endsAt.setMonth(endsAt.getMonth() + 1);
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/admin/promotions')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.send({
|
|
name: 'Duplicate Promo',
|
|
code: 'TESTPROMO2024',
|
|
type: PromotionType.PERCENTAGE,
|
|
value: 10,
|
|
startsAt: startsAt.toISOString(),
|
|
endsAt: endsAt.toISOString(),
|
|
})
|
|
.expect(400);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (createdPromotionId) {
|
|
await promotionRepository.delete({ id: createdPromotionId });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('/admin/products/pending (GET)', () => {
|
|
it('should return pending products for MODERATOR+', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/admin/products/pending')
|
|
.set('Authorization', `Bearer ${moderatorToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.products).toBeDefined();
|
|
expect(Array.isArray(response.body.products)).toBe(true);
|
|
expect(response.body.total).toBeDefined();
|
|
expect(response.body.page).toBeDefined();
|
|
});
|
|
|
|
it('should reject VIEWER', async () => {
|
|
await request(app.getHttpServer())
|
|
.get('/admin/products/pending')
|
|
.set('Authorization', `Bearer ${viewerToken}`)
|
|
.expect(403);
|
|
});
|
|
});
|
|
|
|
describe('/admin/products/:id/approve (POST)', () => {
|
|
let testSubmission: ProductSubmission;
|
|
|
|
beforeAll(async () => {
|
|
testSubmission = await productSubmissionRepository.save({
|
|
userId: regularUser.id,
|
|
storeId: testStore.id,
|
|
name: 'Test Product for Approval',
|
|
category: 'Test',
|
|
status: 'PENDING' as any,
|
|
});
|
|
});
|
|
|
|
it('should approve product for MODERATOR+', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post(`/admin/products/${testSubmission.id}/approve`)
|
|
.set('Authorization', `Bearer ${moderatorToken}`)
|
|
.send({ notes: 'Looks good' })
|
|
.expect(201);
|
|
|
|
expect(response.body.message).toBe('Producto aprobado exitosamente');
|
|
expect(response.body.product.status).toBe('APPROVED');
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await productSubmissionRepository.delete({ id: testSubmission.id });
|
|
});
|
|
});
|
|
|
|
describe('/admin/referrals/fraud-holds (GET)', () => {
|
|
it('should return fraud-hold referrals for MODERATOR+', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/admin/referrals/fraud-holds')
|
|
.set('Authorization', `Bearer ${moderatorToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.referrals).toBeDefined();
|
|
expect(Array.isArray(response.body.referrals)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('/admin/referrals/:id/approve (POST)', () => {
|
|
let testReferral: Referral;
|
|
|
|
beforeAll(async () => {
|
|
testReferral = await referralRepository.save({
|
|
referrerId: adminUser.id,
|
|
referredId: regularUser.id,
|
|
referralCode: 'TESTFRAUD123',
|
|
status: ReferralStatus.PENDING,
|
|
fraudHold: true,
|
|
fraudReason: 'Suspicious activity',
|
|
});
|
|
});
|
|
|
|
it('should approve fraud-hold referral for MODERATOR+', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post(`/admin/referrals/${testReferral.id}/approve`)
|
|
.set('Authorization', `Bearer ${moderatorToken}`)
|
|
.send({ notes: 'Verified legitimate' })
|
|
.expect(201);
|
|
|
|
expect(response.body.message).toBe('Referido aprobado exitosamente');
|
|
expect(response.body.referral.fraudHold).toBe(false);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await referralRepository.delete({ id: testReferral.id });
|
|
});
|
|
});
|
|
|
|
describe('Role Hierarchy Tests', () => {
|
|
it('SUPER_ADMIN should access all endpoints', async () => {
|
|
// Dashboard (VIEWER+)
|
|
await request(app.getHttpServer())
|
|
.get('/admin/dashboard')
|
|
.set('Authorization', `Bearer ${superAdminToken}`)
|
|
.expect(200);
|
|
|
|
// Providers (ADMIN+)
|
|
await request(app.getHttpServer())
|
|
.get('/admin/providers')
|
|
.set('Authorization', `Bearer ${superAdminToken}`)
|
|
.expect(200);
|
|
|
|
// Products pending (MODERATOR+)
|
|
await request(app.getHttpServer())
|
|
.get('/admin/products/pending')
|
|
.set('Authorization', `Bearer ${superAdminToken}`)
|
|
.expect(200);
|
|
});
|
|
|
|
it('ADMIN should access MODERATOR and VIEWER endpoints', async () => {
|
|
// Dashboard (VIEWER+)
|
|
await request(app.getHttpServer())
|
|
.get('/admin/dashboard')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.expect(200);
|
|
|
|
// Products pending (MODERATOR+)
|
|
await request(app.getHttpServer())
|
|
.get('/admin/products/pending')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.expect(200);
|
|
});
|
|
|
|
it('MODERATOR should access VIEWER endpoints but not ADMIN', async () => {
|
|
// Dashboard (VIEWER+)
|
|
await request(app.getHttpServer())
|
|
.get('/admin/dashboard')
|
|
.set('Authorization', `Bearer ${moderatorToken}`)
|
|
.expect(200);
|
|
|
|
// Providers (ADMIN+) - should be rejected
|
|
await request(app.getHttpServer())
|
|
.get('/admin/providers')
|
|
.set('Authorization', `Bearer ${moderatorToken}`)
|
|
.expect(403);
|
|
});
|
|
});
|
|
});
|