feat: validate and describe reserved command arguments

This commit is contained in:
2026-07-26 09:04:48 +00:00
parent b9d22c75f5
commit f84efafc9a
8 changed files with 503 additions and 20 deletions
+63 -2
View File
@@ -77,6 +77,8 @@ const buildContext = (options?: {
general?: GeneralRow | null;
generalTurns?: GeneralTurnRow[];
nationTurns?: NationTurnRow[];
generalTurnWrites?: unknown[];
nationTurnWrites?: unknown[];
auth?: GameSessionTokenPayload | null;
}): GameApiContext => {
const transport = options?.transport ?? new InMemoryTurnDaemonTransport();
@@ -105,7 +107,10 @@ const buildContext = (options?: {
findMany: async ({ where }: { where: { generalId: number } }) =>
generalTurns.filter((row) => row.generalId === where.generalId),
deleteMany: async () => ({}),
createMany: async () => ({}),
createMany: async (args: unknown) => {
options?.generalTurnWrites?.push(args);
return {};
},
},
nationTurn: {
findMany: async ({ where }: { where: { nationId: number; officerLevel: number } }) =>
@@ -113,7 +118,10 @@ const buildContext = (options?: {
(row) => row.nationId === where.nationId && row.officerLevel === where.officerLevel
),
deleteMany: async () => ({}),
createMany: async () => ({}),
createMany: async (args: unknown) => {
options?.nationTurnWrites?.push(args);
return {};
},
},
};
const accessTokenStore = new RedisAccessTokenStore(
@@ -281,6 +289,59 @@ describe('appRouter', () => {
expect(response[0]?.index).toBe(0);
});
it('validates and persists general command arguments from the authenticated owner', async () => {
const general = buildGeneralRow({ id: 13 });
const writes: unknown[] = [];
const caller = appRouter.createCaller(buildContext({ general, generalTurnWrites: writes }));
const response = await caller.turns.reserved.setGeneral({
generalId: 13,
turnIndex: 0,
action: 'che_화계',
args: { destCityId: 7 },
});
expect(response.turns[0]).toMatchObject({ action: 'che_화계', args: { destCityId: 7 } });
expect(writes).toHaveLength(1);
const written = writes[0] as { data: unknown[] };
expect(written.data).toHaveLength(30);
expect(written.data[0]).toMatchObject({
generalId: 13,
turnIdx: 0,
actionCode: 'che_화계',
arg: { destCityId: 7 },
});
});
it('rejects malformed and cross-scope arguments without writing turns', async () => {
const general = buildGeneralRow({ id: 14, nationId: 3, officerLevel: 5 });
const generalWrites: unknown[] = [];
const nationWrites: unknown[] = [];
const caller = appRouter.createCaller(
buildContext({ general, generalTurnWrites: generalWrites, nationTurnWrites: nationWrites })
);
await expect(
caller.turns.reserved.setGeneral({
generalId: 14,
turnIndex: 0,
action: 'che_화계',
args: { destCityId: '7' },
})
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
await expect(
caller.turns.reserved.setGeneral({
generalId: 14,
turnIndex: 0,
action: 'che_포상',
args: { isGold: true, amount: 1, destGeneralId: 7 },
})
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
expect(generalWrites).toHaveLength(0);
expect(nationWrites).toHaveLength(0);
});
it('rejects another user general across actor-owned routers', async () => {
const general = buildGeneralRow({ id: 15, userId: 'user-2' });
const caller = appRouter.createCaller(buildContext({ general }));