mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-09 21:51:29 +08:00
test: add unit tests for daily leaderboards (@fehmer) (#6802)
- **refactor existing test to use it.for** - **use testcontainers**
This commit is contained in:
parent
c4353f6371
commit
9c41fd5d04
4 changed files with 315 additions and 95 deletions
|
|
@ -0,0 +1,298 @@
|
|||
import { Mode, Mode2 } from "@monkeytype/schemas/shared";
|
||||
import * as DailyLeaderboards from "../../../src/utils/daily-leaderboards";
|
||||
import { getConnection, connect as redisSetup } from "../../../src/init/redis";
|
||||
import { Language } from "@monkeytype/schemas/languages";
|
||||
import { describeIntegration } from "..";
|
||||
import { RedisDailyLeaderboardEntry } from "@monkeytype/schemas/leaderboards";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
const dailyLeaderboardsConfig = {
|
||||
enabled: true,
|
||||
maxResults: 10,
|
||||
leaderboardExpirationTimeInDays: 1,
|
||||
validModeRules: [
|
||||
{
|
||||
language: "(english|spanish)",
|
||||
mode: "time",
|
||||
mode2: "(15|60)",
|
||||
},
|
||||
{
|
||||
language: "french",
|
||||
mode: "words",
|
||||
mode2: "\\d+",
|
||||
},
|
||||
],
|
||||
topResultsToAnnounce: 3,
|
||||
xpRewardBrackets: [],
|
||||
scheduleRewardsModeRules: [],
|
||||
};
|
||||
|
||||
describeIntegration()("Daily Leaderboards", () => {
|
||||
beforeAll(async () => {
|
||||
await redisSetup();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await getConnection()?.flushall();
|
||||
});
|
||||
describe("should properly handle valid and invalid modes", () => {
|
||||
const testCases: {
|
||||
language: Language;
|
||||
mode: Mode;
|
||||
mode2: Mode2<any>;
|
||||
expected: boolean;
|
||||
}[] = [
|
||||
{
|
||||
language: "english",
|
||||
mode: "time",
|
||||
mode2: "60",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
language: "spanish",
|
||||
mode: "time",
|
||||
mode2: "15",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
language: "english",
|
||||
mode: "time",
|
||||
mode2: "600",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
language: "spanish",
|
||||
mode: "words",
|
||||
mode2: "150",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
language: "french",
|
||||
mode: "time",
|
||||
mode2: "600",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
language: "french",
|
||||
mode: "words",
|
||||
mode2: "100",
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
it.for(testCases)(
|
||||
`language=$language, mode=$mode mode2=$mode2 expect $expected`,
|
||||
({ language, mode, mode2, expected }) => {
|
||||
const result = DailyLeaderboards.getDailyLeaderboard(
|
||||
language,
|
||||
mode,
|
||||
mode2 as any,
|
||||
dailyLeaderboardsConfig
|
||||
);
|
||||
expect(!!result).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
describe("DailyLeaderboard class", () => {
|
||||
// oxlint-disable-next-line no-non-null-assertion
|
||||
const lb = DailyLeaderboards.getDailyLeaderboard(
|
||||
"english",
|
||||
"time",
|
||||
"60",
|
||||
dailyLeaderboardsConfig
|
||||
)!;
|
||||
describe("addResult", () => {
|
||||
it("adds best result for user", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
await givenResult({ uid, wpm: 50 });
|
||||
const bestResult = await givenResult({ uid, wpm: 55 });
|
||||
await givenResult({ uid, wpm: 53 });
|
||||
|
||||
const user2 = await givenResult({ wpm: 20 });
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(
|
||||
0,
|
||||
5,
|
||||
dailyLeaderboardsConfig,
|
||||
true
|
||||
);
|
||||
//THEN
|
||||
expect(results).toEqual([
|
||||
{ rank: 1, ...bestResult },
|
||||
{ rank: 2, ...user2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("limits max amount of results", async () => {
|
||||
//GIVEN
|
||||
const maxResults = dailyLeaderboardsConfig.maxResults;
|
||||
|
||||
const bob = await givenResult({ wpm: 10 });
|
||||
await Promise.all(
|
||||
new Array(maxResults - 1)
|
||||
.fill(0)
|
||||
.map(() => givenResult({ wpm: 20 + Math.random() * 100 }))
|
||||
);
|
||||
expect(await lb.getCount()).toEqual(maxResults);
|
||||
expect(await lb.getRank(bob.uid, dailyLeaderboardsConfig)).toEqual({
|
||||
rank: maxResults,
|
||||
...bob,
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await givenResult({ wpm: 11 });
|
||||
|
||||
//THEN
|
||||
//max count is still the same, but bob is no longer on the leaderboard
|
||||
expect(await lb.getCount()).toEqual(maxResults);
|
||||
expect(await lb.getRank(bob.uid, dailyLeaderboardsConfig)).toBeNull();
|
||||
});
|
||||
});
|
||||
describe("getResults", () => {
|
||||
it("gets result", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult({ wpm: 50, isPremium: true });
|
||||
const user2 = await givenResult({ wpm: 60 });
|
||||
const user3 = await givenResult({ wpm: 40 });
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(
|
||||
0,
|
||||
5,
|
||||
dailyLeaderboardsConfig,
|
||||
true
|
||||
);
|
||||
//THEN
|
||||
expect(results).toEqual([
|
||||
{ rank: 1, ...user2 },
|
||||
{ rank: 2, ...user1 },
|
||||
{ rank: 3, ...user3 },
|
||||
]);
|
||||
});
|
||||
it("gets result for page", async () => {
|
||||
//GIVEN
|
||||
const user4 = await givenResult({ wpm: 45 });
|
||||
const _user5 = await givenResult({ wpm: 20 });
|
||||
const _user1 = await givenResult({ wpm: 50 });
|
||||
const _user2 = await givenResult({ wpm: 60 });
|
||||
const user3 = await givenResult({ wpm: 40 });
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(
|
||||
1,
|
||||
2,
|
||||
dailyLeaderboardsConfig,
|
||||
true
|
||||
);
|
||||
//THEN
|
||||
expect(results).toEqual([
|
||||
{ rank: 3, ...user4 },
|
||||
{ rank: 4, ...user3 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("gets result without premium", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult({ wpm: 50, isPremium: true });
|
||||
const user2 = await givenResult({ wpm: 60 });
|
||||
const user3 = await givenResult({ wpm: 40, isPremium: true });
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(
|
||||
0,
|
||||
5,
|
||||
dailyLeaderboardsConfig,
|
||||
false
|
||||
);
|
||||
//THEN
|
||||
expect(results).toEqual([
|
||||
{ rank: 1, ...user2, isPremium: undefined },
|
||||
{ rank: 2, ...user1, isPremium: undefined },
|
||||
{ rank: 3, ...user3, isPremium: undefined },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("minWPm", () => {
|
||||
it("gets min wpm", async () => {
|
||||
//GIVEN
|
||||
await givenResult({ wpm: 50 });
|
||||
await givenResult({ wpm: 60 });
|
||||
|
||||
//WHEN
|
||||
const minWpm = await lb.getMinWpm(dailyLeaderboardsConfig);
|
||||
//THEN
|
||||
expect(minWpm).toEqual(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRank", () => {
|
||||
it("gets rank", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult({ wpm: 50 });
|
||||
const _user2 = await givenResult({ wpm: 60 });
|
||||
|
||||
//WHEN
|
||||
const rank = await lb.getRank(user1.uid, dailyLeaderboardsConfig);
|
||||
//THEN
|
||||
expect(rank).toEqual({ rank: 2, ...user1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCount", () => {
|
||||
it("gets count", async () => {
|
||||
//GIVEN
|
||||
await givenResult({ wpm: 50 });
|
||||
await givenResult({ wpm: 60 });
|
||||
|
||||
//WHEN
|
||||
const count = await lb.getCount();
|
||||
//THEN
|
||||
expect(count).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("purgeUserFromDailyLeaderboards", async () => {
|
||||
//GIVEN
|
||||
const cheater = await givenResult({ wpm: 50 });
|
||||
await givenResult({ wpm: 60 });
|
||||
await givenResult({ wpm: 40 });
|
||||
|
||||
//WHEN
|
||||
await DailyLeaderboards.purgeUserFromDailyLeaderboards(
|
||||
cheater.uid,
|
||||
dailyLeaderboardsConfig
|
||||
);
|
||||
//THEN
|
||||
expect(await lb.getRank(cheater.uid, dailyLeaderboardsConfig)).toBeNull();
|
||||
expect(
|
||||
(await lb.getResults(0, 50, dailyLeaderboardsConfig, false)).filter(
|
||||
(it) => it.uid === cheater.uid
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
async function givenResult(
|
||||
entry: Partial<RedisDailyLeaderboardEntry>
|
||||
): Promise<RedisDailyLeaderboardEntry> {
|
||||
const uid = new ObjectId().toHexString();
|
||||
const result = {
|
||||
acc: 85,
|
||||
name: `User ${uid}`,
|
||||
raw: 100,
|
||||
wpm: 95,
|
||||
timestamp: Date.now(),
|
||||
uid: uid,
|
||||
badgeId: 2,
|
||||
consistency: 90,
|
||||
discordAvatar: `${uid}Avatar`,
|
||||
discordId: `${uid}DiscordId`,
|
||||
isPremium: false,
|
||||
...entry,
|
||||
};
|
||||
await lb.addResult(result, dailyLeaderboardsConfig);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";
|
||||
import { isIntegrationTest } from "./__integration__";
|
||||
import { getConnection } from "../src/init/redis";
|
||||
|
||||
let startedMongoContainer: StartedTestContainer | undefined;
|
||||
let startedRedisContainer: StartedTestContainer | undefined;
|
||||
|
||||
export async function setup(): Promise<void> {
|
||||
process.env.TZ = "UTC";
|
||||
|
|
@ -22,11 +24,26 @@ export async function setup(): Promise<void> {
|
|||
27017
|
||||
)}`;
|
||||
process.env["TEST_DB_URL"] = mongoUrl;
|
||||
|
||||
//use testcontainer to start redis
|
||||
const redisContainer = new GenericContainer("redis:6.2.6")
|
||||
.withExposedPorts(6379)
|
||||
.withWaitStrategy(Wait.forLogMessage("Ready to accept connections"));
|
||||
|
||||
startedRedisContainer = await redisContainer.start();
|
||||
|
||||
const redisUrl = `redis://${startedRedisContainer.getHost()}:${startedRedisContainer.getMappedPort(
|
||||
6379
|
||||
)}`;
|
||||
process.env["REDIS_URI"] = redisUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown(): Promise<void> {
|
||||
if (isIntegrationTest) {
|
||||
await startedMongoContainer?.stop();
|
||||
|
||||
await getConnection()?.quit();
|
||||
await startedRedisContainer?.stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ import { setupCommonMocks } from "./setup-common-mocks";
|
|||
|
||||
process.env["MODE"] = "dev";
|
||||
|
||||
if (!process.env["REDIS_URI"]) {
|
||||
// use mock if not set
|
||||
process.env["REDIS_URI"] = "redis://mock";
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
//don't add any configuration here, add to global-setup.ts instead.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
import { Mode } from "@monkeytype/schemas/shared";
|
||||
import { getDailyLeaderboard } from "../../src/utils/daily-leaderboards";
|
||||
|
||||
const dailyLeaderboardsConfig = {
|
||||
enabled: true,
|
||||
maxResults: 3,
|
||||
leaderboardExpirationTimeInDays: 1,
|
||||
validModeRules: [
|
||||
{
|
||||
language: "(english|spanish)",
|
||||
mode: "time",
|
||||
mode2: "(15|60)",
|
||||
},
|
||||
{
|
||||
language: "french",
|
||||
mode: "words",
|
||||
mode2: "\\d+",
|
||||
},
|
||||
],
|
||||
topResultsToAnnounce: 3,
|
||||
xpRewardBrackets: [],
|
||||
scheduleRewardsModeRules: [],
|
||||
};
|
||||
|
||||
describe("Daily Leaderboards", () => {
|
||||
it("should properly handle valid and invalid modes", () => {
|
||||
const modeCases = [
|
||||
{
|
||||
case: {
|
||||
language: "english",
|
||||
mode: "time",
|
||||
mode2: "60",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
case: {
|
||||
language: "spanish",
|
||||
mode: "time",
|
||||
mode2: "15",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
case: {
|
||||
language: "english",
|
||||
mode: "time",
|
||||
mode2: "600",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
case: {
|
||||
language: "spanish",
|
||||
mode: "words",
|
||||
mode2: "150",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
case: {
|
||||
language: "french",
|
||||
mode: "time",
|
||||
mode2: "600",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
case: {
|
||||
language: "french",
|
||||
mode: "words",
|
||||
mode2: "100",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
modeCases.forEach(({ case: { language, mode, mode2 }, expected }) => {
|
||||
const result = getDailyLeaderboard(
|
||||
language,
|
||||
mode as Mode,
|
||||
mode2,
|
||||
dailyLeaderboardsConfig
|
||||
);
|
||||
expect(!!result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Setup Redis mock and test the rest of this
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue