fix(housekeeping): secure radio settings cache
This commit is contained in:
1 parent
845666cf37
commit
f24adfcf66
13 files changed
+796
-188
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import type { SQL } from "drizzle-orm";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
||||
import { HotelMutationFailure } from "./mutations";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
rows: [] as Array<Record<string, unknown>>,
|
||||
writes: [] as Array<Record<string, unknown>>,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const schema = await import("@/db/schema");
|
||||
const database = {
|
||||
select: vi.fn(() => {
|
||||
const query = {
|
||||
from: vi.fn(() => query),
|
||||
where: vi.fn((_condition: SQL | undefined) => {
|
||||
const result = Promise.resolve(state.rows) as Promise<
|
||||
Array<Record<string, unknown>>
|
||||
> & {
|
||||
limit: (count: number) => Promise<Array<Record<string, unknown>>>;
|
||||
};
|
||||
result.limit = vi.fn(() => result);
|
||||
return result;
|
||||
}),
|
||||
};
|
||||
return query;
|
||||
}),
|
||||
insert: vi.fn(() => ({
|
||||
values: (values: Record<string, unknown>) => ({
|
||||
onDuplicateKeyUpdate: async () => {
|
||||
state.writes.push(values);
|
||||
},
|
||||
}),
|
||||
})),
|
||||
};
|
||||
return { ...schema, db: database };
|
||||
});
|
||||
|
||||
import { executeHotelMutationOperation } from "./mutations-runtime";
|
||||
|
||||
const capability: HousekeepingCapabilityContext = {
|
||||
actor: { id: 42, username: "operator", rank: 7 },
|
||||
isSuperAdmin: false,
|
||||
has: () => true,
|
||||
hasAny: () => true,
|
||||
hasAll: () => true,
|
||||
};
|
||||
const context = {
|
||||
capability,
|
||||
correlationId: "radio-runtime",
|
||||
legacy: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
state.rows = [];
|
||||
state.writes = [];
|
||||
});
|
||||
|
||||
describe("Hotel radio settings runtime", () => {
|
||||
it("rejects an unrelated setting key before any write", async () => {
|
||||
await expect(
|
||||
executeHotelMutationOperation(
|
||||
"radio.settings.save-one",
|
||||
{ key: "cms_secret", value: "private" },
|
||||
context,
|
||||
),
|
||||
).rejects.toBeInstanceOf(HotelMutationFailure);
|
||||
expect(state.writes).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects duplicate and invalid bulk entries atomically", async () => {
|
||||
for (const entries of [
|
||||
[
|
||||
{ key: "radio_name", value: "one" },
|
||||
{ key: "radio_name", value: "two" },
|
||||
],
|
||||
[
|
||||
{ key: "radio_name", value: "one" },
|
||||
{ key: "site_name", value: "forged" },
|
||||
],
|
||||
]) {
|
||||
await expect(
|
||||
executeHotelMutationOperation(
|
||||
"radio.settings.save-many",
|
||||
{ entries },
|
||||
context,
|
||||
),
|
||||
).rejects.toBeInstanceOf(HotelMutationFailure);
|
||||
expect(state.writes).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts the complete 102-entry curated form within the 500-entry bound", async () => {
|
||||
const entries = Array.from({ length: 102 }, (_, index) => ({
|
||||
key: `radio_curated_${index}`,
|
||||
value: `value-${index}`,
|
||||
}));
|
||||
|
||||
await executeHotelMutationOperation(
|
||||
"radio.settings.save-many",
|
||||
{ entries },
|
||||
context,
|
||||
);
|
||||
|
||||
expect(state.writes).toHaveLength(102);
|
||||
});
|
||||
|
||||
it("rejects a bulk request above 500 entries", async () => {
|
||||
const entries = Array.from({ length: 501 }, (_, index) => ({
|
||||
key: `radio_setting_${index}`,
|
||||
value: "value",
|
||||
}));
|
||||
|
||||
await expect(
|
||||
executeHotelMutationOperation(
|
||||
"radio.settings.save-many",
|
||||
{ entries },
|
||||
context,
|
||||
),
|
||||
).rejects.toBeInstanceOf(HotelMutationFailure);
|
||||
expect(state.writes).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"radio.settings.save-one" as const,
|
||||
{ key: "radio_stream_url", value: "https://private.example/stream" },
|
||||
],
|
||||
[
|
||||
"radio.settings.save-many" as const,
|
||||
{
|
||||
entries: [{ key: "radio_name", value: "private station name" }],
|
||||
},
|
||||
],
|
||||
[
|
||||
"radio.points.save" as const,
|
||||
{
|
||||
radio_points_enabled: "1",
|
||||
radio_points_per_minute: "17",
|
||||
radio_points_currency: "credits",
|
||||
radio_points_max_per_day: "34",
|
||||
radio_points_min_listeners: "2",
|
||||
},
|
||||
],
|
||||
])(
|
||||
"does not expose private values in %s audit snapshots",
|
||||
async (operation, input) => {
|
||||
const snapshot = await executeHotelMutationOperation(
|
||||
operation,
|
||||
input,
|
||||
context,
|
||||
);
|
||||
const serialized = JSON.stringify(snapshot);
|
||||
for (const privateValue of [
|
||||
"https://private.example/stream",
|
||||
"private station name",
|
||||
"17",
|
||||
"34",
|
||||
]) {
|
||||
expect(serialized).not.toContain(privateValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user