Skip to content

Commit 9c41fd5

Browse files
authored
test: add unit tests for daily leaderboards (@fehmer) (monkeytypegame#6802)
- **refactor existing test to use it.for** - **use testcontainers**
1 parent c4353f6 commit 9c41fd5

File tree

4 files changed

+315
-95
lines changed

4 files changed

+315
-95
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import { Mode, Mode2 } from "@monkeytype/schemas/shared";
2+
import * as DailyLeaderboards from "../../../src/utils/daily-leaderboards";
3+
import { getConnection, connect as redisSetup } from "../../../src/init/redis";
4+
import { Language } from "@monkeytype/schemas/languages";
5+
import { describeIntegration } from "..";
6+
import { RedisDailyLeaderboardEntry } from "@monkeytype/schemas/leaderboards";
7+
import { ObjectId } from "mongodb";
8+
9+
const dailyLeaderboardsConfig = {
10+
enabled: true,
11+
maxResults: 10,
12+
leaderboardExpirationTimeInDays: 1,
13+
validModeRules: [
14+
{
15+
language: "(english|spanish)",
16+
mode: "time",
17+
mode2: "(15|60)",
18+
},
19+
{
20+
language: "french",
21+
mode: "words",
22+
mode2: "\\d+",
23+
},
24+
],
25+
topResultsToAnnounce: 3,
26+
xpRewardBrackets: [],
27+
scheduleRewardsModeRules: [],
28+
};
29+
30+
describeIntegration()("Daily Leaderboards", () => {
31+
beforeAll(async () => {
32+
await redisSetup();
33+
});
34+
afterEach(async () => {
35+
await getConnection()?.flushall();
36+
});
37+
describe("should properly handle valid and invalid modes", () => {
38+
const testCases: {
39+
language: Language;
40+
mode: Mode;
41+
mode2: Mode2<any>;
42+
expected: boolean;
43+
}[] = [
44+
{
45+
language: "english",
46+
mode: "time",
47+
mode2: "60",
48+
expected: true,
49+
},
50+
{
51+
language: "spanish",
52+
mode: "time",
53+
mode2: "15",
54+
expected: true,
55+
},
56+
{
57+
language: "english",
58+
mode: "time",
59+
mode2: "600",
60+
expected: false,
61+
},
62+
{
63+
language: "spanish",
64+
mode: "words",
65+
mode2: "150",
66+
expected: false,
67+
},
68+
{
69+
language: "french",
70+
mode: "time",
71+
mode2: "600",
72+
expected: false,
73+
},
74+
{
75+
language: "french",
76+
mode: "words",
77+
mode2: "100",
78+
expected: true,
79+
},
80+
];
81+
82+
it.for(testCases)(
83+
`language=$language, mode=$mode mode2=$mode2 expect $expected`,
84+
({ language, mode, mode2, expected }) => {
85+
const result = DailyLeaderboards.getDailyLeaderboard(
86+
language,
87+
mode,
88+
mode2 as any,
89+
dailyLeaderboardsConfig
90+
);
91+
expect(!!result).toBe(expected);
92+
}
93+
);
94+
});
95+
describe("DailyLeaderboard class", () => {
96+
// oxlint-disable-next-line no-non-null-assertion
97+
const lb = DailyLeaderboards.getDailyLeaderboard(
98+
"english",
99+
"time",
100+
"60",
101+
dailyLeaderboardsConfig
102+
)!;
103+
describe("addResult", () => {
104+
it("adds best result for user", async () => {
105+
//GIVEN
106+
const uid = new ObjectId().toHexString();
107+
await givenResult({ uid, wpm: 50 });
108+
const bestResult = await givenResult({ uid, wpm: 55 });
109+
await givenResult({ uid, wpm: 53 });
110+
111+
const user2 = await givenResult({ wpm: 20 });
112+
113+
//WHEN
114+
const results = await lb.getResults(
115+
0,
116+
5,
117+
dailyLeaderboardsConfig,
118+
true
119+
);
120+
//THEN
121+
expect(results).toEqual([
122+
{ rank: 1, ...bestResult },
123+
{ rank: 2, ...user2 },
124+
]);
125+
});
126+
127+
it("limits max amount of results", async () => {
128+
//GIVEN
129+
const maxResults = dailyLeaderboardsConfig.maxResults;
130+
131+
const bob = await givenResult({ wpm: 10 });
132+
await Promise.all(
133+
new Array(maxResults - 1)
134+
.fill(0)
135+
.map(() => givenResult({ wpm: 20 + Math.random() * 100 }))
136+
);
137+
expect(await lb.getCount()).toEqual(maxResults);
138+
expect(await lb.getRank(bob.uid, dailyLeaderboardsConfig)).toEqual({
139+
rank: maxResults,
140+
...bob,
141+
});
142+
143+
//WHEN
144+
await givenResult({ wpm: 11 });
145+
146+
//THEN
147+
//max count is still the same, but bob is no longer on the leaderboard
148+
expect(await lb.getCount()).toEqual(maxResults);
149+
expect(await lb.getRank(bob.uid, dailyLeaderboardsConfig)).toBeNull();
150+
});
151+
});
152+
describe("getResults", () => {
153+
it("gets result", async () => {
154+
//GIVEN
155+
const user1 = await givenResult({ wpm: 50, isPremium: true });
156+
const user2 = await givenResult({ wpm: 60 });
157+
const user3 = await givenResult({ wpm: 40 });
158+
159+
//WHEN
160+
const results = await lb.getResults(
161+
0,
162+
5,
163+
dailyLeaderboardsConfig,
164+
true
165+
);
166+
//THEN
167+
expect(results).toEqual([
168+
{ rank: 1, ...user2 },
169+
{ rank: 2, ...user1 },
170+
{ rank: 3, ...user3 },
171+
]);
172+
});
173+
it("gets result for page", async () => {
174+
//GIVEN
175+
const user4 = await givenResult({ wpm: 45 });
176+
const _user5 = await givenResult({ wpm: 20 });
177+
const _user1 = await givenResult({ wpm: 50 });
178+
const _user2 = await givenResult({ wpm: 60 });
179+
const user3 = await givenResult({ wpm: 40 });
180+
181+
//WHEN
182+
const results = await lb.getResults(
183+
1,
184+
2,
185+
dailyLeaderboardsConfig,
186+
true
187+
);
188+
//THEN
189+
expect(results).toEqual([
190+
{ rank: 3, ...user4 },
191+
{ rank: 4, ...user3 },
192+
]);
193+
});
194+
195+
it("gets result without premium", async () => {
196+
//GIVEN
197+
const user1 = await givenResult({ wpm: 50, isPremium: true });
198+
const user2 = await givenResult({ wpm: 60 });
199+
const user3 = await givenResult({ wpm: 40, isPremium: true });
200+
201+
//WHEN
202+
const results = await lb.getResults(
203+
0,
204+
5,
205+
dailyLeaderboardsConfig,
206+
false
207+
);
208+
//THEN
209+
expect(results).toEqual([
210+
{ rank: 1, ...user2, isPremium: undefined },
211+
{ rank: 2, ...user1, isPremium: undefined },
212+
{ rank: 3, ...user3, isPremium: undefined },
213+
]);
214+
});
215+
});
216+
217+
describe("minWPm", () => {
218+
it("gets min wpm", async () => {
219+
//GIVEN
220+
await givenResult({ wpm: 50 });
221+
await givenResult({ wpm: 60 });
222+
223+
//WHEN
224+
const minWpm = await lb.getMinWpm(dailyLeaderboardsConfig);
225+
//THEN
226+
expect(minWpm).toEqual(50);
227+
});
228+
});
229+
230+
describe("getRank", () => {
231+
it("gets rank", async () => {
232+
//GIVEN
233+
const user1 = await givenResult({ wpm: 50 });
234+
const _user2 = await givenResult({ wpm: 60 });
235+
236+
//WHEN
237+
const rank = await lb.getRank(user1.uid, dailyLeaderboardsConfig);
238+
//THEN
239+
expect(rank).toEqual({ rank: 2, ...user1 });
240+
});
241+
});
242+
243+
describe("getCount", () => {
244+
it("gets count", async () => {
245+
//GIVEN
246+
await givenResult({ wpm: 50 });
247+
await givenResult({ wpm: 60 });
248+
249+
//WHEN
250+
const count = await lb.getCount();
251+
//THEN
252+
expect(count).toEqual(2);
253+
});
254+
});
255+
256+
it("purgeUserFromDailyLeaderboards", async () => {
257+
//GIVEN
258+
const cheater = await givenResult({ wpm: 50 });
259+
await givenResult({ wpm: 60 });
260+
await givenResult({ wpm: 40 });
261+
262+
//WHEN
263+
await DailyLeaderboards.purgeUserFromDailyLeaderboards(
264+
cheater.uid,
265+
dailyLeaderboardsConfig
266+
);
267+
//THEN
268+
expect(await lb.getRank(cheater.uid, dailyLeaderboardsConfig)).toBeNull();
269+
expect(
270+
(await lb.getResults(0, 50, dailyLeaderboardsConfig, false)).filter(
271+
(it) => it.uid === cheater.uid
272+
)
273+
).toEqual([]);
274+
});
275+
276+
async function givenResult(
277+
entry: Partial<RedisDailyLeaderboardEntry>
278+
): Promise<RedisDailyLeaderboardEntry> {
279+
const uid = new ObjectId().toHexString();
280+
const result = {
281+
acc: 85,
282+
name: `User ${uid}`,
283+
raw: 100,
284+
wpm: 95,
285+
timestamp: Date.now(),
286+
uid: uid,
287+
badgeId: 2,
288+
consistency: 90,
289+
discordAvatar: `${uid}Avatar`,
290+
discordId: `${uid}DiscordId`,
291+
isPremium: false,
292+
...entry,
293+
};
294+
await lb.addResult(result, dailyLeaderboardsConfig);
295+
return result;
296+
}
297+
});
298+
});

backend/__tests__/global-setup.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";
22
import { isIntegrationTest } from "./__integration__";
3+
import { getConnection } from "../src/init/redis";
34

45
let startedMongoContainer: StartedTestContainer | undefined;
6+
let startedRedisContainer: StartedTestContainer | undefined;
57

68
export async function setup(): Promise<void> {
79
process.env.TZ = "UTC";
@@ -22,11 +24,26 @@ export async function setup(): Promise<void> {
2224
27017
2325
)}`;
2426
process.env["TEST_DB_URL"] = mongoUrl;
27+
28+
//use testcontainer to start redis
29+
const redisContainer = new GenericContainer("redis:6.2.6")
30+
.withExposedPorts(6379)
31+
.withWaitStrategy(Wait.forLogMessage("Ready to accept connections"));
32+
33+
startedRedisContainer = await redisContainer.start();
34+
35+
const redisUrl = `redis://${startedRedisContainer.getHost()}:${startedRedisContainer.getMappedPort(
36+
6379
37+
)}`;
38+
process.env["REDIS_URI"] = redisUrl;
2539
}
2640
}
2741

2842
export async function teardown(): Promise<void> {
2943
if (isIntegrationTest) {
3044
await startedMongoContainer?.stop();
45+
46+
await getConnection()?.quit();
47+
await startedRedisContainer?.stop();
3148
}
3249
}

backend/__tests__/setup-tests.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ import { setupCommonMocks } from "./setup-common-mocks";
44

55
process.env["MODE"] = "dev";
66

7-
if (!process.env["REDIS_URI"]) {
8-
// use mock if not set
9-
process.env["REDIS_URI"] = "redis://mock";
10-
}
11-
127
beforeAll(async () => {
138
//don't add any configuration here, add to global-setup.ts instead.
149

0 commit comments

Comments
 (0)