fix(housekeeping): harden preference persistence

This commit is contained in:
Simo committed 2026-08-27 17:44:53 +02:00
1 parent 64bb230de5
commit 4e0bf598ed
7 files changed
+272 -82

No files matched your search

+50 -49
View File
@@ -1,89 +1,90 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
getHousekeepingCapabilityContext: vi.fn(),
}));
import type { HousekeepingPreferencesRepository } from "@/features/housekeeping/foundation/preferences/repository";
const preferenceRepository = vi.hoisted(() => ({
read: vi.fn(),
upsert: vi.fn(),
}));
vi.mock("@/lib/housekeeping-preferences-repository", () => ({
housekeepingPreferencesRepository: preferenceRepository,
}));
import { defaultHousekeepingPreferences } from "@/features/housekeeping/foundation/preferences/schema";
import type { HousekeepingRegistry } from "@/features/housekeeping/foundation/registry";
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
import {
type HousekeepingPreferencesActionDependencies,
loadHousekeepingPreferences,
saveHousekeepingPreferences,
} from "./housekeeping-preferences";
const registry: HousekeepingRegistry = { domains: [] };
const allowedContext = {
actor: { id: 42, username: "operator", rank: 0 },
isSuperAdmin: false,
has: (slug: string) => slug === "admin.dashboard",
hasAny: (...slugs: string[]) => slugs.includes("admin.dashboard"),
hasAll: (...slugs: string[]) =>
slugs.every((slug) => slug === "admin.dashboard"),
has: () => true,
hasAny: () => true,
hasAll: () => true,
};
function dependencies(
context = allowedContext,
): HousekeepingPreferencesActionDependencies & {
repository: HousekeepingPreferencesRepository;
} {
return {
repository: {
read: vi.fn().mockResolvedValue(defaultHousekeepingPreferences()),
upsert: vi.fn(),
},
registry,
getContext: vi.fn().mockResolvedValue(context),
};
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getHousekeepingCapabilityContext).mockResolvedValue(allowedContext);
preferenceRepository.read.mockResolvedValue(defaultHousekeepingPreferences());
});
describe("housekeeping preference actions", () => {
it("derives the read owner from request capability context", async () => {
const deps = dependencies();
const result = await loadHousekeepingPreferences(deps);
expect(result.ok).toBe(true);
expect(deps.repository.read).toHaveBeenCalledWith(42);
expect(deps.getContext).toHaveBeenCalledOnce();
it("does not expose dependency or user identity parameters to callers", () => {
expect(loadHousekeepingPreferences).toHaveLength(0);
expect(saveHousekeepingPreferences).toHaveLength(1);
});
it("rejects a denied operator without reading or writing another user preferences", async () => {
const deps = dependencies({ ...allowedContext, hasAny: () => false });
it("derives the read owner from the server capability context", async () => {
const result = await loadHousekeepingPreferences();
expect(result.ok).toBe(true);
expect(preferenceRepository.read).toHaveBeenCalledWith(42);
});
it("rejects a denied operator without reading or writing preferences", async () => {
vi.mocked(getHousekeepingCapabilityContext).mockResolvedValueOnce({
...allowedContext,
hasAny: () => false,
});
const result = await saveHousekeepingPreferences(
defaultHousekeepingPreferences(),
deps,
);
expect(result).toMatchObject({ ok: false, error: { code: "FORBIDDEN" } });
expect(deps.repository.read).not.toHaveBeenCalled();
expect(deps.repository.upsert).not.toHaveBeenCalled();
expect(preferenceRepository.read).not.toHaveBeenCalled();
expect(preferenceRepository.upsert).not.toHaveBeenCalled();
});
it("validates writes and persists them for the context actor only", async () => {
const deps = dependencies();
it("reconciles input before persisting or returning it", async () => {
const value = {
...defaultHousekeepingPreferences(),
pinnedRouteIds: ["people.users"],
pinnedRouteIds: ["removed.route"],
pinnedCommandIds: ["removed.command"],
};
const result = await saveHousekeepingPreferences(value, deps);
const result = await saveHousekeepingPreferences(value);
expect(result).toMatchObject({ ok: true, data: value });
expect(deps.repository.upsert).toHaveBeenCalledWith(42, value);
expect(result).toMatchObject({
ok: true,
data: { pinnedRouteIds: [], pinnedCommandIds: [] },
});
expect(preferenceRepository.upsert).toHaveBeenCalledWith(
42,
expect.objectContaining({ pinnedRouteIds: [], pinnedCommandIds: [] }),
);
});
it("returns a validation result before an invalid payload reaches persistence", async () => {
const deps = dependencies();
const result = await saveHousekeepingPreferences(
{ schemaVersion: 2 },
deps,
);
const result = await saveHousekeepingPreferences({ schemaVersion: 2 });
expect(result).toMatchObject({ ok: false, error: { code: "VALIDATION" } });
expect(deps.repository.upsert).not.toHaveBeenCalled();
expect(preferenceRepository.upsert).not.toHaveBeenCalled();
});
});