test(api): add Vitest+supertest integration test suite (eden-qa)
Boots the real Nest app (Test.createTestingModule + AppModule) against a
real Postgres — no mocked DB or guards anywhere in this suite. Uses SWC
(unplugin-swc) instead of Vite's default esbuild transform because
esbuild doesn't implement emitDecoratorMetadata, which silently breaks
NestJS's constructor DI (every provider resolves to undefined).
Covers:
- auth.spec.ts: anti-enumeration login message, refresh-token rotation,
replay-detection revoking the whole session chain, idempotent logout
(ADR-0003).
- role-weight.spec.ts: invalid role_key on invite returns 400 not 500
(regression guard), lower-weight actor forbidden from assigning a
heavier role even when holding users:edit (ADR-0004).
- companies.spec.ts: CNPJ digit-check validation (reject/accept).
- mass-assignment.spec.ts: forbidNonWhitelisted actually rejects
unexpected fields on companies/products.
- reseller-assignment.spec.ts: assign-user always targets the URL's
:id, never a body resellerId (regression guard for commit 8d108e0);
unassign-user clears the assignment.
All fixtures are qa-test-/qa_ tagged and cleaned up (deactivated where
no DELETE endpoint exists, deleted where one does) — verified no
pre-existing seed data (super_admin, Handix Telecom LTDA, Handix revenda
própria) was touched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
124
apps/api/test/role-weight.spec.ts
Normal file
124
apps/api/test/role-weight.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
agent,
|
||||
bearer,
|
||||
closeTestApp,
|
||||
createTestApp,
|
||||
login,
|
||||
loginSuperAdmin,
|
||||
qaEmail,
|
||||
qaKey,
|
||||
type AuthedTokens,
|
||||
} from "./setup.js";
|
||||
|
||||
/**
|
||||
* Protects the role-weight invariant of apps/api/src/auth/role-weight.ts
|
||||
* (docs/adr/0004-permission-model.md, eden.md §1.6): an actor can never
|
||||
* assign/act on a role heavier than their own. Two distinct regressions
|
||||
* are guarded here:
|
||||
*
|
||||
* (a) an unknown role_key must fail as a clean 400, not an unhandled
|
||||
* DB FK-constraint 500 — see role-weight.ts's own comment on this
|
||||
* exact bug ("this function must be the single place that turns
|
||||
* 'role doesn't exist' into a clean 400, not the DB constraint").
|
||||
* (b) the weight comparison itself: a low-weight actor must be
|
||||
* forbidden (403) from assigning a heavier role than their own,
|
||||
* even when they otherwise hold the users:edit permission needed to
|
||||
* reach the check at all.
|
||||
*/
|
||||
describe("role-weight — assignment invariant", () => {
|
||||
let app: INestApplication;
|
||||
let admin: AuthedTokens;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
admin = await loginSuperAdmin(app);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeTestApp(app);
|
||||
});
|
||||
|
||||
it("inviting a user with a role_key that doesn't exist returns 400, not 500 (regression guard)", async () => {
|
||||
const email = qaEmail("role-badkey");
|
||||
const res = await agent(app)
|
||||
.post("/api/v1/users/invite")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ email, roleKey: "does_not_exist_role_xyz" })
|
||||
.expect(400);
|
||||
expect(res.body.message).toContain("does_not_exist_role_xyz");
|
||||
|
||||
// The invite must have been rejected before any row was written —
|
||||
// confirm no user with this email exists.
|
||||
const list = await agent(app)
|
||||
.get("/api/v1/users")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(200);
|
||||
expect(list.body.some((u: { email: string }) => u.email === email.toLowerCase())).toBe(false);
|
||||
});
|
||||
|
||||
describe("a lower-weight actor cannot assign a heavier role", () => {
|
||||
let lowRoleKey: string;
|
||||
let lowActorUserId: string;
|
||||
let lowActorEmail: string;
|
||||
let lowActorPassword: string;
|
||||
let lowActorTokens: AuthedTokens;
|
||||
|
||||
beforeAll(async () => {
|
||||
// 1. A fresh, low-weight role (weight 5 — lighter than every
|
||||
// system role except none; lighter than 'admin' at 80), granted
|
||||
// users:edit/all so the actor reaches the role-weight check itself
|
||||
// rather than being stopped earlier by PermissionGuard — isolating
|
||||
// the specific invariant under test.
|
||||
lowRoleKey = qaKey("lowrole");
|
||||
await agent(app)
|
||||
.post("/api/v1/roles")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ key: lowRoleKey, label: "QA test low-weight role", weight: 5 })
|
||||
.expect(201);
|
||||
await agent(app)
|
||||
.patch(`/api/v1/roles/${lowRoleKey}/permissions`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ permissions: [{ resource: "users", action: "edit", scope: "all" }] })
|
||||
.expect(200);
|
||||
|
||||
// 2. A throwaway user in that role, logged in for real.
|
||||
lowActorEmail = qaEmail("role-lowactor");
|
||||
const invite = await agent(app)
|
||||
.post("/api/v1/users/invite")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ email: lowActorEmail, roleKey: lowRoleKey, applications: ["core"] })
|
||||
.expect(201);
|
||||
lowActorUserId = invite.body.user.id;
|
||||
lowActorPassword = invite.body.tempPassword;
|
||||
lowActorTokens = await login(app, lowActorEmail, lowActorPassword);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Reassign the fixture user back to a system role and deactivate it
|
||||
// so the throwaway role has zero referencing users left, then
|
||||
// delete the throwaway role (RolesService.delete refuses to delete
|
||||
// a role that still has users on it) — full cleanup, not just a
|
||||
// tag, since both a DELETE-role and a PATCH-user endpoint exist.
|
||||
await agent(app)
|
||||
.patch(`/api/v1/users/${lowActorUserId}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ roleKey: "user", isActive: false })
|
||||
.expect(200);
|
||||
await agent(app)
|
||||
.delete(`/api/v1/roles/${lowRoleKey}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.expect(204);
|
||||
});
|
||||
|
||||
it("holding users:edit is not enough — assigning role 'admin' (weight 80 > actor's 5) is forbidden", async () => {
|
||||
const res = await agent(app)
|
||||
.post("/api/v1/users/invite")
|
||||
.set(...bearer(lowActorTokens.accessToken))
|
||||
.send({ email: qaEmail("role-target"), roleKey: "admin" })
|
||||
.expect(403);
|
||||
expect(res.body.message).toMatch(/peso maior/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user