Files
core2026/app/game-api/test/deferredGeneralAccess.integration.test.ts
T
Hide_D 5bc1e3473f perf: 접속 점수를 Redis 배치로 지연 반영
대시보드 벌점을 read-model source와 outbox에서 제외하고 수동 projection의 가점을 Redis에 합산한다. 5초 worker가 월별 traffic과 접속 점수를 멱등 PostgreSQL batch로 반영하며 메인 제한 gate는 Redis TTL marker만 조회한다.
2026-08-17 14:50:34 +00:00

161 lines
5.6 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import {
createGamePostgresConnector,
createRedisConnector,
resolveRedisConfigFromEnv,
type GamePrismaClient,
type RedisConnector,
} from '@sammo-ts/infra';
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import {
DeferredGeneralAccessWorker,
enqueueDeferredGeneralAccess,
getDeferredGeneralAccessLimit,
} from '../src/services/deferredGeneralAccess.js';
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
const liveIntegration = describe.skipIf(!databaseUrl || !process.env.REDIS_URL);
liveIntegration('deferred general access with PostgreSQL and Redis', () => {
const runId = randomUUID();
const profile = `deferred-access-${runId}`;
const userId = `deferred-user-${runId}`;
const scenarioCode = `deferred-scenario-${runId}`;
const batchId = `deferred-batch-${runId}`;
const generalId = 9_981_000 + Math.floor(Math.random() * 900);
const now = new Date('2026-08-17T03:05:00.000Z');
const nextAccessAt = new Date('2026-08-17T03:10:00.000Z');
const auth: GameSessionTokenPayload = {
version: 1,
profile,
issuedAt: '2026-08-17T03:00:00.000Z',
expiresAt: '2026-08-18T03:00:00.000Z',
sessionId: `deferred-session-${runId}`,
user: {
id: userId,
username: userId,
displayName: userId,
roles: [],
},
sanctions: {},
};
let db: GamePrismaClient;
let closeDb: (() => Promise<void>) | undefined;
let redis: RedisConnector;
let worldStateId: number;
beforeAll(async () => {
const dbConnector = createGamePostgresConnector({ url: databaseUrl! });
await dbConnector.connect();
db = dbConnector.prisma;
closeDb = () => dbConnector.disconnect();
redis = createRedisConnector(resolveRedisConfigFromEnv());
await redis.connect();
const world = await db.worldState.create({
data: {
scenarioCode,
currentYear: 200,
currentMonth: 1,
tickSeconds: 600,
config: {},
meta: {
lastTurnTime: '2026-08-17T03:00:00.000Z',
refreshLimit: 1,
},
},
});
worldStateId = world.id;
await db.general.create({
data: {
id: generalId,
userId,
name: '지연접속검증',
turnTime: nextAccessAt,
},
});
});
afterAll(async () => {
if (redis) {
const keys: string[] = [];
for await (const page of redis.client.scanIterator({
MATCH: `sammo:game:general-access:*:${profile}*`,
COUNT: 100,
})) {
keys.push(...page);
}
if (keys.length > 0) await redis.client.del(keys);
await redis.disconnect();
}
if (db) {
await db.generalAccessBatch.deleteMany({ where: { id: batchId } });
await db.generalAccessLog.deleteMany({ where: { generalId } });
await db.general.deleteMany({ where: { id: generalId } });
await db.worldState.deleteMany({ where: { id: worldStateId } });
await closeDb?.();
}
});
it('aggregates Redis increments, flushes once, and installs a lazy limit marker', async () => {
const workerDb = {
worldState: {
findFirst: () =>
db.worldState.findUniqueOrThrow({
where: { id: worldStateId },
select: {
id: true,
currentYear: true,
currentMonth: true,
tickSeconds: true,
meta: true,
},
}),
},
$queryRaw: db.$queryRaw.bind(db),
$executeRaw: db.$executeRaw.bind(db),
};
const worker = new DeferredGeneralAccessWorker(
workerDb as never,
redis.client,
profile,
{ get: async () => 'RUNNING' },
{ now: () => now, createBatchId: () => batchId }
);
await enqueueDeferredGeneralAccess(redis.client, profile, auth, generalId, 1, now);
await enqueueDeferredGeneralAccess(redis.client, profile, auth, generalId, 1, now);
await expect(db.generalAccessLog.findUnique({ where: { generalId } })).resolves.toBeNull();
await worker.flushOnce();
await expect(db.generalAccessLog.findUniqueOrThrow({ where: { generalId } })).resolves.toMatchObject({
userId,
refresh: 2,
refreshTotal: 2,
refreshScore: 2,
refreshScoreTotal: 2,
});
await expect(
db.trafficPeriod.findUniqueOrThrow({
where: {
worldStateId_year_month: {
worldStateId,
year: 200,
month: 1,
},
},
})
).resolves.toMatchObject({ refresh: 2, online: 1 });
await expect(getDeferredGeneralAccessLimit(redis.client, profile, auth, now)).resolves.toEqual({
nextAccessAt,
});
await worker.flushOnce();
await expect(db.generalAccessLog.findUniqueOrThrow({ where: { generalId } })).resolves.toMatchObject({
refreshTotal: 2,
});
});
});