fix: 내부 전용 예약 명령 실행 경계를 분리한다
This commit is contained in:
@@ -13,9 +13,8 @@ import {
|
||||
} from '../../turns/commandTable.js';
|
||||
import { loadMapDefinitionByName } from '../../maps/mapDefinition.js';
|
||||
import {
|
||||
assertReservedTurnActionAvailable,
|
||||
buildEquipmentTradeItemOptions,
|
||||
parseRegisteredTurnArgs,
|
||||
parseReservedTurnArgs,
|
||||
TURN_COMMAND_NATION_COLORS,
|
||||
type TurnCommandInputOptions,
|
||||
} from '../../turns/commandInput.js';
|
||||
@@ -64,9 +63,17 @@ const buildBulkEntrySchema = (turnList: z.ZodType<number[]>) =>
|
||||
args: z.unknown().optional(),
|
||||
});
|
||||
|
||||
const parseCommandArgs = async (scope: 'general' | 'nation', action: string, args: unknown) => {
|
||||
const parseCommandArgs = async (
|
||||
scope: 'general' | 'nation',
|
||||
action: string,
|
||||
args: unknown,
|
||||
worldState: WorldStateRow
|
||||
) => {
|
||||
try {
|
||||
return await parseRegisteredTurnArgs(scope, action, args);
|
||||
// 사용자 입력은 action별 argument schema보다 먼저 현재 scenario의
|
||||
// 선택 가능 profile을 통과해야 한다. 내부 전용 명령의 parser를 외부
|
||||
// 요청이 직접 호출하지 못하게 하는 첫 경계다.
|
||||
return await parseReservedTurnArgs(scope, action, args, asRecord(worldState.config).const);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
@@ -76,22 +83,6 @@ const parseCommandArgs = async (scope: 'general' | 'nation', action: string, arg
|
||||
}
|
||||
};
|
||||
|
||||
const assertScenarioCommandAvailable = async (
|
||||
scope: 'general' | 'nation',
|
||||
action: string,
|
||||
worldState: WorldStateRow
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await assertReservedTurnActionAvailable(scope, action, asRecord(worldState.config).const);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: error instanceof Error ? error.message : 'Unavailable turn command.',
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const mutateReservedTurns = async <T>(mutation: () => Promise<T>): Promise<T> => {
|
||||
try {
|
||||
return await mutation();
|
||||
@@ -428,9 +419,8 @@ export const turnsRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const args = await parseCommandArgs('general', input.action, input.args);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
await assertScenarioCommandAvailable('general', input.action, worldState);
|
||||
const args = await parseCommandArgs('general', input.action, input.args, worldState);
|
||||
await assertReservedTurnPermission(worldState, general, 'general', input.action, args);
|
||||
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
@@ -485,16 +475,15 @@ export const turnsRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
const updates = await Promise.all(
|
||||
input.entries.map(async (entry) => ({
|
||||
turnIndices: expandGeneralTurnIndices(entry.turnList),
|
||||
action: entry.action,
|
||||
args: await parseCommandArgs('general', entry.action, entry.args),
|
||||
args: await parseCommandArgs('general', entry.action, entry.args, worldState),
|
||||
}))
|
||||
);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
for (const update of updates) {
|
||||
await assertScenarioCommandAvailable('general', update.action, worldState);
|
||||
await assertReservedTurnPermission(worldState, general, 'general', update.action, update.args);
|
||||
}
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
@@ -532,9 +521,8 @@ export const turnsRouter = router({
|
||||
message: 'General is not an officer.',
|
||||
});
|
||||
}
|
||||
const args = await parseCommandArgs('nation', input.action, input.args);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
await assertScenarioCommandAvailable('nation', input.action, worldState);
|
||||
const args = await parseCommandArgs('nation', input.action, input.args, worldState);
|
||||
await assertReservedTurnPermission(worldState, general, 'nation', input.action, args);
|
||||
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
@@ -639,16 +627,15 @@ export const turnsRouter = router({
|
||||
message: 'General is not an officer.',
|
||||
});
|
||||
}
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
const updates = await Promise.all(
|
||||
input.entries.map(async (entry) => ({
|
||||
turnIndices: entry.turnList,
|
||||
action: entry.action,
|
||||
args: await parseCommandArgs('nation', entry.action, entry.args),
|
||||
args: await parseCommandArgs('nation', entry.action, entry.args, worldState),
|
||||
}))
|
||||
);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
for (const update of updates) {
|
||||
await assertScenarioCommandAvailable('nation', update.action, worldState);
|
||||
await assertReservedTurnPermission(worldState, general, 'nation', update.action, update.args);
|
||||
}
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
|
||||
@@ -362,7 +362,7 @@ export const loadTurnCommandSpecs = async (scenarioConst?: unknown) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const parseRegisteredTurnArgs = async (
|
||||
const parseRegisteredTurnArgs = async (
|
||||
scope: 'general' | 'nation',
|
||||
action: string,
|
||||
rawArgs: unknown
|
||||
|
||||
@@ -99,6 +99,31 @@ describe('turn command argument input', () => {
|
||||
await expect(parseReservedTurnArgs('general', 'che_포상', {})).rejects.toThrow('Unknown general turn command');
|
||||
});
|
||||
|
||||
it('rejects internal general commands before parsing their arguments or scenario overrides', async () => {
|
||||
await expect(parseReservedTurnArgs('general', 'che_NPC능동', {})).rejects.toThrow(
|
||||
'Unknown general turn command: che_NPC능동'
|
||||
);
|
||||
await expect(parseReservedTurnArgs('general', 'che_방랑', {})).rejects.toThrow(
|
||||
'Unknown general turn command: che_방랑'
|
||||
);
|
||||
await expect(
|
||||
parseReservedTurnArgs('general', 'che_등용수락', { destNationId: 1, destGeneralId: 2 })
|
||||
).rejects.toThrow('Unknown general turn command: che_등용수락');
|
||||
|
||||
await expect(
|
||||
parseReservedTurnArgs(
|
||||
'general',
|
||||
'che_NPC능동',
|
||||
{ optionText: '순간이동', destCityId: 1 },
|
||||
{
|
||||
availableGeneralCommand: {
|
||||
내부: ['휴식', 'che_NPC능동'],
|
||||
},
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('Unknown scenario general command key: che_NPC능동');
|
||||
});
|
||||
|
||||
it('accepts and rejects reserved commands from the real 904/905/910/912 world config', async () => {
|
||||
const scenarioConsts = Object.fromEntries(
|
||||
await Promise.all(
|
||||
|
||||
@@ -1227,7 +1227,12 @@ describe('appRouter', () => {
|
||||
const generalWrites: unknown[] = [];
|
||||
const nationWrites: unknown[] = [];
|
||||
const caller = appRouter.createCaller(
|
||||
buildContext({ general, generalTurnWrites: generalWrites, nationTurnWrites: nationWrites })
|
||||
buildContext({
|
||||
state: buildWorldState(),
|
||||
general,
|
||||
generalTurnWrites: generalWrites,
|
||||
nationTurnWrites: nationWrites,
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -1248,6 +1253,18 @@ describe('appRouter', () => {
|
||||
expectedRevision: 0,
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
||||
await expect(
|
||||
caller.turns.reserved.setGeneral({
|
||||
generalId: 14,
|
||||
turnIndex: 0,
|
||||
action: 'che_NPC능동',
|
||||
args: {},
|
||||
expectedRevision: 0,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Unknown general turn command: che_NPC능동',
|
||||
});
|
||||
|
||||
expect(generalWrites).toHaveLength(0);
|
||||
expect(nationWrites).toHaveLength(0);
|
||||
|
||||
Reference in New Issue
Block a user