Files
core2026/app/game-api/test/generalAccessTracking.test.ts
T
Hide_D 4d7223b7a4 Merge branch 'main' into feature/nation-general-lists
# Conflicts:
#	app/game-frontend/package.json
#	app/game-frontend/src/router/index.ts
2026-07-26 06:32:54 +00:00

106 lines
4.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import type { DatabaseClient } from '../src/context.js';
import { accessPageWeights, recordGeneralAccess, resolveAccessWindows } from '../src/services/generalAccess.js';
const auth = (roles = ['user']): GameSessionTokenPayload => ({
version: 1,
profile: 'che:default',
issuedAt: '2026-07-26T00:00:00.000Z',
expiresAt: '2026-07-27T00:00:00.000Z',
sessionId: 'access-session',
user: {
id: 'user-7',
username: 'user7',
displayName: '사용자7',
roles,
},
sanctions: {},
});
const buildDb = (meta: Record<string, unknown> = {}) => {
const executeRaw = vi.fn(async (_query: unknown) => 1);
const findGeneral = vi.fn(async () => ({ id: 7, userId: 'user-7' }));
const findWorld = vi.fn(async () => ({
tickSeconds: 600,
meta: {
opentime: '2026-07-25T00:00:00.000Z',
lastTurnTime: '2026-07-26T03:00:00.000Z',
...meta,
},
}));
const db = {
$executeRaw: executeRaw,
general: { findFirst: findGeneral },
worldState: { findFirst: findWorld },
} as unknown as DatabaseClient;
return { db, executeRaw, findGeneral, findWorld };
};
describe('general access tracking', () => {
it('uses the legacy weight two for both global directory pages', () => {
expect(accessPageWeights['nation-list']).toBe(2);
expect(accessPageWeights['general-list']).toBe(2);
});
it('resolves the UTC day and latest processed turn windows', () => {
expect(
resolveAccessWindows(new Date('2026-07-26T03:14:15.000Z'), 600, {
lastTurnTime: '2026-07-26T03:10:00.000Z',
})
).toEqual({
dayStartedAt: new Date('2026-07-26T00:00:00.000Z'),
scoreStartedAt: new Date('2026-07-26T03:10:00.000Z'),
});
});
it('uses the session user actor and the legacy page weight in one atomic upsert', async () => {
const { db, executeRaw, findGeneral } = buildDb();
const now = new Date('2026-07-26T03:05:00.000Z');
await expect(recordGeneralAccess({ auth: auth(), db }, 'npc-list', now)).resolves.toBe(true);
expect(findGeneral).toHaveBeenCalledWith({
where: { userId: 'user-7' },
orderBy: { id: 'asc' },
select: { id: true, userId: true },
});
expect(executeRaw).toHaveBeenCalledTimes(1);
const statement = executeRaw.mock.calls[0]![0] as { sql: string; values: unknown[] };
expect(statement.sql).toContain('ON CONFLICT (general_id) DO UPDATE');
expect(statement.sql).toContain('general_access_log.refresh + EXCLUDED.refresh');
expect(statement.values).toEqual([
7,
'user-7',
now,
2,
2,
2,
2,
new Date('2026-07-26T00:00:00.000Z'),
new Date('2026-07-26T03:00:00.000Z'),
]);
});
it('does not write for anonymous/admin users, a future opening, or a finished world', async () => {
const anonymous = buildDb();
await expect(recordGeneralAccess({ auth: null, db: anonymous.db }, 'traffic')).resolves.toBe(false);
expect(anonymous.findGeneral).not.toHaveBeenCalled();
const admin = buildDb();
await expect(recordGeneralAccess({ auth: auth(['admin']), db: admin.db }, 'traffic')).resolves.toBe(false);
expect(admin.findGeneral).not.toHaveBeenCalled();
const future = buildDb({ opentime: '2026-07-27T00:00:00.000Z' });
await expect(
recordGeneralAccess({ auth: auth(), db: future.db }, 'traffic', new Date('2026-07-26T03:05:00.000Z'))
).resolves.toBe(false);
expect(future.executeRaw).not.toHaveBeenCalled();
const united = buildDb({ isUnited: 2 });
await expect(recordGeneralAccess({ auth: auth(), db: united.db }, 'traffic')).resolves.toBe(false);
expect(united.executeRaw).not.toHaveBeenCalled();
});
});