fix: 예약 명령의 Ref 인자 검증 순서를 복원한다
This commit is contained in:
@@ -14,6 +14,8 @@ import {
|
||||
} from '../../turns/commandTable.js';
|
||||
import { loadMapDefinitionByName } from '../../maps/mapDefinition.js';
|
||||
import {
|
||||
assertReservedTurnActionAvailable,
|
||||
assertReservedTurnArgsPassLegacyBasicValidation,
|
||||
buildEquipmentTradeItemOptions,
|
||||
parseReservedTurnArgs,
|
||||
TURN_COMMAND_NATION_COLORS,
|
||||
@@ -85,6 +87,27 @@ const parseCommandArgs = async (
|
||||
}
|
||||
};
|
||||
|
||||
const preflightCommandArgs = async (
|
||||
scope: 'general' | 'nation',
|
||||
action: string,
|
||||
args: unknown,
|
||||
worldState: WorldStateRow
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Ref checks scenario availability and common argument types before
|
||||
// officer/penalty gates. Core keeps actor ownership first, then leaves
|
||||
// required command-specific fields for the later parser.
|
||||
await assertReservedTurnActionAvailable(scope, action, asRecord(worldState.config).const);
|
||||
assertReservedTurnArgsPassLegacyBasicValidation(args);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: error instanceof Error ? error.message : 'Invalid turn command arguments.',
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const mutateReservedTurns = async <T>(mutation: () => Promise<T>): Promise<T> => {
|
||||
try {
|
||||
return await mutation();
|
||||
@@ -484,6 +507,7 @@ export const turnsRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
await preflightCommandArgs('general', input.action, input.args, worldState);
|
||||
const args = await parseCommandArgs('general', input.action, input.args, worldState);
|
||||
await assertReservedTurnPermission(worldState, general, 'general', input.action, args);
|
||||
|
||||
@@ -540,15 +564,20 @@ 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) => ({
|
||||
const firstEntry = input.entries[0];
|
||||
await preflightCommandArgs('general', firstEntry.action, firstEntry.args, worldState);
|
||||
const updates: ReservedTurnUpdate[] = [];
|
||||
for (const [index, entry] of input.entries.entries()) {
|
||||
if (index > 0) {
|
||||
await preflightCommandArgs('general', entry.action, entry.args, worldState);
|
||||
}
|
||||
const update = {
|
||||
turnIndices: expandGeneralTurnIndices(entry.turnList),
|
||||
action: entry.action,
|
||||
args: await parseCommandArgs('general', entry.action, entry.args, worldState),
|
||||
}))
|
||||
);
|
||||
for (const update of updates) {
|
||||
};
|
||||
await assertReservedTurnPermission(worldState, general, 'general', update.action, update.args);
|
||||
updates.push(update);
|
||||
}
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
setGeneralTurns(ctx.db, input.generalId, updates, input.expectedRevision)
|
||||
@@ -573,6 +602,8 @@ export const turnsRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
await preflightCommandArgs('nation', input.action, input.args, worldState);
|
||||
if (general.nationId <= 0) {
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
@@ -585,9 +616,8 @@ export const turnsRouter = router({
|
||||
message: 'General is not an officer.',
|
||||
});
|
||||
}
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
const args = await parseCommandArgs('nation', input.action, input.args, worldState);
|
||||
assertNationTurnInputAllowed(general);
|
||||
const args = await parseCommandArgs('nation', input.action, input.args, worldState);
|
||||
await assertReservedTurnPermission(worldState, general, 'nation', input.action, args);
|
||||
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
@@ -684,6 +714,9 @@ export const turnsRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
const firstEntry = input.entries[0];
|
||||
await preflightCommandArgs('nation', firstEntry.action, firstEntry.args, worldState);
|
||||
if (general.nationId <= 0) {
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
@@ -696,15 +729,17 @@ export const turnsRouter = router({
|
||||
message: 'General is not an officer.',
|
||||
});
|
||||
}
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
const updates: ReservedTurnUpdate[] = [];
|
||||
for (const entry of input.entries) {
|
||||
for (const [index, entry] of input.entries.entries()) {
|
||||
if (index > 0) {
|
||||
await preflightCommandArgs('nation', entry.action, entry.args, worldState);
|
||||
}
|
||||
assertNationTurnInputAllowed(general);
|
||||
const update = {
|
||||
turnIndices: entry.turnList,
|
||||
action: entry.action,
|
||||
args: await parseCommandArgs('nation', entry.action, entry.args, worldState),
|
||||
};
|
||||
assertNationTurnInputAllowed(general);
|
||||
await assertReservedTurnPermission(worldState, general, 'nation', update.action, update.args);
|
||||
updates.push(update);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
isGeneralTurnCommandKey,
|
||||
isNationTurnCommandKey,
|
||||
getLegacyStringWidth,
|
||||
loadGeneralTurnCommandSpecs,
|
||||
loadNationTurnCommandSpecs,
|
||||
type GeneralTurnCommandSpec,
|
||||
@@ -385,6 +386,188 @@ const parseRegisteredTurnArgs = async (
|
||||
return spec.argsSchema.parse(rawArgs);
|
||||
};
|
||||
|
||||
const LEGACY_REMOVED_TURN_ARG_CHARACTERS = new Set([
|
||||
'"',
|
||||
"'",
|
||||
'ⓝ',
|
||||
'ⓜ',
|
||||
'ⓖ',
|
||||
'ⓞ',
|
||||
'ⓧ',
|
||||
'㉥',
|
||||
'\\',
|
||||
'/',
|
||||
'`',
|
||||
'#',
|
||||
'-',
|
||||
'|',
|
||||
]);
|
||||
|
||||
const sanitizeLegacyTurnArgString = (value: string): string => {
|
||||
// Ref StringUtil::neutralize() treats the string "0" as empty because of
|
||||
// PHP truthiness, both before and after removeSpecialCharacter().
|
||||
if (value === '' || value === '0') {
|
||||
return '';
|
||||
}
|
||||
const stripped = Array.from(value)
|
||||
.filter((character) => !LEGACY_REMOVED_TURN_ARG_CHARACTERS.has(character))
|
||||
.join('');
|
||||
if (stripped === '' || stripped === '0') {
|
||||
return '';
|
||||
}
|
||||
return stripped
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replace(/^[\p{Z}\p{C}]+|[\p{Z}\p{C}]+$/gu, '');
|
||||
};
|
||||
|
||||
export const sanitizeReservedTurnArgs = (value: unknown): unknown => {
|
||||
if (typeof value === 'string') {
|
||||
return sanitizeLegacyTurnArgString(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => sanitizeReservedTurnArgs(entry));
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeReservedTurnArgs(entry)]));
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const LEGACY_INTEGER_TURN_ARG_KEYS = new Set([
|
||||
'crewType',
|
||||
'destGeneralId',
|
||||
'destGeneralID',
|
||||
'destCityId',
|
||||
'destCityID',
|
||||
'destNationId',
|
||||
'destNationID',
|
||||
'amount',
|
||||
'colorType',
|
||||
'srcArmType',
|
||||
'destArmType',
|
||||
]);
|
||||
const LEGACY_BOOLEAN_TURN_ARG_KEYS = new Set(['isGold', 'buyRice']);
|
||||
const LEGACY_INTEGER_ARRAY_TURN_ARG_KEYS = new Set([
|
||||
'destNationIdList',
|
||||
'destNationIDList',
|
||||
'destGeneralIdList',
|
||||
'destGeneralIDList',
|
||||
'amountList',
|
||||
]);
|
||||
const LEGACY_NUMERIC_PATTERN = /^[\t\n\r\f\v ]*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[\t\n\r\f\v ]*$/;
|
||||
|
||||
const skipsLegacyOptionalValidation = (value: unknown): boolean => value === null || value === '';
|
||||
|
||||
const isLegacyNumeric = (value: unknown): boolean => {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value);
|
||||
}
|
||||
if (typeof value !== 'string' || !LEGACY_NUMERIC_PATTERN.test(value)) {
|
||||
return false;
|
||||
}
|
||||
return Number.isFinite(Number(value));
|
||||
};
|
||||
|
||||
const throwLegacyBasicTurnArgError = (): never => {
|
||||
throw new Error('턴이 입력되지 않았습니다.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Ref checkCommandArg() only validates common fields that are present. Missing
|
||||
* command-specific fields are deliberately left for command construction, so
|
||||
* nation penalties and officer checks retain their legacy error priority.
|
||||
*/
|
||||
export const assertReservedTurnArgsPassLegacyBasicValidation = (rawArgs: unknown): void => {
|
||||
const sanitizedArgs = sanitizeReservedTurnArgs(rawArgs);
|
||||
if (sanitizedArgs === null || sanitizedArgs === undefined) {
|
||||
return;
|
||||
}
|
||||
if (typeof sanitizedArgs !== 'object') {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
|
||||
const args = sanitizedArgs as Record<string, unknown>;
|
||||
for (const key of LEGACY_INTEGER_TURN_ARG_KEYS) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(args, key) &&
|
||||
!skipsLegacyOptionalValidation(args[key]) &&
|
||||
!Number.isInteger(args[key])
|
||||
) {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
}
|
||||
for (const key of LEGACY_BOOLEAN_TURN_ARG_KEYS) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(args, key) &&
|
||||
!skipsLegacyOptionalValidation(args[key]) &&
|
||||
typeof args[key] !== 'boolean'
|
||||
) {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
}
|
||||
for (const key of LEGACY_INTEGER_ARRAY_TURN_ARG_KEYS) {
|
||||
if (!Object.prototype.hasOwnProperty.call(args, key)) {
|
||||
continue;
|
||||
}
|
||||
const value = args[key];
|
||||
if (skipsLegacyOptionalValidation(value)) {
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(value) || value.some((entry) => !Number.isInteger(entry))) {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
}
|
||||
|
||||
const month = args.month;
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(args, 'month') &&
|
||||
!skipsLegacyOptionalValidation(month) &&
|
||||
(!isLegacyNumeric(month) || Number(month) < 1 || Number(month) > 12)
|
||||
) {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
const year = args.year;
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(args, 'year') &&
|
||||
!skipsLegacyOptionalValidation(year) &&
|
||||
(!isLegacyNumeric(year) || Number(year) < 0)
|
||||
) {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
for (const [key, minimum] of [
|
||||
['destGeneralId', 1],
|
||||
['destGeneralID', 1],
|
||||
['destCityId', 1],
|
||||
['destCityID', 1],
|
||||
['destNationId', 1],
|
||||
['destNationID', 1],
|
||||
['amount', 1],
|
||||
['crewType', 0],
|
||||
] as const) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(args, key) &&
|
||||
!skipsLegacyOptionalValidation(args[key]) &&
|
||||
Number(args[key]) < minimum
|
||||
) {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(args, 'nationName')) {
|
||||
const nationName = args.nationName;
|
||||
if (
|
||||
!skipsLegacyOptionalValidation(nationName) &&
|
||||
(typeof nationName !== 'string' ||
|
||||
getLegacyStringWidth(nationName) < 1 ||
|
||||
getLegacyStringWidth(nationName) > 18)
|
||||
) {
|
||||
throwLegacyBasicTurnArgError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const assertReservedTurnActionAvailable = async (
|
||||
scope: 'general' | 'nation',
|
||||
action: string,
|
||||
@@ -403,5 +586,5 @@ export const parseReservedTurnArgs = async (
|
||||
scenarioConst?: unknown
|
||||
): Promise<Record<string, unknown>> => {
|
||||
await assertReservedTurnActionAvailable(scope, action, scenarioConst);
|
||||
return parseRegisteredTurnArgs(scope, action, rawArgs);
|
||||
return parseRegisteredTurnArgs(scope, action, sanitizeReservedTurnArgs(rawArgs));
|
||||
};
|
||||
|
||||
@@ -8,9 +8,11 @@ import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scena
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
assertReservedTurnArgsPassLegacyBasicValidation,
|
||||
buildEquipmentTradeItemOptions,
|
||||
buildTurnCommandInputFields,
|
||||
parseReservedTurnArgs,
|
||||
sanitizeReservedTurnArgs,
|
||||
} from '../src/turns/commandInput.js';
|
||||
|
||||
const buildShopItem = (key: string, name: string) => ({
|
||||
@@ -99,6 +101,46 @@ describe('turn command argument input', () => {
|
||||
await expect(parseReservedTurnArgs('general', 'che_포상', {})).rejects.toThrow('Unknown general turn command');
|
||||
});
|
||||
|
||||
it('keeps Ref common validation separate from command-specific required fields', async () => {
|
||||
expect(() => assertReservedTurnArgsPassLegacyBasicValidation({})).not.toThrow();
|
||||
expect(() =>
|
||||
assertReservedTurnArgsPassLegacyBasicValidation({
|
||||
isGold: true,
|
||||
amount: '1',
|
||||
destGeneralId: 7,
|
||||
})
|
||||
).toThrow('턴이 입력되지 않았습니다.');
|
||||
expect(() => assertReservedTurnArgsPassLegacyBasicValidation({ month: '12', year: 0 })).not.toThrow();
|
||||
expect(() => assertReservedTurnArgsPassLegacyBasicValidation({ nationName: '0' })).not.toThrow();
|
||||
expect(() => assertReservedTurnArgsPassLegacyBasicValidation({ year: '0x10' })).toThrow(
|
||||
'턴이 입력되지 않았습니다.'
|
||||
);
|
||||
await expect(parseReservedTurnArgs('nation', 'che_국호변경', { nationName: '0' })).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it('recursively sanitizes reserved command strings like Ref before command parsing', async () => {
|
||||
expect(
|
||||
sanitizeReservedTurnArgs({
|
||||
nationName: ' <신-국># ',
|
||||
nested: ['A/B', '0'],
|
||||
})
|
||||
).toEqual({
|
||||
nationName: '<신국>',
|
||||
nested: ['AB', ''],
|
||||
});
|
||||
await expect(
|
||||
parseReservedTurnArgs('nation', 'che_국호변경', {
|
||||
nationName: ' <신-국># ',
|
||||
})
|
||||
).resolves.toEqual({ nationName: '<신국>' });
|
||||
await expect(
|
||||
parseReservedTurnArgs('nation', 'che_피장파장', {
|
||||
destNationId: 2,
|
||||
commandType: 'che_-수몰',
|
||||
})
|
||||
).resolves.toEqual({ destNationId: 2, commandType: 'che_수몰' });
|
||||
});
|
||||
|
||||
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능동'
|
||||
|
||||
@@ -1112,6 +1112,30 @@ describe('appRouter', () => {
|
||||
expect(nationWrites).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('stores Ref-sanitized nation command strings in the reserved queue', async () => {
|
||||
const general = buildGeneralRow({ id: 19, nationId: 3, officerLevel: 12 });
|
||||
const nationWrites: unknown[] = [];
|
||||
const caller = appRouter.createCaller(
|
||||
buildContext({ state: buildWorldState(), general, nationTurnWrites: nationWrites })
|
||||
);
|
||||
|
||||
const response = await caller.turns.reserved.setNation({
|
||||
generalId: general.id,
|
||||
turnIndex: 0,
|
||||
action: 'che_국호변경',
|
||||
args: { nationName: ' <신-국># ' },
|
||||
expectedRevision: 0,
|
||||
});
|
||||
|
||||
expect(response.turns[0]?.args).toEqual({ nationName: '<신국>' });
|
||||
expect(nationWrites).toHaveLength(1);
|
||||
expect(nationWrites[0]).toMatchObject({
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({ turnIdx: 0, arg: { nationName: '<신국>' } }),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves Ref nation-turn penalty key semantics and validation priority', async () => {
|
||||
const general = buildGeneralRow({
|
||||
id: 22,
|
||||
@@ -1159,7 +1183,8 @@ describe('appRouter', () => {
|
||||
singleCaller.turns.reserved.setNation({
|
||||
generalId: general.id,
|
||||
turnIndex: 0,
|
||||
action: '휴식',
|
||||
action: 'che_포상',
|
||||
args: {},
|
||||
expectedRevision: 0,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
@@ -1184,7 +1209,7 @@ describe('appRouter', () => {
|
||||
bulkCaller.turns.reserved.setNationBulk({
|
||||
generalId: general.id,
|
||||
entries: [
|
||||
{ turnList: [0], action: '휴식' },
|
||||
{ turnList: [0], action: 'che_포상', args: {} },
|
||||
{ turnList: [1], action: 'not-a-command' },
|
||||
],
|
||||
expectedRevision: 0,
|
||||
@@ -1195,6 +1220,25 @@ describe('appRouter', () => {
|
||||
});
|
||||
expect(bulkWrites).toHaveLength(0);
|
||||
expect(bulkUpdates).toHaveLength(0);
|
||||
|
||||
const allowedCaller = appRouter.createCaller(
|
||||
buildContext({
|
||||
state: buildWorldState('full', 12),
|
||||
general: buildGeneralRow({
|
||||
...general,
|
||||
penalty: {},
|
||||
}),
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
allowedCaller.turns.reserved.setNation({
|
||||
generalId: general.id,
|
||||
turnIndex: 0,
|
||||
action: 'che_포상',
|
||||
args: {},
|
||||
expectedRevision: 0,
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
||||
});
|
||||
|
||||
it('refills user killturn after a successful nation reservation and invalidates its readers', async () => {
|
||||
|
||||
@@ -524,26 +524,36 @@ const requestReservedGeneral = (accessToken: string | undefined, idempotencyKey:
|
||||
idempotencyKey,
|
||||
});
|
||||
|
||||
const requestReservedNation = (accessToken: string, idempotencyKey: string, targetGeneralId: number) =>
|
||||
const requestReservedNation = (
|
||||
accessToken: string,
|
||||
idempotencyKey: string,
|
||||
targetGeneralId: number,
|
||||
command: { action: string; args: unknown } = { action: '휴식', args: {} }
|
||||
) =>
|
||||
requestTrpc('turns.reserved.setNation', {
|
||||
method: 'POST',
|
||||
input: {
|
||||
generalId: targetGeneralId,
|
||||
turnIndex: 0,
|
||||
action: '휴식',
|
||||
args: {},
|
||||
action: command.action,
|
||||
args: command.args,
|
||||
expectedRevision: 0,
|
||||
},
|
||||
accessToken,
|
||||
idempotencyKey,
|
||||
});
|
||||
|
||||
const requestReservedNationBulk = (accessToken: string, idempotencyKey: string, targetGeneralId: number) =>
|
||||
const requestReservedNationBulk = (
|
||||
accessToken: string,
|
||||
idempotencyKey: string,
|
||||
targetGeneralId: number,
|
||||
command: { action: string; args: unknown } = { action: '휴식', args: {} }
|
||||
) =>
|
||||
requestTrpc('turns.reserved.setNationBulk', {
|
||||
method: 'POST',
|
||||
input: {
|
||||
generalId: targetGeneralId,
|
||||
entries: [{ turnList: [0, 1], action: '휴식', args: {} }],
|
||||
entries: [{ turnList: [0, 1], action: command.action, args: command.args }],
|
||||
expectedRevision: 0,
|
||||
},
|
||||
accessToken,
|
||||
@@ -559,11 +569,12 @@ const requestNationReservation = (
|
||||
kind: NationReservationKind,
|
||||
accessToken: string,
|
||||
idempotencyKey: string,
|
||||
targetGeneralId = generalId
|
||||
targetGeneralId = generalId,
|
||||
command: { action: string; args: unknown } = { action: '휴식', args: {} }
|
||||
) =>
|
||||
kind === 'single'
|
||||
? requestReservedNation(accessToken, idempotencyKey, targetGeneralId)
|
||||
: requestReservedNationBulk(accessToken, idempotencyKey, targetGeneralId);
|
||||
? requestReservedNation(accessToken, idempotencyKey, targetGeneralId, command)
|
||||
: requestReservedNationBulk(accessToken, idempotencyKey, targetGeneralId, command);
|
||||
|
||||
const ownershipDenialCases = [
|
||||
{
|
||||
@@ -680,7 +691,11 @@ integration('game API security over HTTP transport', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
if ((await db.worldState.count()) === 0) {
|
||||
const existingWorlds = await db.worldState.findMany({
|
||||
select: { id: true, scenarioCode: true },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
if (existingWorlds.length === 0) {
|
||||
await db.worldState.create({
|
||||
data: {
|
||||
id: fixtureWorldId,
|
||||
@@ -693,11 +708,23 @@ integration('game API security over HTTP transport', () => {
|
||||
},
|
||||
});
|
||||
createdFixtureWorld = true;
|
||||
} else if (
|
||||
existingWorlds.length === 1 &&
|
||||
existingWorlds[0]?.id === fixtureWorldId &&
|
||||
existingWorlds[0].scenarioCode === 'security-http'
|
||||
) {
|
||||
// A previously interrupted run may leave our own fixture row. It
|
||||
// remains owned by this suite and is removed during teardown.
|
||||
createdFixtureWorld = true;
|
||||
} else {
|
||||
throw new Error(
|
||||
`security transport fixture requires an empty schema or its owned world row, got ${JSON.stringify(existingWorlds)}`
|
||||
);
|
||||
}
|
||||
const reservationWorlds = await db.worldState.findMany({ select: { id: true } });
|
||||
if (reservationWorlds.length !== 1 || !reservationWorlds[0]) {
|
||||
if (reservationWorlds.length !== 1 || reservationWorlds[0]?.id !== fixtureWorldId) {
|
||||
throw new Error(
|
||||
`security transport fixture requires exactly one world row, got ${reservationWorlds.length}`
|
||||
`security transport fixture requires world ${fixtureWorldId}, got ${JSON.stringify(reservationWorlds)}`
|
||||
);
|
||||
}
|
||||
reservationWorldId = reservationWorlds[0].id;
|
||||
@@ -1136,7 +1163,10 @@ integration('game API security over HTTP transport', () => {
|
||||
data: { meta: { killturn: 12 } },
|
||||
});
|
||||
|
||||
const result = await requestNationReservation(kind, accessToken, idempotencyKey);
|
||||
const result = await requestNationReservation(kind, accessToken, idempotencyKey, generalId, {
|
||||
action: 'che_포상',
|
||||
args: {},
|
||||
});
|
||||
|
||||
expect(result.response.status).toBe(412);
|
||||
expect(result.body).toMatchObject({
|
||||
|
||||
Reference in New Issue
Block a user