Track traffic by game month
This commit is contained in:
@@ -6,30 +6,54 @@ import { upsertGeneralAccess } from '../src/services/generalAccess.js';
|
||||
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
|
||||
const integration = describe.skipIf(!databaseUrl);
|
||||
const generalId = 9_980_071;
|
||||
const secondGeneralId = generalId + 1;
|
||||
const rollbackGeneralId = generalId + 2;
|
||||
const scenarioCode = `traffic-period-${generalId}`;
|
||||
|
||||
integration('general access tracking persistence', () => {
|
||||
let db: GamePrismaClient;
|
||||
let closeDb: (() => Promise<void>) | undefined;
|
||||
let worldStateId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const connector = createGamePostgresConnector({ url: databaseUrl! });
|
||||
await connector.connect();
|
||||
db = connector.prisma;
|
||||
closeDb = () => connector.disconnect();
|
||||
await db.generalAccessLog.deleteMany({ where: { generalId } });
|
||||
await db.generalAccessLog.deleteMany({
|
||||
where: { generalId: { in: [generalId, secondGeneralId, rollbackGeneralId] } },
|
||||
});
|
||||
await db.worldState.deleteMany({ where: { scenarioCode } });
|
||||
const world = await db.worldState.create({
|
||||
data: {
|
||||
scenarioCode,
|
||||
currentYear: 185,
|
||||
currentMonth: 3,
|
||||
tickSeconds: 600,
|
||||
config: {},
|
||||
meta: {},
|
||||
},
|
||||
});
|
||||
worldStateId = world.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.generalAccessLog.deleteMany({ where: { generalId } });
|
||||
await db.generalAccessLog.deleteMany({
|
||||
where: { generalId: { in: [generalId, secondGeneralId, rollbackGeneralId] } },
|
||||
});
|
||||
await db.worldState.deleteMany({ where: { id: worldStateId } });
|
||||
await closeDb?.();
|
||||
});
|
||||
|
||||
it('atomically increments concurrent requests and resets only windowed counters', async () => {
|
||||
it('atomically increments one game-month bucket and opens a new bucket at the next month', async () => {
|
||||
const firstWindow = {
|
||||
worldStateId,
|
||||
year: 185,
|
||||
month: 3,
|
||||
generalId,
|
||||
userId: 'access-user-a',
|
||||
now: new Date('2026-07-26T03:05:00.000Z'),
|
||||
dayStartedAt: new Date('2026-07-26T00:00:00.000Z'),
|
||||
periodStartedAt: new Date('2026-07-26T03:00:00.000Z'),
|
||||
scoreStartedAt: new Date('2026-07-26T03:00:00.000Z'),
|
||||
};
|
||||
await upsertGeneralAccess(db, { ...firstWindow, weight: 2 });
|
||||
@@ -50,13 +74,37 @@ integration('general access tracking persistence', () => {
|
||||
refreshScore: 22,
|
||||
refreshScoreTotal: 22,
|
||||
});
|
||||
const firstPeriod = await db.trafficPeriod.findUniqueOrThrow({
|
||||
where: {
|
||||
worldStateId_year_month: {
|
||||
worldStateId,
|
||||
year: 185,
|
||||
month: 3,
|
||||
},
|
||||
},
|
||||
include: { generals: { orderBy: { generalId: 'asc' } } },
|
||||
});
|
||||
expect(firstPeriod).toMatchObject({
|
||||
refresh: 22,
|
||||
online: 1,
|
||||
generals: [
|
||||
{
|
||||
generalId,
|
||||
userId: 'access-user-a',
|
||||
refresh: 22,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await upsertGeneralAccess(db, {
|
||||
worldStateId,
|
||||
year: 185,
|
||||
month: 4,
|
||||
generalId,
|
||||
userId: 'access-user-b',
|
||||
now: new Date('2026-07-27T00:05:00.000Z'),
|
||||
dayStartedAt: new Date('2026-07-27T00:00:00.000Z'),
|
||||
scoreStartedAt: new Date('2026-07-27T00:00:00.000Z'),
|
||||
now: new Date('2026-07-26T03:15:00.000Z'),
|
||||
periodStartedAt: new Date('2026-07-26T03:10:00.000Z'),
|
||||
scoreStartedAt: new Date('2026-07-26T03:10:00.000Z'),
|
||||
weight: 1,
|
||||
});
|
||||
|
||||
@@ -67,5 +115,89 @@ integration('general access tracking persistence', () => {
|
||||
refreshScore: 1,
|
||||
refreshScoreTotal: 23,
|
||||
});
|
||||
|
||||
await upsertGeneralAccess(db, {
|
||||
worldStateId,
|
||||
year: 185,
|
||||
month: 4,
|
||||
generalId: secondGeneralId,
|
||||
userId: 'access-user-c',
|
||||
now: new Date('2026-07-26T03:15:01.000Z'),
|
||||
periodStartedAt: new Date('2026-07-26T03:10:00.000Z'),
|
||||
scoreStartedAt: new Date('2026-07-26T03:10:00.000Z'),
|
||||
weight: 1,
|
||||
});
|
||||
await expect(
|
||||
db.trafficPeriod.findUniqueOrThrow({
|
||||
where: {
|
||||
worldStateId_year_month: {
|
||||
worldStateId,
|
||||
year: 185,
|
||||
month: 4,
|
||||
},
|
||||
},
|
||||
include: { generals: { orderBy: { generalId: 'asc' } } },
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
refresh: 2,
|
||||
online: 2,
|
||||
generals: [
|
||||
{
|
||||
generalId,
|
||||
userId: 'access-user-b',
|
||||
refresh: 1,
|
||||
},
|
||||
{
|
||||
generalId: secondGeneralId,
|
||||
userId: 'access-user-c',
|
||||
refresh: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('rolls back the period and member rows when the legacy access update fails', async () => {
|
||||
await db.generalAccessLog.create({
|
||||
data: {
|
||||
generalId: rollbackGeneralId,
|
||||
userId: 'rollback-user',
|
||||
lastRefresh: new Date('2026-07-26T03:19:00.000Z'),
|
||||
refresh: 1,
|
||||
refreshTotal: 2_147_483_647,
|
||||
refreshScore: 1,
|
||||
refreshScoreTotal: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
upsertGeneralAccess(db, {
|
||||
worldStateId,
|
||||
year: 186,
|
||||
month: 1,
|
||||
generalId: rollbackGeneralId,
|
||||
userId: 'rollback-user',
|
||||
now: new Date('2026-07-26T03:20:00.000Z'),
|
||||
periodStartedAt: new Date('2026-07-26T03:20:00.000Z'),
|
||||
scoreStartedAt: new Date('2026-07-26T03:20:00.000Z'),
|
||||
weight: 1,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
|
||||
await expect(
|
||||
db.trafficPeriod.findUnique({
|
||||
where: {
|
||||
worldStateId_year_month: {
|
||||
worldStateId,
|
||||
year: 186,
|
||||
month: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
db.generalAccessLog.findUniqueOrThrow({ where: { generalId: rollbackGeneralId } })
|
||||
).resolves.toMatchObject({
|
||||
refreshTotal: 2_147_483_647,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user