1
0
Fork 0
polaris-task-force/tests/int/custom-radio-tests.int.spec.ts
Z8MB1E 1cf11745fe feat(radio): add custom radio test authoring, play, and leaderboard
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-08 14:41:18 -04:00

915 lines
29 KiB
TypeScript

import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { Role, User } from "@/payload-types";
import { CustomRadioTests } from "@/collections/minigames/CustomRadioTests";
import { hasPermission } from "@/utils/access-control/hasPermission";
import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions";
import {
isRadioUnlocked,
isValidRadioOrder,
validateMessages,
validateRadioScore,
validateTestName,
} from "@/lib/custom-minigames/radio";
let payload: Payload;
const RUN = `crt-${Date.now().toString(36)}`;
const TIMEOUT = 30_000;
const sampleMessages = [
"VIKTOR ONE, EN ROUTE TO GRID 341-512, STATUS GREEN, OVER",
"ECHO TWO, CONTACT AT CHECKPOINT ALPHA, WEAPONS FREE, OUT",
"FOXTROT THREE, REQUESTING FIRE SUPPORT ON BEARING 270, OVER",
];
describe("Custom radio tests", () => {
let author: User;
let authorId: number;
const testIds: number[] = [];
const scoreIds: number[] = [];
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
author = (await payload.create({
collection: "users",
data: {
username: `${RUN}-author`,
discordUsername: `${RUN}-author`,
displayName: "RADIO AUTHOR",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
authorId = author.id;
});
afterAll(async () => {
// Delete scores first (FK → test)
for (const id of scoreIds) {
await payload
.delete({ collection: "custom-radio-test-scores", id, overrideAccess: true })
.catch(() => {});
}
// Delete tests
for (const id of testIds) {
await payload
.delete({ collection: "custom-radio-tests", id, overrideAccess: true })
.catch(() => {});
}
await payload
.delete({ collection: "users", id: authorId, overrideAccess: true })
.catch(() => {});
});
// ---------------------------------------------------------------------------
// Collection-level field constraints
// ---------------------------------------------------------------------------
describe("Collection field validation", () => {
it("creates a valid test with all required fields", async () => {
const doc = await payload.create({
collection: "custom-radio-tests",
data: {
name: "Test Radio Drill",
author: authorId,
messages: sampleMessages,
messageCount: 3,
order: "fixed",
published: true,
},
overrideAccess: true,
depth: 0,
});
testIds.push(doc.id);
expect(doc.name).toBe("Test Radio Drill");
expect(doc.messageCount).toBe(3);
expect(doc.order).toBe("fixed");
expect(doc.published).toBe(true);
expect(doc.playCount).toBe(0);
});
it("defaults order to fixed and published to true", async () => {
const doc = await payload.create({
collection: "custom-radio-tests",
data: {
name: "Defaults Test",
author: authorId,
messages: sampleMessages,
messageCount: 3,
},
overrideAccess: true,
depth: 0,
});
testIds.push(doc.id);
expect(doc.order).toBe("fixed");
expect(doc.published).toBe(true);
expect(doc.playCount).toBe(0);
});
it("accepts shuffled order", async () => {
const doc = await payload.create({
collection: "custom-radio-tests",
data: {
name: "Shuffled Test",
author: authorId,
messages: sampleMessages,
messageCount: 3,
order: "shuffled",
},
overrideAccess: true,
depth: 0,
});
testIds.push(doc.id);
expect(doc.order).toBe("shuffled");
});
it("rejects a test without a name", async () => {
await expect(
payload.create({
collection: "custom-radio-tests",
data: {
author: authorId,
messages: sampleMessages,
messageCount: 3,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects a test without messages", async () => {
await expect(
payload.create({
collection: "custom-radio-tests",
data: {
name: "Empty",
author: authorId,
messageCount: 0,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects a test without an author", async () => {
await expect(
payload.create({
collection: "custom-radio-tests",
data: {
name: "Orphan",
messages: sampleMessages,
messageCount: 3,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects messageCount below minimum (0 < 1)", async () => {
await expect(
payload.create({
collection: "custom-radio-tests",
data: {
name: "Zero Messages",
author: authorId,
messages: [],
messageCount: 0,
},
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
});
// ---------------------------------------------------------------------------
// Score collection
// ---------------------------------------------------------------------------
describe("Custom radio test scores", () => {
let testId: number;
beforeAll(async () => {
const doc = await payload.create({
collection: "custom-radio-tests",
data: {
name: "Score Test Radio",
author: authorId,
messages: sampleMessages,
messageCount: 3,
},
overrideAccess: true,
depth: 0,
});
testId = doc.id;
testIds.push(testId);
});
it("creates a sprint score with valid fields", async () => {
const score = await payload.create({
collection: "custom-radio-test-scores",
data: {
test: testId,
user: authorId,
score: 850,
accuracy: 0.95,
correctMessages: 9,
incorrectMessages: 1,
mode: "sprint",
timeMs: 45000,
},
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
expect(score.test).toBe(testId);
expect(score.user).toBe(authorId);
expect(score.score).toBe(850);
expect(score.accuracy).toBe(0.95);
expect(score.mode).toBe("sprint");
expect(score.timeMs).toBe(45000);
});
it("creates a watch score without timeMs", async () => {
const score = await payload.create({
collection: "custom-radio-test-scores",
data: {
test: testId,
user: authorId,
score: 320,
accuracy: 0.88,
correctMessages: 8,
incorrectMessages: 2,
mode: "watch",
},
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
expect(score.mode).toBe("watch");
expect(score.timeMs === null || score.timeMs === undefined).toBe(true);
});
it("rejects a score without a test reference", async () => {
await expect(
payload.create({
collection: "custom-radio-test-scores",
data: {
user: authorId,
score: 100,
accuracy: 0.5,
correctMessages: 5,
incorrectMessages: 5,
mode: "sprint",
timeMs: 30000,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects a score without a user reference", async () => {
await expect(
payload.create({
collection: "custom-radio-test-scores",
data: {
test: testId,
score: 100,
accuracy: 0.5,
correctMessages: 5,
incorrectMessages: 5,
mode: "sprint",
timeMs: 30000,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects a score without a mode", async () => {
await expect(
payload.create({
collection: "custom-radio-test-scores",
data: {
test: testId,
user: authorId,
score: 100,
accuracy: 0.5,
correctMessages: 5,
incorrectMessages: 5,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects score updates (update: () => false)", async () => {
const score = await payload.create({
collection: "custom-radio-test-scores",
data: {
test: testId,
user: authorId,
score: 100,
accuracy: 0.5,
correctMessages: 5,
incorrectMessages: 5,
mode: "watch",
},
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
await expect(
payload.update({
collection: "custom-radio-test-scores",
id: score.id,
data: { score: 999 },
overrideAccess: false,
depth: 0,
}),
).rejects.toThrow();
});
});
// ---------------------------------------------------------------------------
// Access control
// ---------------------------------------------------------------------------
describe("Access control", () => {
let viewer: User;
let viewerId: number;
beforeAll(async () => {
viewer = (await payload.create({
collection: "users",
data: {
username: `${RUN}-viewer`,
discordUsername: `${RUN}-viewer`,
displayName: "VIEWER",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
viewerId = viewer.id;
});
it("logged-in user can read published tests", async () => {
const result = await payload.find({
collection: "custom-radio-tests",
where: { published: { equals: true } },
limit: 1,
overrideAccess: true,
depth: 0,
});
expect(result.docs.length).toBeGreaterThanOrEqual(1);
});
it("logged-in user can create a test", async () => {
const doc = await payload.create({
collection: "custom-radio-tests",
data: {
name: "Can Create",
author: viewerId,
messages: sampleMessages,
messageCount: 3,
},
overrideAccess: true,
depth: 0,
});
testIds.push(doc.id);
expect(doc.name).toBe("Can Create");
});
it("published tests are visible to all logged-in users", async () => {
const result = await payload.find({
collection: "custom-radio-tests",
where: { published: { equals: true } },
limit: 20,
overrideAccess: true,
depth: 0,
});
expect(result.docs.some((doc) => doc.name === "Can Create")).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Cascade delete
// ---------------------------------------------------------------------------
describe("Cascade delete", () => {
it("deleting a test removes associated scores", async () => {
const test = await payload.create({
collection: "custom-radio-tests",
data: {
name: "Cascade Test Radio",
author: authorId,
messages: sampleMessages,
messageCount: 3,
},
overrideAccess: true,
depth: 0,
});
const score = await payload.create({
collection: "custom-radio-test-scores",
data: {
test: test.id,
user: authorId,
score: 200,
accuracy: 0.7,
correctMessages: 4,
incorrectMessages: 6,
mode: "sprint",
timeMs: 60000,
},
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
await payload.delete({
collection: "custom-radio-test-scores",
id: score.id,
overrideAccess: true,
});
await payload.delete({
collection: "custom-radio-tests",
id: test.id,
overrideAccess: true,
});
const remaining = await payload.findByID({
collection: "custom-radio-test-scores",
id: score.id,
overrideAccess: true,
depth: 0,
}).catch(() => null);
expect(remaining).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Unlock gating (pure lib: mirrors checkRadioUnlock)
// ---------------------------------------------------------------------------
describe("Unlock gating", () => {
it("locks custom radio tests until both accuracy requirements are met", () => {
expect(isRadioUnlocked({ sprintBestAccuracy: 0, watchBestAccuracy: 0 })).toBe(false);
expect(isRadioUnlocked({ sprintBestAccuracy: 0.9, watchBestAccuracy: 0.5 })).toBe(false);
expect(isRadioUnlocked({ sprintBestAccuracy: 0.5, watchBestAccuracy: 0.85 })).toBe(false);
});
it("unlocks at exactly the sprint (0.9) and watch (0.85) thresholds", () => {
expect(isRadioUnlocked({ sprintBestAccuracy: 0.9, watchBestAccuracy: 0.85 })).toBe(true);
expect(isRadioUnlocked({ sprintBestAccuracy: 1, watchBestAccuracy: 1 })).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Action-level validation (pure lib: mirrors createTest / updateTest /
// submitCustomTestScore)
// ---------------------------------------------------------------------------
describe("Action-level validation", () => {
it("rejects empty, oversized, and non-string names; trims valid ones", () => {
expect(validateTestName("").error).toBe("Name must be 1-100 characters.");
expect(validateTestName(" ").error).toBe("Name must be 1-100 characters.");
expect(validateTestName("x".repeat(101)).error).toBe("Name must be 1-100 characters.");
expect(validateTestName(123).error).toBe("Name must be 1-100 characters.");
expect(validateTestName(" Sweep Drill ")).toEqual({ name: "Sweep Drill" });
});
it("rejects non-array, wrong-size, non-string, and oversized message pools; trims valid ones", () => {
expect(validateMessages("boom").error).toBe("Messages must be a list of strings.");
expect(validateMessages([]).error).toBe("Message pool must contain 1-100 messages.");
expect(
validateMessages(Array.from({ length: 101 }, (_, i) => `m${i}`)).error,
).toBe("Message pool must contain 1-100 messages.");
expect(validateMessages([42]).error).toBe("Every message must be a string.");
expect(validateMessages([" "]).error).toBe("Messages must be 1-500 characters.");
expect(validateMessages(["x".repeat(501)]).error).toBe("Messages must be 1-500 characters.");
expect(validateMessages([" Alpha ", "Bravo"])).toEqual({
messages: ["Alpha", "Bravo"],
});
});
it("accepts only fixed and shuffled orders", () => {
expect(isValidRadioOrder("fixed")).toBe(true);
expect(isValidRadioOrder("shuffled")).toBe(true);
expect(isValidRadioOrder("random")).toBe(false);
expect(isValidRadioOrder("")).toBe(false);
});
it("validates score submissions per mode", () => {
const sprintResult = {
score: 120,
correctMessages: 4,
incorrectMessages: 1,
accuracy: 0.8,
mode: "sprint",
timeMs: 45_000,
};
const watchResult = {
score: 80,
correctMessages: 8,
incorrectMessages: 2,
accuracy: 0.8,
mode: "watch",
};
expect(validateRadioScore(sprintResult)).toEqual({
score: 120,
mode: "sprint",
timeMs: 45_000,
});
expect(validateRadioScore(watchResult)).toEqual({ score: 80, mode: "watch", timeMs: null });
expect(validateRadioScore({ ...watchResult, timeMs: 5_000 })).toEqual({
score: 80,
mode: "watch",
timeMs: null,
});
expect(validateRadioScore({ ...sprintResult, score: 100.9 })).toEqual({
score: 100,
mode: "sprint",
timeMs: 45_000,
});
expect(validateRadioScore({ ...sprintResult, mode: "nope" }).error).toBe("Invalid result.");
expect(validateRadioScore({ ...sprintResult, score: -1 }).error).toBe("Invalid score.");
expect(validateRadioScore({ ...sprintResult, score: Number.NaN }).error).toBe(
"Invalid score.",
);
expect(validateRadioScore({ ...sprintResult, score: 5001 }).error).toBe("Invalid score.");
expect(validateRadioScore({ ...sprintResult, correctMessages: -1 }).error).toBe(
"Invalid result.",
);
expect(validateRadioScore({ ...sprintResult, correctMessages: 101 }).error).toBe(
"Invalid result.",
);
expect(validateRadioScore({ ...sprintResult, incorrectMessages: -1 }).error).toBe(
"Invalid result.",
);
expect(validateRadioScore({ ...sprintResult, accuracy: 1.5 }).error).toBe("Invalid result.");
expect(validateRadioScore({ ...sprintResult, accuracy: -0.1 }).error).toBe(
"Invalid result.",
);
expect(validateRadioScore({ ...sprintResult, timeMs: undefined }).error).toBe(
"Invalid result.",
);
expect(validateRadioScore({ ...sprintResult, timeMs: 0 }).error).toBe("Invalid result.");
expect(validateRadioScore({ ...sprintResult, timeMs: 600_001 }).error).toBe(
"Invalid result.",
);
});
});
// ---------------------------------------------------------------------------
// Leaderboard ordering (DB: replicates CustomTestLeaderboard's query: where
// test + mode, sort -score, best score per player)
// ---------------------------------------------------------------------------
describe("Leaderboard ordering", () => {
let testA: number;
let testB: number;
const tempUserIds: number[] = [];
const makePlayer = async (label: string): Promise<number> => {
const user = (await payload.create({
collection: "users",
data: {
username: `${RUN}-lb-${label}`,
discordUsername: `${RUN}-lb-${label}`,
displayName: `LBOARD ${label.toUpperCase()}`,
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
tempUserIds.push(user.id);
return user.id;
};
const record = async (
testId: number,
userId: number,
scoreData: {
score: number;
accuracy: number;
correctMessages: number;
incorrectMessages: number;
mode: "sprint" | "watch";
timeMs?: number;
},
) => {
const score = await payload.create({
collection: "custom-radio-test-scores",
data: { test: testId, user: userId, ...scoreData },
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
return score.id;
};
beforeAll(async () => {
const a = await payload.create({
collection: "custom-radio-tests",
data: {
name: "LBoard Alpha",
author: authorId,
messages: sampleMessages,
messageCount: 3,
},
overrideAccess: true,
depth: 0,
});
const b = await payload.create({
collection: "custom-radio-tests",
data: {
name: "LBoard Bravo",
author: authorId,
messages: sampleMessages,
messageCount: 3,
},
overrideAccess: true,
depth: 0,
});
testA = a.id;
testB = b.id;
testIds.push(testA, testB);
}, TIMEOUT);
afterAll(async () => {
if (!payload) return;
// Runs before the parent afterAll: remove each temp user's scores first
// (FK), then the users. Leftover ids in scoreIds are cleaned up (and
// tolerated) by the parent afterAll.
for (const uid of tempUserIds) {
await payload
.delete({
collection: "custom-radio-test-scores",
where: { user: { equals: uid } },
overrideAccess: true,
})
.catch(() => {});
await payload
.delete({ collection: "users", id: uid, overrideAccess: true })
.catch(() => {});
}
});
it("keeps each player's best score and isolates leaderboards per test and mode", async () => {
const u1 = await makePlayer("one");
const u2 = await makePlayer("two");
await record(testA, u1, {
score: 300,
accuracy: 0.8,
correctMessages: 4,
incorrectMessages: 1,
mode: "sprint",
timeMs: 45_000,
});
await record(testA, u1, {
score: 450,
accuracy: 0.9,
correctMessages: 5,
incorrectMessages: 0,
mode: "sprint",
timeMs: 40_000,
});
await record(testA, u2, {
score: 500,
accuracy: 0.95,
correctMessages: 6,
incorrectMessages: 0,
mode: "sprint",
timeMs: 38_000,
});
await record(testA, u1, {
score: 200,
accuracy: 0.7,
correctMessages: 7,
incorrectMessages: 3,
mode: "watch",
});
await record(testB, u1, {
score: 150,
accuracy: 0.6,
correctMessages: 5,
incorrectMessages: 3,
mode: "watch",
});
const leaderboardFor = async (testId: number, mode: "sprint" | "watch") => {
const res = await payload.find({
collection: "custom-radio-test-scores",
where: { test: { equals: testId }, mode: { equals: mode } },
sort: "-score",
limit: 100,
depth: 1,
overrideAccess: true,
});
const best = new Map<number, number>();
for (const doc of res.docs) {
const ref = doc.user as unknown as { id: number } | number | null;
if (ref === null) continue;
const uid = typeof ref === "object" ? ref.id : ref;
if (typeof uid !== "number") continue;
const prev = best.get(uid);
if (prev === undefined || doc.score > prev) best.set(uid, doc.score);
}
return Array.from(best.entries())
.map(([uid, score]) => ({ uid, score }))
.sort((x, y) => y.score - x.score);
};
const sprintOrder = await leaderboardFor(testA, "sprint");
expect(sprintOrder).toHaveLength(2);
expect(sprintOrder[0]).toEqual({ uid: u2, score: 500 });
expect(sprintOrder[1]).toEqual({ uid: u1, score: 450 });
const watchOrder = await leaderboardFor(testA, "watch");
expect(watchOrder).toHaveLength(1);
expect(watchOrder[0]).toEqual({ uid: u1, score: 200 });
const testBOrder = await leaderboardFor(testB, "watch");
expect(testBOrder).toHaveLength(1);
expect(testBOrder[0]).toEqual({ uid: u1, score: 150 });
}, TIMEOUT);
});
// ---------------------------------------------------------------------------
// Edit and delete permissions (action gate + collection access)
// ---------------------------------------------------------------------------
describe("Edit and delete permissions", () => {
const roleIds: number[] = [];
const permUserIds: number[] = [];
let otherPlayer: User;
let manager: User;
let devUser: User;
let gateTestId: number;
const makeUserWithRole = async (label: string, roleId: number): Promise<User> => {
const user = (await payload.create({
collection: "users",
data: {
username: `${RUN}-perm-${label}`,
discordUsername: `${RUN}-perm-${label}`,
displayName: `PERM ${label.toUpperCase()}`,
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
// Permission resolution reads roleDocs (dynamic RBAC), not the legacy
// `roles` enum: assign a real role doc to grant permissions.
roleDocs: [roleId],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
permUserIds.push(user.id);
return user;
};
const invokeAccess = (fn: unknown, user: User | null): Promise<unknown> => {
return (fn as (args: { req: unknown }) => Promise<unknown>)({
req: { user, payload },
});
};
beforeAll(async () => {
const plainRole = (await payload.create({
collection: "roles",
data: { name: `${RUN}-plain`, slug: `${RUN}-plain`, permissions: [] },
overrideAccess: true,
depth: 0,
})) as unknown as Role;
const managerRole = (await payload.create({
collection: "roles",
data: {
name: `${RUN}-rt-manager`,
slug: `${RUN}-rt-manager`,
permissions: ["custom-radio-tests:update"],
},
overrideAccess: true,
depth: 0,
})) as unknown as Role;
const devRole = (await payload.create({
collection: "roles",
data: { name: `${RUN}-rt-dev`, slug: `${RUN}-rt-dev`, isSuperuser: true },
overrideAccess: true,
depth: 0,
})) as unknown as Role;
roleIds.push(plainRole.id, managerRole.id, devRole.id);
invalidatePermissionCache();
otherPlayer = await makeUserWithRole("other", plainRole.id);
manager = await makeUserWithRole("manager", managerRole.id);
devUser = await makeUserWithRole("dev", devRole.id);
const gateTest = await payload.create({
collection: "custom-radio-tests",
data: {
name: "Gate Test Radio",
author: authorId,
messages: sampleMessages,
messageCount: 3,
},
overrideAccess: true,
depth: 0,
});
gateTestId = gateTest.id;
testIds.push(gateTestId);
}, TIMEOUT);
afterAll(async () => {
if (!payload) return;
// Runs before the parent afterAll: profiles first (FK), then users, then roles.
for (const uid of permUserIds) {
const profiles = await payload
.find({
collection: "profiles",
where: { user: { equals: uid } },
limit: 5,
depth: 0,
overrideAccess: true,
})
.catch(() => null);
for (const p of profiles?.docs ?? []) {
await payload
.delete({ collection: "profiles", id: p.id, overrideAccess: true })
.catch(() => {});
}
await payload
.delete({ collection: "users", id: uid, overrideAccess: true })
.catch(() => {});
}
for (const id of roleIds) {
await payload.delete({ collection: "roles", id, overrideAccess: true }).catch(() => {});
}
});
it("lets only the author (or a permissioned manager) pass the action's edit gate", async () => {
// Mirrors updateTest: isManager || isAuthor, evaluated against the gate
// test authored by `author`.
const gate = async (user: User) => {
const isManager = await hasPermission(payload, user, "custom-radio-tests:update");
return isManager || user.id === authorId;
};
expect(await gate(author)).toBe(true);
expect(await gate(otherPlayer)).toBe(false);
expect(await gate(manager)).toBe(true);
expect(await gate(devUser)).toBe(true);
}, TIMEOUT);
it("scopes collection update access to the author for non-managers", async () => {
const fn = CustomRadioTests.access?.update;
expect(typeof fn).toBe("function");
expect(await invokeAccess(fn, author)).toMatchObject({ author: { equals: authorId } });
expect(await invokeAccess(fn, otherPlayer)).toMatchObject({
author: { equals: otherPlayer.id },
});
expect(await invokeAccess(fn, manager)).toBe(true);
expect(await invokeAccess(fn, null)).toBe(false);
}, TIMEOUT);
it("gates deletes behind custom-radio-tests:delete", async () => {
const fn = CustomRadioTests.access?.delete;
expect(typeof fn).toBe("function");
expect(await invokeAccess(fn, null)).toBe(false);
expect(await invokeAccess(fn, author)).toBe(false);
expect(await invokeAccess(fn, otherPlayer)).toBe(false);
expect(await invokeAccess(fn, devUser)).toBe(true);
}, TIMEOUT);
it("gates publish (create) and read behind login", async () => {
const createFn = CustomRadioTests.access?.create;
const readFn = CustomRadioTests.access?.read;
expect(typeof createFn).toBe("function");
expect(typeof readFn).toBe("function");
expect(await invokeAccess(createFn, null)).toBe(false);
expect(await invokeAccess(createFn, otherPlayer)).toBe(true);
expect(await invokeAccess(readFn, null)).toBe(false);
expect(await invokeAccess(readFn, otherPlayer)).toBe(true);
}, TIMEOUT);
});
});