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:
Sase Dev
2026-02-12 04:24:55 +00:00
parent ffa9781eb9
commit 8fb7bbbaca
11 changed files with 2270 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NotFoundException } from "@nestjs/common";
import { AdminService } from "./admin.service";
describe("AdminService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("getDashboardStats", () => {
it("should return all 6 stats", async () => {
// getDashboardStats calls Promise.all with 6 db.select() calls:
// 1: select({count}).from(users) — from is terminal
// 2-6: select({count/total}).from(table).where(...) — where is terminal
//
// Trick: from() returns an array that also has a .where() method
const db = {
select: vi.fn().mockImplementation(() => {
const c: Record<string, any> = {};
c.from = vi.fn().mockImplementation(() => {
const result = [{ count: 10, total: 5000 }] as any;
result.where = vi.fn().mockReturnValue([{ count: 10, total: 5000 }]);
return result;
});
return c;
}),
};
const service = new AdminService(db as any);
const result = await service.getDashboardStats();
expect(result).toHaveProperty("totalUsers");
expect(result).toHaveProperty("activeSubscriptions");
expect(result).toHaveProperty("totalRevenue");
expect(result).toHaveProperty("totalQueries");
expect(result).toHaveProperty("newUsersThisMonth");
expect(result).toHaveProperty("pendingPayments");
});
});
describe("getUsers", () => {
it("should return paginated users without search", async () => {
const items = [{ id: "u1", name: "Ali", email: "ali@test.com" }];
// getUsers: Promise.all([
// select().from().where().orderBy().limit().offset() — offset terminal
// select({count}).from().where() — where terminal
// ]), then optionally select().from().where() for subscriptions
let callCount = 0;
const db = {
select: vi.fn().mockImplementation(() => {
callCount++;
const captured = callCount;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
// Count query (2nd): where is terminal
if (captured === 2) return [{ count: 1 }];
// Subscriptions query (3rd): where is terminal
if (captured === 3) return [];
return c;
});
c.orderBy = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue(c);
c.offset = vi.fn().mockReturnValue(items);
c.innerJoin = vi.fn().mockReturnValue(c);
c.inArray = vi.fn().mockReturnValue(c);
return c;
}),
};
const service = new AdminService(db as any);
const result = await service.getUsers(undefined, 1, 20);
expect(result.items).toBeDefined();
expect(result.total).toBe(1);
expect(result.page).toBe(1);
expect(result.limit).toBe(20);
});
it("should filter users with search", async () => {
let callCount = 0;
const db = {
select: vi.fn().mockImplementation(() => {
callCount++;
const captured = callCount;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ count: 0 }];
return c;
});
c.orderBy = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue(c);
c.offset = vi.fn().mockReturnValue([]);
return c;
}),
};
const service = new AdminService(db as any);
const result = await service.getUsers("ali", 1, 20);
expect(result).toBeDefined();
expect(result.page).toBe(1);
});
it("should handle pagination correctly", async () => {
let callCount = 0;
const db = {
select: vi.fn().mockImplementation(() => {
callCount++;
const captured = callCount;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ count: 50 }];
return c;
});
c.orderBy = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue(c);
c.offset = vi.fn().mockReturnValue([]);
return c;
}),
};
const service = new AdminService(db as any);
const result = await service.getUsers(undefined, 3, 10);
expect(result.page).toBe(3);
expect(result.limit).toBe(10);
expect(result.totalPages).toBe(5);
});
});
describe("getUserDetail", () => {
it("should return user with subscriptions and payments", async () => {
const user = { id: "u1", name: "Ali" };
// getUserDetail:
// 1: select().from(users).where().limit(1) — limit terminal
// 2: select().from(userSubscriptions).where().orderBy() — orderBy terminal
// 3: select().from(payments).where().orderBy() — orderBy terminal
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue([user]);
c.orderBy = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ id: "sub-1" }];
return [{ id: "pay-1" }];
});
return c;
}),
};
const service = new AdminService(db as any);
const result = await service.getUserDetail("u1");
expect(result.id).toBe("u1");
expect(result).toHaveProperty("subscriptions");
expect(result).toHaveProperty("payments");
});
it("should throw NotFoundException when user not found", async () => {
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue([]);
const db = { select: vi.fn().mockReturnValue(c) };
const service = new AdminService(db as any);
await expect(service.getUserDetail("nonexistent")).rejects.toThrow(NotFoundException);
});
});
describe("getPendingPayments", () => {
it("should return pending EFT payments with user info", async () => {
const pending = [{ id: "pay-1", userId: "u1", userName: "Ali", method: "eft", status: "pending" }];
// select().from().innerJoin().where().orderBy() — orderBy terminal
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.innerJoin = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockReturnValue(c);
c.orderBy = vi.fn().mockReturnValue(pending);
const db = { select: vi.fn().mockReturnValue(c) };
const service = new AdminService(db as any);
const result = await service.getPendingPayments();
expect(result).toEqual(pending);
});
});
describe("getQueryLogs", () => {
it("should return paginated query logs without filter", async () => {
const logs = [{ id: "log-1", vin: "VIN1" }];
// getQueryLogs: Promise.all([
// select().from().innerJoin().leftJoin().where().orderBy().limit().offset() — offset terminal
// select({count}).from().where() — where terminal
// ])
let callCount = 0;
const db = {
select: vi.fn().mockImplementation(() => {
callCount++;
const captured = callCount;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.innerJoin = vi.fn().mockReturnValue(c);
c.leftJoin = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ count: 1 }]; // terminal for count
return c;
});
c.orderBy = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue(c);
c.offset = vi.fn().mockReturnValue(logs);
return c;
}),
};
const service = new AdminService(db as any);
const result = await service.getQueryLogs(1, 50);
expect(result.items).toEqual(logs);
expect(result.total).toBe(1);
});
it("should filter query logs by userId", async () => {
let callCount = 0;
const db = {
select: vi.fn().mockImplementation(() => {
callCount++;
const captured = callCount;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.innerJoin = vi.fn().mockReturnValue(c);
c.leftJoin = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ count: 0 }];
return c;
});
c.orderBy = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue(c);
c.offset = vi.fn().mockReturnValue([]);
return c;
}),
};
const service = new AdminService(db as any);
const result = await service.getQueryLogs(1, 50, "u1");
expect(result).toBeDefined();
expect(result.page).toBe(1);
expect(result.total).toBe(0);
});
});
describe("getDailyStats", () => {
it("should return aggregated daily stats", async () => {
const stats = [
{ date: "2025-01-01", count: 10, successCount: 8, failureCount: 2 },
{ date: "2025-01-02", count: 15, successCount: 14, failureCount: 1 },
];
// select().from().where().groupBy().orderBy() — orderBy terminal
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockReturnValue(c);
c.groupBy = vi.fn().mockReturnValue(c);
c.orderBy = vi.fn().mockReturnValue(stats);
const db = { select: vi.fn().mockReturnValue(c) };
const service = new AdminService(db as any);
const result = await service.getDailyStats();
expect(result).toEqual(stats);
});
});
});

View File

@@ -0,0 +1,124 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NotFoundException } from "@nestjs/common";
import { BrandsService } from "./brands.service";
function createMockDb(overrides: Record<string, unknown> = {}) {
function chainable(terminalValue: unknown) {
const chain: Record<string, unknown> = {};
const methods = [
"select", "from", "where", "orderBy", "limit", "offset",
"insert", "values", "update", "set", "returning",
];
for (const m of methods) chain[m] = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockReturnValue(terminalValue);
chain.returning = vi.fn().mockReturnValue(terminalValue);
chain.orderBy = vi.fn().mockReturnValue(terminalValue);
return chain;
}
return {
select: vi.fn().mockImplementation(() => chainable(overrides._selectRows ?? [])),
insert: vi.fn().mockImplementation(() => chainable(overrides._insertRows ?? [])),
update: vi.fn().mockImplementation(() => chainable(overrides._updateRows ?? [])),
...overrides,
};
}
describe("BrandsService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("findAll", () => {
it("should return active brands only by default", async () => {
const brands = [{ id: "b1", name: "BMW", isActive: true }];
const db = createMockDb({ _selectRows: brands });
const service = new BrandsService(db as any);
const result = await service.findAll();
expect(result).toEqual(brands);
expect(db.select).toHaveBeenCalled();
});
it("should return all brands when activeOnly is false", async () => {
const brands = [{ id: "b1", name: "BMW" }, { id: "b2", name: "Audi" }];
// When activeOnly=false, chain is: select().from().orderBy() — no where
const chain: Record<string, any> = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnValue(brands),
};
const db = { select: vi.fn().mockReturnValue(chain) };
const service = new BrandsService(db as any);
const result = await service.findAll(false);
expect(result).toEqual(brands);
});
});
describe("findById", () => {
it("should return brand when found", async () => {
const brand = { id: "b1", name: "BMW" };
const db = createMockDb({ _selectRows: [brand] });
const service = new BrandsService(db as any);
const result = await service.findById("b1");
expect(result).toEqual(brand);
});
it("should throw NotFoundException when not found", async () => {
const db = createMockDb({ _selectRows: [] });
const service = new BrandsService(db as any);
await expect(service.findById("nonexistent")).rejects.toThrow(NotFoundException);
});
});
describe("findBySlug", () => {
it("should return brand when found", async () => {
const brand = { id: "b1", slug: "bmw" };
const db = createMockDb({ _selectRows: [brand] });
const service = new BrandsService(db as any);
const result = await service.findBySlug("bmw");
expect(result).toEqual(brand);
});
it("should return null when not found", async () => {
const db = createMockDb({ _selectRows: [] });
const service = new BrandsService(db as any);
const result = await service.findBySlug("nonexistent");
expect(result).toBeNull();
});
});
describe("create", () => {
it("should create and return brand", async () => {
const newBrand = { id: "b1", name: "BMW", slug: "bmw" };
const db = createMockDb({ _insertRows: [newBrand] });
const service = new BrandsService(db as any);
const result = await service.create({ name: "BMW", slug: "bmw" });
expect(result).toEqual(newBrand);
});
});
describe("update", () => {
it("should update and return brand", async () => {
const updated = { id: "b1", name: "BMW Updated" };
const db = createMockDb({ _updateRows: [updated] });
const service = new BrandsService(db as any);
const result = await service.update("b1", { name: "BMW Updated" });
expect(result).toEqual(updated);
});
it("should throw NotFoundException when brand not found", async () => {
const db = createMockDb({ _updateRows: [] });
const service = new BrandsService(db as any);
await expect(service.update("nonexistent", { name: "X" })).rejects.toThrow(NotFoundException);
});
});
});

View File

@@ -0,0 +1,157 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NotFoundException } from "@nestjs/common";
import { CategoriesService } from "./categories.service";
function createService(db: any) {
const redis = {
getJson: vi.fn().mockResolvedValue(null),
setJson: vi.fn().mockResolvedValue(undefined),
};
const pl24Service = {
getCategories: vi.fn().mockResolvedValue([]),
};
const service = new CategoriesService(db as any, redis as any, pl24Service as any);
return { service, db, redis, pl24Service };
}
/** Chainable mock where limit is terminal */
function chain(limitValue: unknown) {
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockReturnValue(c);
c.orderBy = vi.fn().mockReturnValue(c);
c.limit = vi.fn().mockReturnValue(limitValue);
return c;
}
describe("CategoriesService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("getCategoryTree", () => {
it("should return cached tree if available", async () => {
const cachedTree = [{ id: "c1", name: "Engine", children: [] }];
const db = { select: vi.fn() };
const { service, redis } = createService(db);
redis.getJson.mockResolvedValue(cachedTree);
const result = await service.getCategoryTree("v1");
expect(result).toEqual(cachedTree);
expect(redis.getJson).toHaveBeenCalledWith("cat:tree:v1");
});
it("should return categories from DB when cache miss but DB has data", async () => {
const vehicle = { id: "v1", rawData: { vehicleId: "pl24-v1" }, brandName: "BMW" };
const dbCategories = [
{ id: "c1", name: "Engine", parentId: null, vehicleId: "v1" },
{ id: "c2", name: "Oil Filter", parentId: "c1", vehicleId: "v1" },
];
// getCategoryTree:
// 1: select().from(vehicles).where().limit(1) — where→limit
// 2: select().from(categories).where() — where is terminal
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
if (captured === 2) return dbCategories; // terminal for categories
return c;
});
c.limit = vi.fn().mockReturnValue([vehicle]);
return c;
}),
};
const { service, redis } = createService(db);
const result = await service.getCategoryTree("v1");
expect(result).toBeInstanceOf(Array);
expect(result.length).toBe(1); // root node
expect(result[0].children.length).toBe(1); // child node
expect(redis.setJson).toHaveBeenCalled();
});
it("should fetch from PL24 when DB has no categories", async () => {
const vehicle = { id: "v1", rawData: { vehicleId: "pl24-v1" }, brandName: "BMW" };
const pl24Cats = [{ name: "Engine", groupId: "g1" }];
const insertedCats = [{ id: "c1", name: "Engine", parentId: null, vehicleId: "v1" }];
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
if (captured === 2) return []; // no categories in DB (terminal)
return c;
});
c.limit = vi.fn().mockReturnValue([vehicle]);
return c;
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue(insertedCats),
}),
};
const { service, pl24Service, redis } = createService(db);
pl24Service.getCategories.mockResolvedValue(pl24Cats);
const result = await service.getCategoryTree("v1");
expect(pl24Service.getCategories).toHaveBeenCalledWith("pl24-v1", "BMW");
expect(result).toBeInstanceOf(Array);
expect(redis.setJson).toHaveBeenCalled();
});
it("should throw NotFoundException when vehicle not found", async () => {
// select().from(vehicles).where().limit(1) → []
const db = { select: vi.fn().mockReturnValue(chain([])) };
const { service } = createService(db);
await expect(service.getCategoryTree("nonexistent")).rejects.toThrow(NotFoundException);
});
});
describe("getById", () => {
it("should return category with schema pics", async () => {
const category = { id: "c1", name: "Engine" };
const pics = [{ id: "p1", categoryId: "c1", url: "https://pic.test/1.png" }];
// getById:
// 1: select().from(categories).where().limit(1) — limit is terminal
// 2: select().from(schemaPics).where() — where is terminal
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const c: Record<string, any> = {};
c.from = vi.fn().mockReturnValue(c);
c.where = vi.fn().mockImplementation(() => {
if (captured === 2) return pics; // terminal for schemaPics
return c;
});
c.limit = vi.fn().mockReturnValue([category]);
return c;
}),
};
const { service } = createService(db);
const result = await service.getById("c1");
expect(result).toEqual({ ...category, schemaPics: pics });
});
it("should throw NotFoundException when category not found", async () => {
const db = { select: vi.fn().mockReturnValue(chain([])) };
const { service } = createService(db);
await expect(service.getById("nonexistent")).rejects.toThrow(NotFoundException);
});
});
});

View 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);
});
});

View 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);
});
});

View File

@@ -0,0 +1,162 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NotFoundException } from "@nestjs/common";
import { PartsService } from "./parts.service";
function createService(db: any) {
const pl24Service = {
getParts: vi.fn().mockResolvedValue([]),
};
const service = new PartsService(db as any, pl24Service as any);
return { service, db, pl24Service };
}
/** Build a simple chainable mock where limit is the terminal */
function simpleChain(terminalValue: unknown) {
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockReturnValue(chain);
chain.orderBy = vi.fn().mockReturnValue(chain);
chain.innerJoin = vi.fn().mockReturnValue(chain);
chain.leftJoin = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockReturnValue(terminalValue);
return chain;
}
describe("PartsService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("getByCategory", () => {
it("should return parts from DB when cached", async () => {
const dbParts = [{ id: "p1", name: "Oil Filter", oemCode: "OEM-123" }];
// getByCategory: select().from(parts).where(...) — where is terminal here (no limit)
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnValue(dbParts),
}),
};
const { service } = createService(db);
const result = await service.getByCategory("cat-1");
expect(result).toEqual(dbParts);
});
it("should fetch from PL24 when DB is empty", async () => {
const category = { id: "cat-1", vehicleId: "v1", externalId: "g1" };
const vehicle = { id: "v1", rawData: { vehicleId: "pl24-v1" }, brandName: "BMW" };
const pl24Parts = [{ name: "Oil Filter", oemCodes: ["OEM-1"], description: "Filter", quantity: 1, position: null, hotspotIndex: null }];
const insertedParts = [{ id: "p1", name: "Oil Filter", oemCode: "OEM-1" }];
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 1) return []; // no parts in DB (where is terminal)
return chain;
});
chain.limit = vi.fn().mockImplementation(() => {
if (captured === 2) return [category];
if (captured === 3) return [vehicle];
return [];
});
return chain;
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue(insertedParts),
}),
};
const { service, pl24Service } = createService(db);
pl24Service.getParts.mockResolvedValue(pl24Parts);
const result = await service.getByCategory("cat-1");
expect(pl24Service.getParts).toHaveBeenCalledWith("pl24-v1", "g1", "BMW");
expect(result).toEqual(insertedParts);
});
it("should throw NotFoundException when category not found", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 1) return []; // no parts
return chain;
});
chain.limit = vi.fn().mockReturnValue([]); // no category
return chain;
}),
};
const { service } = createService(db);
await expect(service.getByCategory("nonexistent")).rejects.toThrow(NotFoundException);
});
it("should throw NotFoundException when vehicle not found", async () => {
const category = { id: "cat-1", vehicleId: "v1", externalId: "g1" };
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 1) return []; // no parts
return chain;
});
chain.limit = vi.fn().mockImplementation(() => {
if (captured === 2) return [category];
return []; // vehicle not found
});
return chain;
}),
};
const { service } = createService(db);
await expect(service.getByCategory("cat-1")).rejects.toThrow(NotFoundException);
});
});
describe("searchByOem", () => {
it("should return matching parts with vehicle info", async () => {
const results = [{ part: { id: "p1", oemCode: "OEM-123" }, vehicle: { vin: "VIN1" } }];
// searchByOem: select().from().innerJoin().where().limit(50)
const db = { select: vi.fn().mockReturnValue(simpleChain(results)) };
const { service } = createService(db);
const result = await service.searchByOem("OEM-123");
expect(result).toEqual(results);
});
});
describe("getById", () => {
it("should return part when found", async () => {
const part = { id: "p1", name: "Oil Filter" };
// getById: select().from().where().limit(1)
const db = { select: vi.fn().mockReturnValue(simpleChain([part])) };
const { service } = createService(db);
const result = await service.getById("p1");
expect(result).toEqual(part);
});
it("should throw NotFoundException when not found", async () => {
const db = { select: vi.fn().mockReturnValue(simpleChain([])) };
const { service } = createService(db);
await expect(service.getById("nonexistent")).rejects.toThrow(NotFoundException);
});
});
});

View File

@@ -0,0 +1,296 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { BadRequestException, NotFoundException } from "@nestjs/common";
import { PaymentsService } from "./payments.service";
function createMockDb(overrides: Record<string, unknown> = {}) {
function chainable(terminalValue: unknown) {
const chain: Record<string, unknown> = {};
const methods = [
"select", "from", "where", "orderBy", "limit", "offset",
"innerJoin", "leftJoin", "insert", "values", "update", "set",
"delete", "returning", "onConflictDoNothing", "groupBy",
];
for (const m of methods) chain[m] = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockReturnValue(terminalValue);
chain.returning = vi.fn().mockReturnValue(terminalValue);
chain.orderBy = vi.fn().mockReturnValue(terminalValue);
return chain;
}
return {
select: vi.fn().mockImplementation(() => chainable(overrides._selectRows ?? [])),
insert: vi.fn().mockImplementation(() => chainable(overrides._insertRows ?? [])),
update: vi.fn().mockImplementation(() => chainable(overrides._updateRows ?? [])),
delete: vi.fn().mockImplementation(() => chainable(overrides._deleteRows ?? [])),
...overrides,
};
}
function createService(dbOverrides: Record<string, unknown> = {}, subServiceOverrides: Record<string, any> = {}) {
const db = typeof dbOverrides.select === "function" ? dbOverrides : createMockDb(dbOverrides);
const configService = { get: vi.fn().mockReturnValue("test-value") };
const subscriptionsService = {
activateSubscription: vi.fn().mockResolvedValue(undefined),
create: vi.fn().mockResolvedValue({ id: "sub-1", planId: "plan-1", status: "pending" }),
addBrandsToSubscription: vi.fn().mockResolvedValue(undefined),
...subServiceOverrides,
};
const storageService = {
upload: vi.fn().mockResolvedValue("https://storage.test/receipt.pdf"),
};
const service = new PaymentsService(
db as any,
configService as any,
subscriptionsService as any,
storageService as any,
);
return { service, db, configService, subscriptionsService, storageService };
}
describe("PaymentsService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("initializeIyzico", () => {
it("should create subscription, payment and return paymentId", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "plan-1", brandCount: 1, priceMonthly: 20000, priceYearly: 200000, isActive: true }]),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([{ id: "pay-1", status: "pending" }]),
}),
};
const { service, subscriptionsService } = createService(db);
const result = await service.initializeIyzico("u1", "brand1", "monthly", ["brand-id-1"]);
expect(result.paymentId).toBe("pay-1");
expect(result.status).toBe("pending");
expect(subscriptionsService.create).toHaveBeenCalled();
});
it("should throw BadRequestException for invalid plan key", async () => {
const { service } = createService();
await expect(service.initializeIyzico("u1", "invalid", "monthly", [])).rejects.toThrow(BadRequestException);
});
});
describe("handleIyzicoCallback", () => {
it("should activate subscription on success", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "pay-1", subscriptionId: "sub-1", status: "pending" }]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service, subscriptionsService } = createService(db);
const result = await service.handleIyzicoCallback("pay-1", "iyz-123", "success");
expect(result.status).toBe("completed");
expect(subscriptionsService.activateSubscription).toHaveBeenCalledWith("sub-1");
});
it("should set failed status on failure callback", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "pay-1", subscriptionId: "sub-1", status: "pending" }]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service, subscriptionsService } = createService(db);
const result = await service.handleIyzicoCallback("pay-1", "iyz-123", "failure");
expect(result.status).toBe("failed");
expect(subscriptionsService.activateSubscription).not.toHaveBeenCalled();
});
it("should throw NotFoundException when payment not found", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]),
}),
};
const { service } = createService(db);
await expect(service.handleIyzicoCallback("nonexistent", "iyz-1", "success")).rejects.toThrow(NotFoundException);
});
});
describe("createEftPayment", () => {
it("should create subscription, EFT payment and return bank info", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "plan-1", brandCount: 0, priceMonthly: 99900, priceYearly: 999000, isActive: true }]),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([{ id: "pay-eft-1", method: "eft", status: "pending" }]),
}),
};
const { service, subscriptionsService } = createService(db);
const result = await service.createEftPayment("u1", "full", "yearly", []);
expect(result.paymentId).toBe("pay-eft-1");
expect(result.bankInfo).toBeDefined();
expect(result.bankInfo.bankName).toBe("Ziraat Bankası");
expect(subscriptionsService.create).toHaveBeenCalled();
});
it("should throw BadRequestException for invalid plan key", async () => {
const { service } = createService();
await expect(service.createEftPayment("u1", "nonexistent", "monthly", [])).rejects.toThrow(BadRequestException);
});
});
describe("uploadEftReceipt", () => {
it("should upload receipt and return url", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "pay-1", userId: "u1", method: "eft" }]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service } = createService(db);
const result = await service.uploadEftReceipt("pay-1", "u1", Buffer.from("pdf"), "receipt.pdf");
expect(result.receiptUrl).toBe("https://storage.test/receipt.pdf");
});
it("should throw NotFoundException when payment not found", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]),
}),
};
const { service } = createService(db);
await expect(service.uploadEftReceipt("pay-x", "u1", Buffer.from("pdf"), "r.pdf")).rejects.toThrow(NotFoundException);
});
it("should throw BadRequestException when not EFT method", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "pay-1", userId: "u1", method: "iyzico" }]),
}),
};
const { service } = createService(db);
await expect(service.uploadEftReceipt("pay-1", "u1", Buffer.from("pdf"), "r.pdf")).rejects.toThrow(BadRequestException);
});
});
describe("approveEft", () => {
it("should approve and activate subscription", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "pay-1", subscriptionId: "sub-1", method: "eft" }]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service, subscriptionsService } = createService(db);
const result = await service.approveEft("pay-1", "Looks good");
expect(result.status).toBe("completed");
expect(subscriptionsService.activateSubscription).toHaveBeenCalledWith("sub-1");
});
it("should throw NotFoundException when payment not found", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]),
}),
};
const { service } = createService(db);
await expect(service.approveEft("nonexistent")).rejects.toThrow(NotFoundException);
});
it("should throw BadRequestException when not EFT method", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "pay-1", method: "iyzico" }]),
}),
};
const { service } = createService(db);
await expect(service.approveEft("pay-1")).rejects.toThrow(BadRequestException);
});
});
describe("rejectEft", () => {
it("should reject and return failed status", async () => {
const db = {
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service } = createService(db);
const result = await service.rejectEft("pay-1", "Bad receipt");
expect(result.status).toBe("failed");
});
});
describe("getMyPayments", () => {
it("should return list of payments", async () => {
const paymentsList = [{ id: "pay-1" }, { id: "pay-2" }];
const db = createMockDb({ _selectRows: paymentsList });
const { service } = createService(db);
const result = await service.getMyPayments("u1");
expect(result).toEqual(paymentsList);
});
});
describe("getPendingEftPayments", () => {
it("should return pending EFT payments", async () => {
const pending = [{ id: "pay-1", method: "eft", status: "pending" }];
const db = createMockDb({ _selectRows: pending });
const { service } = createService(db);
const result = await service.getPendingEftPayments();
expect(result).toEqual(pending);
});
});
});

View File

@@ -0,0 +1,102 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NotFoundException } from "@nestjs/common";
import { PlansService } from "./plans.service";
function createMockDb(overrides: Record<string, unknown> = {}) {
function chainable(terminalValue: unknown) {
const chain: Record<string, unknown> = {};
const methods = [
"select", "from", "where", "orderBy", "limit", "offset",
"insert", "values", "update", "set", "returning",
];
for (const m of methods) chain[m] = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockReturnValue(terminalValue);
chain.returning = vi.fn().mockReturnValue(terminalValue);
chain.orderBy = vi.fn().mockReturnValue(terminalValue);
return chain;
}
return {
select: vi.fn().mockImplementation(() => chainable(overrides._selectRows ?? [])),
insert: vi.fn().mockImplementation(() => chainable(overrides._insertRows ?? [])),
update: vi.fn().mockImplementation(() => chainable(overrides._updateRows ?? [])),
...overrides,
};
}
describe("PlansService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("findAll", () => {
it("should return active plans only by default", async () => {
const plans = [{ id: "p1", name: "1 Marka", isActive: true }];
const db = createMockDb({ _selectRows: plans });
const service = new PlansService(db as any);
const result = await service.findAll();
expect(result).toEqual(plans);
});
it("should return all plans when activeOnly is false", async () => {
const plans = [{ id: "p1" }, { id: "p2" }];
const chain: Record<string, any> = {
from: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnValue(plans),
};
const db = { select: vi.fn().mockReturnValue(chain) };
const service = new PlansService(db as any);
const result = await service.findAll(false);
expect(result).toEqual(plans);
});
});
describe("findById", () => {
it("should return plan when found", async () => {
const plan = { id: "p1", name: "1 Marka" };
const db = createMockDb({ _selectRows: [plan] });
const service = new PlansService(db as any);
const result = await service.findById("p1");
expect(result).toEqual(plan);
});
it("should throw NotFoundException when not found", async () => {
const db = createMockDb({ _selectRows: [] });
const service = new PlansService(db as any);
await expect(service.findById("nonexistent")).rejects.toThrow(NotFoundException);
});
});
describe("create", () => {
it("should create and return plan", async () => {
const newPlan = { id: "p1", name: "1 Marka", brandCount: 1, priceMonthly: 20000, priceYearly: 200000 };
const db = createMockDb({ _insertRows: [newPlan] });
const service = new PlansService(db as any);
const result = await service.create({ name: "1 Marka", brandCount: 1, priceMonthly: 20000, priceYearly: 200000 });
expect(result).toEqual(newPlan);
});
});
describe("update", () => {
it("should update and return plan", async () => {
const updated = { id: "p1", name: "Updated Plan" };
const db = createMockDb({ _updateRows: [updated] });
const service = new PlansService(db as any);
const result = await service.update("p1", { name: "Updated Plan" });
expect(result).toEqual(updated);
});
it("should throw NotFoundException when plan not found", async () => {
const db = createMockDb({ _updateRows: [] });
const service = new PlansService(db as any);
await expect(service.update("nonexistent", { name: "X" })).rejects.toThrow(NotFoundException);
});
});
});

View File

@@ -0,0 +1,332 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { BadRequestException, NotFoundException } from "@nestjs/common";
import { ReferralsService } from "./referrals.service";
vi.mock("@sase/shared", () => ({
REFERRAL_REWARDS: {
TIER_1: { count: 3, extensionDays: 7 },
TIER_2: { count: 5, extensionDays: 30 },
},
generateReferralCode: vi.fn().mockReturnValue("REF-ABC123"),
}));
function createService(db: any) {
const subscriptionsService = {
extendSubscription: vi.fn().mockResolvedValue(undefined),
};
const service = new ReferralsService(db as any, subscriptionsService as any);
return { service, db, subscriptionsService };
}
describe("ReferralsService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("getStats", () => {
it("should throw NotFoundException when user not found", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]),
}),
};
const { service } = createService(db);
await expect(service.getStats("nonexistent")).rejects.toThrow(NotFoundException);
});
it("should return 0 rewardDays when 0 referrals", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ count: 0 }]; // terminal for count
return chain;
});
chain.limit = vi.fn().mockReturnValue([{ id: "u1" }]);
return chain;
}),
};
const { service } = createService(db);
const result = await service.getStats("u1");
expect(result.totalReferrals).toBe(0);
expect(result.rewardDays).toBe(0);
});
it("should return tier 1 rewardDays when at threshold", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ count: 3 }];
return chain;
});
chain.limit = vi.fn().mockReturnValue([{ id: "u1" }]);
return chain;
}),
};
const { service } = createService(db);
const result = await service.getStats("u1");
expect(result.totalReferrals).toBe(3);
expect(result.rewardDays).toBe(7);
});
it("should return tier 2 rewardDays when at threshold", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ count: 5 }];
return chain;
});
chain.limit = vi.fn().mockReturnValue([{ id: "u1" }]);
return chain;
}),
};
const { service } = createService(db);
const result = await service.getStats("u1");
expect(result.totalReferrals).toBe(5);
expect(result.rewardDays).toBe(30);
});
});
describe("getMyReferrals", () => {
it("should return referrals with code and total", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ id: "r1" }, { id: "r2" }]; // terminal
return chain;
});
chain.limit = vi.fn().mockReturnValue([{ id: "u1", referralCode: "REF-XYZ" }]);
return chain;
}),
};
const { service } = createService(db);
const result = await service.getMyReferrals("u1");
expect(result.referralCode).toBe("REF-XYZ");
expect(result.totalReferrals).toBe(2);
expect(result.referrals).toHaveLength(2);
});
it("should throw NotFoundException when user not found", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]),
}),
};
const { service } = createService(db);
await expect(service.getMyReferrals("nonexistent")).rejects.toThrow(NotFoundException);
});
});
describe("applyReferralCode", () => {
it("should throw NotFoundException for invalid code", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]),
}),
};
const { service } = createService(db);
await expect(service.applyReferralCode("u1", "INVALID")).rejects.toThrow(NotFoundException);
});
it("should throw BadRequestException for self-referral", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "u1", referralCode: "SELF-CODE" }]),
}),
};
const { service } = createService(db);
await expect(service.applyReferralCode("u1", "SELF-CODE")).rejects.toThrow(BadRequestException);
});
it("should throw BadRequestException when already referred", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
if (selectCall === 1) return [{ id: "u2", referralCode: "REF-U2" }];
if (selectCall === 2) return [{ id: "existing" }];
return [];
}),
};
}),
};
const { service } = createService(db);
await expect(service.applyReferralCode("u1", "REF-U2")).rejects.toThrow(BadRequestException);
});
it("should apply referral code successfully", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
// Count query (3rd select) — where is terminal
if (captured === 3) return [{ count: 1 }];
return chain;
});
chain.limit = vi.fn().mockImplementation(() => {
if (captured === 1) return [{ id: "referrer-1", referralCode: "REF-CODE" }];
return []; // not already referred
});
return chain;
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service } = createService(db);
const result = await service.applyReferralCode("u1", "REF-CODE");
expect(result).toEqual({ success: true });
});
it("should trigger tier 1 reward when count reaches threshold", async () => {
// applyReferralCode select calls:
// 1: find referrer by code (where→limit)
// 2: check existing referral (where→limit)
// then: insert + update (not select)
// 3: count referrals (where is terminal)
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 3) return [{ count: 3 }]; // tier 1
return chain;
});
chain.limit = vi.fn().mockImplementation(() => {
if (captured === 1) return [{ id: "referrer-1", referralCode: "REF-CODE" }];
return [];
});
return chain;
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service, subscriptionsService } = createService(db);
await service.applyReferralCode("u1", "REF-CODE");
expect(subscriptionsService.extendSubscription).toHaveBeenCalledWith("referrer-1", 7);
});
it("should trigger tier 2 reward when count reaches threshold", async () => {
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const captured = selectCall;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockImplementation(() => {
if (captured === 3) return [{ count: 5 }]; // tier 2
return chain;
});
chain.limit = vi.fn().mockImplementation(() => {
if (captured === 1) return [{ id: "referrer-1", referralCode: "REF-CODE" }];
return [];
});
return chain;
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service, subscriptionsService } = createService(db);
await service.applyReferralCode("u1", "REF-CODE");
expect(subscriptionsService.extendSubscription).toHaveBeenCalledWith("referrer-1", 30);
});
});
describe("ensureReferralCode", () => {
it("should return existing code if user already has one", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "u1", referralCode: "EXISTING-CODE" }]),
}),
};
const { service } = createService(db);
const result = await service.ensureReferralCode("u1");
expect(result).toBe("EXISTING-CODE");
});
it("should generate and save new code when user has none", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "u1", referralCode: null }]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const { service } = createService(db);
const result = await service.ensureReferralCode("u1");
expect(result).toBe("REF-ABC123");
expect(db.update).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,251 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { BadRequestException, NotFoundException } from "@nestjs/common";
import { UsersService } from "./users.service";
vi.mock("better-auth/crypto", () => ({
hashPassword: vi.fn().mockResolvedValue("hashed-new-password"),
verifyPassword: vi.fn(),
}));
import { verifyPassword } from "better-auth/crypto";
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", "delete",
"returning", "onConflictDoNothing",
];
for (const m of methods) chain[m] = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockReturnValue(terminalValue);
chain.returning = vi.fn().mockReturnValue(terminalValue);
chain.offset = vi.fn().mockReturnValue(terminalValue);
return chain;
}
return {
select: vi.fn().mockImplementation(() => chainable(overrides._selectRows ?? [])),
insert: vi.fn().mockImplementation(() => chainable(overrides._insertRows ?? [])),
update: vi.fn().mockImplementation(() => chainable(overrides._updateRows ?? [])),
delete: vi.fn().mockImplementation(() => chainable(overrides._deleteRows ?? [])),
...overrides,
};
}
describe("UsersService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("findById", () => {
it("should return user when found", async () => {
const user = { id: "u1", name: "Ali", email: "ali@test.com" };
const db = createMockDb({ _selectRows: [user] });
const service = new UsersService(db as any);
const result = await service.findById("u1");
expect(result).toEqual(user);
});
it("should throw NotFoundException when not found", async () => {
const db = createMockDb({ _selectRows: [] });
const service = new UsersService(db as any);
await expect(service.findById("nonexistent")).rejects.toThrow(NotFoundException);
});
});
describe("findByEmail", () => {
it("should return user when found", async () => {
const user = { id: "u1", email: "ali@test.com" };
const db = createMockDb({ _selectRows: [user] });
const service = new UsersService(db as any);
const result = await service.findByEmail("ali@test.com");
expect(result).toEqual(user);
});
it("should return null when not found", async () => {
const db = createMockDb({ _selectRows: [] });
const service = new UsersService(db as any);
const result = await service.findByEmail("nobody@test.com");
expect(result).toBeNull();
});
});
describe("updateProfile", () => {
it("should return updated user on success", async () => {
const updated = { id: "u1", name: "Updated" };
const db = createMockDb({ _updateRows: [updated] });
const service = new UsersService(db as any);
const result = await service.updateProfile("u1", { name: "Updated" });
expect(result).toEqual(updated);
});
it("should throw NotFoundException when user not found", async () => {
const db = createMockDb({ _updateRows: [] });
const service = new UsersService(db as any);
await expect(service.updateProfile("nonexistent", { name: "X" })).rejects.toThrow(NotFoundException);
});
});
describe("getConnections", () => {
it("should return provider map with google true", async () => {
// getConnections: select({providerId}).from(accounts).where(...) — where is terminal
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnValue([{ providerId: "google" }]),
}),
};
const service = new UsersService(db as any);
const result = await service.getConnections("u1");
expect(result).toEqual({ google: true });
});
it("should return provider map with google false when no google account", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnValue([]),
}),
};
const service = new UsersService(db as any);
const result = await service.getConnections("u1");
expect(result).toEqual({ google: false });
});
});
describe("unlinkConnection", () => {
it("should return success when connection deleted", async () => {
const db = createMockDb({ _deleteRows: [{ id: "acc-1" }] });
const service = new UsersService(db as any);
const result = await service.unlinkConnection("u1", "google");
expect(result).toEqual({ success: true });
});
it("should throw NotFoundException when connection not found", async () => {
const db = createMockDb({ _deleteRows: [] });
const service = new UsersService(db as any);
await expect(service.unlinkConnection("u1", "google")).rejects.toThrow(NotFoundException);
});
});
describe("changePassword", () => {
it("should return success when password changed", async () => {
vi.mocked(verifyPassword).mockResolvedValue(true);
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "acc-1", password: "old-hash", providerId: "credential" }]),
}),
update: vi.fn().mockReturnValue({
set: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue(undefined),
}),
};
const service = new UsersService(db as any);
const result = await service.changePassword("u1", "oldpass", "newpassword");
expect(result).toEqual({ success: true });
});
it("should throw BadRequestException when no credential account", async () => {
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]),
}),
};
const service = new UsersService(db as any);
await expect(service.changePassword("u1", "old", "newpassword")).rejects.toThrow(BadRequestException);
});
it("should throw BadRequestException when current password is wrong", async () => {
vi.mocked(verifyPassword).mockResolvedValue(false);
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "acc-1", password: "hash", providerId: "credential" }]),
}),
};
const service = new UsersService(db as any);
await expect(service.changePassword("u1", "wrongpass", "newpassword")).rejects.toThrow(BadRequestException);
});
it("should throw BadRequestException when new password is too short", async () => {
vi.mocked(verifyPassword).mockResolvedValue(true);
const db = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([{ id: "acc-1", password: "hash", providerId: "credential" }]),
}),
};
const service = new UsersService(db as any);
await expect(service.changePassword("u1", "oldpass", "short")).rejects.toThrow(BadRequestException);
});
});
describe("deleteAccount", () => {
it("should return success when account deleted", async () => {
const db = createMockDb({ _deleteRows: [{ id: "u1" }] });
const service = new UsersService(db as any);
const result = await service.deleteAccount("u1");
expect(result).toEqual({ success: true });
});
it("should throw NotFoundException when user not found", async () => {
const db = createMockDb({ _deleteRows: [] });
const service = new UsersService(db as any);
await expect(service.deleteAccount("nonexistent")).rejects.toThrow(NotFoundException);
});
});
describe("findAll", () => {
it("should return paginated items and total", async () => {
const items = [{ id: "u1" }, { id: "u2" }];
// findAll: Promise.all([
// db.select().from(users).limit(limit).offset(offset).orderBy(createdAt),
// db.select({count}).from(users),
// ])
let callCount = 0;
const db = {
select: vi.fn().mockImplementation(() => {
callCount++;
const captured = callCount;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockImplementation(() => {
if (captured === 2) return [{ id: "c" }]; // from is terminal for count query
return chain;
});
chain.limit = vi.fn().mockReturnValue(chain);
chain.offset = vi.fn().mockReturnValue(chain);
chain.orderBy = vi.fn().mockReturnValue(items);
return chain;
}),
};
const service = new UsersService(db as any);
const result = await service.findAll(1, 20);
expect(result.items).toEqual(items);
expect(result.total).toBeDefined();
});
});
});

View File

@@ -0,0 +1,346 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { BadRequestException, ForbiddenException, NotFoundException } from "@nestjs/common";
import { VehiclesService } from "./vehicles.service";
vi.mock("@sase/shared", () => ({
isValidVin: vi.fn(),
}));
import { isValidVin } from "@sase/shared";
function createMockDb(overrides: Record<string, unknown> = {}) {
function chainable(terminalValue: unknown) {
const chain: Record<string, unknown> = {};
const methods = [
"select", "from", "where", "orderBy", "limit", "offset",
"innerJoin", "leftJoin", "insert", "values", "update", "set",
"delete", "returning", "onConflictDoNothing", "groupBy",
];
for (const m of methods) chain[m] = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockReturnValue(terminalValue);
chain.returning = vi.fn().mockReturnValue(terminalValue);
chain.offset = vi.fn().mockReturnValue(terminalValue);
return chain;
}
return {
select: vi.fn().mockImplementation(() => chainable(overrides._selectRows ?? [])),
insert: vi.fn().mockImplementation(() => chainable(overrides._insertRows ?? [])),
update: vi.fn().mockImplementation(() => chainable(overrides._updateRows ?? [])),
delete: vi.fn().mockImplementation(() => chainable(overrides._deleteRows ?? [])),
...overrides,
};
}
function createService(dbOrOverrides: any = {}) {
const db = typeof dbOrOverrides.select === "function" ? dbOrOverrides : createMockDb(dbOrOverrides);
const corgiService = {
decodeVin: vi.fn(),
};
const pl24Service = {
decodeVin: vi.fn(),
};
const vinApiService = {
decodeVin: vi.fn(),
};
const emexService = {
getScrapedVehicle: vi.fn(),
decodeVin: vi.fn(),
};
const service = new VehiclesService(
db as any,
corgiService as any,
pl24Service as any,
vinApiService as any,
emexService as any,
);
return { service, db, corgiService, pl24Service, vinApiService, emexService };
}
describe("VehiclesService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("decodeVin", () => {
it("should throw BadRequestException for invalid VIN", async () => {
vi.mocked(isValidVin).mockReturnValue(false);
const { service } = createService();
await expect(service.decodeVin("BADVIN", "u1")).rejects.toThrow(BadRequestException);
});
it("should return cached vehicle if fresh (<24h)", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
const cached = {
id: "v1",
vin: "WBAPH5C55BA123456",
userId: "u1",
brandId: "b1",
updatedAt: new Date(), // fresh
};
// db.select() calls:
// 1st: cache check (vehicles) → returns cached
// 2nd: logQuery insert (won't be called since we stub insert)
const selectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([cached]),
};
const insertChain = {
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([]),
};
const db = {
select: vi.fn().mockReturnValue(selectChain),
insert: vi.fn().mockReturnValue(insertChain),
};
const { service } = createService(db);
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(cached);
});
it("should throw BadRequestException when corgi doesn't recognize VIN", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
const selectChain = {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]), // no cache
};
const insertChain = {
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([]),
};
const db = {
select: vi.fn().mockReturnValue(selectChain),
insert: vi.fn().mockReturnValue(insertChain),
};
const { service, corgiService } = createService(db);
corgiService.decodeVin.mockReturnValue(null);
await expect(service.decodeVin("WBAPH5C55BA123456", "u1")).rejects.toThrow(BadRequestException);
});
it("should throw BadRequestException when brand not found in DB", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnValue([]), // no cache, no brand
};
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([]),
}),
};
const { service, corgiService } = createService(db);
corgiService.decodeVin.mockReturnValue({ isKnown: true, brandName: "UnknownBrand", modelYear: 2020 });
await expect(service.decodeVin("WBAPH5C55BA123456", "u1")).rejects.toThrow(BadRequestException);
});
it("should throw ForbiddenException when user has no active subscription", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
let selectCall = 0;
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
if (selectCall === 1) return []; // no cache
if (selectCall === 2) return [{ id: "b1", name: "BMW" }]; // brand found
if (selectCall === 3) return []; // no active subscription
return [];
}),
};
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([]),
}),
};
const { service, corgiService } = createService(db);
corgiService.decodeVin.mockReturnValue({ isKnown: true, brandName: "BMW", modelYear: 2020 });
await expect(service.decodeVin("WBAPH5C55BA123456", "u1")).rejects.toThrow(ForbiddenException);
});
it("should use PL24 when available and save vehicle", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
let selectCall = 0;
const savedVehicle = { id: "v-new", vin: "WBAPH5C55BA123456", source: "pl24" };
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
if (selectCall === 1) return []; // no cache
if (selectCall === 2) return [{ id: "b1", name: "BMW" }]; // brand
if (selectCall === 3) return [{ id: "sub-1", status: "active" }]; // subscription
if (selectCall === 4) return [{ id: "ba-1" }]; // brand access
return [];
}),
};
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([savedVehicle]),
}),
};
const { service, corgiService, pl24Service } = createService(db);
corgiService.decodeVin.mockReturnValue({ isKnown: true, brandName: "BMW", modelYear: 2020 });
pl24Service.decodeVin.mockResolvedValue({ vehicleId: "pl24-v1", modelCode: "320i", yearFrom: 2020 });
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(savedVehicle);
});
it("should fallback to emex when PL24 returns null", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
let selectCall = 0;
const savedVehicle = { id: "v-emex", source: "emex" };
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
if (selectCall === 1) return [];
if (selectCall === 2) return [{ id: "b1", name: "BMW" }];
if (selectCall === 3) return [{ id: "sub-1", status: "active" }];
if (selectCall === 4) return [{ id: "ba-1" }];
return [];
}),
};
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([savedVehicle]),
}),
};
const { service, corgiService, pl24Service, emexService } = createService(db);
corgiService.decodeVin.mockReturnValue({ isKnown: true, brandName: "BMW", modelYear: 2020 });
pl24Service.decodeVin.mockResolvedValue(null);
emexService.getScrapedVehicle.mockResolvedValue({ modelCode: "320i", yearFrom: 2020, engine: "N20", rawData: {} });
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(savedVehicle);
expect(emexService.getScrapedVehicle).toHaveBeenCalledWith("WBAPH5C55BA123456");
});
it("should fallback to vinApi when PL24 and emex fail", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
let selectCall = 0;
const savedVehicle = { id: "v-api", source: "vin-api" };
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockImplementation(() => {
if (selectCall === 1) return [];
if (selectCall === 2) return [{ id: "b1", name: "BMW" }];
if (selectCall === 3) return [{ id: "sub-1", status: "active" }];
if (selectCall === 4) return [{ id: "ba-1" }];
return [];
}),
};
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockReturnValue([savedVehicle]),
}),
};
const { service, corgiService, pl24Service, emexService, vinApiService } = createService(db);
corgiService.decodeVin.mockReturnValue({ isKnown: true, brandName: "BMW", modelYear: 2020 });
pl24Service.decodeVin.mockResolvedValue(null);
emexService.getScrapedVehicle.mockResolvedValue(null);
emexService.decodeVin.mockResolvedValue(undefined);
vinApiService.decodeVin.mockResolvedValue({ model: "320i", modelYear: "2020" });
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(savedVehicle);
expect(vinApiService.decodeVin).toHaveBeenCalledWith("WBAPH5C55BA123456");
});
});
describe("getHistory", () => {
it("should return paginated vehicle list", async () => {
const vehicleList = [{ id: "v1" }, { id: "v2" }];
// Chain: select().from().where().orderBy().limit().offset()
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockReturnValue(chain);
chain.orderBy = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockReturnValue(chain);
chain.offset = vi.fn().mockReturnValue(vehicleList);
const db = { select: vi.fn().mockReturnValue(chain) };
const { service } = createService(db);
const result = await service.getHistory("u1", 1, 20);
expect(result).toEqual(vehicleList);
});
});
describe("getById", () => {
it("should return vehicle when found", async () => {
const vehicle = { id: "v1", userId: "u1", vin: "WBAPH5C55BA123456" };
const db = createMockDb({ _selectRows: [vehicle] });
const { service } = createService(db);
const result = await service.getById("v1", "u1");
expect(result).toEqual(vehicle);
});
it("should throw NotFoundException when not found", async () => {
const db = createMockDb({ _selectRows: [] });
const { service } = createService(db);
await expect(service.getById("nonexistent", "u1")).rejects.toThrow(NotFoundException);
});
});
describe("deleteVehicle", () => {
it("should return deleted true on success", async () => {
const db = createMockDb({ _deleteRows: [{ id: "v1" }] });
const { service } = createService(db);
const result = await service.deleteVehicle("v1", "u1");
expect(result).toEqual({ deleted: true });
});
it("should throw NotFoundException when not found", async () => {
const db = createMockDb({ _deleteRows: [] });
const { service } = createService(db);
await expect(service.deleteVehicle("nonexistent", "u1")).rejects.toThrow(NotFoundException);
});
});
});