import { randomUUID } from 'node:crypto'; import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestExpressApplication } from '@nestjs/platform-express'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { AppModule } from '../src/app.module'; import { PrismaService } from '../src/config/prisma.service'; import { hashPassword } from '../src/modules/auth/password.util'; import { applySecurityConfig } from '../src/security.util'; interface TokenPair { access_token: string; refresh_token: string; } interface EquipeUser { id: string; nome: string; email: string; role: string; ativo: boolean; criado_em: string; senha_provisoria?: string; } describe('Equipe (tenant_users) e Perfil — texto (e2e)', () => { let app: NestExpressApplication; let prisma: PrismaService; let httpServer: Parameters[0]; const senha = 'SenhaForteDemo#26'; const suffix = randomUUID().slice(0, 8); const masterEmail = `master-etapa18-${suffix}@teste.com`; let masterId: string; let masterToken: string; const tenantIds: string[] = []; const planoIds: string[] = []; const tenantUserIds: string[] = []; async function authMaster(): Promise { const res = await request(httpServer) .post('/auth/master/login') .send({ email: masterEmail, senha }) .expect(200); return (res.body as TokenPair).access_token; } function asMaster() { return { get: (url: string) => request(httpServer).get(url).set('Authorization', `Bearer ${masterToken}`), post: (url: string) => request(httpServer).post(url).set('Authorization', `Bearer ${masterToken}`), }; } function asTenant(token: string) { return { get: (url: string) => request(httpServer).get(url).set('Authorization', `Bearer ${token}`), post: (url: string) => request(httpServer).post(url).set('Authorization', `Bearer ${token}`), patch: (url: string) => request(httpServer).patch(url).set('Authorization', `Bearer ${token}`), put: (url: string) => request(httpServer).put(url).set('Authorization', `Bearer ${token}`), }; } async function criarPlano(limiteUsuarios?: number) { const res = await asMaster() .post('/master/planos') .send({ nome: `Plano ${suffix} ${randomUUID().slice(0, 4)}`, valor_mensal: 199.9, ...(limiteUsuarios !== undefined ? { limite_usuarios: limiteUsuarios } : {}), }) .expect(201); planoIds.push((res.body as { id: string }).id); return res.body as { id: string }; } async function criarTenantComAdmin(limiteUsuarios?: number) { const plano = await criarPlano(limiteUsuarios); const emailAdmin = `admin-${randomUUID().slice(0, 8)}-${suffix}@teste.com`; const res = await asMaster() .post('/master/tenants') .send({ razao_social: `Tenant Etapa18 ${suffix} ${randomUUID().slice(0, 6)}`, email_contato: `contato-${randomUUID().slice(0, 6)}@teste.com`, plano_id: plano.id, admin: { nome: 'Admin de Teste', email: emailAdmin }, }) .expect(201); const body = res.body as { tenant: { id: string }; admin: { id: string; email: string; senha_provisoria: string }; }; tenantIds.push(body.tenant.id); tenantUserIds.push(body.admin.id); await prisma.tenants.update({ where: { id: body.tenant.id }, data: { status: 'ativo' } }); const login = await request(httpServer) .post('/auth/tenant/login') .send({ email: body.admin.email, senha: body.admin.senha_provisoria }) .expect(200); return { tenantId: body.tenant.id, adminId: body.admin.id, adminEmail: body.admin.email, adminSenha: body.admin.senha_provisoria, token: (login.body as TokenPair).access_token, }; } beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); applySecurityConfig(app, app.get(ConfigService)); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }), ); await app.init(); httpServer = app.getHttpServer(); prisma = app.get(PrismaService); const senha_hash = await hashPassword(senha); const master = await prisma.master_admins.create({ data: { nome: 'Master Etapa 18', email: masterEmail, senha_hash }, }); masterId = master.id; masterToken = await authMaster(); }); afterAll(async () => { await prisma.tenants.deleteMany({ where: { id: { in: tenantIds } } }); await prisma.planos.deleteMany({ where: { id: { in: planoIds } } }); await prisma.auth_refresh_tokens.deleteMany({ where: { usuario_id: { in: [masterId, ...tenantUserIds] } }, }); await prisma.login_attempts.deleteMany({ where: { usuario_tipo: 'master', email: masterEmail }, }); await prisma.audit_logs.deleteMany({ where: { usuario_id: masterId } }); await prisma.master_admins.deleteMany({ where: { id: masterId } }); await app.close(); }); describe('GET /tenant/equipe', () => { it('lista o tenant_admin criado junto com o tenant', async () => { const tenant = await criarTenantComAdmin(); const res = await asTenant(tenant.token).get('/tenant/equipe').expect(200); const lista = res.body as EquipeUser[]; expect(lista).toHaveLength(1); expect(lista[0]).toMatchObject({ email: tenant.adminEmail, role: 'tenant_admin', ativo: true, }); expect(lista[0]).not.toHaveProperty('senha_provisoria'); }); }); describe('POST /tenant/equipe', () => { it('cria um usuário em cada papel possível, com senha provisória exibida uma única vez', async () => { const tenant = await criarTenantComAdmin(); const papeis = ['tenant_admin', 'vendedor', 'financeiro', 'visualizador'] as const; for (const role of papeis) { const res = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: `Usuário ${role}`, email: `${role}-${suffix}@teste.com`, role }) .expect(201); const body = res.body as EquipeUser; tenantUserIds.push(body.id); expect(body.role).toBe(role); expect(body.ativo).toBe(true); expect(typeof body.senha_provisoria).toBe('string'); expect((body.senha_provisoria as string).length).toBeGreaterThanOrEqual(10); // a senha provisória nunca reaparece em GET const lista = await asTenant(tenant.token).get('/tenant/equipe').expect(200); const criado = (lista.body as EquipeUser[]).find((u) => u.id === body.id); expect(criado).not.toHaveProperty('senha_provisoria'); } }); it('aceita senha explícita forte e rejeita senha explícita fraca', async () => { const tenant = await criarTenantComAdmin(); const res = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Com senha própria', email: `com-senha-${suffix}@teste.com`, role: 'vendedor', senha: 'MinhaSenhaForte#2026', }) .expect(201); tenantUserIds.push((res.body as EquipeUser).id); expect((res.body as EquipeUser).senha_provisoria).toBe('MinhaSenhaForte#2026'); await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Senha fraca', email: `senha-fraca-${suffix}@teste.com`, role: 'vendedor', senha: '123', }) .expect(400); }); it('rejeita payload sem role e e-mail duplicado dentro do tenant retorna 409', async () => { const tenant = await criarTenantComAdmin(); await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Sem papel', email: `sem-papel-${suffix}@teste.com` }) .expect(400); const emailDuplicado = `duplicado-${suffix}@teste.com`; const primeiro = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Primeiro', email: emailDuplicado, role: 'vendedor' }) .expect(201); tenantUserIds.push((primeiro.body as EquipeUser).id); await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Segundo', email: emailDuplicado, role: 'financeiro' }) .expect(409); }); it('respeita limite_usuarios do plano, com mensagem clara', async () => { const tenant = await criarTenantComAdmin(1); // só o próprio tenant_admin já ocupa o limite const res = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Além do limite', email: `alem-limite-${suffix}@teste.com`, role: 'vendedor', }) .expect(400); expect((res.body as { message: string }).message).toMatch(/Limite de 1 usuário/); // eleva o limite via SQL (mesma técnica da Etapa 16 para limite_customers) e confirma que passa a funcionar await prisma.planos.update({ where: { id: (await prisma.tenants.findUniqueOrThrow({ where: { id: tenant.tenantId } })) .plano_id as string, }, data: { limite_usuarios: 2 }, }); const ok = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Dentro do limite', email: `dentro-limite-${suffix}@teste.com`, role: 'vendedor', }) .expect(201); tenantUserIds.push((ok.body as EquipeUser).id); }); }); describe('PATCH /tenant/equipe/:id', () => { it('altera o papel de um usuário', async () => { const tenant = await criarTenantComAdmin(); const criado = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Muda papel', email: `muda-papel-${suffix}@teste.com`, role: 'vendedor' }) .expect(201); const id = (criado.body as EquipeUser).id; tenantUserIds.push(id); const atualizado = await asTenant(tenant.token) .patch(`/tenant/equipe/${id}`) .send({ role: 'financeiro' }) .expect(200); expect((atualizado.body as EquipeUser).role).toBe('financeiro'); }); it('desativa um usuário: refresh token é revogado e login volta a ser rejeitado', async () => { const tenant = await criarTenantComAdmin(); const emailNovo = `desativa-${suffix}@teste.com`; const senhaNova = 'SenhaForteNova#26'; const criado = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Vai ser desativado', email: emailNovo, role: 'vendedor', senha: senhaNova }) .expect(201); const id = (criado.body as EquipeUser).id; tenantUserIds.push(id); const loginNovo = await request(httpServer) .post('/auth/tenant/login') .send({ email: emailNovo, senha: senhaNova }) .expect(200); const refreshToken = (loginNovo.body as TokenPair).refresh_token; await asTenant(tenant.token).patch(`/tenant/equipe/${id}`).send({ ativo: false }).expect(200); // login novo rejeitado await request(httpServer) .post('/auth/tenant/login') .send({ email: emailNovo, senha: senhaNova }) .expect(401); // refresh do token emitido antes da desativação também rejeitado (revogado) await request(httpServer) .post('/auth/refresh') .send({ refresh_token: refreshToken }) .expect(401); // reativar permite login de novo await asTenant(tenant.token).patch(`/tenant/equipe/${id}`).send({ ativo: true }).expect(200); await request(httpServer) .post('/auth/tenant/login') .send({ email: emailNovo, senha: senhaNova }) .expect(200); }); it('bloqueia o próprio tenant_admin de se autodesativar ou se autorrebaixar', async () => { const tenant = await criarTenantComAdmin(); await asTenant(tenant.token) .patch(`/tenant/equipe/${tenant.adminId}`) .send({ ativo: false }) .expect(400); await asTenant(tenant.token) .patch(`/tenant/equipe/${tenant.adminId}`) .send({ role: 'vendedor' }) .expect(400); // continua ativo/admin const lista = await asTenant(tenant.token).get('/tenant/equipe').expect(200); const eu = (lista.body as EquipeUser[]).find((u) => u.id === tenant.adminId); expect(eu).toMatchObject({ ativo: true, role: 'tenant_admin' }); }); it('id inexistente retorna 404', async () => { const tenant = await criarTenantComAdmin(); await asTenant(tenant.token) .patch(`/tenant/equipe/${randomUUID()}`) .send({ ativo: false }) .expect(404); }); }); describe('isolamento por RLS entre tenants', () => { it('tenant B não vê nem altera tenant_users do tenant A (404 pelo UUID exato)', async () => { const tenantA = await criarTenantComAdmin(); const tenantB = await criarTenantComAdmin(); const listaB = await asTenant(tenantB.token).get('/tenant/equipe').expect(200); expect((listaB.body as EquipeUser[]).some((u) => u.id === tenantA.adminId)).toBe(false); await asTenant(tenantB.token) .patch(`/tenant/equipe/${tenantA.adminId}`) .send({ ativo: false }) .expect(404); }); }); describe('controle de acesso', () => { it('bloqueia todos os endpoints para papéis diferentes de tenant_admin', async () => { const tenant = await criarTenantComAdmin(); const vendedorEmail = `vendedor-acesso-${suffix}@teste.com`; const vendedorSenha = 'SenhaForteVendedor#26'; const criado = await asTenant(tenant.token) .post('/tenant/equipe') .send({ nome: 'Vendedor', email: vendedorEmail, role: 'vendedor', senha: vendedorSenha }) .expect(201); tenantUserIds.push((criado.body as EquipeUser).id); const loginVendedor = await request(httpServer) .post('/auth/tenant/login') .send({ email: vendedorEmail, senha: vendedorSenha }) .expect(200); const tokenVendedor = (loginVendedor.body as TokenPair).access_token; await asTenant(tokenVendedor).get('/tenant/equipe').expect(403); await asTenant(tokenVendedor) .post('/tenant/equipe') .send({ nome: 'X', email: `x-${suffix}@teste.com`, role: 'vendedor' }) .expect(403); await asTenant(tokenVendedor) .patch(`/tenant/equipe/${tenant.adminId}`) .send({ ativo: false }) .expect(403); }); it('bloqueia todos os endpoints para token master "puro"', async () => { await asMaster().get('/tenant/equipe').expect(403); }); }); describe('PUT /tenant/perfil — nome_fantasia/razao_social', () => { it('atualiza os dois campos de uma vez', async () => { const tenant = await criarTenantComAdmin(); const res = await asTenant(tenant.token) .put('/tenant/perfil') .send({ nome_fantasia: 'Fantasia Nova', razao_social: 'Razão Social Nova Ltda' }) .expect(200); expect(res.body).toMatchObject({ nome_fantasia: 'Fantasia Nova', razao_social: 'Razão Social Nova Ltda', }); const perfil = await asTenant(tenant.token).get('/tenant/perfil').expect(200); expect(perfil.body).toMatchObject({ nome_fantasia: 'Fantasia Nova', razao_social: 'Razão Social Nova Ltda', }); }); it('atualização parcial: só nome_fantasia, razao_social permanece inalterada', async () => { const tenant = await criarTenantComAdmin(); const original = await asTenant(tenant.token).get('/tenant/perfil').expect(200); const razaoOriginal = (original.body as { razao_social: string }).razao_social; const res = await asTenant(tenant.token) .put('/tenant/perfil') .send({ nome_fantasia: 'Só Fantasia Mudou' }) .expect(200); expect((res.body as { nome_fantasia: string }).nome_fantasia).toBe('Só Fantasia Mudou'); expect((res.body as { razao_social: string }).razao_social).toBe(razaoOriginal); }); it('rejeita razao_social vazia/curta demais', async () => { const tenant = await criarTenantComAdmin(); await asTenant(tenant.token).put('/tenant/perfil').send({ razao_social: 'A' }).expect(400); }); it('master "puro" recebe 403', async () => { await asMaster().get('/tenant/perfil').expect(403); }); }); });