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>
This commit is contained in:
+71
-4
@@ -1,10 +1,13 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||
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';
|
||||
|
||||
// O reset de senha (Etapa 8) manda o token por e-mail de verdade — sem SMTP
|
||||
// real disponível neste ambiente, mocka `nodemailer` (mesmo padrão de
|
||||
@@ -21,8 +24,16 @@ interface TokenPair {
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
/** Extrai `refresh_token=valor` do `Set-Cookie` de uma resposta, pronto pra reenviar no header `Cookie`. */
|
||||
function refreshCookieHeader(res: request.Response): string {
|
||||
const setCookie = res.headers['set-cookie'] as unknown as string[];
|
||||
const cookie = setCookie?.find((c) => c.startsWith('refresh_token='));
|
||||
if (!cookie) throw new Error('refresh_token cookie não encontrado na resposta');
|
||||
return cookie.split(';')[0];
|
||||
}
|
||||
|
||||
describe('Auth (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
let app: NestExpressApplication;
|
||||
let prisma: PrismaService;
|
||||
let httpServer: Parameters<typeof request>[0];
|
||||
|
||||
@@ -47,12 +58,13 @@ describe('Auth (e2e)', () => {
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
app = moduleFixture.createNestApplication<NestExpressApplication>();
|
||||
applySecurityConfig(app, moduleFixture.get(ConfigService));
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
|
||||
);
|
||||
await app.init();
|
||||
httpServer = app.getHttpServer() as Parameters<typeof request>[0];
|
||||
httpServer = app.getHttpServer();
|
||||
|
||||
prisma = app.get(PrismaService);
|
||||
|
||||
@@ -143,6 +155,19 @@ describe('Auth (e2e)', () => {
|
||||
expect(body.token_type).toBe('Bearer');
|
||||
});
|
||||
|
||||
it('login também seta o refresh token como cookie httpOnly (Etapa 13 do frontend)', async () => {
|
||||
const res = await request(httpServer)
|
||||
.post('/auth/tenant/login')
|
||||
.send({ email: tenantAAdminEmail, senha })
|
||||
.expect(200);
|
||||
|
||||
const setCookie = res.headers['set-cookie'] as unknown as string[];
|
||||
const refreshCookie = setCookie?.find((c) => c.startsWith('refresh_token='));
|
||||
expect(refreshCookie).toBeDefined();
|
||||
expect(refreshCookie).toMatch(/HttpOnly/i);
|
||||
expect(refreshCookie).toMatch(/Path=\/auth/i);
|
||||
});
|
||||
|
||||
it('login tenant com credenciais corretas retorna access+refresh token com tenant_id/role no payload', async () => {
|
||||
const res = await request(httpServer)
|
||||
.post('/auth/tenant/login')
|
||||
@@ -241,6 +266,26 @@ describe('Auth (e2e)', () => {
|
||||
.send({ refresh_token: 'token-que-nao-existe' })
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('funciona só com o cookie httpOnly, sem nada no corpo (fluxo real do frontend)', async () => {
|
||||
const login = await request(httpServer)
|
||||
.post('/auth/tenant/login')
|
||||
.send({ email: tenantAVendedorEmail, senha })
|
||||
.expect(200);
|
||||
|
||||
const res = await request(httpServer)
|
||||
.post('/auth/refresh')
|
||||
.set('Cookie', [refreshCookieHeader(login)])
|
||||
.send({})
|
||||
.expect(200);
|
||||
|
||||
// resposta também renova o cookie (rotação)
|
||||
expect(refreshCookieHeader(res)).toMatch(/^refresh_token=.+/);
|
||||
});
|
||||
|
||||
it('sem refresh token nem no corpo nem no cookie retorna 401', async () => {
|
||||
await request(httpServer).post('/auth/refresh').send({}).expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
@@ -256,6 +301,28 @@ describe('Auth (e2e)', () => {
|
||||
|
||||
await request(httpServer).post('/auth/refresh').send({ refresh_token }).expect(401);
|
||||
});
|
||||
|
||||
it('funciona só com o cookie httpOnly e limpa o cookie na resposta', async () => {
|
||||
const login = await request(httpServer)
|
||||
.post('/auth/tenant/login')
|
||||
.send({ email: tenantAAdminEmail, senha })
|
||||
.expect(200);
|
||||
|
||||
const cookie = refreshCookieHeader(login);
|
||||
|
||||
const logoutRes = await request(httpServer)
|
||||
.post('/auth/logout')
|
||||
.set('Cookie', [cookie])
|
||||
.send({})
|
||||
.expect(204);
|
||||
|
||||
const clearedCookie = (logoutRes.headers['set-cookie'] as unknown as string[])?.find((c) =>
|
||||
c.startsWith('refresh_token='),
|
||||
);
|
||||
expect(clearedCookie).toMatch(/refresh_token=;/);
|
||||
|
||||
await request(httpServer).post('/auth/refresh').set('Cookie', [cookie]).send({}).expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('esqueci minha senha / reset', () => {
|
||||
|
||||
Reference in New Issue
Block a user