1
0
Fork 0
polaris-task-force/tests/int/custom-minefields.int.spec.ts
Z8MB1E 52328da8f9 feat(eod): add custom minefield 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

874 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { CustomMinefields } from "@/collections/minigames/CustomMinefields";
import { hasPermission } from "@/utils/access-control/hasPermission";
import { invalidatePermissionCache } from "@/utils/access-control/loadUserPermissions";
import {
isEodUnlocked,
minefieldFlagsError,
validateGridSize,
validateMinefieldName,
validateMinefieldScore,
validateMinePositions,
} from "@/lib/custom-minigames/eod";
let payload: Payload;
const RUN = `cmf-${Date.now().toString(36)}`;
const TIMEOUT = 30_000;
/** Minimal valid mine positions for a 9x9 grid. */
const validMines = [
{ row: 0, col: 0 },
{ row: 2, col: 2 },
{ row: 8, col: 8 },
];
describe("Custom minefields", () => {
let author: User;
let authorId: number;
const minefieldIds: 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: "MINEFIELD 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 → minefield)
for (const id of scoreIds) {
await payload
.delete({ collection: "custom-minefield-scores", id, overrideAccess: true })
.catch(() => {});
}
// Delete minefields
for (const id of minefieldIds) {
await payload
.delete({ collection: "custom-minefields", id, overrideAccess: true })
.catch(() => {});
}
// Delete user (and rely on afterChange hook cleanup for profile/bank)
await payload
.delete({ collection: "users", id: authorId, overrideAccess: true })
.catch(() => {});
});
// ---------------------------------------------------------------------------
// Collection-level field constraints
// ---------------------------------------------------------------------------
describe("Collection field validation", () => {
it("creates a valid minefield with all required fields", async () => {
const doc = await payload.create({
collection: "custom-minefields",
data: {
name: "Test Minefield",
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
published: true,
},
overrideAccess: true,
depth: 0,
});
minefieldIds.push(doc.id);
expect(doc.name).toBe("Test Minefield");
expect(doc.rows).toBe(9);
expect(doc.cols).toBe(9);
expect(doc.mineCount).toBe(3);
expect(doc.published).toBe(true);
expect(doc.playCount).toBe(0);
});
it("rejects rows below minimum (8 < 9)", async () => {
await expect(
payload.create({
collection: "custom-minefields",
data: {
name: "Tiny",
author: authorId,
rows: 8,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects cols above maximum (501 > 500)", async () => {
await expect(
payload.create({
collection: "custom-minefields",
data: {
name: "Wide",
author: authorId,
rows: 9,
cols: 501,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects mineCount of 0", async () => {
await expect(
payload.create({
collection: "custom-minefields",
data: {
name: "No Mines",
author: authorId,
rows: 9,
cols: 9,
mines: [],
mineCount: 0,
},
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("defaults published to true and playCount to 0", async () => {
const doc = await payload.create({
collection: "custom-minefields",
data: {
name: "Defaults Test",
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
minefieldIds.push(doc.id);
expect(doc.published).toBe(true);
expect(doc.playCount).toBe(0);
});
it("rejects a minefield without a name", async () => {
await expect(
payload.create({
collection: "custom-minefields",
data: {
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects a minefield without an author", async () => {
await expect(
payload.create({
collection: "custom-minefields",
data: {
name: "Orphan",
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
});
// ---------------------------------------------------------------------------
// Score collection
// ---------------------------------------------------------------------------
describe("Custom minefield scores", () => {
let minefieldId: number;
beforeAll(async () => {
const doc = await payload.create({
collection: "custom-minefields",
data: {
name: "Score Test Field",
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
minefieldId = doc.id;
minefieldIds.push(minefieldId);
});
it("creates a score record with valid fields", async () => {
const score = await payload.create({
collection: "custom-minefield-scores",
data: {
minefield: minefieldId,
user: authorId,
seconds: 45,
flags: 3,
won: true,
},
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
expect(score.minefield).toBe(minefieldId);
expect(score.user).toBe(authorId);
expect(score.seconds).toBe(45);
expect(score.flags).toBe(3);
expect(score.won).toBe(true);
});
it("rejects a score without a minefield reference", async () => {
await expect(
payload.create({
collection: "custom-minefield-scores",
data: {
user: authorId,
seconds: 10,
flags: 0,
won: false,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("rejects a score without a user reference", async () => {
await expect(
payload.create({
collection: "custom-minefield-scores",
data: {
minefield: minefieldId,
seconds: 10,
flags: 0,
won: false,
} as any,
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow();
});
it("creates a losing score with flags=0", async () => {
const score = await payload.create({
collection: "custom-minefield-scores",
data: {
minefield: minefieldId,
user: authorId,
seconds: 120,
flags: 0,
won: false,
},
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
expect(score.won).toBe(false);
expect(score.flags).toBe(0);
});
});
// ---------------------------------------------------------------------------
// Access control
// ---------------------------------------------------------------------------
describe("Access control", () => {
let viewer: User;
let viewerId: number;
let viewerMinefieldId: 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;
const doc = await payload.create({
collection: "custom-minefields",
data: {
name: "Viewer Minefield",
author: viewerId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
viewerMinefieldId = doc.id;
minefieldIds.push(viewerMinefieldId);
});
it("logged-in user can read published minefields", async () => {
const result = await payload.find({
collection: "custom-minefields",
where: { published: { equals: true } },
limit: 1,
overrideAccess: true,
depth: 0,
});
expect(result.docs.length).toBeGreaterThanOrEqual(1);
});
it("logged-in user can create a minefield", async () => {
const doc = await payload.create({
collection: "custom-minefields",
data: {
name: "Can Create",
author: viewerId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
minefieldIds.push(doc.id);
expect(doc.name).toBe("Can Create");
});
it("author can update their own minefield name via overrideAccess", async () => {
const updated = await payload.update({
collection: "custom-minefields",
id: viewerMinefieldId,
data: { name: "Updated Name" },
overrideAccess: true,
depth: 0,
});
expect(updated.name).toBe("Updated Name");
});
it("published minefields are visible to all logged-in users", async () => {
const result = await payload.find({
collection: "custom-minefields",
where: { published: { equals: true } },
limit: 20,
overrideAccess: true,
depth: 0,
});
expect(result.docs.some((doc) => doc.name === "Updated Name")).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Delete flow
// ---------------------------------------------------------------------------
describe("Delete flow", () => {
it("deleting a minefield's scores then the minefield cleans up cleanly", async () => {
const mf = await payload.create({
collection: "custom-minefields",
data: {
name: "Delete Test",
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
const score = await payload.create({
collection: "custom-minefield-scores",
data: {
minefield: mf.id,
user: authorId,
seconds: 25,
flags: 1,
won: true,
},
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
await payload.delete({
collection: "custom-minefield-scores",
id: score.id,
overrideAccess: true,
});
await payload.delete({
collection: "custom-minefields",
id: mf.id,
overrideAccess: true,
});
const remaining = await payload.findByID({
collection: "custom-minefield-scores",
id: score.id,
overrideAccess: true,
depth: 0,
}).catch(() => null);
expect(remaining).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Unlock gating (pure lib: mirrors checkEodUnlock)
// ---------------------------------------------------------------------------
describe("Unlock gating", () => {
it("locks custom minefields until a win is recorded on all three difficulties", () => {
expect(
isEodUnlocked({ bestTrainingTime: 0, bestPerimeterTime: 0, bestSupplyRoadTime: 0 }),
).toBe(false);
expect(
isEodUnlocked({ bestTrainingTime: 45, bestPerimeterTime: 0, bestSupplyRoadTime: 0 }),
).toBe(false);
expect(
isEodUnlocked({ bestTrainingTime: 45, bestPerimeterTime: 60, bestSupplyRoadTime: 0 }),
).toBe(false);
});
it("unlocks once every difficulty has a best time above zero", () => {
expect(
isEodUnlocked({ bestTrainingTime: 1, bestPerimeterTime: 1, bestSupplyRoadTime: 1 }),
).toBe(true);
expect(
isEodUnlocked({ bestTrainingTime: 45, bestPerimeterTime: 60, bestSupplyRoadTime: 120 }),
).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Action-level validation (pure lib: mirrors createMinefield / updateMinefield
// / submitCustomMinefieldScore)
// ---------------------------------------------------------------------------
describe("Action-level validation", () => {
it("rejects empty, oversized, and non-string names; trims valid ones", () => {
expect(validateMinefieldName("").error).toBe("Name must be 1-100 characters.");
expect(validateMinefieldName(" ").error).toBe("Name must be 1-100 characters.");
expect(validateMinefieldName("x".repeat(101)).error).toBe("Name must be 1-100 characters.");
expect(validateMinefieldName(123).error).toBe("Name must be 1-100 characters.");
expect(validateMinefieldName(" Clear Path ")).toEqual({ name: "Clear Path" });
});
it("rejects grid sizes outside 9-500 on either axis and non-integers", () => {
expect(validateGridSize(8, 9).error).toBe("Grid must be 9×9 to 500×500.");
expect(validateGridSize(9, 501).error).toBe("Grid must be 9×9 to 500×500.");
expect(validateGridSize(9.5, 9).error).toBe("Grid must be 9×9 to 500×500.");
expect(validateGridSize("9", 9).error).toBe("Grid must be 9×9 to 500×500.");
expect(validateGridSize(9, 9)).toEqual({ rows: 9, cols: 9 });
expect(validateGridSize(500, 500)).toEqual({ rows: 500, cols: 500 });
});
it("rejects non-array, empty, oversized, out-of-range, and duplicate mine lists", () => {
expect(validateMinePositions("boom", 9, 9).error).toBe("Mines must be an array of positions.");
expect(validateMinePositions([], 9, 9).error).toBe("Mine count must be between 1 and 10000.");
const tooMany = Array.from({ length: 10001 }, (_, i) => ({ row: 0, col: i % 9 }));
expect(validateMinePositions(tooMany, 9, 9).error).toBe(
"Mine count must be between 1 and 10000.",
);
const overflow = Array.from({ length: 82 }, (_, i) => ({
row: Math.floor(i / 9),
col: i % 9,
}));
expect(validateMinePositions(overflow, 9, 9).error).toBe("Mine count exceeds the 9×9 grid.");
expect(validateMinePositions([{ row: 9, col: 0 }], 9, 9).error).toBe(
"Every mine needs a row (0-8) and column (0-8).",
);
expect(validateMinePositions([{ row: 0, col: -1 }], 9, 9).error).toBe(
"Every mine needs a row (0-8) and column (0-8).",
);
expect(validateMinePositions([{ row: 0.5, col: 0 }], 9, 9).error).toBe(
"Every mine needs a row (0-8) and column (0-8).",
);
expect(
validateMinePositions(
[
{ row: 0, col: 0 },
{ row: 0, col: 0 },
],
9,
9,
).error,
).toBe("Each cell can only hold one mine.");
});
it("normalizes valid mine positions", () => {
const result = validateMinePositions(validMines, 9, 9);
expect(result.error).toBeUndefined();
expect(result.positions).toEqual(validMines);
});
it("rejects impossible score results", () => {
expect(validateMinefieldScore({ seconds: 0, flags: 0 }).error).toBe("Invalid result.");
expect(validateMinefieldScore({ seconds: -5, flags: 0 }).error).toBe("Invalid result.");
expect(validateMinefieldScore({ seconds: 2.5, flags: 0 }).error).toBe("Invalid result.");
expect(validateMinefieldScore({ seconds: 3601, flags: 0 }).error).toBe("Invalid result.");
expect(validateMinefieldScore({ seconds: 30, flags: -1 }).error).toBe("Invalid result.");
expect(validateMinefieldScore({ seconds: Number.NaN, flags: 0 }).error).toBe(
"Invalid result.",
);
expect(validateMinefieldScore({ seconds: 30, flags: 2 })).toEqual({ seconds: 30, flags: 2 });
expect(validateMinefieldScore({ seconds: 3600, flags: 0 })).toEqual({
seconds: 3600,
flags: 0,
});
});
it("rejects flag counts above the minefield's declared mine count", () => {
expect(minefieldFlagsError(4, 3)).toBe("Invalid result.");
expect(minefieldFlagsError(3, 3)).toBeUndefined();
expect(minefieldFlagsError(0, 3)).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Leaderboard ordering (DB: replicates CustomMinefieldLeaderboard's query:
// where minefield + won=true, sort seconds,flags, best won time per player)
// ---------------------------------------------------------------------------
describe("Leaderboard ordering", () => {
let fieldA: number;
let fieldB: 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 (
minefieldId: number,
userId: number,
seconds: number,
flags: number,
won: boolean,
) => {
const score = await payload.create({
collection: "custom-minefield-scores",
data: { minefield: minefieldId, user: userId, seconds, flags, won },
overrideAccess: true,
depth: 0,
});
scoreIds.push(score.id);
return score.id;
};
beforeAll(async () => {
const a = await payload.create({
collection: "custom-minefields",
data: {
name: "LBoard Alpha",
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
const b = await payload.create({
collection: "custom-minefields",
data: {
name: "LBoard Bravo",
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
fieldA = a.id;
fieldB = b.id;
minefieldIds.push(fieldA, fieldB);
}, 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-minefield-scores",
where: { user: { equals: uid } },
overrideAccess: true,
})
.catch(() => {});
await payload
.delete({ collection: "users", id: uid, overrideAccess: true })
.catch(() => {});
}
});
it("keeps each player's best won time and isolates leaderboards per minefield", async () => {
const u1 = await makePlayer("one");
const u2 = await makePlayer("two");
// Field A: u1 40s then improved 35s, u2 30s, plus a faster-but-lost run.
await record(fieldA, u1, 40, 2, true);
await record(fieldA, u1, 35, 2, true);
await record(fieldA, u2, 30, 1, true);
await record(fieldA, u1, 5, 0, false);
// Field B: only u1 plays (50s).
await record(fieldB, u1, 50, 1, true);
const leaderboardFor = async (minefieldId: number) => {
const res = await payload.find({
collection: "custom-minefield-scores",
where: { minefield: { equals: minefieldId }, won: { equals: true } },
sort: "seconds,flags",
limit: 100,
depth: 1,
overrideAccess: true,
});
const best = new Map<number, { seconds: number; flags: 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 || doc.seconds < prev.seconds) {
best.set(uid, { seconds: doc.seconds, flags: doc.flags ?? 0 });
}
}
return Array.from(best.entries())
.map(([uid, v]) => ({ uid, ...v }))
.sort((x, y) => x.seconds - y.seconds || x.flags - y.flags);
};
const ordered = await leaderboardFor(fieldA);
expect(ordered).toHaveLength(2);
// u2's 30s beats u1's best won 35s; the 5s loss never counts.
expect(ordered[0].uid).toBe(u2);
expect(ordered[0].seconds).toBe(30);
expect(ordered[1].uid).toBe(u1);
expect(ordered[1].seconds).toBe(35);
expect(ordered[1].flags).toBe(2);
const orderedB = await leaderboardFor(fieldB);
expect(orderedB).toHaveLength(1);
expect(orderedB[0].uid).toBe(u1);
expect(orderedB[0].seconds).toBe(50);
}, 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 gateFieldId: 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}-mf-manager`,
slug: `${RUN}-mf-manager`,
permissions: ["custom-minefields:update"],
},
overrideAccess: true,
depth: 0,
})) as unknown as Role;
const devRole = (await payload.create({
collection: "roles",
data: { name: `${RUN}-mf-dev`, slug: `${RUN}-mf-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 gateField = await payload.create({
collection: "custom-minefields",
data: {
name: "Gate Field",
author: authorId,
rows: 9,
cols: 9,
mines: validMines,
mineCount: 3,
},
overrideAccess: true,
depth: 0,
});
gateFieldId = gateField.id;
minefieldIds.push(gateFieldId);
}, 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 updateMinefield: isManager || isAuthor, evaluated against the
// gate field authored by `author`.
const gate = async (user: User) => {
const isManager = await hasPermission(payload, user, "custom-minefields: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 = CustomMinefields.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-minefields:delete", async () => {
const fn = CustomMinefields.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 = CustomMinefields.access?.create;
const readFn = CustomMinefields.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);
});
});