권위 실행 context를 화면 진입 때 한 번 전달하고 준비 응답은 seed base만 반환한다. 반복별 seed 배열은 결정적 파생값으로 대체하되 구형 queue seed 우선순위와 고정 seed 동작을 보존한다.
678 lines
23 KiB
TypeScript
678 lines
23 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { BattleSimJobPayload, BattleSimResultPayload } from '../src/battleSim/types.js';
|
|
import type { BattleSimTransport } from '../src/battleSim/transport.js';
|
|
import type { DatabaseClient, GameApiContext, GameProfile, WorldStateRow } from '../src/context.js';
|
|
import type { RedisConnector } from '@sammo-ts/infra';
|
|
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
|
|
import { InMemoryFlushStore } from '../src/auth/flushStore.js';
|
|
import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js';
|
|
import { appRouter } from '../src/router.js';
|
|
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
|
|
|
const profile: GameProfile = {
|
|
id: 'che',
|
|
scenario: 'default',
|
|
name: 'che:default',
|
|
};
|
|
|
|
class QueuedBattleSimTransport implements BattleSimTransport {
|
|
public simulateCalls = 0;
|
|
public lastPayload: BattleSimJobPayload | null = null;
|
|
public lastRequesterUserId: string | null = null;
|
|
private readonly owners = new Map<string, string>();
|
|
private readonly results = new Map<string, BattleSimResultPayload>();
|
|
|
|
async simulate(payload: BattleSimJobPayload, requesterUserId: string) {
|
|
this.simulateCalls += 1;
|
|
this.lastPayload = payload;
|
|
this.lastRequesterUserId = requesterUserId;
|
|
const jobId = `job-${this.simulateCalls}`;
|
|
this.owners.set(jobId, requesterUserId);
|
|
return { status: 'queued', jobId } as const;
|
|
}
|
|
|
|
async getSimulationResult(jobId: string, requesterUserId: string) {
|
|
if (this.owners.get(jobId) !== requesterUserId) {
|
|
return null;
|
|
}
|
|
return this.results.get(jobId) ?? null;
|
|
}
|
|
|
|
pushResult(jobId: string, requesterUserId: string, payload: BattleSimResultPayload) {
|
|
if (this.owners.get(jobId) !== requesterUserId) {
|
|
throw new Error('requester mismatch');
|
|
}
|
|
this.results.set(jobId, payload);
|
|
}
|
|
}
|
|
|
|
const buildBattleRequest = () => ({
|
|
action: 'battle' as const,
|
|
repeatCnt: 1,
|
|
year: 200,
|
|
month: 1,
|
|
seed: 'test-seed',
|
|
attackerGeneral: {
|
|
no: 1,
|
|
name: 'Attacker',
|
|
nation: 1,
|
|
turntime: '2026-01-01 00:00:00',
|
|
personal: null,
|
|
special2: null,
|
|
crew: 1000,
|
|
crewtype: 100,
|
|
atmos: 100,
|
|
train: 100,
|
|
intel: 70,
|
|
intel_exp: 0,
|
|
book: null,
|
|
strength: 70,
|
|
strength_exp: 0,
|
|
weapon: null,
|
|
injury: 0,
|
|
leadership: 70,
|
|
leadership_exp: 0,
|
|
horse: null,
|
|
item: null,
|
|
explevel: 0,
|
|
experience: 100,
|
|
dedication: 100,
|
|
officer_level: 3,
|
|
officer_city: 1,
|
|
gold: 1000,
|
|
rice: 1000,
|
|
dex1: 0,
|
|
dex2: 0,
|
|
dex3: 0,
|
|
dex4: 0,
|
|
dex5: 0,
|
|
defence_train: 0,
|
|
recent_war: null,
|
|
warnum: 0,
|
|
killnum: 0,
|
|
killcrew: 0,
|
|
},
|
|
attackerCity: {
|
|
city: 1,
|
|
nation: 1,
|
|
supply: 1,
|
|
name: 'AttackerCity',
|
|
pop: 10000,
|
|
agri: 1000,
|
|
comm: 1000,
|
|
secu: 1000,
|
|
def: 100,
|
|
wall: 100,
|
|
trust: 100,
|
|
level: 2,
|
|
pop_max: 10000,
|
|
agri_max: 1000,
|
|
comm_max: 1000,
|
|
secu_max: 1000,
|
|
def_max: 200,
|
|
wall_max: 200,
|
|
dead: 0,
|
|
state: 0,
|
|
conflict: '{}',
|
|
},
|
|
attackerNation: {
|
|
type: 'che_도적',
|
|
tech: 1000,
|
|
level: 1,
|
|
capital: 1,
|
|
nation: 1,
|
|
name: 'AttackerNation',
|
|
gold: 1000,
|
|
rice: 1000,
|
|
gennum: 1,
|
|
},
|
|
defenderGenerals: [
|
|
{
|
|
no: 2,
|
|
name: 'Defender',
|
|
nation: 2,
|
|
turntime: '2026-01-01 00:00:00',
|
|
personal: null,
|
|
special2: null,
|
|
crew: 1000,
|
|
crewtype: 100,
|
|
atmos: 100,
|
|
train: 100,
|
|
intel: 60,
|
|
intel_exp: 0,
|
|
book: null,
|
|
strength: 60,
|
|
strength_exp: 0,
|
|
weapon: null,
|
|
injury: 0,
|
|
leadership: 60,
|
|
leadership_exp: 0,
|
|
horse: null,
|
|
item: null,
|
|
explevel: 0,
|
|
experience: 100,
|
|
dedication: 100,
|
|
officer_level: 3,
|
|
officer_city: 2,
|
|
gold: 1000,
|
|
rice: 1000,
|
|
dex1: 0,
|
|
dex2: 0,
|
|
dex3: 0,
|
|
dex4: 0,
|
|
dex5: 0,
|
|
defence_train: 0,
|
|
recent_war: null,
|
|
warnum: 0,
|
|
killnum: 0,
|
|
killcrew: 0,
|
|
},
|
|
],
|
|
defenderCity: {
|
|
city: 2,
|
|
nation: 2,
|
|
supply: 1,
|
|
name: 'DefenderCity',
|
|
pop: 10000,
|
|
agri: 1000,
|
|
comm: 1000,
|
|
secu: 1000,
|
|
def: 100,
|
|
wall: 100,
|
|
trust: 100,
|
|
level: 2,
|
|
pop_max: 10000,
|
|
agri_max: 1000,
|
|
comm_max: 1000,
|
|
secu_max: 1000,
|
|
def_max: 200,
|
|
wall_max: 200,
|
|
dead: 0,
|
|
state: 0,
|
|
conflict: '{}',
|
|
},
|
|
defenderNation: {
|
|
type: 'che_도적',
|
|
tech: 1000,
|
|
level: 1,
|
|
capital: 2,
|
|
nation: 2,
|
|
name: 'DefenderNation',
|
|
gold: 1000,
|
|
rice: 1000,
|
|
gennum: 1,
|
|
},
|
|
});
|
|
|
|
const buildContext = (options: {
|
|
state: WorldStateRow;
|
|
battleSim: BattleSimTransport;
|
|
userId?: string | null;
|
|
db?: Partial<DatabaseClient>;
|
|
}): GameApiContext => {
|
|
const db = options.db ?? {
|
|
worldState: {
|
|
findFirst: async () => options.state,
|
|
},
|
|
};
|
|
const accessTokenStore = new RedisAccessTokenStore(
|
|
{
|
|
get: async () => null,
|
|
set: async () => null,
|
|
},
|
|
profile.name
|
|
);
|
|
const auth: GameSessionTokenPayload | null =
|
|
options.userId === null
|
|
? null
|
|
: {
|
|
version: 1,
|
|
profile: profile.name,
|
|
issuedAt: new Date('2026-01-01T00:00:00Z').toISOString(),
|
|
expiresAt: new Date('2026-01-02T00:00:00Z').toISOString(),
|
|
sessionId: 'session-1',
|
|
user: {
|
|
id: options.userId ?? 'user-1',
|
|
username: 'tester',
|
|
displayName: 'Tester',
|
|
roles: [],
|
|
},
|
|
sanctions: {},
|
|
};
|
|
return {
|
|
db: db as unknown as DatabaseClient,
|
|
turnDaemon: new InMemoryTurnDaemonTransport(),
|
|
battleSim: options.battleSim,
|
|
profile,
|
|
auth,
|
|
uploadDir: 'uploads',
|
|
uploadPath: '/uploads',
|
|
uploadPublicUrl: null,
|
|
redis: {} as unknown as RedisConnector['client'],
|
|
accessTokenStore,
|
|
flushStore: new InMemoryFlushStore(),
|
|
gameTokenSecret: 'test-secret',
|
|
};
|
|
};
|
|
|
|
describe('battle router orchestration', () => {
|
|
it('returns one repeat seed base without reading world state or echoing the browser-worker payload', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: { environment: { scenarioEffect: 'event_MoreEffect' } },
|
|
meta: { scenarioMeta: { startYear: 180 } },
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
let worldStateReads = 0;
|
|
const db = {
|
|
worldState: {
|
|
findFirst: async () => {
|
|
worldStateReads += 1;
|
|
return state;
|
|
},
|
|
},
|
|
} as unknown as DatabaseClient;
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim, db }));
|
|
const request = { ...buildBattleRequest(), repeatCnt: 1000 };
|
|
delete (request as Partial<typeof request>).seed;
|
|
|
|
const prepared = await caller.battle.prepareSimulation(request);
|
|
|
|
expect(prepared.seedBase).toMatch(/^[0-9a-f-]{36}$/u);
|
|
expect(Object.keys(prepared)).toEqual(['seedBase']);
|
|
expect(Buffer.byteLength(JSON.stringify(prepared))).toBeLessThan(128);
|
|
expect(worldStateReads).toBe(0);
|
|
expect(battleSim.simulateCalls).toBe(0);
|
|
});
|
|
|
|
it('does not allocate a repeat seed base when the client supplies a fixed seed', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: {},
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim }));
|
|
|
|
const prepared = await caller.battle.prepareSimulation(buildBattleRequest());
|
|
|
|
expect(prepared).toEqual({ seedBase: null });
|
|
expect(battleSim.simulateCalls).toBe(0);
|
|
});
|
|
|
|
it('loads the full authoritative execution context once with the simulator form options', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: { environment: { scenarioEffect: 'event_MoreEffect' } },
|
|
meta: { scenarioMeta: { startYear: 180 } },
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
let worldStateReads = 0;
|
|
const db = {
|
|
worldState: {
|
|
findFirst: async () => {
|
|
worldStateReads += 1;
|
|
return state;
|
|
},
|
|
},
|
|
} as unknown as DatabaseClient;
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim, db }));
|
|
|
|
const context = await caller.battle.getSimulatorContext();
|
|
|
|
expect(context).toMatchObject({
|
|
world: { startYear: 180, currentYear: 200, currentMonth: 1 },
|
|
scenarioEffect: 'event_MoreEffect',
|
|
config: { armPerPhase: 500, maxTrainByWar: 110, maxAtmosByWar: 150 },
|
|
});
|
|
expect(context.unitSet.crewTypes?.[0]).toMatchObject({
|
|
id: expect.any(Number),
|
|
attack: expect.any(Number),
|
|
defence: expect.any(Number),
|
|
});
|
|
expect(worldStateReads).toBe(1);
|
|
});
|
|
|
|
it('rejects the neutral storage nation type before preparing or queuing a simulation', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: {},
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim }));
|
|
const request = buildBattleRequest();
|
|
request.attackerNation.type = 'che_중립';
|
|
|
|
await expect(caller.battle.prepareSimulation(request)).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
await expect(caller.battle.simulate(request)).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
expect(battleSim.simulateCalls).toBe(0);
|
|
});
|
|
|
|
it('returns queued then completed results via transport', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: { environment: { scenarioEffect: 'event_MoreEffect' } },
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim }));
|
|
|
|
const response = await caller.battle.simulate(buildBattleRequest());
|
|
expect(response.status).toBe('queued');
|
|
expect(battleSim.simulateCalls).toBe(1);
|
|
expect(battleSim.lastRequesterUserId).toBe('user-1');
|
|
expect(battleSim.lastPayload?.scenarioEffect).toBe('event_MoreEffect');
|
|
|
|
const queued = await caller.battle.getSimulation({ jobId: response.jobId });
|
|
expect(queued.status).toBe('queued');
|
|
|
|
battleSim.pushResult(response.jobId, 'user-1', { result: true, reason: 'success', avgWar: 1 });
|
|
|
|
const completed = await caller.battle.getSimulation({ jobId: response.jobId });
|
|
expect(completed.status).toBe('completed');
|
|
expect(completed.payload?.result).toBe(true);
|
|
});
|
|
|
|
it('queues one seed base instead of 1000 repeated UUID strings for the server fallback', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: {},
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim }));
|
|
const request = { ...buildBattleRequest(), repeatCnt: 1000 };
|
|
delete (request as Partial<typeof request>).seed;
|
|
|
|
await caller.battle.simulate(request);
|
|
|
|
const payload = battleSim.lastPayload;
|
|
if (!payload) {
|
|
throw new Error('Expected the fallback transport payload.');
|
|
}
|
|
expect(payload?.seedBase).toMatch(/^[0-9a-f-]{36}$/u);
|
|
expect(payload?.seeds).toBeUndefined();
|
|
const legacyPayload = {
|
|
...payload,
|
|
seedBase: undefined,
|
|
seeds: Array.from({ length: 1000 }, (_, index) => String(index).padStart(36, '0')),
|
|
};
|
|
expect(Buffer.byteLength(JSON.stringify(payload))).toBeLessThan(
|
|
Buffer.byteLength(JSON.stringify(legacyPayload)) * 0.45
|
|
);
|
|
});
|
|
|
|
it('uses the stored scenario effect even when a client sends a same-named field', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: { environment: { scenarioEffect: 'event_MoreEffect' } },
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim }));
|
|
const maliciousRequest = {
|
|
...buildBattleRequest(),
|
|
scenarioEffect: 'event_StrongAttacker',
|
|
} as ReturnType<typeof buildBattleRequest>;
|
|
|
|
await expect(caller.battle.simulate(maliciousRequest)).resolves.toMatchObject({ status: 'queued' });
|
|
expect(battleSim.lastPayload?.scenarioEffect).toBe('event_MoreEffect');
|
|
});
|
|
|
|
it('rejects an unknown stored scenario effect before queuing the simulation', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: { environment: { scenarioEffect: 'event_Missing' } },
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
const caller = appRouter.createCaller(buildContext({ state, battleSim }));
|
|
|
|
await expect(caller.battle.simulate(buildBattleRequest())).rejects.toThrow(
|
|
'Unknown scenario effect: event_Missing'
|
|
);
|
|
expect(battleSim.simulateCalls).toBe(0);
|
|
});
|
|
|
|
it('requires login, allows a user without a general, and does not open an input-event transaction', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: {},
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
let transactionCalls = 0;
|
|
const db = {
|
|
worldState: { findFirst: async () => state },
|
|
$transaction: async () => {
|
|
transactionCalls += 1;
|
|
throw new Error('simulation must not create an input event transaction');
|
|
},
|
|
} as unknown as DatabaseClient;
|
|
|
|
const anonymous = appRouter.createCaller(buildContext({ state, battleSim, userId: null, db }));
|
|
await expect(anonymous.battle.prepareSimulation(buildBattleRequest())).rejects.toMatchObject({
|
|
code: 'UNAUTHORIZED',
|
|
});
|
|
await expect(anonymous.battle.simulate(buildBattleRequest())).rejects.toMatchObject({
|
|
code: 'UNAUTHORIZED',
|
|
});
|
|
|
|
const noGeneralUser = appRouter.createCaller(
|
|
buildContext({ state, battleSim, userId: 'user-without-general', db })
|
|
);
|
|
await expect(noGeneralUser.battle.prepareSimulation(buildBattleRequest())).resolves.toMatchObject({
|
|
seedBase: null,
|
|
});
|
|
await expect(noGeneralUser.battle.simulate(buildBattleRequest())).resolves.toMatchObject({
|
|
status: 'queued',
|
|
});
|
|
expect(transactionCalls).toBe(0);
|
|
expect(battleSim.lastRequesterUserId).toBe('user-without-general');
|
|
});
|
|
|
|
it('does not expose queued results across authenticated users', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: {},
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
const owner = appRouter.createCaller(buildContext({ state, battleSim, userId: 'owner-user' }));
|
|
const other = appRouter.createCaller(buildContext({ state, battleSim, userId: 'other-user' }));
|
|
const response = await owner.battle.simulate(buildBattleRequest());
|
|
battleSim.pushResult(response.jobId, 'owner-user', { result: true, reason: 'success', avgWar: 7 });
|
|
|
|
await expect(owner.battle.getSimulation({ jobId: response.jobId })).resolves.toMatchObject({
|
|
status: 'completed',
|
|
payload: { avgWar: 7 },
|
|
});
|
|
await expect(other.battle.getSimulation({ jobId: response.jobId })).resolves.toEqual({
|
|
status: 'queued',
|
|
jobId: response.jobId,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('battle simulator general import permissions', () => {
|
|
const state: WorldStateRow = {
|
|
id: 1,
|
|
scenarioCode: 'default',
|
|
currentYear: 200,
|
|
currentMonth: 1,
|
|
tickSeconds: 600,
|
|
config: {},
|
|
meta: {},
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
};
|
|
|
|
const buildGeneral = (overrides: Record<string, unknown>) => ({
|
|
id: 1,
|
|
userId: 'same-nation-user',
|
|
name: '관전자',
|
|
npcState: 0,
|
|
nationId: 1,
|
|
leadership: 70,
|
|
strength: 71,
|
|
intel: 72,
|
|
officerLevel: 1,
|
|
injury: 0,
|
|
rice: 9000,
|
|
crew: 5000,
|
|
crewTypeId: 100,
|
|
atmos: 100,
|
|
train: 100,
|
|
experience: 400,
|
|
horseCode: null,
|
|
weaponCode: null,
|
|
bookCode: null,
|
|
itemCode: null,
|
|
personalCode: null,
|
|
specialCode: null,
|
|
special2Code: null,
|
|
meta: {},
|
|
...overrides,
|
|
});
|
|
|
|
const actor = buildGeneral({ id: 1, userId: 'same-nation-user', nationId: 1 });
|
|
const ally = buildGeneral({
|
|
id: 2,
|
|
userId: 'ally-user',
|
|
name: '아군 장수',
|
|
nationId: 1,
|
|
officerLevel: 4,
|
|
rice: 4321,
|
|
crew: 3210,
|
|
train: 97,
|
|
atmos: 96,
|
|
horseCode: 'che_적토마',
|
|
weaponCode: 'che_의천검',
|
|
bookCode: 'che_손자병법',
|
|
itemCode: 'che_옥새',
|
|
specialCode: 'che_event_신산',
|
|
meta: {
|
|
dex1: 10000,
|
|
rank_warnum: 33,
|
|
rank_killnum: 22,
|
|
rank_killcrew: 1111,
|
|
},
|
|
});
|
|
const foreignActor = buildGeneral({ id: 3, userId: 'foreign-user', nationId: 2 });
|
|
const generals = [actor, ally, foreignActor];
|
|
const db = {
|
|
worldState: { findFirst: async () => state },
|
|
general: {
|
|
findFirst: async ({ where }: { where: { userId: string } }) =>
|
|
generals.find((general) => general.userId === where.userId) ?? null,
|
|
findUnique: async ({ where }: { where: { id: number } }) =>
|
|
generals.find((general) => general.id === where.id) ?? null,
|
|
},
|
|
} as unknown as DatabaseClient;
|
|
|
|
it('returns full ally details to the same nation but redacts them for another nation', async () => {
|
|
const battleSim = new QueuedBattleSimTransport();
|
|
const sameNation = appRouter.createCaller(buildContext({ state, battleSim, userId: 'same-nation-user', db }));
|
|
const foreign = appRouter.createCaller(buildContext({ state, battleSim, userId: 'foreign-user', db }));
|
|
|
|
const visible = await sameNation.battle.getGeneralDetail({ generalId: ally.id });
|
|
expect(visible.general).toMatchObject({
|
|
name: '아군 장수',
|
|
officer_level: 4,
|
|
horse: 'che_적토마',
|
|
crew: 3210,
|
|
rice: 4321,
|
|
train: 97,
|
|
atmos: 96,
|
|
warnum: 33,
|
|
killnum: 22,
|
|
killcrew: 1111,
|
|
special: 'che_event_신산',
|
|
});
|
|
|
|
const redacted = await foreign.battle.getGeneralDetail({ generalId: ally.id });
|
|
expect(redacted.general).toMatchObject({
|
|
name: '아군 장수',
|
|
officer_level: 1,
|
|
horse: null,
|
|
weapon: null,
|
|
book: null,
|
|
item: null,
|
|
crew: 0,
|
|
rice: 10000,
|
|
dex1: 0,
|
|
warnum: 0,
|
|
killnum: 0,
|
|
killcrew: 0,
|
|
special: 'che_event_신산',
|
|
});
|
|
});
|
|
|
|
it('requires a game general only for server-side general import', async () => {
|
|
const caller = appRouter.createCaller(
|
|
buildContext({
|
|
state,
|
|
battleSim: new QueuedBattleSimTransport(),
|
|
userId: 'user-without-general',
|
|
db,
|
|
})
|
|
);
|
|
|
|
await expect(caller.battle.getGeneralDetail({ generalId: ally.id })).rejects.toMatchObject({
|
|
code: 'NOT_FOUND',
|
|
message: 'General not found',
|
|
});
|
|
});
|
|
});
|