test: add comprehensive unit tests for all API services and guards
Add 11 new test files covering roles guard, brand-access guard, users, brands, plans, payments, vehicles, categories, parts, referrals, and admin services. Total test count increases from ~52 to 164, all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
156
apps/api/src/common/guards/brand-access.guard.spec.ts
Normal file
156
apps/api/src/common/guards/brand-access.guard.spec.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ForbiddenException } from "@nestjs/common";
|
||||
import { BrandAccessGuard } from "./brand-access.guard";
|
||||
|
||||
function createMockDb(overrides: Record<string, unknown> = {}) {
|
||||
function chainable(terminalValue: unknown) {
|
||||
const chain: Record<string, unknown> = {};
|
||||
const methods = ["select", "from", "where", "orderBy", "limit", "offset", "innerJoin", "insert", "values", "update", "set", "returning"];
|
||||
for (const m of methods) chain[m] = vi.fn().mockReturnValue(chain);
|
||||
chain.limit = vi.fn().mockReturnValue(terminalValue);
|
||||
return chain;
|
||||
}
|
||||
|
||||
return {
|
||||
select: vi.fn().mockImplementation(() => chainable(overrides._selectRows ?? [])),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockContext(options: { user?: any; params?: any; body?: any }) {
|
||||
const request: Record<string, unknown> = {
|
||||
params: options.params ?? {},
|
||||
body: options.body ?? {},
|
||||
};
|
||||
if (options.user) request.user = options.user;
|
||||
|
||||
return {
|
||||
switchToHttp: vi.fn().mockReturnValue({
|
||||
getRequest: vi.fn().mockReturnValue(request),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("BrandAccessGuard", () => {
|
||||
it("should throw ForbiddenException when no user", async () => {
|
||||
const db = createMockDb();
|
||||
const guard = new BrandAccessGuard(db as any);
|
||||
const context = createMockContext({});
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should allow admin users (bypass)", async () => {
|
||||
const db = createMockDb();
|
||||
const guard = new BrandAccessGuard(db as any);
|
||||
const context = createMockContext({
|
||||
user: { id: "u1", role: "admin" },
|
||||
params: { brandId: "brand-1" },
|
||||
});
|
||||
|
||||
const result = await guard.canActivate(context as any);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow when no brandId in request", async () => {
|
||||
const db = createMockDb();
|
||||
const guard = new BrandAccessGuard(db as any);
|
||||
const context = createMockContext({
|
||||
user: { id: "u1", role: "user" },
|
||||
});
|
||||
|
||||
const result = await guard.canActivate(context as any);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw ForbiddenException when no active subscription", async () => {
|
||||
const db = createMockDb({ _selectRows: [] });
|
||||
const guard = new BrandAccessGuard(db as any);
|
||||
const context = createMockContext({
|
||||
user: { id: "u1", role: "user" },
|
||||
params: { brandId: "brand-1" },
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should throw ForbiddenException when active sub but no brand access", async () => {
|
||||
let callCount = 0;
|
||||
const db = {
|
||||
select: vi.fn().mockImplementation(() => {
|
||||
callCount++;
|
||||
const chain: Record<string, any> = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockImplementation(() => {
|
||||
if (callCount === 1) return [{ id: "sub-1", userId: "u1", status: "active" }];
|
||||
return []; // no brand access
|
||||
}),
|
||||
};
|
||||
return chain;
|
||||
}),
|
||||
};
|
||||
|
||||
const guard = new BrandAccessGuard(db as any);
|
||||
const context = createMockContext({
|
||||
user: { id: "u1", role: "user" },
|
||||
params: { brandId: "brand-1" },
|
||||
});
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should allow when active sub + brand access", async () => {
|
||||
let callCount = 0;
|
||||
const db = {
|
||||
select: vi.fn().mockImplementation(() => {
|
||||
callCount++;
|
||||
const chain: Record<string, any> = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockImplementation(() => {
|
||||
if (callCount === 1) return [{ id: "sub-1", userId: "u1", status: "active" }];
|
||||
return [{ id: "ba-1", userId: "u1", brandId: "brand-1" }];
|
||||
}),
|
||||
};
|
||||
return chain;
|
||||
}),
|
||||
};
|
||||
|
||||
const guard = new BrandAccessGuard(db as any);
|
||||
const context = createMockContext({
|
||||
user: { id: "u1", role: "user" },
|
||||
params: { brandId: "brand-1" },
|
||||
});
|
||||
|
||||
const result = await guard.canActivate(context as any);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should read brandId from body when not in params", async () => {
|
||||
let callCount = 0;
|
||||
const db = {
|
||||
select: vi.fn().mockImplementation(() => {
|
||||
callCount++;
|
||||
const chain: Record<string, any> = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockImplementation(() => {
|
||||
if (callCount === 1) return [{ id: "sub-1", userId: "u1", status: "active" }];
|
||||
return [{ id: "ba-1", userId: "u1", brandId: "brand-body" }];
|
||||
}),
|
||||
};
|
||||
return chain;
|
||||
}),
|
||||
};
|
||||
|
||||
const guard = new BrandAccessGuard(db as any);
|
||||
const context = createMockContext({
|
||||
user: { id: "u1", role: "user" },
|
||||
body: { brandId: "brand-body" },
|
||||
});
|
||||
|
||||
const result = await guard.canActivate(context as any);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
70
apps/api/src/common/guards/roles.guard.spec.ts
Normal file
70
apps/api/src/common/guards/roles.guard.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ForbiddenException } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { RolesGuard } from "./roles.guard";
|
||||
|
||||
function createMockExecutionContext(user?: { role: string }) {
|
||||
const request: Record<string, unknown> = {};
|
||||
if (user) request.user = user;
|
||||
|
||||
return {
|
||||
getHandler: vi.fn(),
|
||||
getClass: vi.fn(),
|
||||
switchToHttp: vi.fn().mockReturnValue({
|
||||
getRequest: vi.fn().mockReturnValue(request),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("RolesGuard", () => {
|
||||
let guard: RolesGuard;
|
||||
let reflector: Reflector;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
reflector = { getAllAndOverride: vi.fn() } as unknown as Reflector;
|
||||
guard = new RolesGuard(reflector);
|
||||
});
|
||||
|
||||
it("should allow access when no roles are required", () => {
|
||||
vi.mocked(reflector.getAllAndOverride).mockReturnValue(undefined);
|
||||
const context = createMockExecutionContext({ role: "user" });
|
||||
|
||||
expect(guard.canActivate(context as any)).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow access when roles array is empty", () => {
|
||||
vi.mocked(reflector.getAllAndOverride).mockReturnValue([]);
|
||||
const context = createMockExecutionContext({ role: "user" });
|
||||
|
||||
expect(guard.canActivate(context as any)).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow access when user has matching role", () => {
|
||||
vi.mocked(reflector.getAllAndOverride).mockReturnValue(["admin"]);
|
||||
const context = createMockExecutionContext({ role: "admin" });
|
||||
|
||||
expect(guard.canActivate(context as any)).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw ForbiddenException when user has non-matching role", () => {
|
||||
vi.mocked(reflector.getAllAndOverride).mockReturnValue(["admin"]);
|
||||
const context = createMockExecutionContext({ role: "user" });
|
||||
|
||||
expect(() => guard.canActivate(context as any)).toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should throw ForbiddenException when no user on request", () => {
|
||||
vi.mocked(reflector.getAllAndOverride).mockReturnValue(["admin"]);
|
||||
const context = createMockExecutionContext();
|
||||
|
||||
expect(() => guard.canActivate(context as any)).toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it("should allow when user matches one of multiple required roles", () => {
|
||||
vi.mocked(reflector.getAllAndOverride).mockReturnValue(["admin", "moderator"]);
|
||||
const context = createMockExecutionContext({ role: "moderator" });
|
||||
|
||||
expect(guard.canActivate(context as any)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user