This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import * as Migration from "../../../__migration__/testActivity";
|
||||
import * as UserTestData from "../../__testData__/users";
|
||||
import * as UserDal from "../../../src/dal/user";
|
||||
import * as ResultDal from "../../../src/dal/result";
|
||||
import { DBResult } from "../../../src/utils/result";
|
||||
|
||||
describe("testActivity migration", () => {
|
||||
it("migrates users without results", async () => {
|
||||
//given
|
||||
const user1 = await UserTestData.createUser();
|
||||
const user2 = await UserTestData.createUser();
|
||||
|
||||
//when
|
||||
await Migration.migrate();
|
||||
|
||||
//then
|
||||
const readUser1 = await UserDal.getUser(user1.uid, "");
|
||||
expect(readUser1.testActivity).toEqual({});
|
||||
|
||||
const readUser2 = await UserDal.getUser(user2.uid, "");
|
||||
expect(readUser2.testActivity).toEqual({});
|
||||
});
|
||||
|
||||
it("migrates users with results", async () => {
|
||||
//given
|
||||
const withResults = await UserTestData.createUserWithoutMigration();
|
||||
const withoutResults = await UserTestData.createUserWithoutMigration();
|
||||
|
||||
const uid = withResults.uid;
|
||||
|
||||
//2023-01-02
|
||||
await createResult(uid, 1672621200000);
|
||||
|
||||
//2024-01-01
|
||||
await createResult(uid, 1704070800000);
|
||||
await createResult(uid, 1704070800000 + 3600000);
|
||||
await createResult(uid, 1704070800000 + 3600000);
|
||||
|
||||
//2024-01-02
|
||||
await createResult(uid, 1704157200000);
|
||||
//2024-01-03
|
||||
await createResult(uid, 1704243600000);
|
||||
|
||||
//when
|
||||
await Migration.migrate();
|
||||
|
||||
//then
|
||||
const readWithResults = await UserDal.getUser(withResults.uid, "");
|
||||
expect(readWithResults.testActivity).toEqual({
|
||||
"2023": [null, 1],
|
||||
"2024": [3, 1, 1],
|
||||
});
|
||||
|
||||
const readWithoutResults = await UserDal.getUser(withoutResults.uid, "");
|
||||
expect(readWithoutResults.testActivity).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
async function createResult(uid: string, timestamp: number): Promise<void> {
|
||||
await ResultDal.addResult(uid, {
|
||||
wpm: 0,
|
||||
rawWpm: 0,
|
||||
charStats: [1, 2, 3, 4],
|
||||
acc: 0,
|
||||
mode: "time",
|
||||
mode2: "60",
|
||||
timestamp: timestamp,
|
||||
testDuration: 1,
|
||||
consistency: 0,
|
||||
keyConsistency: 0,
|
||||
chartData: "toolong",
|
||||
name: "",
|
||||
} as unknown as DBResult);
|
||||
}
|
||||
30
backend/__tests__/__integration__/dal/admin-uids.spec.ts
Normal file
30
backend/__tests__/__integration__/dal/admin-uids.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as AdminUidsDal from "../../../src/dal/admin-uids";
|
||||
|
||||
describe("AdminUidsDal", () => {
|
||||
describe("isAdmin", () => {
|
||||
it("should return true for existing admin user", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
await AdminUidsDal.getCollection().insertOne({
|
||||
_id: new ObjectId(),
|
||||
uid: uid,
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
expect(await AdminUidsDal.isAdmin(uid)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-existing admin user", async () => {
|
||||
//GIVEN
|
||||
await AdminUidsDal.getCollection().insertOne({
|
||||
_id: new ObjectId(),
|
||||
uid: "admin",
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
expect(await AdminUidsDal.isAdmin("regularUser")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
107
backend/__tests__/__integration__/dal/ape-keys.spec.ts
Normal file
107
backend/__tests__/__integration__/dal/ape-keys.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ObjectId } from "mongodb";
|
||||
import {
|
||||
addApeKey,
|
||||
DBApeKey,
|
||||
editApeKey,
|
||||
getApeKey,
|
||||
updateLastUsedOn,
|
||||
} from "../../../src/dal/ape-keys";
|
||||
|
||||
describe("ApeKeysDal", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
describe("addApeKey", () => {
|
||||
it("should be able to add a new ape key", async () => {
|
||||
const apeKey = buildApeKey();
|
||||
|
||||
const apeKeyId = await addApeKey(apeKey);
|
||||
|
||||
expect(apeKeyId).toBe(apeKey._id.toHexString());
|
||||
|
||||
const read = await getApeKey(apeKeyId);
|
||||
expect(read).toEqual({
|
||||
...apeKey,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("editApeKey", () => {
|
||||
it("should edit name of an existing ape key", async () => {
|
||||
//GIVEN
|
||||
const apeKey = buildApeKey({ useCount: 5, enabled: true });
|
||||
const apeKeyId = await addApeKey(apeKey);
|
||||
|
||||
//WHEN
|
||||
const newName = "new name";
|
||||
await editApeKey(apeKey.uid, apeKeyId, newName, undefined);
|
||||
|
||||
//THENa
|
||||
const readAfterEdit = (await getApeKey(apeKeyId)) as DBApeKey;
|
||||
expect(readAfterEdit).toEqual({
|
||||
...apeKey,
|
||||
name: newName,
|
||||
modifiedOn: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
it("should edit enabled of an existing ape key", async () => {
|
||||
//GIVEN
|
||||
const apeKey = buildApeKey({ useCount: 5, enabled: true });
|
||||
const apeKeyId = await addApeKey(apeKey);
|
||||
|
||||
//WHEN
|
||||
|
||||
await editApeKey(apeKey.uid, apeKeyId, undefined, false);
|
||||
|
||||
//THEN
|
||||
const readAfterEdit = (await getApeKey(apeKeyId)) as DBApeKey;
|
||||
expect(readAfterEdit).toEqual({
|
||||
...apeKey,
|
||||
enabled: false,
|
||||
modifiedOn: Date.now(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateLastUsedOn", () => {
|
||||
it("should update lastUsedOn and increment useCount when editing with lastUsedOn", async () => {
|
||||
//GIVEN
|
||||
const apeKey = buildApeKey({
|
||||
useCount: 5,
|
||||
lastUsedOn: 42,
|
||||
});
|
||||
const apeKeyId = await addApeKey(apeKey);
|
||||
|
||||
//WHEN
|
||||
await updateLastUsedOn(apeKey.uid, apeKeyId);
|
||||
await updateLastUsedOn(apeKey.uid, apeKeyId);
|
||||
|
||||
//THENa
|
||||
const readAfterEdit = (await getApeKey(apeKeyId)) as DBApeKey;
|
||||
expect(readAfterEdit).toEqual({
|
||||
...apeKey,
|
||||
modifiedOn: readAfterEdit.modifiedOn,
|
||||
lastUsedOn: Date.now(),
|
||||
useCount: 5 + 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildApeKey(overrides: Partial<DBApeKey> = {}): DBApeKey {
|
||||
return {
|
||||
_id: new ObjectId(),
|
||||
uid: "123",
|
||||
name: "test",
|
||||
hash: "12345",
|
||||
createdOn: Date.now(),
|
||||
modifiedOn: Date.now(),
|
||||
lastUsedOn: Date.now(),
|
||||
useCount: 0,
|
||||
enabled: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
363
backend/__tests__/__integration__/dal/blocklist.spec.ts
Normal file
363
backend/__tests__/__integration__/dal/blocklist.spec.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as BlacklistDal from "../../../src/dal/blocklist";
|
||||
|
||||
describe("BlocklistDal", () => {
|
||||
beforeAll(async () => {
|
||||
await BlacklistDal.createIndicies();
|
||||
});
|
||||
describe("add", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it("adds user", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name, email });
|
||||
|
||||
//THEN
|
||||
await expect(
|
||||
BlacklistDal.getCollection().findOne({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
await expect(
|
||||
BlacklistDal.getCollection().findOne({
|
||||
usernameHash: BlacklistDal.hash(name),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
usernameHash: BlacklistDal.hash(name),
|
||||
timestamp: now,
|
||||
});
|
||||
});
|
||||
it("adds user with discordId", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
|
||||
//THEN
|
||||
await expect(
|
||||
BlacklistDal.getCollection().findOne({
|
||||
discordIdHash: BlacklistDal.hash(discordId),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
discordIdHash: BlacklistDal.hash(discordId),
|
||||
timestamp: now,
|
||||
});
|
||||
});
|
||||
it("adds user should not create duplicate name", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const email2 = `${name}@otherdomain.com`;
|
||||
await BlacklistDal.add({ name, email });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name, email: email2 });
|
||||
|
||||
//THEN
|
||||
await expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
usernameHash: BlacklistDal.hash(name),
|
||||
})
|
||||
.toArray(),
|
||||
).resolves.toHaveLength(1);
|
||||
await expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
})
|
||||
.toArray(),
|
||||
).resolves.toHaveLength(1);
|
||||
await expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
emailHash: BlacklistDal.hash(email2),
|
||||
})
|
||||
.toArray(),
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
it("adds user should not create duplicate email", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const name2 = "user" + new ObjectId().toHexString();
|
||||
await BlacklistDal.add({ name, email });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name: name2, email });
|
||||
|
||||
//THEN
|
||||
await expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
})
|
||||
.toArray(),
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
it("adds user should not create duplicate discordId", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const name2 = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name: name2, email, discordId });
|
||||
|
||||
//THEN
|
||||
|
||||
await expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
discordIdHash: BlacklistDal.hash(discordId),
|
||||
})
|
||||
.toArray(),
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
});
|
||||
describe("contains", () => {
|
||||
it("contains user", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN / THEN
|
||||
//by name
|
||||
await expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: name.toUpperCase() }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ name, email: "unknown", discordId: "unknown" }),
|
||||
).resolves.toBeTruthy();
|
||||
|
||||
//by email
|
||||
await expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ email: email.toUpperCase() }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: "unknown", email, discordId: "unknown" }),
|
||||
).resolves.toBeTruthy();
|
||||
|
||||
//by discordId
|
||||
await expect(BlacklistDal.contains({ discordId })).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ discordId: discordId.toUpperCase() }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: "unknown", email: "unknown", discordId }),
|
||||
).resolves.toBeTruthy();
|
||||
|
||||
//by name and email and discordId
|
||||
await expect(
|
||||
BlacklistDal.contains({ name, email, discordId }),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("does not contain user", async () => {
|
||||
//GIVEN
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
await BlacklistDal.add({ name: "test2", email: "test2@example.com" });
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: "unknown" }),
|
||||
).resolves.toBeFalsy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ email: "unknown" }),
|
||||
).resolves.toBeFalsy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ discordId: "unknown" }),
|
||||
).resolves.toBeFalsy();
|
||||
await expect(
|
||||
BlacklistDal.contains({
|
||||
name: "unknown",
|
||||
email: "unknown",
|
||||
discordId: "unknown",
|
||||
}),
|
||||
).resolves.toBeFalsy();
|
||||
|
||||
await expect(BlacklistDal.contains({})).resolves.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("removes existing username", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
await BlacklistDal.add({ name, email });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ name });
|
||||
|
||||
//THEN
|
||||
await expect(BlacklistDal.contains({ name })).resolves.toBeFalsy();
|
||||
await expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
|
||||
//decoy still exists
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: "test" }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" }),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("removes existing email", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
await BlacklistDal.add({ name, email });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ email });
|
||||
|
||||
//THEN
|
||||
await expect(BlacklistDal.contains({ email })).resolves.toBeFalsy();
|
||||
await expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
|
||||
//decoy still exists
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: "test" }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" }),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("removes existing discordId", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({
|
||||
name: "test",
|
||||
email: "test@example.com",
|
||||
discordId: "testDiscordId",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ discordId });
|
||||
|
||||
//THEN
|
||||
await expect(BlacklistDal.contains({ discordId })).resolves.toBeFalsy();
|
||||
await expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
await expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
|
||||
//decoy still exists
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: "test" }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ discordId: "testDiscordId" }),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("removes existing username,email and discordId", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({
|
||||
name: "test",
|
||||
email: "test@example.com",
|
||||
discordId: "testDiscordId",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ name, email, discordId });
|
||||
|
||||
//THEN
|
||||
await expect(BlacklistDal.contains({ email })).resolves.toBeFalsy();
|
||||
await expect(BlacklistDal.contains({ name })).resolves.toBeFalsy();
|
||||
await expect(BlacklistDal.contains({ discordId })).resolves.toBeFalsy();
|
||||
|
||||
//decoy still exists
|
||||
await expect(
|
||||
BlacklistDal.contains({ name: "test" }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" }),
|
||||
).resolves.toBeTruthy();
|
||||
await expect(
|
||||
BlacklistDal.contains({ discordId: "testDiscordId" }),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not remove for empty user", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({});
|
||||
|
||||
//THEN
|
||||
await expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
await expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
await expect(BlacklistDal.contains({ discordId })).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe("hash", () => {
|
||||
it("hashes case insensitive", () => {
|
||||
["test", "TEST", "tESt"].forEach((value) =>
|
||||
expect(BlacklistDal.hash(value)).toEqual(
|
||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
42
backend/__tests__/__integration__/dal/config.spec.ts
Normal file
42
backend/__tests__/__integration__/dal/config.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as ConfigDal from "../../../src/dal/config";
|
||||
|
||||
const getConfigCollection = ConfigDal.__testing.getConfigCollection;
|
||||
|
||||
describe("ConfigDal", () => {
|
||||
describe("saveConfig", () => {
|
||||
it("should save and update user configuration correctly", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toString();
|
||||
await getConfigCollection().insertOne({
|
||||
uid,
|
||||
config: {
|
||||
ads: "on",
|
||||
time: 60,
|
||||
quickTab: true, //legacy value
|
||||
},
|
||||
} as any);
|
||||
|
||||
//WHEN
|
||||
await ConfigDal.saveConfig(uid, {
|
||||
ads: "on",
|
||||
difficulty: "normal",
|
||||
} as any);
|
||||
|
||||
//WHEN
|
||||
await ConfigDal.saveConfig(uid, { ads: "off" });
|
||||
|
||||
//THEN
|
||||
const savedConfig = (await ConfigDal.getConfig(
|
||||
uid,
|
||||
)) as ConfigDal.DBConfig;
|
||||
|
||||
expect(savedConfig.config.ads).toBe("off");
|
||||
expect(savedConfig.config.time).toBe(60);
|
||||
|
||||
//should remove legacy values
|
||||
expect((savedConfig.config as any)["quickTab"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
493
backend/__tests__/__integration__/dal/connections.spec.ts
Normal file
493
backend/__tests__/__integration__/dal/connections.spec.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
} from "vitest";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
import * as ConnectionsDal from "../../../src/dal/connections";
|
||||
import { createConnection } from "../../__testData__/connections";
|
||||
import { createUser } from "../../__testData__/users";
|
||||
|
||||
describe("ConnectionsDal", () => {
|
||||
beforeAll(async () => {
|
||||
await ConnectionsDal.createIndicies();
|
||||
});
|
||||
|
||||
describe("getRequests", () => {
|
||||
it("get by uid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const initOne = await createConnection({ initiatorUid: uid });
|
||||
const initTwo = await createConnection({ initiatorUid: uid });
|
||||
const friendOne = await createConnection({ receiverUid: uid });
|
||||
const _decoy = await createConnection({});
|
||||
|
||||
//WHEN / THEM
|
||||
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
receiverUid: uid,
|
||||
}),
|
||||
).toStrictEqual([initOne, initTwo, friendOne]);
|
||||
});
|
||||
|
||||
it("get by uid and status", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const initAccepted = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const _initPending = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "pending",
|
||||
});
|
||||
const initBlocked = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
const friendAccepted = await createConnection({
|
||||
receiverUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const _friendPending = await createConnection({
|
||||
receiverUid: uid,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
const _decoy = await createConnection({ status: "accepted" });
|
||||
|
||||
//WHEN / THEN
|
||||
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
receiverUid: uid,
|
||||
status: ["accepted", "blocked"],
|
||||
}),
|
||||
).toStrictEqual([initAccepted, initBlocked, friendAccepted]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
const now = 1715082588;
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should fail creating duplicates", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN/THEN
|
||||
await expect(
|
||||
createConnection({
|
||||
initiatorUid: first.receiverUid,
|
||||
receiverUid: uid,
|
||||
}),
|
||||
).rejects.toThrow("Connection request already sent");
|
||||
});
|
||||
|
||||
it("should create", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const receiverUid = new ObjectId().toHexString();
|
||||
|
||||
//WHEN
|
||||
const created = await ConnectionsDal.create(
|
||||
{ uid, name: "Bob" },
|
||||
{ uid: receiverUid, name: "Kevin" },
|
||||
2,
|
||||
);
|
||||
|
||||
//THEN
|
||||
expect(created).toEqual({
|
||||
_id: created._id,
|
||||
initiatorUid: uid,
|
||||
initiatorName: "Bob",
|
||||
receiverUid: receiverUid,
|
||||
receiverName: "Kevin",
|
||||
lastModified: now,
|
||||
status: "pending",
|
||||
key: `${uid}/${receiverUid}`,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail if maximum connections are reached", async () => {
|
||||
//GIVEN
|
||||
const initiatorUid = new ObjectId().toHexString();
|
||||
await createConnection({ initiatorUid });
|
||||
await createConnection({ initiatorUid });
|
||||
|
||||
//WHEN / THEM
|
||||
await expect(createConnection({ initiatorUid }, 2)).rejects.toThrow(
|
||||
"Maximum number of connections reached\nStack: create connection request",
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail creating if blocked", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
//WHEN/THEN
|
||||
await expect(
|
||||
createConnection({
|
||||
initiatorUid: first.receiverUid,
|
||||
receiverUid: uid,
|
||||
}),
|
||||
).rejects.toThrow("Connection blocked");
|
||||
});
|
||||
});
|
||||
describe("updateStatus", () => {
|
||||
const now = 1715082588;
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it("should update the status", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createConnection({
|
||||
receiverUid: uid,
|
||||
lastModified: 100,
|
||||
});
|
||||
const second = await createConnection({
|
||||
receiverUid: uid,
|
||||
lastModified: 200,
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await ConnectionsDal.updateStatus(
|
||||
uid,
|
||||
first._id.toHexString(),
|
||||
"accepted",
|
||||
);
|
||||
|
||||
//THEN
|
||||
expect(await ConnectionsDal.getConnections({ receiverUid: uid })).toEqual(
|
||||
[{ ...first, status: "accepted", lastModified: now }, second],
|
||||
);
|
||||
|
||||
//can update twice to the same status
|
||||
await ConnectionsDal.updateStatus(
|
||||
uid,
|
||||
first._id.toHexString(),
|
||||
"accepted",
|
||||
);
|
||||
});
|
||||
it("should fail if uid does not match the reeceiverUid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
ConnectionsDal.updateStatus(uid, first._id.toHexString(), "accepted"),
|
||||
).rejects.toThrow("No permission or connection not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteById", () => {
|
||||
it("should delete by initiator", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
const second = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await ConnectionsDal.deleteById(uid, first._id.toHexString());
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({ initiatorUid: uid }),
|
||||
).toStrictEqual([second]);
|
||||
});
|
||||
|
||||
it("should delete by receiver", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createConnection({
|
||||
receiverUid: uid,
|
||||
});
|
||||
const second = await createConnection({
|
||||
receiverUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await ConnectionsDal.deleteById(uid, first._id.toHexString());
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: second.initiatorUid,
|
||||
}),
|
||||
).toStrictEqual([second]);
|
||||
});
|
||||
|
||||
it("should fail if uid does not match", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = await createConnection({
|
||||
initiatorUid: uid,
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
ConnectionsDal.deleteById("Bob", first._id.toHexString()),
|
||||
).rejects.toThrow("No permission or connection not found");
|
||||
});
|
||||
|
||||
it("should fail if initiator deletes blocked by receiver", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const myRequestWasBlocked = await createConnection({
|
||||
initiatorName: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
ConnectionsDal.deleteById(uid, myRequestWasBlocked._id.toHexString()),
|
||||
).rejects.toThrow("No permission or connection not found");
|
||||
});
|
||||
it("allow receiver to delete blocked", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const myBlockedUser = await createConnection({
|
||||
receiverUid: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await ConnectionsDal.deleteById(uid, myBlockedUser._id.toHexString());
|
||||
|
||||
//THEN
|
||||
expect(await ConnectionsDal.getConnections({ receiverUid: uid })).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteByUid", () => {
|
||||
it("should delete by uid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const _initOne = await createConnection({ initiatorUid: uid });
|
||||
const _initTwo = await createConnection({ initiatorUid: uid });
|
||||
const _friendOne = await createConnection({ receiverUid: uid });
|
||||
const decoy = await createConnection({});
|
||||
|
||||
//WHEN
|
||||
await ConnectionsDal.deleteByUid(uid);
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
receiverUid: uid,
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: decoy.initiatorUid,
|
||||
}),
|
||||
).toEqual([decoy]);
|
||||
});
|
||||
});
|
||||
describe("updateName", () => {
|
||||
it("should update the name", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const initOne = await createConnection({
|
||||
initiatorUid: uid,
|
||||
initiatorName: "Bob",
|
||||
});
|
||||
const initTwo = await createConnection({
|
||||
initiatorUid: uid,
|
||||
initiatorName: "Bob",
|
||||
});
|
||||
const friendOne = await createConnection({
|
||||
receiverUid: uid,
|
||||
receiverName: "Bob",
|
||||
});
|
||||
const decoy = await createConnection({});
|
||||
|
||||
//WHEN
|
||||
await ConnectionsDal.updateName(uid, "King Bob");
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: uid,
|
||||
receiverUid: uid,
|
||||
}),
|
||||
).toEqual([
|
||||
{ ...initOne, initiatorName: "King Bob" },
|
||||
{ ...initTwo, initiatorName: "King Bob" },
|
||||
{ ...friendOne, receiverName: "King Bob" },
|
||||
]);
|
||||
|
||||
expect(
|
||||
await ConnectionsDal.getConnections({
|
||||
initiatorUid: decoy.initiatorUid,
|
||||
}),
|
||||
).toEqual([decoy]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFriendsUids", () => {
|
||||
it("should return friend uids", async () => {
|
||||
//GIVE
|
||||
const uid = new ObjectId().toHexString();
|
||||
const friendOne = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const friendTwo = await createConnection({
|
||||
receiverUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const friendThree = await createConnection({
|
||||
receiverUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const _pending = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "pending",
|
||||
});
|
||||
const _blocked = await createConnection({
|
||||
initiatorUid: uid,
|
||||
status: "blocked",
|
||||
});
|
||||
const _decoy = await createConnection({});
|
||||
|
||||
//WHEN
|
||||
const friendUids = await ConnectionsDal.getFriendsUids(uid);
|
||||
|
||||
//THEN
|
||||
expect(friendUids).toEqual([
|
||||
uid,
|
||||
friendOne.receiverUid,
|
||||
friendTwo.initiatorUid,
|
||||
friendThree.initiatorUid,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("aggregateWithAcceptedConnections", () => {
|
||||
it("should return friend uids", async () => {
|
||||
//GIVE
|
||||
const uid = (await createUser()).uid;
|
||||
const friendOne = await createConnection({
|
||||
initiatorUid: uid,
|
||||
receiverUid: (await createUser()).uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const friendTwo = await createConnection({
|
||||
initiatorUid: (await createUser()).uid,
|
||||
receiverUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const friendThree = await createConnection({
|
||||
initiatorUid: (await createUser()).uid,
|
||||
receiverUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
const _pending = await createConnection({
|
||||
initiatorUid: uid,
|
||||
receiverUid: (await createUser()).uid,
|
||||
status: "pending",
|
||||
});
|
||||
const _blocked = await createConnection({
|
||||
initiatorUid: uid,
|
||||
receiverUid: (await createUser()).uid,
|
||||
status: "blocked",
|
||||
});
|
||||
const _decoy = await createConnection({
|
||||
receiverUid: (await createUser()).uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
const friendUids = await ConnectionsDal.aggregateWithAcceptedConnections<{
|
||||
uid: string;
|
||||
}>({ collectionName: "users", uid }, [{ $project: { uid: true } }]);
|
||||
|
||||
//THEN
|
||||
expect(friendUids.flatMap((it) => it.uid).toSorted()).toEqual([
|
||||
uid,
|
||||
friendOne.receiverUid,
|
||||
friendTwo.initiatorUid,
|
||||
friendThree.initiatorUid,
|
||||
]);
|
||||
});
|
||||
it("should return friend uids and metaData", async () => {
|
||||
//GIVE
|
||||
const me = await createUser();
|
||||
const friend = await createUser();
|
||||
|
||||
const connection = await createConnection({
|
||||
initiatorUid: me.uid,
|
||||
receiverUid: friend.uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
const friendUids = await ConnectionsDal.aggregateWithAcceptedConnections(
|
||||
{ collectionName: "users", uid: me.uid, includeMetaData: true },
|
||||
[
|
||||
{
|
||||
$project: {
|
||||
uid: true,
|
||||
lastModified: "$connectionMeta.lastModified",
|
||||
connectionId: "$connectionMeta._id",
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
//THEN
|
||||
expect(friendUids).toEqual([
|
||||
{
|
||||
_id: friend._id,
|
||||
connectionId: connection._id,
|
||||
lastModified: connection.lastModified,
|
||||
uid: friend.uid,
|
||||
},
|
||||
{
|
||||
_id: me._id,
|
||||
uid: me.uid,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,544 @@
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as UserDal from "../../../src/dal/user";
|
||||
import * as LeaderboardsDal from "../../../src/dal/leaderboards";
|
||||
import * as PublicDal from "../../../src/dal/public";
|
||||
import type { DBLeaderboardEntry } from "../../../src/dal/leaderboards";
|
||||
import type { PersonalBest } from "@monkeytype/schemas/shared";
|
||||
|
||||
import * as DB from "../../../src/init/db";
|
||||
import { LbPersonalBests } from "../../../src/utils/pb";
|
||||
|
||||
import { pb } from "../../__testData__/users";
|
||||
import { createConnection } from "../../__testData__/connections";
|
||||
import { omit } from "../../../src/utils/misc";
|
||||
|
||||
describe("LeaderboardsDal", () => {
|
||||
afterEach(async () => {
|
||||
await DB.collection("users").deleteMany({});
|
||||
});
|
||||
describe("update", () => {
|
||||
it("should ignore unapplicable users on leaderboard", async () => {
|
||||
//GIVEN
|
||||
const lbPersonalBests = lbBests(pb(100), pb(90));
|
||||
const applicableUser = await createUser(lbPersonalBests);
|
||||
await createUser(lbPersonalBests, { banned: true });
|
||||
await createUser(lbPersonalBests, { lbOptOut: true });
|
||||
await createUser(lbPersonalBests, { needsToChangeName: true });
|
||||
await createUser(lbPersonalBests, { timeTyping: 0 });
|
||||
await createUser(lbBests(pb(0, 90, 1)));
|
||||
await createUser(lbBests(pb(60, 0, 1)));
|
||||
await createUser(lbBests(pb(60, 90, 0)));
|
||||
await createUser(lbBests(undefined, pb(60)));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const results = await LeaderboardsDal.get("time", "15", "english", 0, 50);
|
||||
|
||||
//THEN
|
||||
expect(results).toHaveLength(1);
|
||||
expect(
|
||||
(results as LeaderboardsDal.DBLeaderboardEntry[])[0],
|
||||
).toHaveProperty("uid", applicableUser.uid);
|
||||
});
|
||||
|
||||
it("should create leaderboard time english 15", async () => {
|
||||
//GIVEN
|
||||
const rank1 = await createUser(lbBests(pb(100, 90, 2)));
|
||||
const rank2 = await createUser(lbBests(pb(100, 90, 1)));
|
||||
const rank3 = await createUser(lbBests(pb(100, 80, 2)));
|
||||
const rank4 = await createUser(lbBests(pb(90, 100, 1)));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const results = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
)) as DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
|
||||
const lb = results.map((it) => omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("15", { rank: 1, user: rank1 }),
|
||||
expectedLbEntry("15", { rank: 2, user: rank2 }),
|
||||
expectedLbEntry("15", { rank: 3, user: rank3 }),
|
||||
expectedLbEntry("15", { rank: 4, user: rank4 }),
|
||||
]);
|
||||
});
|
||||
it("should create leaderboard time english 60", async () => {
|
||||
//GIVEN
|
||||
const rank1 = await createUser(lbBests(pb(90), pb(100, 90, 2)));
|
||||
const rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
|
||||
const rank3 = await createUser(lbBests(undefined, pb(100, 80, 2)));
|
||||
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
const results = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"60",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
)) as LeaderboardsDal.DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = results.map((it) => omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("60", { rank: 1, user: rank1 }),
|
||||
expectedLbEntry("60", { rank: 2, user: rank2 }),
|
||||
expectedLbEntry("60", { rank: 3, user: rank3 }),
|
||||
expectedLbEntry("60", { rank: 4, user: rank4 }),
|
||||
]);
|
||||
});
|
||||
it("should not include discord properties for users without discord connection", async () => {
|
||||
//GIVEN
|
||||
await createUser(lbBests(pb(90), pb(100, 90, 2)), {
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
const lb = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"60",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
)) as DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
expect(lb[0]).not.toHaveProperty("discordId");
|
||||
expect(lb[0]).not.toHaveProperty("discordAvatar");
|
||||
});
|
||||
|
||||
it("should remove consistency from results if null", async () => {
|
||||
//GIVEN
|
||||
const stats = pb(100, 90, 2);
|
||||
//@ts-ignore
|
||||
stats.consistency = undefined;
|
||||
|
||||
await createUser(lbBests(stats));
|
||||
|
||||
//WHEN
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const lb = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
)) as DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
expect(lb[0]).not.toHaveProperty("consistency");
|
||||
});
|
||||
|
||||
it("should update public speedHistogram for time english 15", async () => {
|
||||
//GIVEN
|
||||
await createUser(lbBests(pb(10), pb(60)));
|
||||
await createUser(lbBests(pb(24)));
|
||||
await createUser(lbBests(pb(28)));
|
||||
await createUser(lbBests(pb(31)));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const result = await PublicDal.getSpeedHistogram("english", "time", "15");
|
||||
|
||||
//THEN
|
||||
expect(result).toEqual({ "10": 1, "20": 2, "30": 1 });
|
||||
});
|
||||
|
||||
it("should update public speedHistogram for time english 60", async () => {
|
||||
//GIVEN
|
||||
await createUser(lbBests(pb(60), pb(20)));
|
||||
await createUser(lbBests(undefined, pb(21)));
|
||||
await createUser(lbBests(undefined, pb(110)));
|
||||
await createUser(lbBests(undefined, pb(115)));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
const result = await PublicDal.getSpeedHistogram("english", "time", "60");
|
||||
|
||||
//THEN
|
||||
expect(result).toEqual({ "20": 2, "110": 2 });
|
||||
});
|
||||
|
||||
it("should create leaderboard with badges", async () => {
|
||||
//GIVEN
|
||||
const noBadge = await createUser(lbBests(pb(4)));
|
||||
const oneBadgeSelected = await createUser(lbBests(pb(3)), {
|
||||
inventory: { badges: [{ id: 1, selected: true }] },
|
||||
});
|
||||
const oneBadgeNotSelected = await createUser(lbBests(pb(2)), {
|
||||
inventory: { badges: [{ id: 1, selected: false }] },
|
||||
});
|
||||
const multipleBadges = await createUser(lbBests(pb(1)), {
|
||||
inventory: {
|
||||
badges: [
|
||||
{ id: 1, selected: false },
|
||||
{ id: 2, selected: true },
|
||||
{ id: 3, selected: true },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const result = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
)) as DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = result.map((it) => omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("15", { rank: 1, user: noBadge }),
|
||||
expectedLbEntry("15", {
|
||||
rank: 2,
|
||||
user: oneBadgeSelected,
|
||||
badgeId: 1,
|
||||
}),
|
||||
expectedLbEntry("15", { rank: 3, user: oneBadgeNotSelected }),
|
||||
expectedLbEntry("15", {
|
||||
rank: 4,
|
||||
user: multipleBadges,
|
||||
badgeId: 2,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create leaderboard with premium", async () => {
|
||||
//GIVEN
|
||||
vi.useRealTimers(); //timestamp for premium is calculated in mongo
|
||||
const noPremium = await createUser(lbBests(pb(4)));
|
||||
const lifetime = await createUser(lbBests(pb(3)), premium(-1));
|
||||
const validPremium = await createUser(lbBests(pb(2)), premium(1000));
|
||||
const expiredPremium = await createUser(lbBests(pb(1)), premium(-10));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
|
||||
const result = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
true,
|
||||
)) as DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = result.map((it) => omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("15", { rank: 1, user: noPremium }),
|
||||
expectedLbEntry("15", {
|
||||
rank: 2,
|
||||
user: lifetime,
|
||||
isPremium: true,
|
||||
}),
|
||||
expectedLbEntry("15", {
|
||||
rank: 3,
|
||||
user: validPremium,
|
||||
isPremium: true,
|
||||
}),
|
||||
expectedLbEntry("15", { rank: 4, user: expiredPremium }),
|
||||
]);
|
||||
});
|
||||
it("should create leaderboard without premium if feature disabled", async () => {
|
||||
//GIVEN
|
||||
// const lifetime = await createUser(lbBests(pb(3)), premium(-1));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const results = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
false,
|
||||
)) as DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
expect(results[0]?.isPremium).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("should get for page", async () => {
|
||||
//GIVEN
|
||||
const _rank1 = await createUser(lbBests(pb(90), pb(105, 90, 2)));
|
||||
const _rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
|
||||
const rank3 = await createUser(lbBests(undefined, pb(95, 80, 2)));
|
||||
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
|
||||
//WHEN
|
||||
|
||||
const results = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"60",
|
||||
"english",
|
||||
1,
|
||||
2,
|
||||
true,
|
||||
)) as LeaderboardsDal.DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = results.map((it) => omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("60", { rank: 3, user: rank3 }),
|
||||
expectedLbEntry("60", { rank: 4, user: rank4 }),
|
||||
]);
|
||||
});
|
||||
it("should get for friends only", async () => {
|
||||
//GIVEN
|
||||
const rank1 = await createUser(lbBests(pb(90), pb(100, 90, 2)));
|
||||
const uid = rank1.uid;
|
||||
const _rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
|
||||
const _rank3 = await createUser(lbBests(undefined, pb(100, 80, 2)));
|
||||
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
|
||||
|
||||
//two friends, one is not on the leaderboard
|
||||
await createConnection({
|
||||
initiatorUid: uid,
|
||||
receiverUid: rank4.uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
await createConnection({ initiatorUid: uid, status: "accepted" });
|
||||
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
|
||||
//WHEN
|
||||
|
||||
const results = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"60",
|
||||
"english",
|
||||
0,
|
||||
50,
|
||||
false,
|
||||
uid,
|
||||
)) as LeaderboardsDal.DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = results.map((it) => omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("60", { rank: 1, user: rank1, friendsRank: 1 }),
|
||||
expectedLbEntry("60", { rank: 4, user: rank4, friendsRank: 2 }),
|
||||
]);
|
||||
});
|
||||
it("should get for friends only with page", async () => {
|
||||
//GIVEN
|
||||
const rank1 = await createUser(lbBests(pb(90), pb(105, 90, 2)));
|
||||
const uid = rank1.uid;
|
||||
const rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
|
||||
const _rank3 = await createUser(lbBests(undefined, pb(95, 80, 2)));
|
||||
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
|
||||
await createConnection({
|
||||
initiatorUid: uid,
|
||||
receiverUid: rank2.uid,
|
||||
status: "accepted",
|
||||
});
|
||||
await createConnection({
|
||||
initiatorUid: rank4.uid,
|
||||
receiverUid: uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
const results = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"60",
|
||||
"english",
|
||||
1,
|
||||
2,
|
||||
false,
|
||||
uid,
|
||||
)) as LeaderboardsDal.DBLeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = results.map((it) => omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("60", { rank: 4, user: rank4, friendsRank: 3 }),
|
||||
]);
|
||||
});
|
||||
it("should return empty list if no friends", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
|
||||
//WHEN
|
||||
const results = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"60",
|
||||
"english",
|
||||
1,
|
||||
2,
|
||||
false,
|
||||
uid,
|
||||
)) as LeaderboardsDal.DBLeaderboardEntry[];
|
||||
//THEN
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
describe("getCount / getRank", () => {
|
||||
it("should get count", async () => {
|
||||
//GIVEN
|
||||
await createUser(lbBests(undefined, pb(105)), { name: "One" });
|
||||
await createUser(lbBests(undefined, pb(100)), { name: "Two" });
|
||||
const me = await createUser(lbBests(undefined, pb(95)), { name: "Me" });
|
||||
await createUser(lbBests(undefined, pb(90)), { name: "Three" });
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
|
||||
//WHEN / THEN
|
||||
|
||||
expect(await LeaderboardsDal.getCount("time", "60", "english")) //
|
||||
.toEqual(4);
|
||||
expect(await LeaderboardsDal.getRank("time", "60", "english", me.uid)) //
|
||||
.toEqual(
|
||||
expect.objectContaining({
|
||||
wpm: 95,
|
||||
rank: 3,
|
||||
name: me.name,
|
||||
uid: me.uid,
|
||||
}),
|
||||
);
|
||||
});
|
||||
it("should get for friends only", async () => {
|
||||
//GIVEN
|
||||
const friendOne = await createUser(lbBests(undefined, pb(105)));
|
||||
await createUser(lbBests(undefined, pb(100)));
|
||||
await createUser(lbBests(undefined, pb(95)));
|
||||
const friendTwo = await createUser(lbBests(undefined, pb(90)));
|
||||
const me = await createUser(lbBests(undefined, pb(99)));
|
||||
await LeaderboardsDal.update("time", "60", "english");
|
||||
|
||||
await createConnection({
|
||||
initiatorUid: me.uid,
|
||||
receiverUid: friendOne.uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
await createConnection({
|
||||
initiatorUid: friendTwo.uid,
|
||||
receiverUid: me.uid,
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
//WHEN / THEN
|
||||
|
||||
expect(await LeaderboardsDal.getCount("time", "60", "english", me.uid)) //
|
||||
.toEqual(3);
|
||||
expect(
|
||||
await LeaderboardsDal.getRank("time", "60", "english", me.uid, true),
|
||||
) //
|
||||
.toEqual(
|
||||
expect.objectContaining({
|
||||
wpm: 99,
|
||||
rank: 3,
|
||||
friendsRank: 2,
|
||||
name: me.name,
|
||||
uid: me.uid,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectedLbEntry(
|
||||
time: string,
|
||||
{ rank, user, badgeId, isPremium, friendsRank }: ExpectedLbEntry,
|
||||
) {
|
||||
// @ts-expect-error
|
||||
const lbBest: PersonalBest =
|
||||
// @ts-expect-error
|
||||
user.lbPersonalBests?.time[Number.parseInt(time)].english;
|
||||
|
||||
return {
|
||||
rank,
|
||||
uid: user.uid,
|
||||
name: user.name,
|
||||
wpm: lbBest.wpm,
|
||||
acc: lbBest.acc,
|
||||
timestamp: lbBest.timestamp,
|
||||
raw: lbBest.raw,
|
||||
consistency: lbBest.consistency,
|
||||
discordId: user.discordId,
|
||||
discordAvatar: user.discordAvatar,
|
||||
badgeId,
|
||||
isPremium,
|
||||
friendsRank,
|
||||
};
|
||||
}
|
||||
|
||||
async function createUser(
|
||||
lbPersonalBests?: LbPersonalBests,
|
||||
userProperties?: Partial<UserDal.DBUser>,
|
||||
): Promise<UserDal.DBUser> {
|
||||
const uid = new ObjectId().toHexString();
|
||||
await UserDal.addUser("User " + uid, uid + "@example.com", uid);
|
||||
|
||||
await DB.getDb()
|
||||
?.collection<UserDal.DBUser>("users")
|
||||
.updateOne(
|
||||
{ uid },
|
||||
{
|
||||
$set: {
|
||||
timeTyping: 7200,
|
||||
discordId: "discord " + uid,
|
||||
discordAvatar: "avatar " + uid,
|
||||
...userProperties,
|
||||
lbPersonalBests,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return await UserDal.getUser(uid, "test");
|
||||
}
|
||||
|
||||
function lbBests(pb15?: PersonalBest, pb60?: PersonalBest): LbPersonalBests {
|
||||
const result: LbPersonalBests = { time: {} };
|
||||
if (pb15) result.time["15"] = { english: pb15 };
|
||||
if (pb60) result.time["60"] = { english: pb60 };
|
||||
return result;
|
||||
}
|
||||
|
||||
function premium(expirationDeltaSeconds: number) {
|
||||
return {
|
||||
premium: {
|
||||
startTimestamp: 0,
|
||||
expirationTimestamp:
|
||||
expirationDeltaSeconds === -1
|
||||
? -1
|
||||
: Date.now() + expirationDeltaSeconds * 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type ExpectedLbEntry = {
|
||||
rank: number;
|
||||
user: UserDal.DBUser;
|
||||
badgeId?: number;
|
||||
isPremium?: boolean;
|
||||
friendsRank?: number;
|
||||
};
|
||||
482
backend/__tests__/__integration__/dal/preset.spec.ts
Normal file
482
backend/__tests__/__integration__/dal/preset.spec.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as PresetDal from "../../../src/dal/preset";
|
||||
|
||||
describe("PresetDal", () => {
|
||||
describe("readPreset", () => {
|
||||
it("should read", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const decoyUid = new ObjectId().toHexString();
|
||||
const first = await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
});
|
||||
const second = await PresetDal.addPreset(uid, {
|
||||
name: "second",
|
||||
settingGroups: ["hideElements"],
|
||||
config: {
|
||||
showKeyTips: true,
|
||||
capsLockWarning: true,
|
||||
showOutOfFocusWarning: true,
|
||||
showAverage: "off",
|
||||
},
|
||||
});
|
||||
|
||||
await PresetDal.addPreset(decoyUid, {
|
||||
name: "unknown",
|
||||
config: {},
|
||||
});
|
||||
|
||||
//WHEN
|
||||
const read = await PresetDal.getPresets(uid);
|
||||
|
||||
//THEN
|
||||
expect(read).toHaveLength(2);
|
||||
expect(read).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first.presetId),
|
||||
uid: uid,
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(second.presetId),
|
||||
uid: uid,
|
||||
name: "second",
|
||||
settingGroups: ["hideElements"],
|
||||
config: {
|
||||
showKeyTips: true,
|
||||
capsLockWarning: true,
|
||||
showOutOfFocusWarning: true,
|
||||
showAverage: "off",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addPreset", () => {
|
||||
it("should return error if maximum is reached", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await PresetDal.addPreset(uid, { name: "test", config: {} });
|
||||
}
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(() =>
|
||||
PresetDal.addPreset(uid, { name: "max", config: {} }),
|
||||
).rejects.toThrow("Too many presets");
|
||||
});
|
||||
it("should add preset", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
for (let i = 0; i < 9; i++) {
|
||||
await PresetDal.addPreset(uid, { name: "test", config: {} });
|
||||
}
|
||||
|
||||
//WHEN
|
||||
const newPreset = await PresetDal.addPreset(uid, {
|
||||
name: "new",
|
||||
config: {
|
||||
ads: "sellout",
|
||||
},
|
||||
});
|
||||
|
||||
//THEN
|
||||
const read = await PresetDal.getPresets(uid);
|
||||
|
||||
expect(read).toHaveLength(10);
|
||||
expect(read).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(newPreset.presetId),
|
||||
uid: uid,
|
||||
name: "new",
|
||||
config: { ads: "sellout" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editPreset", () => {
|
||||
it("should not fail if preset is unknown", async () => {
|
||||
const uid = new ObjectId().toHexString();
|
||||
await PresetDal.editPreset(uid, {
|
||||
_id: new ObjectId().toHexString(),
|
||||
name: "new",
|
||||
config: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should edit", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const decoyUid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
})
|
||||
).presetId;
|
||||
const second = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "second",
|
||||
config: {
|
||||
ads: "result",
|
||||
},
|
||||
})
|
||||
).presetId;
|
||||
const decoy = (
|
||||
await PresetDal.addPreset(decoyUid, {
|
||||
name: "unknown",
|
||||
config: { ads: "result" },
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN
|
||||
await PresetDal.editPreset(uid, {
|
||||
_id: first,
|
||||
name: "newName",
|
||||
config: { ads: "off" },
|
||||
});
|
||||
|
||||
//THEN
|
||||
const read = await PresetDal.getPresets(uid);
|
||||
expect(read).toHaveLength(2);
|
||||
expect(read).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first),
|
||||
uid: uid,
|
||||
name: "newName",
|
||||
config: { ads: "off" },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(second),
|
||||
uid: uid,
|
||||
name: "second",
|
||||
config: { ads: "result" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(await PresetDal.getPresets(decoyUid)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(decoy),
|
||||
uid: decoyUid,
|
||||
name: "unknown",
|
||||
config: { ads: "result" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should edit with name only - full preset", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN empty
|
||||
await PresetDal.editPreset(uid, {
|
||||
_id: first,
|
||||
name: "newName",
|
||||
});
|
||||
expect(await PresetDal.getPresets(uid)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first),
|
||||
uid: uid,
|
||||
name: "newName",
|
||||
config: { ads: "sellout" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
it("should edit with name only - partial preset", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
settingGroups: ["hideElements"],
|
||||
config: {
|
||||
showKeyTips: true,
|
||||
capsLockWarning: true,
|
||||
showOutOfFocusWarning: true,
|
||||
showAverage: "off",
|
||||
},
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN empty
|
||||
await PresetDal.editPreset(uid, {
|
||||
_id: first,
|
||||
name: "newName",
|
||||
});
|
||||
expect(await PresetDal.getPresets(uid)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first),
|
||||
uid: uid,
|
||||
name: "newName",
|
||||
settingGroups: ["hideElements"],
|
||||
config: {
|
||||
showKeyTips: true,
|
||||
capsLockWarning: true,
|
||||
showOutOfFocusWarning: true,
|
||||
showAverage: "off",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
it("should not edit present not matching uid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const decoyUid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN
|
||||
await PresetDal.editPreset(decoyUid, {
|
||||
_id: first,
|
||||
name: "newName",
|
||||
config: { ads: "off" },
|
||||
});
|
||||
|
||||
//THEN
|
||||
const read = await PresetDal.getPresets(uid);
|
||||
|
||||
expect(read).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first),
|
||||
uid: uid,
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
it("should edit when partial is edited to full", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
settingGroups: ["hideElements"],
|
||||
config: {
|
||||
showKeyTips: true,
|
||||
capsLockWarning: true,
|
||||
showOutOfFocusWarning: true,
|
||||
showAverage: "off",
|
||||
},
|
||||
})
|
||||
).presetId;
|
||||
//WHEN
|
||||
await PresetDal.editPreset(uid, {
|
||||
_id: first,
|
||||
name: "newName",
|
||||
settingGroups: null,
|
||||
config: { ads: "off" },
|
||||
});
|
||||
|
||||
//THEN
|
||||
expect(await PresetDal.getPresets(uid)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first),
|
||||
uid: uid,
|
||||
name: "newName",
|
||||
config: { ads: "off" },
|
||||
settingGroups: null,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
it("should edit when full is edited to partial", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
config: {
|
||||
ads: "off",
|
||||
},
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN
|
||||
await PresetDal.editPreset(uid, {
|
||||
_id: first,
|
||||
name: "newName",
|
||||
settingGroups: ["hideElements"],
|
||||
config: {
|
||||
showKeyTips: true,
|
||||
capsLockWarning: true,
|
||||
showOutOfFocusWarning: true,
|
||||
showAverage: "off",
|
||||
},
|
||||
});
|
||||
|
||||
//THEN
|
||||
expect(await PresetDal.getPresets(uid)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first),
|
||||
uid: uid,
|
||||
name: "newName",
|
||||
settingGroups: ["hideElements"],
|
||||
config: {
|
||||
showKeyTips: true,
|
||||
capsLockWarning: true,
|
||||
showOutOfFocusWarning: true,
|
||||
showAverage: "off",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removePreset", () => {
|
||||
it("should fail if preset is unknown", async () => {
|
||||
const uid = new ObjectId().toHexString();
|
||||
await expect(() =>
|
||||
PresetDal.removePreset(uid, new ObjectId().toHexString()),
|
||||
).rejects.toThrow("Preset not found");
|
||||
});
|
||||
it("should remove", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const decoyUid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, { name: "first", config: {} })
|
||||
).presetId;
|
||||
const second = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "second",
|
||||
config: { ads: "result" },
|
||||
})
|
||||
).presetId;
|
||||
const decoy = (
|
||||
await PresetDal.addPreset(decoyUid, {
|
||||
name: "unknown",
|
||||
config: { ads: "result" },
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN
|
||||
await PresetDal.removePreset(uid, first);
|
||||
|
||||
//THEN
|
||||
const read = await PresetDal.getPresets(uid);
|
||||
expect(read).toHaveLength(1);
|
||||
expect(read).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(second),
|
||||
uid: uid,
|
||||
name: "second",
|
||||
config: { ads: "result" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(await PresetDal.getPresets(decoyUid)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(decoy),
|
||||
uid: decoyUid,
|
||||
name: "unknown",
|
||||
config: { ads: "result" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
it("should not remove present not matching uid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const decoyUid = new ObjectId().toHexString();
|
||||
const first = (
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN
|
||||
await expect(() =>
|
||||
PresetDal.removePreset(decoyUid, first),
|
||||
).rejects.toThrow("Preset not found");
|
||||
|
||||
//THEN
|
||||
const read = await PresetDal.getPresets(uid);
|
||||
|
||||
expect(read).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(first),
|
||||
uid: uid,
|
||||
name: "first",
|
||||
config: { ads: "sellout" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteAllPresets", () => {
|
||||
it("should not fail if preset is unknown", async () => {
|
||||
const uid = new ObjectId().toHexString();
|
||||
await PresetDal.deleteAllPresets(uid);
|
||||
});
|
||||
it("should delete all", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const decoyUid = new ObjectId().toHexString();
|
||||
await PresetDal.addPreset(uid, { name: "first", config: {} });
|
||||
await PresetDal.addPreset(uid, {
|
||||
name: "second",
|
||||
config: { ads: "result" },
|
||||
});
|
||||
const decoy = (
|
||||
await PresetDal.addPreset(decoyUid, {
|
||||
name: "unknown",
|
||||
config: { ads: "result" },
|
||||
})
|
||||
).presetId;
|
||||
|
||||
//WHEN
|
||||
await PresetDal.deleteAllPresets(uid);
|
||||
|
||||
//THEN
|
||||
const read = await PresetDal.getPresets(uid);
|
||||
expect(read).toHaveLength(0);
|
||||
|
||||
expect(await PresetDal.getPresets(decoyUid)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: new ObjectId(decoy),
|
||||
uid: decoyUid,
|
||||
name: "unknown",
|
||||
config: { ads: "result" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
26
backend/__tests__/__integration__/dal/public.spec.ts
Normal file
26
backend/__tests__/__integration__/dal/public.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import * as PublicDAL from "../../../src/dal/public";
|
||||
|
||||
describe("PublicDAL", function () {
|
||||
it("should be able to update stats", async function () {
|
||||
// checks it doesn't throw an error. the actual values are checked in another test.
|
||||
await PublicDAL.updateStats(1, 15);
|
||||
});
|
||||
|
||||
it("should be able to get typing stats", async function () {
|
||||
const typingStats = await PublicDAL.getTypingStats();
|
||||
expect(typingStats).toHaveProperty("testsCompleted");
|
||||
expect(typingStats).toHaveProperty("testsStarted");
|
||||
expect(typingStats).toHaveProperty("timeTyping");
|
||||
});
|
||||
|
||||
it("should increment stats on update", async function () {
|
||||
// checks that both functions are working on the same data in mongo
|
||||
const priorStats = await PublicDAL.getTypingStats();
|
||||
await PublicDAL.updateStats(1, 60);
|
||||
const afterStats = await PublicDAL.getTypingStats();
|
||||
expect(afterStats.testsCompleted).toBe(priorStats.testsCompleted + 1);
|
||||
expect(afterStats.testsStarted).toBe(priorStats.testsStarted + 2);
|
||||
expect(afterStats.timeTyping).toBe(priorStats.timeTyping + 60);
|
||||
});
|
||||
});
|
||||
190
backend/__tests__/__integration__/dal/result.spec.ts
Normal file
190
backend/__tests__/__integration__/dal/result.spec.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import * as ResultDal from "../../../src/dal/result";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as UserDal from "../../../src/dal/user";
|
||||
import { DBResult } from "../../../src/utils/result";
|
||||
import * as ResultUtils from "../../../src/utils/result";
|
||||
|
||||
let uid: string;
|
||||
const timestamp = Date.now() - 60000;
|
||||
|
||||
async function createDummyData(
|
||||
uid: string,
|
||||
count: number,
|
||||
modify?: Partial<DBResult>,
|
||||
): Promise<void> {
|
||||
const dummyUser: UserDal.DBUser = {
|
||||
_id: new ObjectId(),
|
||||
uid,
|
||||
addedAt: 0,
|
||||
email: "test@example.com",
|
||||
name: "Bob",
|
||||
personalBests: {
|
||||
time: {},
|
||||
words: {},
|
||||
quote: {},
|
||||
custom: {},
|
||||
zen: {},
|
||||
},
|
||||
};
|
||||
|
||||
vi.spyOn(UserDal, "getUser").mockResolvedValue(dummyUser);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await ResultDal.addResult(uid, {
|
||||
...{
|
||||
_id: new ObjectId(),
|
||||
wpm: i,
|
||||
rawWpm: i,
|
||||
charStats: [0, 0, 0, 0],
|
||||
acc: 0,
|
||||
mode: "time",
|
||||
mode2: "10" as never,
|
||||
quoteLength: 1,
|
||||
timestamp,
|
||||
restartCount: 0,
|
||||
incompleteTestSeconds: 0,
|
||||
incompleteTests: [],
|
||||
testDuration: 10,
|
||||
afkDuration: 0,
|
||||
tags: [],
|
||||
consistency: 100,
|
||||
keyConsistency: 100,
|
||||
chartData: { wpm: [], burst: [], err: [] },
|
||||
uid,
|
||||
keySpacingStats: { average: 0, sd: 0 },
|
||||
keyDurationStats: { average: 0, sd: 0 },
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
isPb: false,
|
||||
name: "Test",
|
||||
funbox: ["58008", "read_ahead"],
|
||||
},
|
||||
...modify,
|
||||
});
|
||||
}
|
||||
}
|
||||
describe("ResultDal", () => {
|
||||
const replaceLegacyValuesMock = vi.spyOn(ResultUtils, "replaceLegacyValues");
|
||||
|
||||
beforeEach(() => {
|
||||
uid = new ObjectId().toHexString();
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (uid) await ResultDal.deleteAll(uid);
|
||||
replaceLegacyValuesMock.mockClear();
|
||||
});
|
||||
describe("getResults", () => {
|
||||
it("should read lastest 10 results ordered by timestamp", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 10, { timestamp: timestamp - 2000 });
|
||||
await createDummyData(uid, 20, { tags: ["current"] });
|
||||
|
||||
//WHEN
|
||||
const results = await ResultDal.getResults(uid, { limit: 10 });
|
||||
|
||||
//THEN
|
||||
expect(results).toHaveLength(10);
|
||||
let last = results[0]?.timestamp as number;
|
||||
results.forEach((it) => {
|
||||
expect(it.tags).toContain("current");
|
||||
expect(it.timestamp).toBeGreaterThanOrEqual(last);
|
||||
last = it.timestamp;
|
||||
});
|
||||
});
|
||||
it("should read all if not limited", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 10, { timestamp: timestamp - 2000 });
|
||||
await createDummyData(uid, 20);
|
||||
|
||||
//WHEN
|
||||
const results = await ResultDal.getResults(uid, {});
|
||||
|
||||
//THEN
|
||||
expect(results).toHaveLength(30);
|
||||
});
|
||||
it("should read results onOrAfterTimestamp", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 10, { timestamp: timestamp - 2000 });
|
||||
await createDummyData(uid, 20, { tags: ["current"] });
|
||||
|
||||
//WHEN
|
||||
const results = await ResultDal.getResults(uid, {
|
||||
onOrAfterTimestamp: timestamp,
|
||||
});
|
||||
|
||||
//THEN
|
||||
expect(results).toHaveLength(20);
|
||||
results.forEach((it) => {
|
||||
expect(it.tags).toContain("current");
|
||||
});
|
||||
});
|
||||
it("should read next 10 results", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 10, {
|
||||
timestamp: timestamp - 2000,
|
||||
tags: ["old"],
|
||||
});
|
||||
await createDummyData(uid, 20);
|
||||
|
||||
//WHEN
|
||||
const results = await ResultDal.getResults(uid, {
|
||||
limit: 10,
|
||||
offset: 20,
|
||||
});
|
||||
|
||||
//THEN
|
||||
expect(results).toHaveLength(10);
|
||||
results.forEach((it) => {
|
||||
expect(it.tags).toContain("old");
|
||||
});
|
||||
});
|
||||
it("should call replaceLegacyValues", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 1);
|
||||
|
||||
//WHEN
|
||||
await ResultDal.getResults(uid);
|
||||
|
||||
//THEN
|
||||
expect(replaceLegacyValuesMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("getResult", () => {
|
||||
it("should call replaceLegacyValues", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 1);
|
||||
const resultId = (await ResultDal.getLastResult(uid))._id.toHexString();
|
||||
|
||||
//WHEN
|
||||
await ResultDal.getResult(uid, resultId);
|
||||
|
||||
//THEN
|
||||
expect(replaceLegacyValuesMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("getLastResult", () => {
|
||||
it("should call replaceLegacyValues", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 1);
|
||||
|
||||
//WHEN
|
||||
await ResultDal.getLastResult(uid);
|
||||
|
||||
//THEN
|
||||
expect(replaceLegacyValuesMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("getResultByTimestamp", () => {
|
||||
it("should call replaceLegacyValues", async () => {
|
||||
//GIVEN
|
||||
await createDummyData(uid, 1);
|
||||
|
||||
//WHEN
|
||||
await ResultDal.getResultByTimestamp(uid, timestamp);
|
||||
|
||||
//THEN
|
||||
expect(replaceLegacyValuesMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
2218
backend/__tests__/__integration__/dal/user.spec.ts
Normal file
2218
backend/__tests__/__integration__/dal/user.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
51
backend/__tests__/__integration__/global-setup.ts
Normal file
51
backend/__tests__/__integration__/global-setup.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";
|
||||
|
||||
let startedMongoContainer: StartedTestContainer | undefined;
|
||||
let startedRedisContainer: StartedTestContainer | undefined;
|
||||
|
||||
export async function setup(): Promise<void> {
|
||||
process.env.TZ = "UTC";
|
||||
|
||||
//use testcontainer to start mongodb
|
||||
console.log("\x1b[36mMongoDB starting...\x1b[0m");
|
||||
const mongoContainer = new GenericContainer("mongo:5.0.13")
|
||||
.withExposedPorts(27017)
|
||||
.withWaitStrategy(Wait.forListeningPorts());
|
||||
|
||||
startedMongoContainer = await mongoContainer.start();
|
||||
|
||||
const mongoUrl = `mongodb://${startedMongoContainer?.getHost()}:${startedMongoContainer?.getMappedPort(
|
||||
27017,
|
||||
)}`;
|
||||
process.env["TEST_DB_URL"] = mongoUrl;
|
||||
console.log(`\x1b[32mMongoDB is running on ${mongoUrl}\x1b[0m`);
|
||||
|
||||
//use testcontainer to start redis
|
||||
console.log("\x1b[36mRedis starting...\x1b[0m");
|
||||
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;
|
||||
console.log(`\x1b[32mRedis is running on ${redisUrl}\x1b[0m`);
|
||||
}
|
||||
|
||||
async function stopContainers(): Promise<void> {
|
||||
console.log("\x1b[36mMongoDB stopping...\x1b[0m");
|
||||
await startedMongoContainer?.stop();
|
||||
console.log("\x1b[36mRedis stopping...\x1b[0m");
|
||||
await startedRedisContainer?.stop();
|
||||
console.log(`\x1b[32mContainers stopped.\x1b[0m`);
|
||||
}
|
||||
|
||||
export async function teardown(): Promise<void> {
|
||||
await stopContainers();
|
||||
}
|
||||
|
||||
process.on("SIGTERM", stopContainers);
|
||||
process.on("SIGINT", stopContainers);
|
||||
11
backend/__tests__/__integration__/redis.ts
Normal file
11
backend/__tests__/__integration__/redis.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { getConnection, connect } from "../../src/init/redis";
|
||||
|
||||
export async function redisSetup(): Promise<void> {
|
||||
await connect();
|
||||
}
|
||||
export async function cleanupKeys(prefix: string): Promise<void> {
|
||||
// oxlint-disable-next-line no-non-null-assertion
|
||||
const connection = getConnection()!;
|
||||
const keys = await connection.keys(`${prefix}*`);
|
||||
await Promise.all(keys?.map((it) => connection.del(it)));
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from "vitest";
|
||||
import * as WeeklyXpLeaderboard from "../../../src/services/weekly-xp-leaderboard";
|
||||
import { Configuration } from "@monkeytype/schemas/configuration";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { RedisXpLeaderboardEntry } from "@monkeytype/schemas/leaderboards";
|
||||
import { cleanupKeys, redisSetup } from "../redis";
|
||||
|
||||
const leaderboardsConfig: Configuration["leaderboards"]["weeklyXp"] = {
|
||||
enabled: true,
|
||||
expirationTimeInDays: 7,
|
||||
xpRewardBrackets: [],
|
||||
};
|
||||
|
||||
describe("Weekly XP Leaderboards", () => {
|
||||
beforeAll(async () => {
|
||||
await redisSetup();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await cleanupKeys(WeeklyXpLeaderboard.__testing.namespace);
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("should get if enabled", () => {
|
||||
expect(WeeklyXpLeaderboard.get(leaderboardsConfig)).toBeInstanceOf(
|
||||
WeeklyXpLeaderboard.WeeklyXpLeaderboard,
|
||||
);
|
||||
});
|
||||
it("should return null if disabled", () => {
|
||||
expect(WeeklyXpLeaderboard.get({ enabled: false } as any)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("WeeklyXpLeaderboard class", () => {
|
||||
// oxlint-disable-next-line no-non-null-assertion
|
||||
const lb = WeeklyXpLeaderboard.get(leaderboardsConfig)!;
|
||||
|
||||
describe("addResult", () => {
|
||||
it("adds results for user", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult(100, { timeTypedSeconds: 5 });
|
||||
await givenResult(50, { ...user1, timeTypedSeconds: 5 });
|
||||
const user2 = await givenResult(100, {
|
||||
isPremium: true,
|
||||
timeTypedSeconds: 7,
|
||||
});
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(0, 10, leaderboardsConfig, true);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 2,
|
||||
entries: [
|
||||
{
|
||||
...user1,
|
||||
rank: 1,
|
||||
timeTypedSeconds: 10,
|
||||
totalXp: 150,
|
||||
isPremium: false,
|
||||
},
|
||||
{
|
||||
...user2,
|
||||
rank: 2,
|
||||
timeTypedSeconds: 7,
|
||||
totalXp: 100,
|
||||
isPremium: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResults", () => {
|
||||
it("gets results", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult(150);
|
||||
const user2 = await givenResult(100);
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(0, 10, leaderboardsConfig, true);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 2,
|
||||
entries: [
|
||||
{ rank: 1, totalXp: 150, ...user1 },
|
||||
{ rank: 2, totalXp: 100, ...user2 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("gets results for page", async () => {
|
||||
//GIVEN
|
||||
const _user1 = await givenResult(100);
|
||||
const _user2 = await givenResult(75);
|
||||
const user3 = await givenResult(50);
|
||||
const user4 = await givenResult(25);
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(1, 2, leaderboardsConfig, true);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 4,
|
||||
entries: [
|
||||
{ rank: 3, totalXp: 50, ...user3 },
|
||||
{ rank: 4, totalXp: 25, ...user4 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("gets results without premium", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult(150, { isPremium: true });
|
||||
const user2 = await givenResult(100);
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(0, 10, leaderboardsConfig, false);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 2,
|
||||
entries: [
|
||||
{ rank: 1, totalXp: 150, ...user1, isPremium: undefined },
|
||||
{ rank: 2, totalXp: 100, ...user2, isPremium: undefined },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("gets results for friends only", async () => {
|
||||
//GIVEN
|
||||
const _user1 = await givenResult(100);
|
||||
const user2 = await givenResult(75);
|
||||
const _user3 = await givenResult(50);
|
||||
const user4 = await givenResult(25);
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(0, 5, leaderboardsConfig, true, [
|
||||
user2.uid,
|
||||
user4.uid,
|
||||
new ObjectId().toHexString(),
|
||||
]);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 2,
|
||||
entries: [
|
||||
{ rank: 2, friendsRank: 1, totalXp: 75, ...user2 },
|
||||
{ rank: 4, friendsRank: 2, totalXp: 25, ...user4 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("gets results for friends only with page", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult(100);
|
||||
const user2 = await givenResult(75);
|
||||
const _user3 = await givenResult(50);
|
||||
const user4 = await givenResult(25);
|
||||
const _user5 = await givenResult(5);
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(1, 2, leaderboardsConfig, true, [
|
||||
user1.uid,
|
||||
user2.uid,
|
||||
user4.uid,
|
||||
new ObjectId().toHexString(),
|
||||
]);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 3,
|
||||
entries: [{ rank: 4, friendsRank: 3, totalXp: 25, ...user4 }],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty list if no friends", async () => {
|
||||
//GIVEN
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(0, 5, leaderboardsConfig, true, []);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 0,
|
||||
entries: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRank", () => {
|
||||
it("gets rank", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult(100);
|
||||
const _user2 = await givenResult(150);
|
||||
|
||||
//WHEN
|
||||
const rank = await lb.getRank(user1.uid, leaderboardsConfig);
|
||||
//THEN
|
||||
expect(rank).toEqual({ rank: 2, totalXp: 100, ...user1 });
|
||||
});
|
||||
|
||||
it("should return null for unknown user", async () => {
|
||||
expect(await lb.getRank("decoy", leaderboardsConfig)).toBeNull();
|
||||
expect(
|
||||
await lb.getRank("decoy", leaderboardsConfig, [
|
||||
"unknown",
|
||||
"unknown2",
|
||||
]),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("gets rank for friends", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult(50);
|
||||
const user2 = await givenResult(60);
|
||||
const _user3 = await givenResult(70);
|
||||
|
||||
const friends = [user1.uid, user2.uid, "decoy"];
|
||||
|
||||
//WHEN / THEN
|
||||
expect(
|
||||
await lb.getRank(user2.uid, leaderboardsConfig, friends),
|
||||
).toEqual({ rank: 2, friendsRank: 1, totalXp: 60, ...user2 });
|
||||
|
||||
expect(
|
||||
await lb.getRank(user1.uid, leaderboardsConfig, friends),
|
||||
).toEqual({ rank: 3, friendsRank: 2, totalXp: 50, ...user1 });
|
||||
});
|
||||
});
|
||||
|
||||
it("purgeUserFromDailyLeaderboards", async () => {
|
||||
//GIVEN
|
||||
const cheater = await givenResult(50);
|
||||
const validUser = await givenResult(1000);
|
||||
|
||||
//WHEN
|
||||
await WeeklyXpLeaderboard.purgeUserFromXpLeaderboards(
|
||||
cheater.uid,
|
||||
leaderboardsConfig,
|
||||
);
|
||||
//THEN
|
||||
expect(await lb.getRank(cheater.uid, leaderboardsConfig)).toBeNull();
|
||||
expect(await lb.getResults(0, 50, leaderboardsConfig, true)).toEqual({
|
||||
count: 1,
|
||||
entries: [{ rank: 1, totalXp: 1000, ...validUser }],
|
||||
});
|
||||
});
|
||||
|
||||
async function givenResult(
|
||||
xpGained: number,
|
||||
entry?: Partial<RedisXpLeaderboardEntry>,
|
||||
): Promise<RedisXpLeaderboardEntry> {
|
||||
const uid = new ObjectId().toHexString();
|
||||
const result: RedisXpLeaderboardEntry = {
|
||||
uid,
|
||||
name: `User ${uid}`,
|
||||
lastActivityTimestamp: Date.now(),
|
||||
timeTypedSeconds: 42,
|
||||
badgeId: 2,
|
||||
discordAvatar: `${uid}Avatar`,
|
||||
discordId: `${uid}DiscordId`,
|
||||
isPremium: false,
|
||||
...entry,
|
||||
};
|
||||
|
||||
await lb.addResult(leaderboardsConfig, { xpGained, entry: result });
|
||||
return result;
|
||||
}
|
||||
});
|
||||
});
|
||||
46
backend/__tests__/__integration__/setup-integration-tests.ts
Normal file
46
backend/__tests__/__integration__/setup-integration-tests.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { afterAll, beforeAll, afterEach, vi } from "vitest";
|
||||
import { Collection, Db, MongoClient, WithId } from "mongodb";
|
||||
import { setupCommonMocks } from "../setup-common-mocks";
|
||||
import { getConnection } from "../../src/init/redis";
|
||||
|
||||
process.env["MODE"] = "dev";
|
||||
|
||||
let db: Db;
|
||||
let client: MongoClient;
|
||||
|
||||
beforeAll(async () => {
|
||||
client = new MongoClient(process.env["TEST_DB_URL"] as string);
|
||||
await client.connect();
|
||||
db = client.db();
|
||||
|
||||
vi.mock("../../src/init/db", () => ({
|
||||
__esModule: true,
|
||||
getDb: (): Db => db,
|
||||
collection: <T>(name: string): Collection<WithId<T>> =>
|
||||
db.collection<WithId<T>>(name),
|
||||
close: () => {
|
||||
//
|
||||
},
|
||||
}));
|
||||
|
||||
setupCommonMocks();
|
||||
|
||||
//we compare the time in mongodb to calculate premium status, so we have to use real time here
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
//nothing
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await client?.close();
|
||||
// @ts-ignore
|
||||
db = undefined;
|
||||
//@ts-ignore
|
||||
client = undefined;
|
||||
|
||||
await getConnection()?.quit();
|
||||
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
@@ -0,0 +1,404 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from "vitest";
|
||||
import { Mode, Mode2 } from "@monkeytype/schemas/shared";
|
||||
import * as DailyLeaderboards from "../../../src/utils/daily-leaderboards";
|
||||
import { cleanupKeys, redisSetup } from "../redis";
|
||||
import { Language } from "@monkeytype/schemas/languages";
|
||||
|
||||
import { RedisDailyLeaderboardEntry } from "@monkeytype/schemas/leaderboards";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { Configuration } from "@monkeytype/schemas/configuration";
|
||||
|
||||
const dailyLeaderboardsConfig: Configuration["dailyLeaderboards"] = {
|
||||
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: [],
|
||||
};
|
||||
|
||||
describe("Daily Leaderboards", () => {
|
||||
beforeAll(async () => {
|
||||
await redisSetup();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await cleanupKeys(DailyLeaderboards.__testing.namespace);
|
||||
});
|
||||
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({
|
||||
count: 2,
|
||||
minWpm: 20,
|
||||
entries: [
|
||||
{ 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.getResults(0, 5, dailyLeaderboardsConfig, true),
|
||||
).toEqual(expect.objectContaining({ count: 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.getResults(0, 5, dailyLeaderboardsConfig, true),
|
||||
).toEqual(expect.objectContaining({ count: 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({
|
||||
count: 3,
|
||||
minWpm: 40,
|
||||
entries: [
|
||||
{ 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({
|
||||
count: 5,
|
||||
minWpm: 20,
|
||||
entries: [
|
||||
{ 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({
|
||||
count: 3,
|
||||
minWpm: 40,
|
||||
entries: [
|
||||
{ rank: 1, ...user2, isPremium: undefined },
|
||||
{ rank: 2, ...user1, isPremium: undefined },
|
||||
{ rank: 3, ...user3, isPremium: undefined },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should get for friends only", async () => {
|
||||
//GIVEN
|
||||
const _user1 = await givenResult({ wpm: 90 });
|
||||
const user2 = await givenResult({ wpm: 80 });
|
||||
const _user3 = await givenResult({ wpm: 70 });
|
||||
const user4 = await givenResult({ wpm: 60 });
|
||||
const _user5 = await givenResult({ wpm: 50 });
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(
|
||||
0,
|
||||
5,
|
||||
dailyLeaderboardsConfig,
|
||||
true,
|
||||
[user2.uid, user4.uid, new ObjectId().toHexString()],
|
||||
);
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 2,
|
||||
minWpm: 60,
|
||||
entries: [
|
||||
{ rank: 2, friendsRank: 1, ...user2 },
|
||||
{ rank: 4, friendsRank: 2, ...user4 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should get for friends only with page", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult({ wpm: 105 });
|
||||
const user2 = await givenResult({ wpm: 100 });
|
||||
const _user3 = await givenResult({ wpm: 95 });
|
||||
const user4 = await givenResult({ wpm: 90 });
|
||||
const _user5 = await givenResult({ wpm: 70 });
|
||||
|
||||
//WHEN
|
||||
|
||||
const results = await lb.getResults(
|
||||
1,
|
||||
2,
|
||||
dailyLeaderboardsConfig,
|
||||
true,
|
||||
[user1.uid, user2.uid, user4.uid, new ObjectId().toHexString()],
|
||||
);
|
||||
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 3,
|
||||
minWpm: 90,
|
||||
entries: [{ rank: 4, friendsRank: 3, ...user4 }],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty list if no friends", async () => {
|
||||
//GIVEN
|
||||
|
||||
//WHEN
|
||||
const results = await lb.getResults(
|
||||
0,
|
||||
5,
|
||||
dailyLeaderboardsConfig,
|
||||
true,
|
||||
[],
|
||||
);
|
||||
//THEN
|
||||
expect(results).toEqual({
|
||||
count: 0,
|
||||
minWpm: 0,
|
||||
entries: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRank", () => {
|
||||
it("gets rank", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult({ wpm: 50 });
|
||||
const user2 = await givenResult({ wpm: 60 });
|
||||
|
||||
//WHEN / THEN
|
||||
expect(await lb.getRank(user1.uid, dailyLeaderboardsConfig)).toEqual({
|
||||
rank: 2,
|
||||
...user1,
|
||||
});
|
||||
expect(await lb.getRank(user2.uid, dailyLeaderboardsConfig)).toEqual({
|
||||
rank: 1,
|
||||
...user2,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null for unknown user", async () => {
|
||||
expect(await lb.getRank("decoy", dailyLeaderboardsConfig)).toBeNull();
|
||||
expect(
|
||||
await lb.getRank("decoy", dailyLeaderboardsConfig, [
|
||||
"unknown",
|
||||
"unknown2",
|
||||
]),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("gets rank for friends", async () => {
|
||||
//GIVEN
|
||||
const user1 = await givenResult({ wpm: 50 });
|
||||
const user2 = await givenResult({ wpm: 60 });
|
||||
const _user3 = await givenResult({ wpm: 70 });
|
||||
const friends = [user1.uid, user2.uid, "decoy"];
|
||||
|
||||
//WHEN / THEN
|
||||
expect(
|
||||
await lb.getRank(user2.uid, dailyLeaderboardsConfig, friends),
|
||||
).toEqual({ rank: 2, friendsRank: 1, ...user2 });
|
||||
|
||||
expect(
|
||||
await lb.getRank(user1.uid, dailyLeaderboardsConfig, friends),
|
||||
).toEqual({ rank: 3, friendsRank: 2, ...user1 });
|
||||
});
|
||||
});
|
||||
|
||||
it("purgeUserFromDailyLeaderboards", async () => {
|
||||
//GIVEN
|
||||
const cheater = await givenResult({ wpm: 50 });
|
||||
const user1 = await givenResult({ wpm: 60 });
|
||||
const user2 = 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, true)).toEqual(
|
||||
{
|
||||
count: 2,
|
||||
minWpm: 40,
|
||||
entries: [
|
||||
{ rank: 1, ...user1 },
|
||||
{ rank: 2, ...user2 },
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user