Files
SeConectai_Vendas/test/equipe-perfil.e2e-spec.ts
T
tiagoandClaude Sonnet 5 323912748f Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)
Adiciona o frontend Next.js em frontend/ (setup, design system,
autenticação/tema, layout/navegação, área master, área tenant completa:
clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas,
equipe e perfil). Estende o backend de forma aditiva e autorizada:
cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe +
edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo
e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:41:13 -03:00

453 lines
16 KiB
TypeScript

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<typeof request>[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<string> {
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<NestExpressApplication>();
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);
});
});
});