Files
Termix/src/backend/tests/hosts/locks.test.ts
T

114 lines
3.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("../../utils/logger.js", () => ({
pluginLogger: { warn: vi.fn() },
}));
const {
assertHostsUnlocked,
assertManualCreateAllowed,
clearHostLocks,
hasHostLocks,
hostLock,
HostLockedError,
manualCreateBlockedBy,
provideHostLocks,
requireManualCreate,
requireUnlockedHost,
} = await import("../../hosts/locks.js");
const provider = (locked: number[], allowManualCreate = true) => ({
isLocked: (hostId: number) => locked.includes(hostId),
note: (hostId: number) => `id ${hostId}`,
allowManualCreate: () => allowManualCreate,
});
function fakeRes() {
const res = {
code: 0,
body: null as unknown,
status(code: number) {
res.code = code;
return { json: (body: unknown) => (res.body = body) };
},
};
return res;
}
afterEach(() => clearHostLocks());
describe("host locks", () => {
it("reports nothing without providers", () => {
expect(hasHostLocks()).toBe(false);
expect(hostLock(1)).toBeNull();
expect(() => assertHostsUnlocked([1, 2])).not.toThrow();
expect(manualCreateBlockedBy()).toBeNull();
});
it("names the plugin and note for a locked host", () => {
provideHostLocks("cfg", "Config", provider([3]));
expect(hostLock(3)).toEqual({
pluginId: "cfg",
pluginName: "Config",
note: "id 3",
});
expect(hostLock(4)).toBeNull();
expect(() => assertHostsUnlocked([4, 3])).toThrow(HostLockedError);
});
it("lets the locking plugin through", () => {
provideHostLocks("cfg", "Config", provider([3], false));
expect(() => assertHostsUnlocked([3], "cfg")).not.toThrow();
expect(() => assertManualCreateAllowed("cfg")).not.toThrow();
expect(() => assertManualCreateAllowed()).toThrow(/managed by Config/);
});
it("drops a provider when removed", () => {
const remove = provideHostLocks("cfg", "Config", provider([3], false));
remove();
expect(hostLock(3)).toBeNull();
expect(manualCreateBlockedBy()).toBeNull();
});
it("never locks when a provider throws", () => {
provideHostLocks("cfg", "Config", {
isLocked: () => {
throw new Error("broken");
},
note: () => null,
allowManualCreate: () => {
throw new Error("broken");
},
});
expect(hostLock(1)).toBeNull();
expect(manualCreateBlockedBy()).toBeNull();
});
});
describe("lock route guards", () => {
it("answers 403 HOST_LOCKED for a locked :id", () => {
provideHostLocks("cfg", "Config", provider([7]));
const res = fakeRes();
const next = vi.fn();
requireUnlockedHost({ params: { id: "7" } }, res, next);
expect(next).not.toHaveBeenCalled();
expect(res.code).toBe(403);
expect(res.body).toMatchObject({ code: "HOST_LOCKED", lockedBy: "cfg" });
requireUnlockedHost({ params: { id: "8" } }, fakeRes(), next);
expect(next).toHaveBeenCalledTimes(1);
});
it("blocks manual creates only while a provider says so", () => {
const next = vi.fn();
requireManualCreate({}, fakeRes(), next);
expect(next).toHaveBeenCalledTimes(1);
provideHostLocks("cfg", "Config", provider([], false));
const res = fakeRes();
requireManualCreate({}, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(res.code).toBe(403);
});
});