Files
findr-api/test/boxes.service.test.ts
T
mars3142 cadc280761 Scaffold Fastify + Drizzle API with pick-by-light
Layered: routes -> services -> repositories -> db, over PostgreSQL 18 with
split roles (findr_migrator for DDL, findr_app for DML). Keycloak JWT auth
with per-endpoint scope guards. Drizzle migrations run with the DDL role at
container start.

Pick-by-light: one ESP32 controller drives one WS2812 chain through several
boxes; each box owns a contiguous LED slice. MQTT contract in
src/lib/mqtt-topics.ts / docs/mqtt.md. /v1/pick controls light/idle/off and
/v1/boxes + /v1/controllers manage the strip mapping.

Reference resource /v1/parts wired end to end. Vitest, Biome, multi-stage
Dockerfile, local compose.yml with Postgres.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-03 01:07:35 +02:00

141 lines
4.0 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { ConflictError, ValidationError } from "../src/lib/errors.js";
import type { Box, BoxesRepository, Controller } from "../src/repositories/boxes.repository.js";
import { createBoxesService } from "../src/services/boxes.service.js";
const controller: Controller = {
id: "c1",
espId: "esp-a",
name: "Wall",
ledCount: 160,
online: true,
lastSeenAt: null,
firmware: null,
createdAt: new Date(),
updatedAt: new Date(),
};
function box(over: Partial<Box>): Box {
return {
id: "b?",
number: 1,
name: "Box",
controllerId: "c1",
ledOffset: 0,
ledCount: 40,
columns: 8,
rows: 5,
wiring: "progressive",
createdAt: new Date(),
updatedAt: new Date(),
...over,
};
}
function repo(over: Partial<BoxesRepository> = {}): BoxesRepository {
return {
listControllers: vi.fn(),
findControllerById: vi.fn().mockResolvedValue(controller),
findControllerByEspId: vi.fn().mockResolvedValue(controller),
createController: vi.fn(),
updateController: vi.fn(),
upsertControllerByEspId: vi.fn(),
listBoxes: vi.fn(),
boxesForController: vi.fn().mockResolvedValue([]),
findBoxByNumber: vi.fn().mockResolvedValue(undefined),
createBox: vi.fn((d) => Promise.resolve(box(d))),
updateBox: vi.fn(),
setLocationLedIndex: vi.fn(),
...over,
} as unknown as BoxesRepository;
}
describe("boxesService.upsertBox — LED slice validation", () => {
it("rejects a slice that runs past the controller's chain", async () => {
const svc = createBoxesService(repo());
await expect(
svc.upsertBox({
number: 2,
name: "B2",
controllerEspId: "esp-a",
ledOffset: 140,
ledCount: 40,
}),
).rejects.toBeInstanceOf(ValidationError);
});
it("rejects a slice that overlaps a sibling box", async () => {
const svc = createBoxesService(
repo({
boxesForController: vi
.fn()
.mockResolvedValue([box({ number: 1, ledOffset: 0, ledCount: 40 })]),
}),
);
await expect(
svc.upsertBox({
number: 2,
name: "B2",
controllerEspId: "esp-a",
ledOffset: 20,
ledCount: 40,
}),
).rejects.toBeInstanceOf(ConflictError);
});
it("accepts a non-overlapping slice and returns its config", async () => {
const svc = createBoxesService(
repo({
boxesForController: vi
.fn()
.mockResolvedValue([box({ number: 1, ledOffset: 0, ledCount: 40 })]),
}),
);
const { config } = await svc.upsertBox({
number: 2,
name: "B2",
controllerEspId: "esp-a",
ledOffset: 40,
ledCount: 40,
wiring: "serpentine",
});
expect(config).toMatchObject({
boxNumber: 2,
controllerEspId: "esp-a",
ledOffset: 40,
wiring: "serpentine",
});
});
});
describe("boxesService.resolvePickTargets", () => {
it("uses a registered compartment's explicit LED index", async () => {
const b = {
...box({ number: 3, columns: 8, rows: 5, wiring: "progressive" }),
locations: [
{
id: "l1",
boxId: "b?",
code: "K3·D4",
column: "D",
row: 4,
ledIndex: 99,
description: null,
createdAt: new Date(),
updatedAt: new Date(),
},
],
};
const svc = createBoxesService(repo({ findBoxByNumber: vi.fn().mockResolvedValue(b) }));
const targets = await svc.resolvePickTargets(3, [{ cell: "D4", mode: "take" }]);
expect(targets).toEqual([{ cell: "D4", mode: "take", ledIndex: 99 }]);
});
it("falls back to the grid formula for an unregistered cell", async () => {
const b = { ...box({ number: 3, columns: 8, rows: 5, wiring: "progressive" }), locations: [] };
const svc = createBoxesService(repo({ findBoxByNumber: vi.fn().mockResolvedValue(b) }));
const targets = await svc.resolvePickTargets(3, [{ cell: "D4", mode: "put" }]);
expect(targets).toEqual([{ cell: "D4", mode: "put", ledIndex: 27 }]);
});
});