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>
737 lines
21 KiB
TypeScript
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.");
|
|
});
|
|
});
|