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:
120
apps/api/test/reseller-assignment.spec.ts
Normal file
120
apps/api/test/reseller-assignment.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { query } from "@eden/database";
|
||||
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, qaEmail, type AuthedTokens } from "./setup.js";
|
||||
|
||||
/**
|
||||
* Protects the fix in commit 8d108e0 ("fix: assign-user de reseller usava
|
||||
* resellerId do body, não o :id da URL"): the reseller a user is assigned
|
||||
* to must always be the `:id` in the URL, never a value read from the
|
||||
* request body. AssignUserToResellerDto (apps/api/src/resellers/dto/
|
||||
* assign-user-to-reseller.dto.ts) now only declares `userId` — it doesn't
|
||||
* accept `resellerId` at all — so the strongest proof available at the
|
||||
* HTTP layer is that a body containing `resellerId` is rejected outright
|
||||
* by the mass-assignment whitelist (a body that could "win" over the URL
|
||||
* literally cannot reach the service layer any more). This is backed up
|
||||
* with a direct read of `users.reseller_id` after a legitimate call,
|
||||
* confirming the URL's reseller is the one actually persisted.
|
||||
*
|
||||
* Fixtures: two throwaway resellers + one throwaway user (all
|
||||
* `qa-test-`/`qa_` tagged). Cleanup: unassign the user (reseller_id back
|
||||
* to null, exercised by test (b) itself), then deactivate the user and
|
||||
* both resellers — no DELETE endpoint exists for either resource.
|
||||
*/
|
||||
describe("resellers — assign-user URL-wins-over-body invariant", () => {
|
||||
let app: INestApplication;
|
||||
let admin: AuthedTokens;
|
||||
let resellerAId: string;
|
||||
let resellerBId: string;
|
||||
let userId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
admin = await loginSuperAdmin(app);
|
||||
|
||||
const resellerA = await agent(app)
|
||||
.post("/api/v1/resellers")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: "qa-test- Reseller A (target)" })
|
||||
.expect(201);
|
||||
resellerAId = resellerA.body.id;
|
||||
|
||||
const resellerB = await agent(app)
|
||||
.post("/api/v1/resellers")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ name: "qa-test- Reseller B (decoy in body)" })
|
||||
.expect(201);
|
||||
resellerBId = resellerB.body.id;
|
||||
|
||||
const invite = await agent(app)
|
||||
.post("/api/v1/users/invite")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ email: qaEmail("reseller-assignee"), roleKey: "user", applications: ["core"] })
|
||||
.expect(201);
|
||||
userId = invite.body.user.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await agent(app)
|
||||
.patch(`/api/v1/users/${userId}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ isActive: false })
|
||||
.expect(200);
|
||||
await agent(app)
|
||||
.patch(`/api/v1/resellers/${resellerAId}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ active: false })
|
||||
.expect(200);
|
||||
await agent(app)
|
||||
.patch(`/api/v1/resellers/${resellerBId}`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ active: false })
|
||||
.expect(200);
|
||||
await closeTestApp(app);
|
||||
});
|
||||
|
||||
it("a 'resellerId' in the body is rejected by the whitelist — there is no way to redirect the assignment via the body", async () => {
|
||||
await agent(app)
|
||||
.patch(`/api/v1/resellers/${resellerAId}/assign-user`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ userId, resellerId: resellerBId })
|
||||
.expect(400);
|
||||
|
||||
// Confirm the rejected request didn't partially apply anything.
|
||||
const { rows } = await query<{ reseller_id: string | null }>(
|
||||
"SELECT reseller_id FROM users WHERE id = $1",
|
||||
[userId],
|
||||
);
|
||||
expect(rows[0]?.reseller_id).toBeNull();
|
||||
});
|
||||
|
||||
it("assigning via the URL's :id actually persists that reseller (not the decoy)", async () => {
|
||||
const res = await agent(app)
|
||||
.patch(`/api/v1/resellers/${resellerAId}/assign-user`)
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ userId })
|
||||
.expect(200);
|
||||
expect(res.body.resellerId).toBe(resellerAId);
|
||||
|
||||
const { rows } = await query<{ reseller_id: string | null }>(
|
||||
"SELECT reseller_id FROM users WHERE id = $1",
|
||||
[userId],
|
||||
);
|
||||
expect(rows[0]?.reseller_id).toBe(resellerAId);
|
||||
});
|
||||
|
||||
it("POST /v1/resellers/unassign-user clears the assignment", async () => {
|
||||
const res = await agent(app)
|
||||
.post("/api/v1/resellers/unassign-user")
|
||||
.set(...bearer(admin.accessToken))
|
||||
.send({ userId })
|
||||
.expect(201);
|
||||
expect(res.body.resellerId).toBeNull();
|
||||
|
||||
const { rows } = await query<{ reseller_id: string | null }>(
|
||||
"SELECT reseller_id FROM users WHERE id = $1",
|
||||
[userId],
|
||||
);
|
||||
expect(rows[0]?.reseller_id).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user