test(int): cover the Friday open-slot notice
Notice-day gating in Eastern time across DST, upcoming-Friday range, stable dedup token, slot-open semantics (drafts hold the slot, cancelled do not, other days do not count), and claimant resolution (superuser exclusion, earliest-created wins, cancel reverts the claim).
This commit is contained in:
parent
4fad7cd259
commit
fd9f36fc46
1 changed files with 297 additions and 0 deletions
297
tests/int/friday-ops-notice.int.spec.ts
Normal file
297
tests/int/friday-ops-notice.int.spec.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
import { getPayload, Payload } from "payload";
|
||||
import config from "@/payload.config";
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { Campaign, Map as MissionMap, Mission, User } from "@/payload-types";
|
||||
import {
|
||||
getFridayClaimant,
|
||||
hasMissionOnFriday,
|
||||
isNoticeDue,
|
||||
noticeToken,
|
||||
upcomingFridayRange,
|
||||
} from "@/lib/intelligence/fridayOpsNotice";
|
||||
|
||||
let payload: Payload;
|
||||
|
||||
const RUN = `fon-${Date.now().toString(36)}`;
|
||||
|
||||
// Hermetic window: 2027-01-13 is a Wednesday far enough out that the shared
|
||||
// dev database holds no real missions on its Friday (2027-01-15).
|
||||
const WEDNESDAY = new Date(2027, 0, 13, 12, 0, 0, 0);
|
||||
const TUESDAY = new Date(2027, 0, 12, 12, 0, 0, 0);
|
||||
const FRIDAY = new Date(2027, 0, 15, 12, 0, 0, 0);
|
||||
const FRIDAY_EVENING = new Date(2027, 0, 15, 20, 0, 0, 0);
|
||||
const SATURDAY = new Date(2027, 0, 16, 20, 0, 0, 0);
|
||||
|
||||
describe("Friday open-slot notice", () => {
|
||||
let mapId: number;
|
||||
let campaignId: number;
|
||||
let authorId: number;
|
||||
let zeusId: number;
|
||||
let superuserId: number;
|
||||
const missionIds: number[] = [];
|
||||
const userIds: number[] = [];
|
||||
const roleIds: number[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
const payloadConfig = await config;
|
||||
payload = await getPayload({ config: payloadConfig });
|
||||
|
||||
// Sweep leftovers from crashed earlier runs: a stray Concept mission in
|
||||
// the fixture window would flip the first hasMissionOnFriday assertion.
|
||||
const stale = await payload.find({
|
||||
collection: "missions",
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
limit: 100,
|
||||
where: { codeName: { like: "fon-" } },
|
||||
});
|
||||
for (const doc of stale.docs) {
|
||||
await payload.delete({ collection: "missions", id: doc.id, overrideAccess: true }).catch(() => {});
|
||||
}
|
||||
|
||||
const map = (await payload.create({
|
||||
collection: "maps",
|
||||
data: { name: `${RUN} Map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image" },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as MissionMap;
|
||||
mapId = map.id;
|
||||
|
||||
const campaign = (await payload.create({
|
||||
collection: "campaigns",
|
||||
data: { name: `${RUN} Campaign`, summary: "Friday notice test campaign", status: "concept", campaignMode: "custom" },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as Campaign;
|
||||
campaignId = campaign.id;
|
||||
|
||||
const author = (await payload.create({
|
||||
collection: "users",
|
||||
data: {
|
||||
username: `${RUN}-author`,
|
||||
discordUsername: `${RUN}-author`,
|
||||
displayName: "AUTHOR",
|
||||
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
|
||||
password: "Test123",
|
||||
roles: ["user"],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as User;
|
||||
authorId = author.id;
|
||||
userIds.push(authorId);
|
||||
|
||||
const zeus = (await payload.create({
|
||||
collection: "users",
|
||||
data: {
|
||||
username: `${RUN}-zeus`,
|
||||
discordUsername: `${RUN}-zeus`,
|
||||
displayName: "ZEUS",
|
||||
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
|
||||
password: "Test123",
|
||||
roles: ["user"],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as User;
|
||||
zeusId = zeus.id;
|
||||
userIds.push(zeusId);
|
||||
|
||||
const superRole = (await payload.create({
|
||||
collection: "roles",
|
||||
data: { name: `FON Superuser ${RUN}`, slug: "", isSuperuser: true },
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as { id: number };
|
||||
roleIds.push(superRole.id);
|
||||
|
||||
const superuser = (await payload.create({
|
||||
collection: "users",
|
||||
data: {
|
||||
username: `${RUN}-su`,
|
||||
discordUsername: `${RUN}-su`,
|
||||
displayName: "SU",
|
||||
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
|
||||
password: "Test123",
|
||||
roles: ["user"],
|
||||
roleDocs: [superRole.id],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as User;
|
||||
superuserId = superuser.id;
|
||||
userIds.push(superuserId);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
for (const id of missionIds) {
|
||||
await payload.delete({ collection: "missions", id, overrideAccess: true }).catch(() => {});
|
||||
}
|
||||
await payload
|
||||
.delete({ collection: "campaigns", id: campaignId, overrideAccess: true })
|
||||
.catch(() => {});
|
||||
await payload.delete({ collection: "maps", id: mapId, overrideAccess: true }).catch(() => {});
|
||||
// User deletion trips FK constraints unless the hook-provisioned
|
||||
// personal bank account and profile are removed first.
|
||||
for (const uid of userIds) {
|
||||
const accounts = await payload
|
||||
.find({
|
||||
collection: "bank-accounts",
|
||||
where: { ownerUser: { equals: uid } },
|
||||
limit: 5,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
.catch(() => null);
|
||||
for (const account of accounts?.docs ?? []) {
|
||||
await payload
|
||||
.delete({ collection: "bank-accounts", id: account.id, overrideAccess: true })
|
||||
.catch(() => {});
|
||||
}
|
||||
const profiles = await payload
|
||||
.find({
|
||||
collection: "profiles",
|
||||
where: { user: { equals: uid } },
|
||||
limit: 5,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
.catch(() => null);
|
||||
for (const profile of profiles?.docs ?? []) {
|
||||
await payload
|
||||
.delete({ collection: "profiles", id: profile.id, overrideAccess: true })
|
||||
.catch(() => {});
|
||||
}
|
||||
await payload.delete({ collection: "users", id: uid, overrideAccess: true }).catch(() => {});
|
||||
}
|
||||
for (const rid of roleIds) {
|
||||
await payload.delete({ collection: "roles", id: rid, overrideAccess: true }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
const makeMission = async (
|
||||
label: string,
|
||||
start: Date,
|
||||
status: Mission["ownershipAndStatus"]["status"] = "Scheduled",
|
||||
creatorId?: number,
|
||||
): Promise<Mission> => {
|
||||
const mission = (await payload.create({
|
||||
collection: "missions",
|
||||
data: {
|
||||
name: `${RUN} ${label}`,
|
||||
codeName: `${RUN}-${label}`,
|
||||
summary: "Friday notice test mission",
|
||||
operationType: "side",
|
||||
classification: {
|
||||
map: mapId,
|
||||
missionType: "PvE",
|
||||
campaign: campaignId,
|
||||
startDateTime: start.toISOString(),
|
||||
estimatedDuration: 60,
|
||||
},
|
||||
ownershipAndStatus: {
|
||||
authors: [authorId],
|
||||
status,
|
||||
visibility: "unit",
|
||||
},
|
||||
missionRoles: {
|
||||
maxPlayers: 16,
|
||||
},
|
||||
gameDetails: {
|
||||
serverDetails: {
|
||||
serverIp: "127.0.0.1",
|
||||
serverPort: 2302,
|
||||
},
|
||||
},
|
||||
briefing: [],
|
||||
...(creatorId ? { createdBy: creatorId } : {}),
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
})) as unknown as Mission;
|
||||
missionIds.push(mission.id);
|
||||
return mission;
|
||||
};
|
||||
|
||||
it("is due on Wednesdays at 8 PM Eastern, until the ET day rolls over", () => {
|
||||
// 2026-09-16 is a Wednesday in EDT (UTC-4); January is EST (UTC-5).
|
||||
expect(isNoticeDue(new Date("2026-09-17T00:30:00Z"))).toBe(true); // Wed 20:30 ET
|
||||
expect(isNoticeDue(new Date("2026-09-17T03:59:00Z"))).toBe(true); // Wed 23:59 ET
|
||||
expect(isNoticeDue(new Date("2026-09-15T20:30:00Z"))).toBe(false); // Tue 16:30 ET
|
||||
expect(isNoticeDue(new Date("2026-09-16T16:00:00Z"))).toBe(false); // Wed 12:00 ET
|
||||
expect(isNoticeDue(new Date("2026-09-16T23:59:00Z"))).toBe(false); // Wed 19:59 ET
|
||||
expect(isNoticeDue(new Date("2026-09-17T00:00:00Z"))).toBe(true); // Wed 20:00 ET sharp
|
||||
expect(isNoticeDue(new Date("2026-09-17T04:00:00Z"))).toBe(false); // Thu 00:00 ET
|
||||
expect(isNoticeDue(new Date("2026-01-15T01:00:00Z"))).toBe(true); // Wed 20:00 ET (EST)
|
||||
});
|
||||
|
||||
it("targets the upcoming Friday, strictly in the future", () => {
|
||||
const fromWednesday = upcomingFridayRange(WEDNESDAY);
|
||||
expect(fromWednesday.start).toEqual(new Date(2027, 0, 15, 0, 0, 0, 0));
|
||||
expect(fromWednesday.end).toEqual(new Date(2027, 0, 15, 23, 59, 59, 999));
|
||||
expect(fromWednesday.epoch).toBe(Math.floor(fromWednesday.start.getTime() / 1000));
|
||||
|
||||
// Earlier in the week still lands on the same Friday.
|
||||
expect(upcomingFridayRange(new Date(2027, 0, 11, 8, 0, 0, 0)).start).toEqual(
|
||||
fromWednesday.start,
|
||||
);
|
||||
// On a Friday itself, the next Friday is targeted instead.
|
||||
expect(upcomingFridayRange(FRIDAY).start).toEqual(new Date(2027, 0, 22, 0, 0, 0, 0));
|
||||
});
|
||||
|
||||
it("builds a stable per-Friday dedup token", () => {
|
||||
expect(noticeToken(WEDNESDAY)).toBe(noticeToken(TUESDAY));
|
||||
expect(noticeToken(WEDNESDAY)).toBe(`<t:${upcomingFridayRange(WEDNESDAY).epoch}:D>`);
|
||||
expect(noticeToken(FRIDAY)).not.toBe(noticeToken(WEDNESDAY));
|
||||
});
|
||||
|
||||
it("treats the Friday slot as open only when a non-cancelled mission starts that Friday", async () => {
|
||||
await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(false);
|
||||
|
||||
const concept = await makeMission("concept-friday", FRIDAY_EVENING, "Concept");
|
||||
await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(true);
|
||||
|
||||
await makeMission("cancelled-friday", FRIDAY_EVENING, "Cancelled");
|
||||
await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(true);
|
||||
|
||||
await payload.delete({ collection: "missions", id: concept.id, overrideAccess: true });
|
||||
await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(false);
|
||||
|
||||
const saturday = await makeMission("saturday-mission", SATURDAY);
|
||||
await expect(hasMissionOnFriday(payload, WEDNESDAY)).resolves.toBe(false);
|
||||
|
||||
await payload.delete({ collection: "missions", id: saturday.id, overrideAccess: true });
|
||||
});
|
||||
|
||||
it("credits the earliest non-superuser creator as the claimant", async () => {
|
||||
await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toBeNull();
|
||||
|
||||
const zeusMission = await makeMission("zeus-claim", FRIDAY_EVENING, "Scheduled", zeusId);
|
||||
await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" });
|
||||
|
||||
await makeMission("su-claim", FRIDAY_EVENING, "Scheduled", superuserId);
|
||||
await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" });
|
||||
|
||||
await payload.delete({ collection: "missions", id: zeusMission.id, overrideAccess: true });
|
||||
await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toBeNull();
|
||||
|
||||
const first = await makeMission("zeus-first", FRIDAY_EVENING, "Scheduled", zeusId);
|
||||
const second = await makeMission("zeus-second", FRIDAY_EVENING, "Scheduled", zeusId);
|
||||
await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" });
|
||||
expect(first.createdAt <= second.createdAt).toBe(true);
|
||||
|
||||
await payload.delete({ collection: "missions", id: first.id, overrideAccess: true });
|
||||
await payload.delete({ collection: "missions", id: second.id, overrideAccess: true });
|
||||
|
||||
const claimer = await makeMission("zeus-cancelled", FRIDAY_EVENING, "Scheduled", zeusId);
|
||||
await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toEqual({ userName: "ZEUS" });
|
||||
await payload.update({
|
||||
collection: "missions",
|
||||
id: claimer.id,
|
||||
data: { ownershipAndStatus: { status: "Cancelled" } },
|
||||
overrideAccess: true,
|
||||
});
|
||||
await expect(getFridayClaimant(payload, WEDNESDAY)).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue