Skip to content

Commit 53158f9

Browse files
committed
refactor: move src/lib types to src/bot/api/types, add missing tests
Signed-off-by: Michael Cramer <michael@bigmichi1.de>
1 parent 86fa59c commit 53158f9

12 files changed

Lines changed: 609 additions & 11402 deletions

Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ RUN bin/mise run install
3737
# Copy TypeScript source files
3838
COPY tsconfig.bot.json ./
3939
COPY src/bot ./src/bot
40-
COPY src/lib ./src/lib
4140

4241
# Build the production bundle
4342
RUN bin/mise run prod:build

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default [
1313
'**/*.js',
1414
'bun.lock',
1515
'package-lock.json',
16-
'src/lib/**',
16+
'src/bot/api/types/**',
1717
],
1818
},
1919
{

src/bot/api/idleChampionsApi.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
/// <reference path="../../lib/player_data.d.ts" />
2-
/// <reference path="../../lib/redeem_code_response.d.ts" />
3-
/// <reference path="../../lib/server_definitions.d.ts" />
4-
/// <reference path="../../lib/blacksmith_response.d.ts" />
1+
/// <reference path="./types/player_data.d.ts" />
2+
/// <reference path="./types/redeem_code_response.d.ts" />
3+
/// <reference path="./types/server_definitions.d.ts" />
4+
/// <reference path="./types/blacksmith_response.d.ts" />
55

66
import logger from '../utils/logger';
77
import { apiRequestLogger } from '../utils/apiRequestLogger';
File renamed without changes.
File renamed without changes.
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import { describe, test, expect, beforeAll, beforeEach, afterAll } from 'bun:test';
2+
import { db, initializeDatabase } from './db';
3+
import { backfillOperations } from './schema/index';
4+
import { backfillManager } from './backfillManager';
5+
6+
const USER_A = 'discord-backfill-a';
7+
const USER_B = 'discord-backfill-b';
8+
9+
beforeAll(() => {
10+
initializeDatabase();
11+
});
12+
13+
beforeEach(() => {
14+
db.delete(backfillOperations).run();
15+
// Reset the in-memory lock by force if any test left it set.
16+
// We do this by exploiting the fact that updateBackfill resets backfillInProgress.
17+
// If the flag is stuck, insert a dummy row and complete it.
18+
if (backfillManager.isBackfillInProgress()) {
19+
const row = db
20+
.insert(backfillOperations)
21+
.values({ initiatedBy: '__reset__', status: 'in_progress' })
22+
.returning({ id: backfillOperations.id })
23+
.get();
24+
if (row) {
25+
backfillManager.updateBackfill(row.id, 0, 0, 'completed');
26+
}
27+
db.delete(backfillOperations).run();
28+
}
29+
});
30+
31+
afterAll(() => {
32+
db.delete(backfillOperations).run();
33+
});
34+
35+
// ---------------------------------------------------------------------------
36+
// isBackfillInProgress
37+
// ---------------------------------------------------------------------------
38+
describe('isBackfillInProgress', () => {
39+
test('returns false when no backfill is running', () => {
40+
expect(backfillManager.isBackfillInProgress()).toBe(false);
41+
});
42+
43+
test('returns true after startBackfill is called', async () => {
44+
await backfillManager.startBackfill(USER_A);
45+
expect(backfillManager.isBackfillInProgress()).toBe(true);
46+
// Clean up the flag
47+
const rows = db.select().from(backfillOperations).all();
48+
await backfillManager.updateBackfill(rows[0]!.id, 0, 0, 'completed');
49+
});
50+
51+
test('returns false after updateBackfill completes', async () => {
52+
const id = await backfillManager.startBackfill(USER_A);
53+
await backfillManager.updateBackfill(id, 5, 3, 'completed');
54+
expect(backfillManager.isBackfillInProgress()).toBe(false);
55+
});
56+
});
57+
58+
// ---------------------------------------------------------------------------
59+
// startBackfill
60+
// ---------------------------------------------------------------------------
61+
describe('startBackfill', () => {
62+
test('inserts an in_progress row and returns its id', async () => {
63+
const id = await backfillManager.startBackfill(USER_A);
64+
expect(id).toBeGreaterThan(0);
65+
const rows = db.select().from(backfillOperations).all();
66+
expect(rows).toHaveLength(1);
67+
expect(rows[0]!.initiatedBy).toBe(USER_A);
68+
expect(rows[0]!.status).toBe('in_progress');
69+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
70+
});
71+
72+
test('throws when a backfill is already in progress', async () => {
73+
const id = await backfillManager.startBackfill(USER_A);
74+
await expect(backfillManager.startBackfill(USER_B)).rejects.toThrow(
75+
'A backfill operation is already in progress'
76+
);
77+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
78+
});
79+
});
80+
81+
// ---------------------------------------------------------------------------
82+
// updateBackfill
83+
// ---------------------------------------------------------------------------
84+
describe('updateBackfill', () => {
85+
test('updates row with codesFound and codesRedeemed', async () => {
86+
const id = await backfillManager.startBackfill(USER_A);
87+
await backfillManager.updateBackfill(id, 10, 7, 'completed');
88+
const row = db.select().from(backfillOperations).get();
89+
expect(row?.codesFound).toBe(10);
90+
expect(row?.codesRedeemed).toBe(7);
91+
expect(row?.status).toBe('completed');
92+
});
93+
94+
test('marks status as failed', async () => {
95+
const id = await backfillManager.startBackfill(USER_A);
96+
await backfillManager.updateBackfill(id, 0, 0, 'failed');
97+
const row = db.select().from(backfillOperations).get();
98+
expect(row?.status).toBe('failed');
99+
});
100+
101+
test('resets backfillInProgress flag', async () => {
102+
const id = await backfillManager.startBackfill(USER_A);
103+
expect(backfillManager.isBackfillInProgress()).toBe(true);
104+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
105+
expect(backfillManager.isBackfillInProgress()).toBe(false);
106+
});
107+
});
108+
109+
// ---------------------------------------------------------------------------
110+
// getBackfillById
111+
// ---------------------------------------------------------------------------
112+
describe('getBackfillById', () => {
113+
test('returns undefined for non-existent id', async () => {
114+
const result = await backfillManager.getBackfillById(9999);
115+
expect(result).toBeUndefined();
116+
});
117+
118+
test('returns the row with the matching id', async () => {
119+
const id = await backfillManager.startBackfill(USER_A);
120+
await backfillManager.updateBackfill(id, 3, 2, 'completed');
121+
const row = await backfillManager.getBackfillById(id);
122+
expect(row).toBeDefined();
123+
expect(row!.id).toBe(id);
124+
expect(row!.initiatedBy).toBe(USER_A);
125+
expect(row!.codesFound).toBe(3);
126+
});
127+
});
128+
129+
// ---------------------------------------------------------------------------
130+
// getLastBackfill
131+
// ---------------------------------------------------------------------------
132+
describe('getLastBackfill', () => {
133+
test('returns undefined when no completed backfills exist', async () => {
134+
expect(await backfillManager.getLastBackfill()).toBeUndefined();
135+
});
136+
137+
test('returns undefined when only in_progress operations exist', async () => {
138+
const id = await backfillManager.startBackfill(USER_A);
139+
expect(await backfillManager.getLastBackfill()).toBeUndefined();
140+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
141+
});
142+
143+
test('returns a completed backfill when one exists', async () => {
144+
const id = await backfillManager.startBackfill(USER_A);
145+
await backfillManager.updateBackfill(id, 5, 3, 'completed');
146+
const last = await backfillManager.getLastBackfill();
147+
expect(last).toBeDefined();
148+
expect(last!.status).toBe('completed');
149+
expect(last!.codesFound).toBe(5);
150+
});
151+
});
152+
153+
// ---------------------------------------------------------------------------
154+
// canUserInitiateBackfill
155+
// ---------------------------------------------------------------------------
156+
describe('canUserInitiateBackfill', () => {
157+
test('returns true when user has no previous backfills', async () => {
158+
expect(await backfillManager.canUserInitiateBackfill(USER_A)).toBe(true);
159+
});
160+
161+
test('returns false immediately after a completed backfill (within 1-hour window)', async () => {
162+
const id = await backfillManager.startBackfill(USER_A);
163+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
164+
// completedAt is CURRENT_TIMESTAMP — definitely within 1 hour
165+
expect(await backfillManager.canUserInitiateBackfill(USER_A)).toBe(false);
166+
});
167+
168+
test('returns true for a different user even if USER_A is within cooldown', async () => {
169+
const id = await backfillManager.startBackfill(USER_A);
170+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
171+
expect(await backfillManager.canUserInitiateBackfill(USER_B)).toBe(true);
172+
});
173+
174+
test('returns true when user only has in_progress or failed operations', async () => {
175+
const id = await backfillManager.startBackfill(USER_A);
176+
await backfillManager.updateBackfill(id, 0, 0, 'failed');
177+
expect(await backfillManager.canUserInitiateBackfill(USER_A)).toBe(true);
178+
});
179+
});
180+
181+
// ---------------------------------------------------------------------------
182+
// shouldRunStartupBackfill
183+
// ---------------------------------------------------------------------------
184+
describe('shouldRunStartupBackfill', () => {
185+
test('returns true when no backfills have ever run', async () => {
186+
expect(await backfillManager.shouldRunStartupBackfill()).toBe(true);
187+
});
188+
189+
test('returns false immediately after a recent completed backfill (within 6-hour window)', async () => {
190+
const id = await backfillManager.startBackfill(USER_A);
191+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
192+
expect(await backfillManager.shouldRunStartupBackfill()).toBe(false);
193+
});
194+
});
195+
196+
// ---------------------------------------------------------------------------
197+
// hasUserBackfillOperations
198+
// ---------------------------------------------------------------------------
199+
describe('hasUserBackfillOperations', () => {
200+
test('returns false when user has no backfill operations', async () => {
201+
expect(await backfillManager.hasUserBackfillOperations(USER_A)).toBe(false);
202+
});
203+
204+
test('returns true when user has at least one operation', async () => {
205+
const id = await backfillManager.startBackfill(USER_A);
206+
expect(await backfillManager.hasUserBackfillOperations(USER_A)).toBe(true);
207+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
208+
});
209+
210+
test('returns false for a different user', async () => {
211+
const id = await backfillManager.startBackfill(USER_A);
212+
expect(await backfillManager.hasUserBackfillOperations(USER_B)).toBe(false);
213+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
214+
});
215+
});
216+
217+
// ---------------------------------------------------------------------------
218+
// hasUserActiveBackfill
219+
// ---------------------------------------------------------------------------
220+
describe('hasUserActiveBackfill', () => {
221+
test('returns false when no active backfill exists for user', async () => {
222+
expect(await backfillManager.hasUserActiveBackfill(USER_A)).toBe(false);
223+
});
224+
225+
test('returns true when user has an in_progress backfill', async () => {
226+
const id = await backfillManager.startBackfill(USER_A);
227+
expect(await backfillManager.hasUserActiveBackfill(USER_A)).toBe(true);
228+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
229+
});
230+
231+
test('returns false after the backfill completes', async () => {
232+
const id = await backfillManager.startBackfill(USER_A);
233+
await backfillManager.updateBackfill(id, 0, 0, 'completed');
234+
expect(await backfillManager.hasUserActiveBackfill(USER_A)).toBe(false);
235+
});
236+
});
237+
238+
// ---------------------------------------------------------------------------
239+
// deleteUserBackfillOperations
240+
// ---------------------------------------------------------------------------
241+
describe('deleteUserBackfillOperations', () => {
242+
test('returns 0 when user has no operations', async () => {
243+
const count = await backfillManager.deleteUserBackfillOperations(USER_A);
244+
expect(count).toBe(0);
245+
});
246+
247+
test('deletes all operations for the user and returns the count', async () => {
248+
const id1 = await backfillManager.startBackfill(USER_A);
249+
await backfillManager.updateBackfill(id1, 1, 1, 'completed');
250+
const id2 = await backfillManager.startBackfill(USER_A);
251+
await backfillManager.updateBackfill(id2, 2, 2, 'completed');
252+
const count = await backfillManager.deleteUserBackfillOperations(USER_A);
253+
expect(count).toBe(2);
254+
expect(db.select().from(backfillOperations).all()).toHaveLength(0);
255+
});
256+
257+
test('does not delete operations belonging to another user', async () => {
258+
const idA = await backfillManager.startBackfill(USER_A);
259+
await backfillManager.updateBackfill(idA, 0, 0, 'completed');
260+
const idB = await backfillManager.startBackfill(USER_B);
261+
await backfillManager.updateBackfill(idB, 0, 0, 'completed');
262+
await backfillManager.deleteUserBackfillOperations(USER_A);
263+
const remaining = db.select().from(backfillOperations).all();
264+
expect(remaining).toHaveLength(1);
265+
expect(remaining[0]!.initiatedBy).toBe(USER_B);
266+
});
267+
});

src/bot/database/codeManager.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,86 @@ describe('getRedeemedCodesByUsers', () => {
504504
expect(result.get(USER_A)?.has('CODE2222BBBB')).toBe(true);
505505
});
506506
});
507+
508+
// ---------------------------------------------------------------------------
509+
// getRedeemedCodes (code-only list, up to 100)
510+
// ---------------------------------------------------------------------------
511+
describe('getRedeemedCodes', () => {
512+
test('returns empty array when user has no codes', async () => {
513+
expect(await codeManager.getRedeemedCodes(USER_A)).toEqual([]);
514+
});
515+
516+
test('returns codes for the specified user', async () => {
517+
await codeManager.addRedeemedCode('CODE1111AAAA', USER_A, 'Success');
518+
await codeManager.addRedeemedCode('CODE2222BBBB', USER_A, 'Code Expired');
519+
const codes = await codeManager.getRedeemedCodes(USER_A);
520+
expect(codes).toHaveLength(2);
521+
expect(codes).toContain('CODE1111AAAA');
522+
expect(codes).toContain('CODE2222BBBB');
523+
});
524+
525+
test('does not return codes belonging to another user', async () => {
526+
await codeManager.addRedeemedCode('CODE1111AAAA', USER_A, 'Success');
527+
await codeManager.addRedeemedCode('CODE2222BBBB', USER_B, 'Success');
528+
expect(await codeManager.getRedeemedCodes(USER_A)).toEqual(['CODE1111AAAA']);
529+
});
530+
});
531+
532+
// ---------------------------------------------------------------------------
533+
// getSuccessfulRedeemCount
534+
// ---------------------------------------------------------------------------
535+
describe('getSuccessfulRedeemCount', () => {
536+
test('returns 0 when no one has redeemed the code', async () => {
537+
expect(await codeManager.getSuccessfulRedeemCount('UNKNOWN1ABCD')).toBe(0);
538+
});
539+
540+
test('returns 1 when one user has a Success row', async () => {
541+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Success');
542+
expect(await codeManager.getSuccessfulRedeemCount('CODE1234ABCD')).toBe(1);
543+
});
544+
545+
test('returns 2 when two users have Success rows for the same code', async () => {
546+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Success');
547+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_B, 'Success');
548+
expect(await codeManager.getSuccessfulRedeemCount('CODE1234ABCD')).toBe(2);
549+
});
550+
551+
test('does not count Code Expired rows', async () => {
552+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Code Expired');
553+
expect(await codeManager.getSuccessfulRedeemCount('CODE1234ABCD')).toBe(0);
554+
});
555+
});
556+
557+
// ---------------------------------------------------------------------------
558+
// getPublicUnexpiredCodes
559+
// ---------------------------------------------------------------------------
560+
describe('getPublicUnexpiredCodes', () => {
561+
test('returns empty array when no public codes exist', async () => {
562+
expect(await codeManager.getPublicUnexpiredCodes()).toEqual([]);
563+
});
564+
565+
test('returns public Success codes', async () => {
566+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Success', undefined, true);
567+
const rows = await codeManager.getPublicUnexpiredCodes();
568+
expect(rows).toHaveLength(1);
569+
expect(rows[0]!.code).toBe('CODE1234ABCD');
570+
});
571+
572+
test('does not return private codes', async () => {
573+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Success', undefined, false);
574+
expect(await codeManager.getPublicUnexpiredCodes()).toEqual([]);
575+
});
576+
577+
test('does not return expired public codes', async () => {
578+
await codeManager.addRedeemedCode('CODE1234ABCD', USER_A, 'Success', undefined, true);
579+
await codeManager.markCodeAsExpired('CODE1234ABCD');
580+
expect(await codeManager.getPublicUnexpiredCodes()).toEqual([]);
581+
});
582+
583+
test('returns multiple public unexpired codes', async () => {
584+
await codeManager.addRedeemedCode('CODE1111AAAA', USER_A, 'Success', undefined, true);
585+
await codeManager.addRedeemedCode('CODE2222BBBB', USER_B, 'Success', undefined, true);
586+
const rows = await codeManager.getPublicUnexpiredCodes();
587+
expect(rows).toHaveLength(2);
588+
});
589+
});

0 commit comments

Comments
 (0)