Files
findr-api/test/parts.service.test.ts
T
mars3142 b308ed86ae
CI / test (push) Successful in 2m15s
CI / build-and-push (push) Successful in 21s
CI / deploy (push) Successful in 6s
Add distributors, parameters, projects and pick lists to schema
Extends the parts domain with what the mockups already show but the
schema didn't cover yet:

- part_parameters replaces the parameters JSON blob (comment said
  'promote to a table later') - text and min/typical/max+unit values,
  so numeric specs stay filterable/sortable later without another
  migration.
- distributors + part_distributors: one row per (part, distributor)
  price, matching the LCSC/Mouser/Reichelt tab on the part detail page.
- projects + project_parts: BOM ('used in'), with a single status
  (planned/building/built) driving the 'N planned' vs 'N built' wording
  instead of two separate counters.
- pick_lists + pick_list_items: checklist for one build batch,
  independent of projects since a pick list can be ad-hoc.

The below-minimum reorder list needs no new table - it's already the
existing belowMinimum filter on GET /v1/parts.

Verified by running both migrations against a real Postgres 18 (ICU
de-DE collation) and the existing test suite.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 17:06:54 +02:00

70 lines
2.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { ConflictError, NotFoundError } from "../src/lib/errors.js";
import type { Part, PartsRepository } from "../src/repositories/parts.repository.js";
import { createPartsService } from "../src/services/parts.service.js";
function fakePart(over: Partial<Part> = {}): Part {
return {
id: "00000000-0000-0000-0000-000000000001",
mpn: "ESP32-WROOM-32E",
manufacturer: "Espressif",
description: "",
categoryId: null,
package: null,
minStock: 5,
lcscId: null,
datasheetUrl: null,
photoUrl: null,
createdAt: new Date(),
updatedAt: new Date(),
...over,
};
}
function repoStub(over: Partial<PartsRepository> = {}): PartsRepository {
return {
list: vi.fn(),
findById: vi.fn(),
findByLcscId: vi.fn(),
create: vi.fn(),
update: vi.fn(),
...over,
} as unknown as PartsRepository;
}
describe("partsService.get", () => {
it("returns the part when it exists", async () => {
const part = fakePart();
const service = createPartsService(repoStub({ findById: vi.fn().mockResolvedValue(part) }));
await expect(service.get(part.id)).resolves.toBe(part);
});
it("throws NotFoundError when missing", async () => {
const service = createPartsService(
repoStub({ findById: vi.fn().mockResolvedValue(undefined) }),
);
await expect(service.get("missing")).rejects.toBeInstanceOf(NotFoundError);
});
});
describe("partsService.create", () => {
it("rejects a duplicate LCSC id", async () => {
const service = createPartsService(
repoStub({ findByLcscId: vi.fn().mockResolvedValue(fakePart()) }),
);
await expect(service.create({ mpn: "X", lcscId: "C2913196" })).rejects.toBeInstanceOf(
ConflictError,
);
});
it("creates when the LCSC id is free", async () => {
const created = fakePart({ mpn: "X" });
const create = vi.fn().mockResolvedValue(created);
const service = createPartsService(
repoStub({ findByLcscId: vi.fn().mockResolvedValue(undefined), create }),
);
await expect(service.create({ mpn: "X", lcscId: "C2913196" })).resolves.toBe(created);
expect(create).toHaveBeenCalledWith(expect.objectContaining({ mpn: "X", lcscId: "C2913196" }));
});
});