Ref와 같이 확인자와 확인 대상에게 시스템 개인 메시지를 저장하고, 두 mailbox 변경을 durable journal에 기록한다. 실패 경계와 실제 PostgreSQL 저장 회귀 테스트를 포함한다.
229 lines
9.7 KiB
TypeScript
229 lines
9.7 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
|
import { createGamePostgresConnector, type GamePrismaClient, type RedisConnector } from '@sammo-ts/infra';
|
|
|
|
import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js';
|
|
import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js';
|
|
import type { GameApiContext } from '../src/context.js';
|
|
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
|
|
import { InMemoryFlushStore } from '../src/auth/flushStore.js';
|
|
import { appRouter } from '../src/router.js';
|
|
|
|
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
|
|
const integration = describe.skipIf(!databaseUrl);
|
|
const actorGeneralId = 8_701;
|
|
const checkedGeneralId = 8_702;
|
|
const actorNationId = 871;
|
|
const checkedNationId = 872;
|
|
const actorUserId = 'inherit-owner-message-actor';
|
|
const checkedUserId = 'inherit-owner-message-checked';
|
|
|
|
const auth: GameSessionTokenPayload = {
|
|
version: 1,
|
|
profile: 'che:inherit-owner-message',
|
|
issuedAt: '2026-08-19T00:00:00.000Z',
|
|
expiresAt: '2026-08-20T00:00:00.000Z',
|
|
sessionId: 'inherit-owner-message-session',
|
|
user: {
|
|
id: actorUserId,
|
|
username: actorUserId,
|
|
displayName: '확인자 계정',
|
|
roles: ['user'],
|
|
},
|
|
sanctions: {},
|
|
};
|
|
|
|
const hasMailboxChange = (payload: unknown): boolean => {
|
|
if (!payload || typeof payload !== 'object' || !('changes' in payload)) return false;
|
|
const changes = (payload as { changes?: unknown }).changes;
|
|
if (!Array.isArray(changes)) return false;
|
|
const mailboxes = new Set([actorGeneralId, checkedGeneralId]);
|
|
return changes.some(
|
|
(change) =>
|
|
Array.isArray(change) &&
|
|
change[0] === 'messages.mailbox' &&
|
|
typeof change[1] === 'number' &&
|
|
mailboxes.has(change[1])
|
|
);
|
|
};
|
|
|
|
integration('inherit owner lookup private messages', () => {
|
|
let db: GamePrismaClient;
|
|
let closeDb: (() => Promise<void>) | undefined;
|
|
let worldStateId: number;
|
|
|
|
const buildContext = (requestId: string): GameApiContext => {
|
|
const redisClient = {
|
|
get: async () => null,
|
|
set: async () => null,
|
|
};
|
|
return {
|
|
requestId,
|
|
db,
|
|
redis: redisClient as unknown as RedisConnector['client'],
|
|
turnDaemon: new InMemoryTurnDaemonTransport(),
|
|
battleSim: new InMemoryBattleSimTransport(),
|
|
profile: { id: 'che', scenario: 'inherit-owner-message', name: 'che:inherit-owner-message' },
|
|
uploadDir: 'uploads',
|
|
uploadPath: '/uploads',
|
|
uploadPublicUrl: null,
|
|
auth,
|
|
accessTokenStore: new RedisAccessTokenStore(redisClient, 'che:inherit-owner-message'),
|
|
flushStore: new InMemoryFlushStore(),
|
|
gameTokenSecret: 'test-secret',
|
|
readModelOutbox: { wake: vi.fn() },
|
|
};
|
|
};
|
|
|
|
beforeAll(async () => {
|
|
const connector = createGamePostgresConnector({ url: databaseUrl! });
|
|
await connector.connect();
|
|
db = connector.prisma;
|
|
closeDb = () => connector.disconnect();
|
|
|
|
await db.inputEvent.deleteMany({ where: { actorUserId } });
|
|
await db.message.deleteMany({ where: { mailbox: { in: [actorGeneralId, checkedGeneralId] } } });
|
|
await db.inheritanceLog.deleteMany({ where: { userId: actorUserId } });
|
|
await db.inheritancePoint.deleteMany({ where: { userId: actorUserId } });
|
|
await db.general.deleteMany({ where: { id: { in: [actorGeneralId, checkedGeneralId] } } });
|
|
await db.nation.deleteMany({ where: { id: { in: [actorNationId, checkedNationId] } } });
|
|
await db.readModelRevision.deleteMany({
|
|
where: { domain: 'messages.mailbox', entityId: { in: [actorGeneralId, checkedGeneralId] } },
|
|
});
|
|
|
|
await db.nation.createMany({
|
|
data: [
|
|
{ id: actorNationId, name: '확인국', color: '#123456', level: 2 },
|
|
{ id: checkedNationId, name: '피확인국', color: '#654321', level: 3 },
|
|
],
|
|
});
|
|
await db.general.createMany({
|
|
data: [
|
|
{
|
|
id: actorGeneralId,
|
|
userId: actorUserId,
|
|
name: '확인장수',
|
|
nationId: actorNationId,
|
|
cityId: 1,
|
|
npcState: 0,
|
|
turnTime: new Date('0200-01-01T00:00:00.000Z'),
|
|
meta: { ownerName: '확인자 계정' },
|
|
},
|
|
{
|
|
id: checkedGeneralId,
|
|
userId: checkedUserId,
|
|
name: '피확인장수',
|
|
nationId: checkedNationId,
|
|
cityId: 1,
|
|
npcState: 0,
|
|
turnTime: new Date('0200-01-01T00:00:00.000Z'),
|
|
meta: { ownerName: '피확인 계정' },
|
|
},
|
|
],
|
|
});
|
|
await db.inheritancePoint.create({
|
|
data: { userId: actorUserId, key: 'previous', value: 1_500 },
|
|
});
|
|
const world = await db.worldState.create({
|
|
data: {
|
|
scenarioCode: 'inherit-owner-message',
|
|
currentYear: 200,
|
|
currentMonth: 4,
|
|
tickSeconds: 600,
|
|
config: { const: { inheritCheckOwnerPoint: 1_000 } },
|
|
meta: { isUnited: 0 },
|
|
},
|
|
});
|
|
worldStateId = world.id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
const outboxes = await db.readModelOutbox.findMany({ select: { id: true, payload: true } });
|
|
const outboxIds = outboxes.filter(({ payload }) => hasMailboxChange(payload)).map(({ id }) => id);
|
|
if (outboxIds.length > 0) {
|
|
await db.readModelOutbox.deleteMany({ where: { id: { in: outboxIds } } });
|
|
}
|
|
await db.inputEvent.deleteMany({ where: { actorUserId } });
|
|
await db.message.deleteMany({ where: { mailbox: { in: [actorGeneralId, checkedGeneralId] } } });
|
|
await db.inheritanceLog.deleteMany({ where: { userId: actorUserId } });
|
|
await db.inheritancePoint.deleteMany({ where: { userId: actorUserId } });
|
|
await db.general.deleteMany({ where: { id: { in: [actorGeneralId, checkedGeneralId] } } });
|
|
await db.nation.deleteMany({ where: { id: { in: [actorNationId, checkedNationId] } } });
|
|
await db.readModelRevision.deleteMany({
|
|
where: { domain: 'messages.mailbox', entityId: { in: [actorGeneralId, checkedGeneralId] } },
|
|
});
|
|
await db.worldState.delete({ where: { id: worldStateId } });
|
|
await closeDb?.();
|
|
});
|
|
|
|
it('commits the point charge, log, and both Ref-compatible private messages', async () => {
|
|
const requestId = 'integration:inherit-owner-message:success';
|
|
await expect(
|
|
appRouter.createCaller(buildContext(requestId)).inherit.checkOwner({ targetGeneralId: checkedGeneralId })
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
ownerName: '피확인 계정',
|
|
targetName: '피확인장수',
|
|
});
|
|
|
|
await expect(
|
|
db.inheritancePoint.findUniqueOrThrow({
|
|
where: { userId_key: { userId: actorUserId, key: 'previous' } },
|
|
})
|
|
).resolves.toMatchObject({ value: 500 });
|
|
await expect(db.inheritanceLog.findMany({ where: { userId: actorUserId } })).resolves.toEqual([
|
|
expect.objectContaining({
|
|
year: 200,
|
|
month: 4,
|
|
text: '1000 포인트로 장수 소유자 확인',
|
|
}),
|
|
]);
|
|
|
|
const messages = await db.message.findMany({
|
|
where: { mailbox: { in: [actorGeneralId, checkedGeneralId] } },
|
|
orderBy: { mailbox: 'asc' },
|
|
});
|
|
expect(messages).toHaveLength(2);
|
|
expect(
|
|
messages.map(({ mailbox, type, src, dest, message }) => ({ mailbox, type, src, dest, message }))
|
|
).toEqual([
|
|
{
|
|
mailbox: actorGeneralId,
|
|
type: 'private',
|
|
src: 0,
|
|
dest: actorGeneralId,
|
|
message: expect.objectContaining({
|
|
src: expect.objectContaining({ generalId: 0, nationName: 'System' }),
|
|
dest: expect.objectContaining({ generalId: actorGeneralId, generalName: '확인장수' }),
|
|
text: '피확인장수의 소유자는 피확인 계정 입니다.',
|
|
}),
|
|
},
|
|
{
|
|
mailbox: checkedGeneralId,
|
|
type: 'private',
|
|
src: 0,
|
|
dest: checkedGeneralId,
|
|
message: expect.objectContaining({
|
|
src: expect.objectContaining({ generalId: 0, nationName: 'System' }),
|
|
dest: expect.objectContaining({ generalId: checkedGeneralId, generalName: '피확인장수' }),
|
|
text: '소유자명이 누군가에 의해 확인되었습니다.',
|
|
}),
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
db.inputEvent.findUniqueOrThrow({ where: { requestId: `${requestId}:inherit.checkOwner` } })
|
|
).resolves.toMatchObject({ status: 'SUCCEEDED', actorUserId });
|
|
await expect(
|
|
db.readModelRevision.findMany({
|
|
where: { domain: 'messages.mailbox', entityId: { in: [actorGeneralId, checkedGeneralId] } },
|
|
orderBy: { entityId: 'asc' },
|
|
})
|
|
).resolves.toEqual([
|
|
expect.objectContaining({ domain: 'messages.mailbox', entityId: actorGeneralId, revision: 1n }),
|
|
expect.objectContaining({ domain: 'messages.mailbox', entityId: checkedGeneralId, revision: 1n }),
|
|
]);
|
|
});
|
|
});
|