1
0
Fork 0
polaris-task-force/tests/int/award-advancement.int.spec.ts
Z8MB1E 19113e39c9 feat(awards): add award advancement series
Awards can now upgrade through a series: collection fields and migration, grant flow support, profile and account UI updates, and regression coverage.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-11 17:07:37 -04:00

737 lines
21 KiB
TypeScript

import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it, vi, type MockInstance } from "vitest";
import type { User, Award } from "@/payload-types";
import { grantAward } from "@/app/(frontend)/awards/actions";
import { EventTypes } from "@/utils/event-log/eventTypes";
// Mock next/headers to avoid "headers was called outside a request scope" error
vi.mock("next/headers", () => ({
headers: async () => new Headers(),
}));
let payload: Payload;
let authSpy: MockInstance;
const RUN = `award-${Date.now().toString(36)}`;
describe("Award Advancement Series", () => {
let adminUser: User;
let adminUserId: number;
let userA: User;
let userAId: number;
let userB: User;
let userBId: number;
let userC: User;
let userCId: number;
let tier1Award: Award;
let tier2Award: Award;
let tier3Award: Award;
let superRoleId: number;
async function profileIdFor(userId: number): Promise<number> {
const res = await payload.find({
collection: "profiles",
where: { user: { equals: userId } },
limit: 1,
depth: 0,
overrideAccess: true,
});
if (!res.docs[0]) throw new Error(`No profile found for user ${userId}`);
return res.docs[0].id;
}
beforeAll(async () => {
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
// Create a superuser role (RBAC resolves via roleDocs, not the legacy enum)
const superRole = await payload.create({
collection: "roles",
data: {
name: `Advancement Superuser ${RUN}`,
slug: `${RUN}-superuser`,
isSuperuser: true,
isSystem: false,
permissions: [],
},
overrideAccess: true,
depth: 0,
});
superRoleId = superRole.id;
// Create an admin user
adminUser = (await payload.create({
collection: "users",
data: {
username: `${RUN}-admin`,
discordUsername: `${RUN}-admin`,
displayName: "AWARD ADMIN",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["admin"],
roleDocs: [superRole.id],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
adminUserId = adminUser.id;
// Create users
userA = (await payload.create({
collection: "users",
data: {
username: `${RUN}-userA`,
discordUsername: `${RUN}-userA`,
displayName: "USER A",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userAId = userA.id;
userB = (await payload.create({
collection: "users",
data: {
username: `${RUN}-userB`,
discordUsername: `${RUN}-userB`,
displayName: "USER B",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userBId = userB.id;
userC = (await payload.create({
collection: "users",
data: {
username: `${RUN}-userC`,
discordUsername: `${RUN}-userC`,
displayName: "USER C",
steamId: `7656119${Math.floor(Math.random() * 1e9)}`,
password: "Test123",
roles: ["user"],
},
overrideAccess: true,
depth: 0,
})) as unknown as User;
userCId = userC.id;
// Set up auth spy for testing authentication
authSpy = vi.spyOn(payload, "auth");
// Create awards for testing advancement
tier1Award = (await payload.create({
collection: "awards",
data: {
name: `Tier 1 Award-${RUN}`,
description: {
root: {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "text",
text: "Tier 1 Award",
format: 0,
style: "",
mode: "normal",
detail: 0,
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
textFormat: 0,
textStyle: "",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
version: 1,
},
},
type: "ribbon",
advancement: {
seriesKey: `${RUN}-years-of-service`,
tier: 1,
},
ribbonDesign: {
stripes: [
{ color: "#b91c1c", width: 30 },
{ color: "#f5f5f4", width: 5 },
{ color: "#1e3a5f", width: 30 },
],
devices: [],
mirrored: false,
},
experiencePoints: 25,
grantConfig: {
allowedGrantorAssignmentTypes: ["division"],
recipientScope: "same_command",
maxGrantsPerGrantor: null,
},
},
overrideAccess: true,
depth: 0,
})) as unknown as Award;
tier2Award = (await payload.create({
collection: "awards",
data: {
name: `Tier 2 Award-${RUN}`,
description: {
root: {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "text",
text: "Tier 2 Award",
format: 0,
style: "",
mode: "normal",
detail: 0,
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
textFormat: 0,
textStyle: "",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
version: 1,
},
},
type: "ribbon",
advancement: {
seriesKey: `${RUN}-years-of-service`,
tier: 2,
},
ribbonDesign: {
stripes: [
{ color: "#1e3a5f", width: 30 },
{ color: "#f5f5f4", width: 5 },
{ color: "#b91c1c", width: 30 },
],
devices: [],
mirrored: false,
},
experiencePoints: 50,
grantConfig: {
allowedGrantorAssignmentTypes: ["division"],
recipientScope: "same_command",
maxGrantsPerGrantor: null,
},
},
overrideAccess: true,
depth: 0,
})) as unknown as Award;
tier3Award = (await payload.create({
collection: "awards",
data: {
name: `Tier 3 Award-${RUN}`,
description: {
root: {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "text",
text: "Tier 3 Award",
format: 0,
style: "",
mode: "normal",
detail: 0,
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
textFormat: 0,
textStyle: "",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
version: 1,
},
},
type: "ribbon",
advancement: {
seriesKey: `${RUN}-years-of-service`,
tier: 3,
},
ribbonDesign: {
stripes: [
{ color: "#1e3a5f", width: 30 },
{ color: "#f5f5f4", width: 5 },
{ color: "#b91c1c", width: 30 },
],
devices: [],
mirrored: false,
},
experiencePoints: 100,
grantConfig: {
allowedGrantorAssignmentTypes: ["division"],
recipientScope: "same_command",
maxGrantsPerGrantor: 1,
},
},
overrideAccess: true,
depth: 0,
})) as unknown as Award;
});
afterAll(async () => {
// Clean up users and awards
try {
await payload.delete({ collection: "users", id: userAId, overrideAccess: true });
} catch {
// Ignore if already deleted
}
try {
await payload.delete({ collection: "users", id: userBId, overrideAccess: true });
} catch {
// Ignore if already deleted
}
try {
await payload.delete({ collection: "users", id: userCId, overrideAccess: true });
} catch {
// Ignore if already deleted
}
try {
await payload.delete({ collection: "users", id: adminUserId, overrideAccess: true });
} catch {
// Ignore if already deleted
}
try {
await payload.delete({ collection: "awards", id: tier1Award.id, overrideAccess: true });
} catch {
// Ignore if already deleted
}
try {
await payload.delete({ collection: "awards", id: tier2Award.id, overrideAccess: true });
} catch {
// Ignore if already deleted
}
try {
await payload.delete({ collection: "awards", id: tier3Award.id, overrideAccess: true });
} catch {
// Ignore if already deleted
}
try {
await payload.delete({ collection: "roles", id: superRoleId, overrideAccess: true });
} catch {
// Ignore if already deleted
}
// Restore auth spy
authSpy.mockRestore();
});
it("should grant tier 1 to recipient A", async () => {
// Mock authentication for admin user
authSpy.mockResolvedValue({ user: adminUser });
const result = await grantAward({
recipientUserIds: [userAId],
awardId: tier1Award.id,
reason: "Test grant",
});
expect(result.success).toBe(true);
expect(result.data?.results).toHaveLength(1);
expect(result.data?.results[0].success).toBe(true);
expect(result.data?.granted).toBe(1);
expect(result.data?.failed).toBe(0);
// Verify the profile has the award entry
const profile = await payload.findByID({
collection: "profiles",
id: await profileIdFor(userAId),
depth: 0,
overrideAccess: true,
});
const entries = profile.progression?.awards ?? [];
expect(entries).toHaveLength(1);
expect(entries[0]?.award).toBe(tier1Award.id);
expect(entries[0]?.upgradedFrom).toBeNull();
expect(entries[0]?.upgradedAt).toBeNull();
});
it("should upgrade recipient A from tier 1 to tier 2", async () => {
// Mock authentication for admin user
authSpy.mockResolvedValue({ user: adminUser });
const result = await grantAward({
recipientUserIds: [userAId],
awardId: tier2Award.id,
reason: "Test upgrade",
});
expect(result.success).toBe(true);
expect(result.data?.results).toHaveLength(1);
expect(result.data?.results[0].success).toBe(true);
expect(result.data?.granted).toBe(1);
expect(result.data?.failed).toBe(0);
// Verify the profile has the upgraded award entry (same array position, replaced)
const profile = await payload.findByID({
collection: "profiles",
id: await profileIdFor(userAId),
depth: 0,
overrideAccess: true,
});
const entries = profile.progression?.awards ?? [];
expect(entries).toHaveLength(1);
expect(entries[0]?.award).toBe(tier2Award.id);
expect(entries[0]?.upgradedFrom).toBe(tier1Award.id);
expect(entries[0]?.upgradedAt).toBeDefined();
expect(entries[0]?.awardedAt).toBeDefined(); // Preserved from tier 1
// Verify event was emitted (award names carry the RUN suffix, so the
// message match is idempotent across runs)
const events = await payload.find({
collection: "game-event-logs",
where: {
and: [
{ type: { equals: EventTypes.AwardUpgrade } },
{ message: { like: `Tier 2 Award-${RUN}` } },
],
},
depth: 0,
overrideAccess: true,
});
expect(events.docs).toHaveLength(1);
});
it("should upgrade recipient A from tier 2 to tier 3", async () => {
// Mock authentication for admin user
authSpy.mockResolvedValue({ user: adminUser });
const result = await grantAward({
recipientUserIds: [userAId],
awardId: tier3Award.id,
reason: "Test upgrade 3",
});
expect(result.success).toBe(true);
expect(result.data?.results).toHaveLength(1);
expect(result.data?.results[0].success).toBe(true);
expect(result.data?.granted).toBe(1);
expect(result.data?.failed).toBe(0);
// Verify the profile has the upgraded award entry (same array position, replaced)
const profile = await payload.findByID({
collection: "profiles",
id: await profileIdFor(userAId),
depth: 0,
overrideAccess: true,
});
const entries = profile.progression?.awards ?? [];
expect(entries).toHaveLength(1);
expect(entries[0]?.award).toBe(tier3Award.id);
expect(entries[0]?.upgradedFrom).toBe(tier2Award.id);
expect(entries[0]?.upgradedAt).toBeDefined();
expect(entries[0]?.awardedAt).toBeDefined(); // Preserved from tier 1
});
it("should fail when recipient A tries to get a higher tier of the same ribbon", async () => {
// Mock authentication for admin user
authSpy.mockResolvedValue({ user: adminUser });
const result = await grantAward({
recipientUserIds: [userAId],
awardId: tier1Award.id,
reason: "Test downgrade",
});
expect(result.success).toBe(true);
expect(result.data?.results).toHaveLength(1);
expect(result.data?.results[0].success).toBe(false);
expect(result.data?.results[0].error).toBe(
"Recipient already holds a higher tier of this ribbon.",
);
});
it("should grant tier 2 to fresh recipient B", async () => {
// Mock authentication for admin user
authSpy.mockResolvedValue({ user: adminUser });
const result = await grantAward({
recipientUserIds: [userBId],
awardId: tier2Award.id,
reason: "Fresh grant to B",
});
expect(result.success).toBe(true);
expect(result.data?.results).toHaveLength(1);
expect(result.data?.results[0].success).toBe(true);
expect(result.data?.granted).toBe(1);
expect(result.data?.failed).toBe(0);
// Verify the profile has the award entry (no upgradedFrom)
const profile = await payload.findByID({
collection: "profiles",
id: await profileIdFor(userBId),
depth: 0,
overrideAccess: true,
});
const entries = profile.progression?.awards ?? [];
expect(entries).toHaveLength(1);
expect(entries[0]?.award).toBe(tier2Award.id);
expect(entries[0]?.upgradedFrom).toBeNull();
});
it("should allow upgrade from tier 2 to tier 3 despite remaining 0", async () => {
// First grant tier 3 to user C (consumes the slot)
authSpy.mockResolvedValue({ user: adminUser });
const result1 = await grantAward({
recipientUserIds: [userCId],
awardId: tier3Award.id,
reason: "Grant to C",
});
expect(result1.success).toBe(true);
expect(result1.data?.results).toHaveLength(1);
expect(result1.data?.results[0].success).toBe(true);
// Now try to upgrade user B (tier 2) to tier 3 - should succeed despite remaining 0
authSpy.mockResolvedValue({ user: adminUser });
const result2 = await grantAward({
recipientUserIds: [userBId],
awardId: tier3Award.id,
reason: "Upgrade to B",
});
expect(result2.success).toBe(true);
expect(result2.data?.results).toHaveLength(1);
expect(result2.data?.results[0].success).toBe(true);
// Check that both have the tier-3 award
const profileC = await payload.findByID({
collection: "profiles",
id: await profileIdFor(userCId),
depth: 0,
overrideAccess: true,
});
const profileB = await payload.findByID({
collection: "profiles",
id: await profileIdFor(userBId),
depth: 0,
overrideAccess: true,
});
const entriesC = profileC.progression?.awards ?? [];
const entriesB = profileB.progression?.awards ?? [];
expect(entriesC).toHaveLength(1);
expect(entriesC[0]?.award).toBe(tier3Award.id);
expect(entriesB).toHaveLength(1);
expect(entriesB[0]?.award).toBe(tier3Award.id);
});
it("should fail when trying to grant same tier award to a holder", async () => {
// Mock authentication for admin user
authSpy.mockResolvedValue({ user: adminUser });
// By this point the exemption test has upgraded userB to tier 3, so
// granting tier 3 again must hit the same-tier guard
const result = await grantAward({
recipientUserIds: [userBId],
awardId: tier3Award.id,
reason: "Same tier",
});
expect(result.success).toBe(true);
expect(result.data?.results).toHaveLength(1);
expect(result.data?.results[0].success).toBe(false);
expect(result.data?.results[0].error).toBe("Recipient already has this award.");
});
it("should not affect non-series awards", async () => {
// Create a non-series award
const nonSeriesAward = await payload.create({
collection: "awards",
data: {
name: `Non-Series Award-${RUN}`,
description: {
root: {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "text",
text: "Non-Series Award",
format: 0,
style: "",
mode: "normal",
detail: 0,
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
textFormat: 0,
textStyle: "",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
version: 1,
},
},
type: "ribbon",
ribbonDesign: {
stripes: [
{ color: "#1e3a5f", width: 30 },
{ color: "#f5f5f4", width: 5 },
{ color: "#b91c1c", width: 30 },
],
devices: [],
mirrored: false,
},
experiencePoints: 25,
grantConfig: {
allowedGrantorAssignmentTypes: ["division"],
recipientScope: "same_command",
maxGrantsPerGrantor: null,
},
},
overrideAccess: true,
depth: 0,
});
// Grant it to user A
authSpy.mockResolvedValue({ user: adminUser });
const result1 = await grantAward({
recipientUserIds: [userAId],
awardId: nonSeriesAward.id,
reason: "Non-series award",
});
expect(result1.success).toBe(true);
expect(result1.data?.results).toHaveLength(1);
expect(result1.data?.results[0].success).toBe(true);
// Try to grant it again - should fail
const result2 = await grantAward({
recipientUserIds: [userAId],
awardId: nonSeriesAward.id,
reason: "Non-series award again",
});
expect(result2.success).toBe(true);
expect(result2.data?.results).toHaveLength(1);
expect(result2.data?.results[0].success).toBe(false);
expect(result2.data?.results[0].error).toBe("Recipient already has this award.");
// Cleanup
try {
await payload.delete({ collection: "awards", id: nonSeriesAward.id, overrideAccess: true });
} catch {
// Ignore if already deleted
}
});
it("should fail when creating two awards with same seriesKey + tier", async () => {
// Try to create a duplicate award with same seriesKey and tier
await expect(
payload.create({
collection: "awards",
data: {
name: `Duplicate Award-${RUN}`,
description: {
root: {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "text",
text: "Duplicate Award",
format: 0,
style: "",
mode: "normal",
detail: 0,
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
textFormat: 0,
textStyle: "",
version: 1,
},
],
direction: "ltr",
format: "",
indent: 0,
version: 1,
},
},
type: "ribbon",
advancement: {
seriesKey: `${RUN}-years-of-service`,
tier: 1,
},
ribbonDesign: {
stripes: [
{ color: "#1e3a5f", width: 30 },
{ color: "#f5f5f4", width: 5 },
{ color: "#b91c1c", width: 30 },
],
devices: [],
mirrored: false,
},
experiencePoints: 25,
grantConfig: {
allowedGrantorAssignmentTypes: ["division"],
recipientScope: "same_command",
maxGrantsPerGrantor: null,
},
},
overrideAccess: true,
depth: 0,
}),
).rejects.toThrow("Tier 1 of this series already exists.");
});
});