import { describe, expect, it, vi } from 'vitest'; import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken'; import type { RedisConnector } from '@sammo-ts/infra'; import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js'; import { InMemoryFlushStore } from '../src/auth/flushStore.js'; import type { DatabaseClient, GameApiContext, GeneralRow } from '../src/context.js'; import { appRouter } from '../src/router.js'; const now = new Date('2026-01-01T01:02:00Z'); const general = (overrides: Partial = {}): GeneralRow => ({ id: 1, userId: 'u1', name: '장수', nationId: 1, cityId: 1, troopId: 0, npcState: 0, affinity: null, bornYear: 180, deadYear: 300, picture: null, imageServer: 0, leadership: 70, strength: 60, intel: 50, injury: 0, experience: 900, dedication: 100, officerLevel: 1, gold: 1000, rice: 2000, crew: 300, crewTypeId: 1, train: 90, atmos: 90, weaponCode: 'None', bookCode: 'None', horseCode: 'None', itemCode: 'None', turnTime: now, recentWarTime: null, age: 20, startAge: 20, personalCode: 'None', specialCode: 'None', special2Code: 'None', lastTurn: {}, meta: { belong: 1, defence_train: 80, killturn: 7 }, penalty: {}, createdAt: now, updatedAt: now, ...overrides, }); const token = (userId: string): GameSessionTokenPayload => ({ version: 1, profile: 'che:default', issuedAt: now.toISOString(), expiresAt: new Date(now.getTime() + 86400000).toISOString(), sessionId: userId, user: { id: userId, username: userId, displayName: userId, roles: [] }, sanctions: {}, }); const fixture = (generals: GeneralRow[], userId = 'u1') => { const db = { general: { findFirst: vi.fn(async ({ where }: { where: { userId: string } }) => generals.find((g) => g.userId === where.userId) ), findMany: vi.fn(async ({ where }: { where: { nationId: number } }) => generals.filter((g) => g.nationId === where.nationId) ), }, nation: { findUnique: vi.fn(async () => ({ id: 1, name: '위', color: '#080', level: 3, typeCode: 'che_중립', capitalCityId: 1, meta: { secretlimit: 3 }, })), }, city: { findMany: vi.fn(async () => [{ id: 1, name: '업' }]) }, troop: { findMany: vi.fn(async () => [{ troopLeaderId: 2, name: '선봉대' }]) }, worldState: { findFirst: vi.fn(async () => null) }, generalTurn: { findMany: vi.fn(async () => [{ generalId: 1, turnIdx: 0, actionCode: '징병' }]) }, generalAccessLog: { findMany: vi.fn(async () => generals.map((g) => ({ generalId: g.id, refreshScoreTotal: g.id * 10 }))), }, }; const redis = { get: vi.fn(async () => null), set: vi.fn(async () => null) } as unknown as RedisConnector['client']; const context: GameApiContext = { db: db as unknown as DatabaseClient, redis, turnDaemon: {} as GameApiContext['turnDaemon'], battleSim: {} as GameApiContext['battleSim'], profile: { id: 'che', scenario: 'default', name: 'che:default' }, auth: token(userId), uploadDir: 'uploads', uploadPath: '/uploads', uploadPublicUrl: null, accessTokenStore: new RedisAccessTokenStore(redis, 'che:default'), flushStore: new InMemoryFlushStore(), gameTokenSecret: 'secret', }; return { caller: appRouter.createCaller(context), db }; }; describe('nation general and secret office permissions', () => { it('redacts ordinary-member details and denies the secret office', async () => { const { caller } = fixture([general()]); const result = await caller.nation.getGeneralList(); expect(result.viewer).toEqual({ generalId: 1, permission: 0 }); expect(result.generals[0]).toMatchObject({ officerLevel: 1, cityName: null, troopName: null, refreshScoreTotal: 10, }); expect(result.generals[0]).not.toHaveProperty('crew'); await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' }); }); it('uses the session-owned general and scopes secret rows to that nation', async () => { const first = general(); const actor = general({ id: 2, userId: 'u2', officerLevel: 5, meta: { belong: 1 } }); const ally = general({ id: 3, userId: 'u3', gold: 3000, crew: 200, train: 80, atmos: 80 }); const foreign = general({ id: 4, userId: 'u4', nationId: 2, gold: 99999 }); const { caller, db } = fixture([first, actor, ally, foreign], 'u2'); const result = await caller.nation.getSecretGeneralList(); expect(result.viewer).toEqual({ generalId: 2, permission: 2 }); expect(result.generals.map((g) => g.id)).toEqual([1, 2, 3]); expect(result.summary).toMatchObject({ gold: 5000, crew: 800, generalCount: 3 }); expect(db.general.findFirst).toHaveBeenCalledWith({ where: { userId: 'u2' } }); expect(db.general.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { nationId: 1 } })); }); it('honors general penalties after a user switch', async () => { const penalized = general({ officerLevel: 5, penalty: { noChief: true } }); const { caller } = fixture([penalized]); await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' }); }); });