adding monkeytype
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled

This commit is contained in:
Benjamin Falch
2026-04-23 13:53:44 +02:00
parent e214a2fd35
commit 2bc741fb78
1930 changed files with 7590652 additions and 0 deletions

View File

@@ -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);
}

View 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);
});
});
});

View 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,
};
}

View 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",
),
);
});
});
});

View 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();
});
});
});

View 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,
},
]);
});
});
});

View File

@@ -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;
};

View 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" },
}),
]),
);
});
});
});

View 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);
});
});

View 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();
});
});
});

File diff suppressed because it is too large Load Diff

View 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);

View 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)));
}

View File

@@ -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;
}
});
});

View 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();
});

View File

@@ -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;
}
});
});

View File

@@ -0,0 +1,83 @@
import { expect, vi } from "vitest";
import { Configuration } from "@monkeytype/schemas/configuration";
import { randomBytes } from "crypto";
import { hash } from "bcrypt";
import { ObjectId } from "mongodb";
import { base64UrlEncode } from "../../src/utils/misc";
import * as ApeKeyDal from "../../src/dal/ape-keys";
import { DecodedIdToken } from "firebase-admin/auth";
import * as AuthUtils from "../../src/utils/auth";
export async function mockAuthenticateWithApeKey(
uid: string,
config: Configuration,
): Promise<string> {
if (!config.apeKeys.acceptKeys) {
throw Error("config.apeKeys.acceptedKeys needs to be set to true");
}
const { apeKeyBytes, apeKeySaltRounds } = config.apeKeys;
const apiKey = randomBytes(apeKeyBytes).toString("base64url");
const saltyHash = await hash(apiKey, apeKeySaltRounds);
const apeKey: ApeKeyDal.DBApeKey = {
_id: new ObjectId(),
name: "bob",
enabled: true,
uid,
hash: saltyHash,
createdOn: Date.now(),
modifiedOn: Date.now(),
lastUsedOn: -1,
useCount: 0,
};
const apeKeyId = new ObjectId().toHexString();
vi.spyOn(ApeKeyDal, "getApeKey").mockResolvedValue(apeKey);
vi.spyOn(ApeKeyDal, "updateLastUsedOn").mockResolvedValue();
return base64UrlEncode(`${apeKeyId}.${apiKey}`);
}
export function mockBearerAuthentication(uid: string) {
const mockDecodedToken = {
uid,
email: "newuser@mail.com",
iat: Date.now(),
} as DecodedIdToken;
const verifyIdTokenMock = vi.spyOn(AuthUtils, "verifyIdToken");
return {
/**
* Reset the mock and return a default token. Call this method in the `beforeEach` of all tests.
*/
beforeEach: (): void => {
verifyIdTokenMock.mockClear();
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);
},
/**
* Reset the mock results in the authentication to fail.
*/
noAuth: (): void => {
verifyIdTokenMock.mockClear();
},
/**
* verify the authentication has been called
*/
expectToHaveBeenCalled: (): void => {
expect(verifyIdTokenMock).toHaveBeenCalled();
},
/**
* modify the token returned by the mock. This can be used to e.g. return a stale token.
* @param customize
*/
modifyToken: (customize: Partial<DecodedIdToken>): void => {
verifyIdTokenMock.mockClear();
verifyIdTokenMock.mockResolvedValue({
...mockDecodedToken,
...customize,
});
},
};
}

View File

@@ -0,0 +1,24 @@
import { ObjectId } from "mongodb";
import * as ConnectionsDal from "../../src/dal/connections";
export async function createConnection(
data: Partial<ConnectionsDal.DBConnection>,
maxPerUser = 25,
): Promise<ConnectionsDal.DBConnection> {
const defaultName = "user" + new ObjectId().toHexString();
const result = await ConnectionsDal.create(
{
uid: data.initiatorUid ?? new ObjectId().toHexString(),
name: data.initiatorName ?? defaultName,
},
{
uid: data.receiverUid ?? new ObjectId().toHexString(),
name: data.receiverName ?? defaultName,
},
maxPerUser,
);
await ConnectionsDal.__testing
.getCollection()
.updateOne({ _id: result._id }, { $set: data });
return { ...result, ...data };
}

View File

@@ -0,0 +1,17 @@
import request from "supertest";
import app from "../../src/app";
import { ObjectId } from "mongodb";
import { mockBearerAuthentication } from "./auth";
import { beforeEach } from "vitest";
export function setup() {
const mockApp = request(app);
const uid = new ObjectId().toHexString();
const mockAuth = mockBearerAuthentication(uid);
beforeEach(() => {
mockAuth.beforeEach();
});
return { mockApp, uid, mockAuth };
}

View File

@@ -0,0 +1,21 @@
import { expect } from "vitest";
import MonkeyError from "../../src/utils/error";
import { MatcherResult } from "../vitest";
export function enableMonkeyErrorExpects(): void {
expect.extend({
toMatchMonkeyError(
received: MonkeyError,
expected: MonkeyError,
): MatcherResult {
return {
pass:
received.status === expected.status &&
received.message === expected.message,
message: () => "MonkeyError does not match:",
actual: { status: received.status, message: received.message },
expected: { status: expected.status, message: expected.message },
};
},
});
}

View File

@@ -0,0 +1,30 @@
import { expect } from "vitest";
import { REQUEST_MULTIPLIER } from "../../src/middlewares/rate-limit";
import { MatcherResult, ExpectedRateLimit } from "../vitest";
import { Test as SuperTest } from "supertest";
export function enableRateLimitExpects(): void {
expect.extend({
toBeRateLimited: async (
received: SuperTest,
expected: ExpectedRateLimit,
): Promise<MatcherResult> => {
const now = Date.now();
const { headers } = await received.expect(200);
const max =
parseInt(headers["x-ratelimit-limit"] as string) / REQUEST_MULTIPLIER;
const windowMs =
parseInt(headers["x-ratelimit-reset"] as string) * 1000 - now;
return {
pass:
max === expected.max && Math.abs(expected.windowMs - windowMs) < 2500,
message: () =>
"Rate limit max not matching or windowMs is off by more then 2500ms",
actual: { max, windowMs },
expected: expected,
};
},
});
}

View File

@@ -0,0 +1,45 @@
import * as DB from "../../src/init/db";
import * as UserDAL from "../../src/dal/user";
import { ObjectId } from "mongodb";
import { PersonalBest } from "@monkeytype/schemas/shared";
export async function createUser(
user?: Partial<UserDAL.DBUser>,
): Promise<UserDAL.DBUser> {
const uid = new ObjectId().toHexString();
await UserDAL.addUser("user" + uid, uid + "@example.com", uid);
await DB.collection("users").updateOne({ uid }, { $set: { ...user } });
return await UserDAL.getUser(uid, "test");
}
export async function createUserWithoutMigration(
user?: Partial<UserDAL.DBUser>,
): Promise<UserDAL.DBUser> {
const uid = new ObjectId().toHexString();
await UserDAL.addUser("user" + uid, uid + "@example.com", uid);
await DB.collection("users").updateOne({ uid }, { $set: { ...user } });
await DB.collection("users").updateOne(
{ uid },
{ $unset: { testActivity: "" } },
);
return await UserDAL.getUser(uid, "test");
}
export function pb(
wpm: number,
acc: number = 90,
timestamp: number = 1,
): PersonalBest {
return {
acc,
consistency: 100,
difficulty: "normal",
lazyMode: false,
language: "english",
punctuation: false,
raw: wpm + 1,
wpm,
timestamp,
};
}

View File

@@ -0,0 +1,580 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import { ObjectId } from "mongodb";
import * as Configuration from "../../../src/init/configuration";
import * as AdminUuidDal from "../../../src/dal/admin-uids";
import * as UserDal from "../../../src/dal/user";
import * as ReportDal from "../../../src/dal/report";
import * as LogsDal from "../../../src/dal/logs";
import GeorgeQueue from "../../../src/queues/george-queue";
import * as AuthUtil from "../../../src/utils/auth";
import { enableRateLimitExpects } from "../../__testData__/rate-limit";
import Test from "supertest/lib/test";
const { mockApp, uid } = setup();
const configuration = Configuration.getCachedConfiguration();
enableRateLimitExpects();
describe("AdminController", () => {
const isAdminMock = vi.spyOn(AdminUuidDal, "isAdmin");
const logsAddImportantLog = vi.spyOn(LogsDal, "addImportantLog");
beforeEach(async () => {
isAdminMock.mockClear();
await enableAdminEndpoints(true);
isAdminMock.mockResolvedValue(true);
logsAddImportantLog.mockClear().mockResolvedValue();
});
describe("check for admin", () => {
it("should succeed if user is admin", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.get("/admin")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toEqual({
message: "OK",
data: null,
});
expect(isAdminMock).toHaveBeenCalledWith(uid);
});
it("should fail if user is no admin", async () => {
await expectFailForNonAdmin(
mockApp.get("/admin").set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if admin endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp.get("/admin").set("Authorization", `Bearer ${uid}`),
);
});
it("should be rate limited", async () => {
await expect(
mockApp.get("/admin").set("Authorization", `Bearer ${uid}`),
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
describe("toggle ban", () => {
const userBannedMock = vi.spyOn(UserDal, "setBanned");
const georgeBannedMock = vi.spyOn(GeorgeQueue, "userBanned");
const getUserMock = vi.spyOn(UserDal, "getPartialUser");
beforeEach(() => {
[userBannedMock, georgeBannedMock, getUserMock].forEach((it) =>
it.mockClear(),
);
userBannedMock.mockResolvedValue();
});
it("should ban user with discordId", async () => {
//GIVEN
const victimUid = new ObjectId().toHexString();
getUserMock.mockResolvedValue({
banned: false,
discordId: "discordId",
} as any);
//WHEN
const { body } = await mockApp
.post("/admin/toggleBan")
.send({ uid: victimUid })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toEqual({
message: "Ban toggled",
data: { banned: true },
});
expect(getUserMock).toHaveBeenCalledWith(victimUid, "toggle ban", [
"banned",
"discordId",
]);
expect(userBannedMock).toHaveBeenCalledWith(victimUid, true);
expect(georgeBannedMock).toHaveBeenCalledWith("discordId", true);
});
it("should unban user without discordId", async () => {
//GIVEN
const victimUid = new ObjectId().toHexString();
getUserMock.mockResolvedValue({
banned: true,
} as any);
//WHEN
const { body } = await mockApp
.post("/admin/toggleBan")
.send({ uid: victimUid })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toEqual({
message: "Ban toggled",
data: { banned: false },
});
expect(getUserMock).toHaveBeenCalledWith(victimUid, "toggle ban", [
"banned",
"discordId",
]);
expect(userBannedMock).toHaveBeenCalledWith(victimUid, false);
expect(georgeBannedMock).not.toHaveBeenCalled();
});
it("should fail without mandatory properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/admin/toggleBan")
.send({})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"uid" Required'],
});
});
it("should fail with unknown properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/admin/toggleBan")
.send({ uid: new ObjectId().toHexString(), extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if user is no admin", async () => {
await expectFailForNonAdmin(
mockApp
.post("/admin/toggleBan")
.send({ uid: new ObjectId().toHexString() })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if admin endpoints are disabled", async () => {
//GIVEN
await expectFailForDisabledEndpoint(
mockApp
.post("/admin/toggleBan")
.send({ uid: new ObjectId().toHexString() })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should be rate limited", async () => {
//GIVEN
const victimUid = new ObjectId().toHexString();
getUserMock.mockResolvedValue({
banned: false,
discordId: "discordId",
} as any);
//WHEN
await expect(
mockApp
.post("/admin/toggleBan")
.send({ uid: victimUid })
.set("Authorization", `Bearer ${uid}`),
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
describe("clear streak hour offset", () => {
const clearStreakHourOffset = vi.spyOn(UserDal, "clearStreakHourOffset");
beforeEach(() => {
clearStreakHourOffset.mockClear();
clearStreakHourOffset.mockResolvedValue();
});
it("should clear streak hour offset for user", async () => {
//GIVEN
const victimUid = new ObjectId().toHexString();
//WHEN
const { body } = await mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: victimUid })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toEqual({
message: "Streak hour offset cleared",
data: null,
});
expect(clearStreakHourOffset).toHaveBeenCalledWith(victimUid);
});
it("should fail without mandatory properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/admin/clearStreakHourOffset")
.send({})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"uid" Required'],
});
});
it("should fail with unknown properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: new ObjectId().toHexString(), extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if user is no admin", async () => {
await expectFailForNonAdmin(
mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: new ObjectId().toHexString() })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if admin endpoints are disabled", async () => {
//GIVEN
await expectFailForDisabledEndpoint(
mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: new ObjectId().toHexString() })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should be rate limited", async () => {
//GIVEN
const victimUid = new ObjectId().toHexString();
//WHEN
await expect(
mockApp
.post("/admin/clearStreakHourOffset")
.send({ uid: victimUid })
.set("Authorization", `Bearer ${uid}`),
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
describe("accept reports", () => {
const getReportsMock = vi.spyOn(ReportDal, "getReports");
const deleteReportsMock = vi.spyOn(ReportDal, "deleteReports");
const addToInboxMock = vi.spyOn(UserDal, "addToInbox");
beforeEach(() => {
[getReportsMock, deleteReportsMock, addToInboxMock].forEach((it) =>
it.mockClear(),
);
deleteReportsMock.mockResolvedValue();
});
it("should accept reports", async () => {
//GIVEN
const reportOne = {
id: "1",
reason: "one",
} as any as ReportDal.DBReport;
const reportTwo = {
id: "2",
reason: "two",
} as any as ReportDal.DBReport;
getReportsMock.mockResolvedValue([reportOne, reportTwo]);
//WHEN
const { body } = await mockApp
.post("/admin/report/accept")
.send({
reports: [{ reportId: reportOne.id }, { reportId: reportTwo.id }],
})
.set("Authorization", `Bearer ${uid}`)
.expect(200);
expect(body).toEqual({
message: "Reports removed and users notified.",
data: null,
});
expect(addToInboxMock).toHaveBeenCalledTimes(2);
expect(deleteReportsMock).toHaveBeenCalledWith(["1", "2"]);
});
it("should fail wihtout mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/admin/report/accept")
.send({})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"reports" Required'],
});
});
it("should fail with empty reports", async () => {
//WHEN
const { body } = await mockApp
.post("/admin/report/accept")
.send({ reports: [] })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
'"reports" Array must contain at least 1 element(s)',
],
});
});
it("should fail with unknown properties", async () => {
//WHEN
const { body } = await mockApp
.post("/admin/report/accept")
.send({ reports: [{ reportId: "1", extra2: "value" }], extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
`"reports.0" Unrecognized key(s) in object: 'extra2'`,
"Unrecognized key(s) in object: 'extra'",
],
});
});
it("should fail if user is no admin", async () => {
await expectFailForNonAdmin(
mockApp
.post("/admin/report/accept")
.send({ reports: [] })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if admin endpoints are disabled", async () => {
//GIVEN
await expectFailForDisabledEndpoint(
mockApp
.post("/admin/report/accept")
.send({ reports: [] })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should be rate limited", async () => {
//GIVEN
getReportsMock.mockResolvedValue([{ id: "1", reason: "one" } as any]);
//WHEN
await expect(
mockApp
.post("/admin/report/accept")
.send({ reports: [{ reportId: "1" }] })
.set("Authorization", `Bearer ${uid}`),
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
describe("reject reports", () => {
const getReportsMock = vi.spyOn(ReportDal, "getReports");
const deleteReportsMock = vi.spyOn(ReportDal, "deleteReports");
const addToInboxMock = vi.spyOn(UserDal, "addToInbox");
beforeEach(() => {
[getReportsMock, deleteReportsMock, addToInboxMock].forEach((it) => {
it.mockClear();
deleteReportsMock.mockResolvedValue();
});
});
it("should reject reports", async () => {
//GIVEN
const reportOne = {
id: "1",
reason: "one",
} as any as ReportDal.DBReport;
const reportTwo = {
id: "2",
reason: "two",
} as any as ReportDal.DBReport;
getReportsMock.mockResolvedValue([reportOne, reportTwo]);
//WHEN
const { body } = await mockApp
.post("/admin/report/reject")
.send({
reports: [
{ reportId: reportOne.id, reason: "test" },
{ reportId: reportTwo.id },
],
})
.set("Authorization", `Bearer ${uid}`)
.expect(200);
expect(body).toEqual({
message: "Reports removed and users notified.",
data: null,
});
expect(addToInboxMock).toHaveBeenCalledTimes(2);
expect(deleteReportsMock).toHaveBeenCalledWith(["1", "2"]);
});
it("should fail wihtout mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/admin/report/reject")
.send({})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"reports" Required'],
});
});
it("should fail with empty reports", async () => {
//WHEN
const { body } = await mockApp
.post("/admin/report/reject")
.send({ reports: [] })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
'"reports" Array must contain at least 1 element(s)',
],
});
});
it("should fail with unknown properties", async () => {
//WHEN
const { body } = await mockApp
.post("/admin/report/reject")
.send({ reports: [{ reportId: "1", extra2: "value" }], extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
`"reports.0" Unrecognized key(s) in object: 'extra2'`,
"Unrecognized key(s) in object: 'extra'",
],
});
});
it("should fail if user is no admin", async () => {
await expectFailForNonAdmin(
mockApp
.post("/admin/report/reject")
.send({ reports: [] })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if admin endpoints are disabled", async () => {
//GIVEN
await expectFailForDisabledEndpoint(
mockApp
.post("/admin/report/reject")
.send({ reports: [] })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should be rate limited", async () => {
//GIVEN
getReportsMock.mockResolvedValue([{ id: "1", reason: "one" } as any]);
//WHEN
await expect(
mockApp
.post("/admin/report/reject")
.send({ reports: [{ reportId: "1" }] })
.set("Authorization", `Bearer ${uid}`),
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
describe("send forgot password email", () => {
const sendForgotPasswordEmailMock = vi.spyOn(
AuthUtil,
"sendForgotPasswordEmail",
);
beforeEach(() => {
sendForgotPasswordEmailMock.mockClear();
});
it("should send forgot password link", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/admin/sendForgotPasswordEmail")
.send({ email: "meowdec@example.com" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toEqual({
message: "Password reset request email sent.",
data: null,
});
expect(sendForgotPasswordEmailMock).toHaveBeenCalledWith(
"meowdec@example.com",
);
});
it("should be rate limited", async () => {
//WHEN
await expect(
mockApp
.post("/admin/sendForgotPasswordEmail")
.send({ email: "meowdec@example.com" })
.set("Authorization", `Bearer ${uid}`),
).toBeRateLimited({ max: 1, windowMs: 5000 });
});
});
async function expectFailForNonAdmin(call: Test): Promise<void> {
isAdminMock.mockResolvedValue(false);
const { body } = await call.expect(403);
expect(body.message).toEqual("You don't have permission to do this.");
}
async function expectFailForDisabledEndpoint(call: Test): Promise<void> {
await enableAdminEndpoints(false);
const { body } = await call.expect(503);
expect(body.message).toEqual("Admin endpoints are currently disabled.");
}
});
async function enableAdminEndpoints(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.admin = { ...mockConfig.admin, endpointsEnabled: enabled };
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}

View File

@@ -0,0 +1,373 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import { Test as SuperTest } from "supertest";
import * as ApeKeyDal from "../../../src/dal/ape-keys";
import { ObjectId } from "mongodb";
import * as Configuration from "../../../src/init/configuration";
import * as UserDal from "../../../src/dal/user";
const { mockApp, uid } = setup();
const configuration = Configuration.getCachedConfiguration();
describe("ApeKeyController", () => {
const getUserMock = vi.spyOn(UserDal, "getPartialUser");
beforeEach(async () => {
await enableApeKeysEndpoints(true);
getUserMock.mockResolvedValue(user(uid, {}));
vi.useFakeTimers();
vi.setSystemTime(1000);
});
afterEach(() => {
getUserMock.mockClear();
vi.useRealTimers();
});
describe("get ape keys", () => {
const getApeKeysMock = vi.spyOn(ApeKeyDal, "getApeKeys");
afterEach(() => {
getApeKeysMock.mockClear();
});
it("should get the users config", async () => {
//GIVEN
const keyOne = apeKeyDb(uid);
const keyTwo = apeKeyDb(uid);
getApeKeysMock.mockResolvedValue([keyOne, keyTwo]);
//WHEN
const { body } = await mockApp
.get("/ape-keys")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toHaveProperty("message", "ApeKeys retrieved");
expect(body.data).toHaveProperty(keyOne._id.toHexString(), {
name: keyOne.name,
enabled: keyOne.enabled,
createdOn: keyOne.createdOn,
modifiedOn: keyOne.modifiedOn,
lastUsedOn: keyOne.lastUsedOn,
});
expect(body.data).toHaveProperty(keyTwo._id.toHexString(), {
name: keyTwo.name,
enabled: keyTwo.enabled,
createdOn: keyTwo.createdOn,
modifiedOn: keyTwo.modifiedOn,
lastUsedOn: keyTwo.lastUsedOn,
});
expect(body.data).keys([keyOne._id, keyTwo._id]);
expect(getApeKeysMock).toHaveBeenCalledWith(uid);
});
it("should fail if apeKeys endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp.get("/ape-keys").set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if user has no apeKey permissions", async () => {
await expectFailForNoPermissions(
mockApp.get("/ape-keys").set("Authorization", `Bearer ${uid}`),
);
});
});
describe("add ape key", () => {
const addApeKeyMock = vi.spyOn(ApeKeyDal, "addApeKey");
const countApeKeysMock = vi.spyOn(ApeKeyDal, "countApeKeysForUser");
beforeEach(() => {
countApeKeysMock.mockResolvedValue(0);
});
afterEach(() => {
addApeKeyMock.mockClear();
countApeKeysMock.mockClear();
});
it("should add ape key", async () => {
//GIVEN
addApeKeyMock.mockResolvedValue("1");
//WHEN
const { body } = await mockApp
.post("/ape-keys")
.set("Authorization", `Bearer ${uid}`)
.send({ name: "test", enabled: true })
.expect(200);
expect(body.message).toEqual("ApeKey generated");
expect(body.data).keys("apeKey", "apeKeyDetails", "apeKeyId");
expect(body.data.apeKey).not.toBeNull();
expect(body.data.apeKeyDetails).toStrictEqual({
createdOn: 1000,
enabled: true,
lastUsedOn: -1,
modifiedOn: 1000,
name: "test",
});
expect(body.data.apeKeyId).toEqual("1");
expect(addApeKeyMock).toHaveBeenCalledWith(
expect.objectContaining({
createdOn: 1000,
enabled: true,
lastUsedOn: -1,
modifiedOn: 1000,
name: "test",
uid: uid,
useCount: 0,
}),
);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/ape-keys")
.send({})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [`"name" Required`, `"enabled" Required`],
});
});
it("should fail with extra properties", async () => {
//WHEN
const { body } = await mockApp
.post("/ape-keys")
.send({ name: "test", enabled: true, extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if max apeKeys is reached", async () => {
//GIVEN
countApeKeysMock.mockResolvedValue(1);
//WHEN
const { body } = await mockApp
.post("/ape-keys")
.send({ name: "test", enabled: false })
.set("Authorization", `Bearer ${uid}`)
.expect(409);
//THEN
expect(body.message).toEqual(
"Maximum number of ApeKeys have been generated",
);
});
it("should fail if apeKeys endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp
.post("/ape-keys")
.send({ name: "test", enabled: false })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if user has no apeKey permissions", async () => {
await expectFailForNoPermissions(
mockApp
.post("/ape-keys")
.send({ name: "test", enabled: false })
.set("Authorization", `Bearer ${uid}`),
);
});
});
describe("edit ape key", () => {
const editApeKeyMock = vi.spyOn(ApeKeyDal, "editApeKey");
const apeKeyId = new ObjectId().toHexString();
afterEach(() => {
editApeKeyMock.mockClear();
});
it("should edit ape key", async () => {
//GIVEN
editApeKeyMock.mockResolvedValue();
//WHEN
const { body } = await mockApp
.patch(`/ape-keys/${apeKeyId}`)
.send({ name: "new", enabled: false })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body.message).toEqual("ApeKey updated");
expect(editApeKeyMock).toHaveBeenCalledWith(uid, apeKeyId, "new", false);
});
it("should edit ape key with single property", async () => {
//GIVEN
editApeKeyMock.mockResolvedValue();
//WHEN
const { body } = await mockApp
.patch(`/ape-keys/${apeKeyId}`)
.send({ name: "new" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body.message).toEqual("ApeKey updated");
expect(editApeKeyMock).toHaveBeenCalledWith(
uid,
apeKeyId,
"new",
undefined,
);
});
it("should fail with missing path", async () => {
//GIVEN
//WHEN
await mockApp
.patch(`/ape-keys/`)
.set("Authorization", `Bearer ${uid}`)
.expect(404);
});
it("should fail with extra properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.patch(`/ape-keys/${apeKeyId}`)
.send({ name: "new", extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if apeKeys endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp
.patch(`/ape-keys/${apeKeyId}`)
.send({ name: "test", enabled: false })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if user has no apeKey permissions", async () => {
await expectFailForNoPermissions(
mockApp
.patch(`/ape-keys/${apeKeyId}`)
.send({ name: "test", enabled: false })
.set("Authorization", `Bearer ${uid}`),
);
});
});
describe("delete ape key", () => {
const deleteApeKeyMock = vi.spyOn(ApeKeyDal, "deleteApeKey");
const apeKeyId = new ObjectId().toHexString();
afterEach(() => {
deleteApeKeyMock.mockClear();
});
it("should delete ape key", async () => {
//GIVEN
deleteApeKeyMock.mockResolvedValue();
//WHEN
const { body } = await mockApp
.delete(`/ape-keys/${apeKeyId}`)
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body.message).toEqual("ApeKey deleted");
expect(deleteApeKeyMock).toHaveBeenCalledWith(uid, apeKeyId);
});
it("should fail with missing path", async () => {
//GIVEN
//WHEN
await mockApp
.delete(`/ape-keys/`)
.set("Authorization", `Bearer ${uid}`)
.expect(404);
});
it("should fail if apeKeys endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp
.delete(`/ape-keys/${apeKeyId}`)
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail if user has no apeKey permissions", async () => {
await expectFailForNoPermissions(
mockApp
.delete(`/ape-keys/${apeKeyId}`)
.set("Authorization", `Bearer ${uid}`),
);
});
});
async function expectFailForNoPermissions(call: SuperTest): Promise<void> {
getUserMock.mockResolvedValue(user(uid, { canManageApeKeys: false }));
const { body } = await call.expect(403);
expect(body.message).toEqual(
"You have lost access to ape keys, please contact support",
);
}
async function expectFailForDisabledEndpoint(call: SuperTest): Promise<void> {
await enableApeKeysEndpoints(false);
const { body } = await call.expect(503);
expect(body.message).toEqual("ApeKeys are currently disabled.");
}
});
function apeKeyDb(
uid: string,
data?: Partial<ApeKeyDal.DBApeKey>,
): ApeKeyDal.DBApeKey {
return {
_id: new ObjectId(),
uid,
hash: "hash",
useCount: 1,
name: "name",
enabled: true,
createdOn: Math.random() * Date.now(),
lastUsedOn: Math.random() * Date.now(),
modifiedOn: Math.random() * Date.now(),
...data,
};
}
async function enableApeKeysEndpoints(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.apeKeys = {
...mockConfig.apeKeys,
endpointsEnabled: enabled,
maxKeysPerUser: 1,
};
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}
function user(uid: string, data: Partial<UserDal.DBUser>): UserDal.DBUser {
return {
uid,
...data,
} as UserDal.DBUser;
}

View File

@@ -0,0 +1,133 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as ConfigDal from "../../../src/dal/config";
import { ObjectId } from "mongodb";
const { mockApp, uid } = setup();
describe("ConfigController", () => {
describe("get config", () => {
const getConfigMock = vi.spyOn(ConfigDal, "getConfig");
afterEach(() => {
getConfigMock.mockClear();
});
it("should get the users config", async () => {
//GIVEN
getConfigMock.mockResolvedValue({
_id: new ObjectId(),
uid: uid,
config: { language: "english" },
});
//WHEN
const { body } = await mockApp
.get("/configs")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Configuration retrieved",
data: { language: "english" },
});
expect(getConfigMock).toHaveBeenCalledWith(uid);
});
});
describe("update config", () => {
const saveConfigMock = vi.spyOn(ConfigDal, "saveConfig");
afterEach(() => {
saveConfigMock.mockClear();
});
it("should update the users config", async () => {
//GIVEN
saveConfigMock.mockResolvedValue({} as any);
//WHEN
const { body } = await mockApp
.patch("/configs")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({ language: "english" })
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Config updated",
data: null,
});
expect(saveConfigMock).toHaveBeenCalledWith(uid, {
language: "english",
});
});
it("should fail with unknown config", async () => {
//WHEN
const { body } = await mockApp
.patch("/configs")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({ unknownValue: "unknown" })
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [`Unrecognized key(s) in object: 'unknownValue'`],
});
expect(saveConfigMock).not.toHaveBeenCalled();
});
it("should fail with invalid configs", async () => {
//WHEN
const { body } = await mockApp
.patch("/configs")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({ autoSwitchTheme: "yes", confidenceMode: "pretty" })
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [
`"confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
`"autoSwitchTheme" Expected boolean, received string`,
],
});
expect(saveConfigMock).not.toHaveBeenCalled();
});
});
describe("delete config", () => {
const deleteConfigMock = vi.spyOn(ConfigDal, "deleteConfig");
afterEach(() => {
deleteConfigMock.mockClear();
});
it("should delete the users config", async () => {
//GIVEN
deleteConfigMock.mockResolvedValue();
//WHEN
const { body } = await mockApp
.delete("/configs")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Config deleted",
data: null,
});
expect(deleteConfigMock).toHaveBeenCalledWith(uid);
});
});
});

View File

@@ -0,0 +1,183 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import {
BASE_CONFIGURATION,
CONFIGURATION_FORM_SCHEMA,
} from "../../../src/constants/base-configuration";
import * as Configuration from "../../../src/init/configuration";
import type { Configuration as ConfigurationType } from "@monkeytype/schemas/configuration";
import * as Misc from "../../../src/utils/misc";
import * as AdminUuids from "../../../src/dal/admin-uids";
const { mockApp, uid, mockAuth } = setup();
describe("Configuration Controller", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
const isAdminMock = vi.spyOn(AdminUuids, "isAdmin");
beforeEach(() => {
isAdminMock.mockClear();
isDevEnvironmentMock.mockClear();
isDevEnvironmentMock.mockReturnValue(true);
isAdminMock.mockResolvedValue(true);
});
describe("getConfiguration", () => {
it("should get without authentication", async () => {
//GIVEN
//WHEN
const { body } = await mockApp.get("/configuration").expect(200);
//THEN
expect(body).toEqual({
message: "Configuration retrieved",
data: BASE_CONFIGURATION,
});
});
});
describe("getConfigurationSchema", () => {
it("should get without authentication on dev", async () => {
//GIVEN
mockAuth.noAuth();
//WHEN
const { body } = await mockApp.get("/configuration/schema").expect(200);
//THEN
expect(body).toEqual({
message: "Configuration schema retrieved",
data: CONFIGURATION_FORM_SCHEMA,
});
});
it("should fail without authentication on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
//WHEN
await mockApp.get("/configuration/schema").expect(401);
});
it("should get with authentication on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
//WHEN
const { body } = await mockApp
.get("/configuration/schema")
.set("Authorization", "Bearer 123456789")
.expect(200);
//THEN
expect(body).toEqual({
message: "Configuration schema retrieved",
data: CONFIGURATION_FORM_SCHEMA,
});
mockAuth.expectToHaveBeenCalled();
});
it("should fail with non-admin user on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
isAdminMock.mockResolvedValue(false);
//WHEN
const { body } = await mockApp
.get("/configuration/schema")
.set("Authorization", "Bearer 123456789")
.expect(403);
//THEN
expect(body.message).toEqual("You don't have permission to do this.");
mockAuth.expectToHaveBeenCalled();
expect(isAdminMock).toHaveBeenCalledWith(uid);
});
});
describe("updateConfiguration", () => {
const patchConfigurationMock = vi.spyOn(
Configuration,
"patchConfiguration",
);
beforeEach(() => {
patchConfigurationMock.mockClear();
patchConfigurationMock.mockResolvedValue(true);
});
it("should update without authentication on dev", async () => {
//GIVEN
mockAuth.noAuth();
const patch = {
users: {
premium: {
enabled: true,
},
},
} as Partial<ConfigurationType>;
//WHEN
const { body } = await mockApp
.patch("/configuration")
.send({ configuration: patch })
.expect(200);
//THEN
expect(body).toEqual({
message: "Configuration updated",
data: null,
});
expect(patchConfigurationMock).toHaveBeenCalledWith(patch);
});
it("should fail update without authentication on prod", async () => {
//GIVEN
mockAuth.noAuth();
isDevEnvironmentMock.mockReturnValue(false);
//WHEN
await mockApp
.patch("/configuration")
.send({ configuration: {} })
.expect(401);
//THEN
expect(patchConfigurationMock).not.toHaveBeenCalled();
});
it("should update with authentication on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
//WHEN
await mockApp
.patch("/configuration")
.set("Authorization", "Bearer 123456789")
.send({ configuration: {} })
.expect(200);
//THEN
expect(patchConfigurationMock).toHaveBeenCalled();
mockAuth.expectToHaveBeenCalled();
});
it("should fail for non admin users on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
isAdminMock.mockResolvedValue(false);
//WHEN
await mockApp
.patch("/configuration")
.set("Authorization", "Bearer 123456789")
.send({ configuration: {} })
.expect(403);
//THEN
expect(patchConfigurationMock).not.toHaveBeenCalled();
expect(isAdminMock).toHaveBeenCalledWith(uid);
});
});
});

View File

@@ -0,0 +1,397 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import request, { Test as SuperTest } from "supertest";
import app from "../../../src/app";
import { mockBearerAuthentication } from "../../__testData__/auth";
import * as Configuration from "../../../src/init/configuration";
import { ObjectId } from "mongodb";
import * as ConnectionsDal from "../../../src/dal/connections";
import * as UserDal from "../../../src/dal/user";
const mockApp = request(app);
const configuration = Configuration.getCachedConfiguration();
const uid = new ObjectId().toHexString();
const mockAuth = mockBearerAuthentication(uid);
describe("ConnectionsController", () => {
beforeEach(async () => {
await enableConnectionsEndpoints(true);
vi.useFakeTimers();
vi.setSystemTime(1000);
mockAuth.beforeEach();
});
describe("get connections", () => {
const getConnectionsMock = vi.spyOn(ConnectionsDal, "getConnections");
beforeEach(() => {
getConnectionsMock.mockClear();
});
it("should get for the current user", async () => {
//GIVEN
const friend: ConnectionsDal.DBConnection = {
_id: new ObjectId(),
lastModified: 42,
initiatorUid: new ObjectId().toHexString(),
initiatorName: "Bob",
receiverUid: new ObjectId().toHexString(),
receiverName: "Kevin",
status: "pending",
key: "key",
};
getConnectionsMock.mockResolvedValue([friend]);
//WHEN
const { body } = await mockApp
.get("/connections")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body.data).toEqual([
{ ...friend, _id: friend._id.toHexString(), key: undefined },
]);
expect(getConnectionsMock).toHaveBeenCalledWith({
initiatorUid: uid,
receiverUid: uid,
});
});
it("should filter by status", async () => {
//GIVEN
getConnectionsMock.mockResolvedValue([]);
//WHEN
await mockApp
.get("/connections")
.query({ status: "accepted" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(getConnectionsMock).toHaveBeenCalledWith({
initiatorUid: uid,
receiverUid: uid,
status: ["accepted"],
});
});
it("should filter by multiple status", async () => {
//GIVEN
getConnectionsMock.mockResolvedValue([]);
//WHEN
await mockApp
.get("/connections")
.query({ status: ["accepted", "blocked"] })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(getConnectionsMock).toHaveBeenCalledWith({
initiatorUid: uid,
receiverUid: uid,
status: ["accepted", "blocked"],
});
});
it("should filter by type incoming", async () => {
//GIVEN
getConnectionsMock.mockResolvedValue([]);
//WHEN
await mockApp
.get("/connections")
.query({ type: "incoming" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(getConnectionsMock).toHaveBeenCalledWith({
receiverUid: uid,
});
});
it("should filter by type outgoing", async () => {
//GIVEN
getConnectionsMock.mockResolvedValue([]);
//WHEN
await mockApp
.get("/connections")
.query({ type: "outgoing" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(getConnectionsMock).toHaveBeenCalledWith({
initiatorUid: uid,
});
});
it("should filter by multiple types", async () => {
//GIVEN
getConnectionsMock.mockResolvedValue([]);
//WHEN
await mockApp
.get("/connections")
.query({ type: ["incoming", "outgoing"] })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(getConnectionsMock).toHaveBeenCalledWith({
initiatorUid: uid,
receiverUid: uid,
});
});
it("should fail if connections endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp.get("/connections").set("Authorization", `Bearer ${uid}`),
);
});
it("should fail without authentication", async () => {
await mockApp.get("/connections").expect(401);
});
it("should fail for unknown query parameter", async () => {
const { body } = await mockApp
.get("/connections")
.query({ extra: "yes" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toStrictEqual({
message: "Invalid query schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
});
describe("create connection", () => {
const getUserByNameMock = vi.spyOn(UserDal, "getUserByName");
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
const createUserMock = vi.spyOn(ConnectionsDal, "create");
beforeEach(() => {
[getUserByNameMock, getPartialUserMock, createUserMock].forEach((it) =>
it.mockClear(),
);
});
it("should create", async () => {
//GIVEN
const me = { uid, name: "Bob" };
const myFriend = { uid: new ObjectId().toHexString(), name: "Kevin" };
getUserByNameMock.mockResolvedValue(myFriend as any);
getPartialUserMock.mockResolvedValue(me as any);
const result: ConnectionsDal.DBConnection = {
_id: new ObjectId(),
lastModified: 42,
initiatorUid: me.uid,
initiatorName: me.name,
receiverUid: myFriend.uid,
receiverName: myFriend.name,
key: "test",
status: "pending",
};
createUserMock.mockResolvedValue(result);
//WHEN
const { body } = await mockApp
.post("/connections")
.send({ receiverName: "Kevin" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body.data).toEqual({
_id: result._id.toHexString(),
lastModified: 42,
initiatorUid: me.uid,
initiatorName: me.name,
receiverUid: myFriend.uid,
receiverName: myFriend.name,
status: "pending",
});
expect(getUserByNameMock).toHaveBeenCalledWith(
"Kevin",
"create connection",
);
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"create connection",
["uid", "name"],
);
expect(createUserMock).toHaveBeenCalledWith(me, myFriend, 100);
});
it("should fail if user and receiver are the same", async () => {
//GIVEN
const me = { uid, name: "Bob" };
getUserByNameMock.mockResolvedValue(me as any);
getPartialUserMock.mockResolvedValue(me as any);
//WHEN
const { body } = await mockApp
.post("/connections")
.send({ receiverName: "Bob" })
.set("Authorization", `Bearer ${uid}`)
.expect(400);
//THEN
expect(body.message).toEqual("You cannot be your own friend, sorry.");
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/connections")
.send({})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [`"receiverName" Required`],
});
});
it("should fail with extra properties", async () => {
//WHEN
const { body } = await mockApp
.post("/connections")
.send({ receiverName: "1", extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if connections endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp
.post("/connections")
.send({ receiverName: "1" })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail without authentication", async () => {
await mockApp.post("/connections").expect(401);
});
});
describe("delete connection", () => {
const deleteByIdMock = vi.spyOn(ConnectionsDal, "deleteById");
beforeEach(() => {
deleteByIdMock.mockClear().mockResolvedValue();
});
it("should delete by id", async () => {
//WHEN
await mockApp
.delete("/connections/1")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(deleteByIdMock).toHaveBeenCalledWith(uid, "1");
});
it("should fail if connections endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp.delete("/connections/1").set("Authorization", `Bearer ${uid}`),
);
});
it("should fail without authentication", async () => {
await mockApp.delete("/connections/1").expect(401);
});
});
describe("update connection", () => {
const updateStatusMock = vi.spyOn(ConnectionsDal, "updateStatus");
beforeEach(() => {
updateStatusMock.mockClear().mockResolvedValue();
});
it("should accept", async () => {
//WHEN
await mockApp
.patch("/connections/1")
.send({ status: "accepted" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(updateStatusMock).toHaveBeenCalledWith(uid, "1", "accepted");
});
it("should block", async () => {
//WHEN
await mockApp
.patch("/connections/1")
.send({ status: "blocked" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(updateStatusMock).toHaveBeenCalledWith(uid, "1", "blocked");
});
it("should fail for invalid status", async () => {
const { body } = await mockApp
.patch("/connections/1")
.send({ status: "invalid" })
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
`"status" Invalid enum value. Expected 'accepted' | 'blocked', received 'invalid'`,
],
});
});
it("should fail if connections endpoints are disabled", async () => {
await expectFailForDisabledEndpoint(
mockApp
.patch("/connections/1")
.send({ status: "accepted" })
.set("Authorization", `Bearer ${uid}`),
);
});
it("should fail without authentication", async () => {
await mockApp
.patch("/connections/1")
.send({ status: "accepted" })
.expect(401);
});
});
});
async function enableConnectionsEndpoints(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.connections = { ...mockConfig.connections, enabled };
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}
async function expectFailForDisabledEndpoint(call: SuperTest): Promise<void> {
await enableConnectionsEndpoints(false);
const { body } = await call.expect(503);
expect(body.message).toEqual("Connections are not available at this time.");
}

View File

@@ -0,0 +1,56 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as Misc from "../../../src/utils/misc";
const { mockApp } = setup();
describe("DevController", () => {
describe("generate testData", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
beforeEach(() => {
isDevEnvironmentMock.mockClear();
isDevEnvironmentMock.mockReturnValue(true);
});
it("should fail on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.set("Authorization", "Bearer 123456789")
.send({ username: "test" })
.expect(503);
//THEN
expect(body.message).toEqual(
"Development endpoints are only available in DEV mode.",
);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({})
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [`"username" Required`],
});
});
it("should fail with unknown properties", async () => {
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({ username: "Bob", extra: "value" })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,501 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as PresetDal from "../../../src/dal/preset";
import { ObjectId } from "mongodb";
const { mockApp, uid } = setup();
describe("PresetController", () => {
describe("get presets", () => {
const getPresetsMock = vi.spyOn(PresetDal, "getPresets");
afterEach(() => {
getPresetsMock.mockClear();
});
it("should get the users presets", async () => {
//GIVEN
const presetOne = {
_id: new ObjectId(),
uid: uid,
name: "test1",
config: { language: "english" },
};
const presetTwo = {
_id: new ObjectId(),
uid: uid,
name: "test2",
settingGroups: ["hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
};
//@ts-expect-error
getPresetsMock.mockResolvedValue([presetOne, presetTwo]);
//WHEN
const { body } = await mockApp
.get("/presets")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Presets retrieved",
data: [
{
_id: presetOne._id.toHexString(),
name: "test1",
config: { language: "english" },
},
{
_id: presetTwo._id.toHexString(),
name: "test2",
settingGroups: ["hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
},
],
});
expect(getPresetsMock).toHaveBeenCalledWith(uid);
});
it("should return empty array if user has no presets", async () => {
//GIVEN
getPresetsMock.mockResolvedValue([]);
//WHEN
const { body } = await mockApp
.get("/presets")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Presets retrieved",
data: [],
});
expect(getPresetsMock).toHaveBeenCalledWith(uid);
});
});
describe("add preset", () => {
const addPresetMock = vi.spyOn(PresetDal, "addPreset");
afterEach(() => {
addPresetMock.mockClear();
});
it("should add the users full preset", async () => {
//GIVEN
addPresetMock.mockResolvedValue({ presetId: "1" });
//WHEN
const { body } = await mockApp
.post("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
name: "new",
config: {
language: "english",
tags: ["one", "two"],
},
})
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Preset created",
data: { presetId: "1" },
});
expect(addPresetMock).toHaveBeenCalledWith(uid, {
name: "new",
config: { language: "english", tags: ["one", "two"] },
});
});
it("should add the users partial preset", async () => {
//GIVEN
addPresetMock.mockResolvedValue({ presetId: "1" });
//WHEN
const { body } = await mockApp
.post("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
name: "new",
settingGroups: ["hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
})
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Preset created",
data: { presetId: "1" },
});
expect(addPresetMock).toHaveBeenCalledWith(uid, {
name: "new",
settingGroups: ["hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
});
});
it("should fail for no setting groups in partial presets", async () => {
//WHEN
const { body } = await mockApp
.post("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
name: "update",
settingGroups: [],
config: {},
})
.expect(422);
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [
`"settingGroups" Array must contain at least 1 element(s)`,
],
});
expect(addPresetMock).not.toHaveBeenCalled();
});
it("should not fail with emtpy config", async () => {
//GIVEN
addPresetMock.mockResolvedValue({ presetId: "1" });
//WHEN
const { body } = await mockApp
.post("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({ name: "new", config: {} })
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Preset created",
data: { presetId: "1" },
});
expect(addPresetMock).toHaveBeenCalledWith(uid, {
name: "new",
config: {},
});
});
it("should fail with missing mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({})
.expect(422);
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [`"name" Required`, `"config" Required`],
});
expect(addPresetMock).not.toHaveBeenCalled();
});
it("should fail with invalid preset", async () => {
//WHEN
const { body } = await mockApp
.post("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
_id: "1",
name: "update",
extra: "extra",
config: {
extra: "extra",
autoSwitchTheme: "yes",
confidenceMode: "pretty",
},
})
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [
`"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
`"config.autoSwitchTheme" Expected boolean, received string`,
`"config" Unrecognized key(s) in object: 'extra'`,
`Unrecognized key(s) in object: '_id', 'extra'`,
],
});
expect(addPresetMock).not.toHaveBeenCalled();
});
it("should fail with duplicate group settings in partial preset", async () => {
//WHEN
const { body } = await mockApp
.post("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
name: "new",
settingGroups: ["hideElements", "hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
})
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [`"settingGroups" No duplicates allowed.`],
});
expect(addPresetMock).not.toHaveBeenCalled();
});
});
describe("update preset", () => {
const editPresetMock = vi.spyOn(PresetDal, "editPreset");
afterEach(() => {
editPresetMock.mockClear();
});
it("should update the users preset", async () => {
//GIVEN
editPresetMock.mockResolvedValue({} as any);
//WHEN
const { body } = await mockApp
.patch("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
_id: "1",
name: "new",
config: {
language: "english",
tags: ["one", "two"],
},
})
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Preset updated",
data: null,
});
expect(editPresetMock).toHaveBeenCalledWith(uid, {
_id: "1",
name: "new",
config: { language: "english", tags: ["one", "two"] },
});
});
it("should update the users partial preset", async () => {
//GIVEN
editPresetMock.mockResolvedValue({} as any);
//WHEN
const { body } = await mockApp
.patch("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
_id: "1",
name: "new",
settingGroups: ["hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
})
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Preset updated",
data: null,
});
expect(editPresetMock).toHaveBeenCalledWith(uid, {
_id: "1",
name: "new",
settingGroups: ["hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
});
});
it("should not fail with emtpy config", async () => {
//GIVEN
editPresetMock.mockResolvedValue({} as any);
//WHEN
const { body } = await mockApp
.patch("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({ _id: "1", name: "new", config: {} })
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Preset updated",
data: null,
});
expect(editPresetMock).toHaveBeenCalledWith(uid, {
_id: "1",
name: "new",
config: {},
});
});
it("should fail with missing mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.patch("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({})
.expect(422);
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [`"_id" Required`, `"name" Required`],
});
expect(editPresetMock).not.toHaveBeenCalled();
});
it("should fail with invalid preset", async () => {
//WHEN
const { body } = await mockApp
.patch("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
_id: "1",
name: "update",
extra: "extra",
settingGroups: ["mappers"],
config: {
extra: "extra",
autoSwitchTheme: "yes",
confidenceMode: "pretty",
},
})
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [
`"settingGroups.0" Invalid enum value. Expected 'test' | 'behavior' | 'input' | 'sound' | 'caret' | 'appearance' | 'theme' | 'hideElements' | 'hidden' | 'ads', received 'mappers'`,
`"config.confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
`"config.autoSwitchTheme" Expected boolean, received string`,
`"config" Unrecognized key(s) in object: 'extra'`,
`Unrecognized key(s) in object: 'extra'`,
],
});
expect(editPresetMock).not.toHaveBeenCalled();
});
it("should fail with duplicate group settings in partial preset", async () => {
//WHEN
const { body } = await mockApp
.patch("/presets")
.set("Authorization", `Bearer ${uid}`)
.accept("application/json")
.send({
_id: "1",
name: "new",
settingGroups: ["hideElements", "hideElements"],
config: {
showKeyTips: true,
capsLockWarning: true,
showOutOfFocusWarning: true,
showAverage: "off",
},
})
.expect(422);
//THEN
expect(body).toStrictEqual({
message: "Invalid request data schema",
validationErrors: [`"settingGroups" No duplicates allowed.`],
});
expect(editPresetMock).not.toHaveBeenCalled();
});
});
describe("delete config", () => {
const deletePresetMock = vi.spyOn(PresetDal, "removePreset");
afterEach(() => {
deletePresetMock.mockClear();
});
it("should delete the users preset", async () => {
//GIVEN
deletePresetMock.mockResolvedValue();
//WHEN
const { body } = await mockApp
.delete("/presets/1")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toStrictEqual({
message: "Preset deleted",
data: null,
});
expect(deletePresetMock).toHaveBeenCalledWith(uid, "1");
});
it("should fail without preset _id", async () => {
//GIVEN
deletePresetMock.mockResolvedValue();
//WHEN
await mockApp
.delete("/presets/")
.set("Authorization", `Bearer ${uid}`)
.expect(404);
expect(deletePresetMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,81 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as PsaDal from "../../../src/dal/psa";
import * as Prometheus from "../../../src/utils/prometheus";
import { ObjectId } from "mongodb";
const { mockApp, uid } = setup();
describe("Psa Controller", () => {
describe("get psa", () => {
const getPsaMock = vi.spyOn(PsaDal, "get");
const recordClientVersionMock = vi.spyOn(Prometheus, "recordClientVersion");
afterEach(() => {
getPsaMock.mockClear();
recordClientVersionMock.mockClear();
});
it("get psas without authorization", async () => {
//GIVEN
const psaOne: PsaDal.DBPSA = {
_id: new ObjectId(),
message: "test2",
date: 1000,
level: 1,
sticky: true,
};
const psaTwo: PsaDal.DBPSA = {
_id: new ObjectId(),
message: "test2",
date: 2000,
level: 2,
sticky: false,
};
getPsaMock.mockResolvedValue([psaOne, psaTwo]);
//WHEN
const { body } = await mockApp.get("/psas").expect(200);
//THEN
expect(body).toEqual({
message: "PSAs retrieved",
data: [
{
_id: psaOne._id.toHexString(),
date: 1000,
level: 1,
message: "test2",
sticky: true,
},
{
_id: psaTwo._id.toHexString(),
date: 2000,
level: 2,
message: "test2",
sticky: false,
},
],
});
expect(recordClientVersionMock).toHaveBeenCalledWith("unknown");
});
it("get psas with authorization", async () => {
await mockApp
.get("/psas")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
});
it("get psas records x-client-version", async () => {
await mockApp.get("/psas").set("x-client-version", "1.0").expect(200);
expect(recordClientVersionMock).toHaveBeenCalledWith("1.0");
});
it("get psas records client-version", async () => {
await mockApp.get("/psas").set("client-version", "2.0").expect(200);
expect(recordClientVersionMock).toHaveBeenCalledWith("2.0");
});
});
});

View File

@@ -0,0 +1,144 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as PublicDal from "../../../src/dal/public";
const { mockApp } = setup();
describe("PublicController", () => {
describe("get speed histogram", () => {
const getSpeedHistogramMock = vi.spyOn(PublicDal, "getSpeedHistogram");
afterEach(() => {
getSpeedHistogramMock.mockClear();
});
it("gets for english time 60", async () => {
//GIVEN
getSpeedHistogramMock.mockResolvedValue({ "0": 1, "10": 2 });
//WHEN
const { body } = await mockApp
.get("/public/speedHistogram")
.query({ language: "english", mode: "time", mode2: "60" })
.expect(200);
//THEN
expect(body).toEqual({
message: "Public speed histogram retrieved",
data: { "0": 1, "10": 2 },
});
expect(getSpeedHistogramMock).toHaveBeenCalledWith(
"english",
"time",
"60",
);
});
it("gets for mode", async () => {
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
const response = await mockApp
.get("/public/speedHistogram")
.query({ language: "english", mode, mode2: "custom" });
expect(response.status, "for mode " + mode).toEqual(200);
}
});
it("gets for mode2", async () => {
for (const mode2 of [
"10",
"25",
"50",
"100",
"15",
"30",
"60",
"120",
"zen",
"custom",
]) {
const response = await mockApp
.get("/public/speedHistogram")
.query({ language: "english", mode: "words", mode2 });
expect(response.status, "for mode2 " + mode2).toEqual(200);
}
});
it("fails for missing query", async () => {
const { body } = await mockApp.get("/public/speedHistogram").expect(422);
expect(body).toEqual({
message: "Invalid query schema",
validationErrors: [
'"language" Required',
'"mode" Required',
'"mode2" Needs to be either a number, "zen" or "custom".',
],
});
});
it("fails for invalid query", async () => {
const { body } = await mockApp
.get("/public/speedHistogram")
.query({
language: "en?gli.sh",
mode: "unknownMode",
mode2: "unknownMode2",
})
.expect(422);
expect(body).toEqual({
message: "Invalid query schema",
validationErrors: [
'"language" Invalid enum value. Must be a supported language',
`"mode" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'unknownMode'`,
'"mode2" Needs to be a number or a number represented as a string e.g. "10".',
],
});
});
it("fails for unknown query", async () => {
const { body } = await mockApp
.get("/public/speedHistogram")
.query({
language: "english",
mode: "time",
mode2: "60",
extra: "value",
})
.expect(422);
expect(body).toEqual({
message: "Invalid query schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
});
describe("get typing stats", () => {
const getTypingStatsMock = vi.spyOn(PublicDal, "getTypingStats");
afterEach(() => {
getTypingStatsMock.mockClear();
});
it("gets without authentication", async () => {
//GIVEN
getTypingStatsMock.mockResolvedValue({
testsCompleted: 23,
testsStarted: 42,
timeTyping: 1000,
} as any);
//WHEN
const { body } = await mockApp.get("/public/typingStats").expect(200);
//THEN
expect(body).toEqual({
message: "Public typing stats retrieved",
data: {
testsCompleted: 23,
testsStarted: 42,
timeTyping: 1000,
},
});
});
});
});

View File

@@ -0,0 +1,896 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as Configuration from "../../../src/init/configuration";
import * as UserDal from "../../../src/dal/user";
import * as NewQuotesDal from "../../../src/dal/new-quotes";
import type { DBNewQuote } from "../../../src/dal/new-quotes";
import * as QuoteRatingsDal from "../../../src/dal/quote-ratings";
import * as ReportDal from "../../../src/dal/report";
import * as LogsDal from "../../../src/dal/logs";
import * as Captcha from "../../../src/utils/captcha";
import { ObjectId } from "mongodb";
import { ApproveQuote } from "@monkeytype/schemas/quotes";
const { mockApp, uid } = setup();
const configuration = Configuration.getCachedConfiguration();
describe("QuotesController", () => {
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
const logsAddLogMock = vi.spyOn(LogsDal, "addLog");
beforeEach(() => {
enableQuotes(true);
const user = { quoteMod: true, name: "Bob" } as any;
getPartialUserMock.mockClear().mockResolvedValue(user);
logsAddLogMock.mockClear().mockResolvedValue();
});
describe("getQuotes", () => {
const getQuotesMock = vi.spyOn(NewQuotesDal, "get");
beforeEach(() => {
getQuotesMock.mockClear();
getQuotesMock.mockResolvedValue([]);
});
it("should return quotes", async () => {
//GIVEN
const quoteOne: DBNewQuote = {
_id: new ObjectId(),
text: "test",
source: "Bob",
language: "english",
submittedBy: "Kevin",
timestamp: 1000,
approved: true,
};
const quoteTwo: DBNewQuote = {
_id: new ObjectId(),
text: "test2",
source: "Stuart",
language: "english",
submittedBy: "Kevin",
timestamp: 2000,
approved: false,
};
getQuotesMock.mockResolvedValue([quoteOne, quoteTwo]);
//WHEN
const { body } = await mockApp
.get("/quotes")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body.message).toEqual("Quote submissions retrieved");
expect(body.data).toEqual([
{ ...quoteOne, _id: quoteOne._id.toHexString() },
{
...quoteTwo,
_id: quoteTwo._id.toHexString(),
},
]);
expect(getQuotesMock).toHaveBeenCalledWith("all");
});
it("should return quotes with quoteMod", async () => {
//GIVEN
getPartialUserMock
.mockClear()
.mockResolvedValue({ quoteMod: "english" } as any);
//WHEN
await mockApp
.get("/quotes")
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(getQuotesMock).toHaveBeenCalledWith("english");
});
it("should fail with quoteMod false", async () => {
//GIVEN
getPartialUserMock
.mockClear()
.mockResolvedValue({ quoteMod: false } as any);
//WHEN
const { body } = await mockApp
.get("/quotes")
.set("Authorization", `Bearer ${uid}`)
.expect(403);
//THEN
expect(body.message).toEqual("You don't have permission to do this.");
expect(getQuotesMock).not.toHaveBeenCalled();
});
it("should fail with quoteMod empty", async () => {
//GIVEN
getPartialUserMock.mockClear().mockResolvedValue({ quoteMod: "" } as any);
//WHEN
const { body } = await mockApp
.get("/quotes")
.set("Authorization", `Bearer ${uid}`)
.expect(403);
//THEN
expect(body.message).toEqual("You don't have permission to do this.");
expect(getQuotesMock).not.toHaveBeenCalled();
});
it("should fail without authentication", async () => {
await mockApp.get("/quotes").expect(401);
});
});
describe("isSubmissionsEnabled", () => {
it("should return for quotes enabled without authentication", async () => {
//GIVEN
enableQuotes(true);
//WHEN
const { body } = await mockApp
.get("/quotes/isSubmissionEnabled")
.expect(200);
expect(body).toEqual({
message: "Quote submission enabled",
data: { isEnabled: true },
});
});
it("should return for quotes disabled without authentication", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.get("/quotes/isSubmissionEnabled")
.expect(200);
expect(body).toEqual({
message: "Quote submission enabled",
data: { isEnabled: true },
});
});
});
describe("addQuote", () => {
const addQuoteMock = vi.spyOn(NewQuotesDal, "add");
const verifyCaptchaMock = vi.spyOn(Captcha, "verify");
beforeEach(() => {
addQuoteMock.mockClear();
addQuoteMock.mockResolvedValue({} as any);
verifyCaptchaMock.mockClear();
verifyCaptchaMock.mockResolvedValue(true);
});
it("should add quote", async () => {
//GIVEN
const newQuote = {
text: new Array(60).fill("a").join(""),
source: "Bob",
language: "english",
captcha: "captcha",
};
//WHEN
const { body } = await mockApp
.post("/quotes")
.set("Authorization", `Bearer ${uid}`)
.send(newQuote)
.expect(200);
//THEN
expect(body).toEqual({
message: "Quote submission added",
data: null,
});
expect(addQuoteMock).toHaveBeenCalledWith(
newQuote.text,
newQuote.source,
newQuote.language,
uid,
);
expect(verifyCaptchaMock).toHaveBeenCalledWith(newQuote.captcha);
});
it("should fail without authentication", async () => {
await mockApp.post("/quotes").expect(401);
});
it("should fail if feature is disabled", async () => {
//GIVEN
enableQuotes(false);
//WHEN
const { body } = await mockApp
.post("/quotes")
.set("Authorization", `Bearer ${uid}`)
.expect(503);
//THEN
expect(body.message).toEqual(
"Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up.",
);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes")
.set("Authorization", `Bearer ${uid}`)
.expect(422);
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
'"text" Required',
'"source" Required',
'"language" Required',
'"captcha" Required',
],
});
});
it("should fail with unknown properties", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes")
.send({
text: new Array(60).fill("a").join(""),
source: "Bob",
language: "english",
captcha: "captcha",
extra: "value",
})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail with invalid capture", async () => {
//GIVEN
verifyCaptchaMock.mockResolvedValue(false);
//WHEN
const { body } = await mockApp
.post("/quotes")
.send({
text: new Array(60).fill("a").join(""),
source: "Bob",
language: "english",
captcha: "captcha",
})
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body.message).toEqual("Captcha check failed");
});
});
describe("approveQuote", () => {
const approveQuoteMock = vi.spyOn(NewQuotesDal, "approve");
beforeEach(() => {
approveQuoteMock.mockClear();
});
it("should approve", async () => {
//GiVEN
const quoteId = new ObjectId().toHexString();
const quote: ApproveQuote = {
id: 100,
text: "text",
source: "source",
length: 10,
approvedBy: "Kevin",
};
approveQuoteMock.mockResolvedValue({
message: "ok",
quote,
});
//WHEN
const { body } = await mockApp
.post("/quotes/approve")
.set("Authorization", `Bearer ${uid}`)
.send({
quoteId,
editText: "editedText",
editSource: "editedSource",
})
.expect(200);
//THEN
expect(body).toEqual({
message: "ok",
data: quote,
});
expect(approveQuoteMock).toHaveBeenCalledWith(
quoteId,
"editedText",
"editedSource",
"Bob",
);
});
it("should approve with optional parameters as null", async () => {
//GiVEN
const quoteId = new ObjectId().toHexString();
approveQuoteMock.mockResolvedValue({
message: "ok",
quote: {} as any,
});
//WHEN
const { body } = await mockApp
.post("/quotes/approve")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId, editText: null, editSource: null })
.expect(200);
//THEN
expect(body).toEqual({
message: "ok",
data: {},
});
expect(approveQuoteMock).toHaveBeenCalledWith(
quoteId,
undefined,
undefined,
"Bob",
);
});
it("should approve without optional parameters", async () => {
//GiVEN
const quoteId = new ObjectId().toHexString();
approveQuoteMock.mockResolvedValue({
message: "ok",
quote: {} as any,
});
//WHEN
const { body } = await mockApp
.post("/quotes/approve")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId })
.expect(200);
//THEN
expect(body).toEqual({
message: "ok",
data: {},
});
expect(approveQuoteMock).toHaveBeenCalledWith(
quoteId,
undefined,
undefined,
"Bob",
);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/approve")
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"quoteId" Required'],
});
});
it("should fail with unknown properties", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/approve")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId: new ObjectId().toHexString(), extra: "value" })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if user is no quote mod", async () => {
//GIVEN
getPartialUserMock.mockClear().mockResolvedValue({} as any);
//WHEN
const { body } = await mockApp
.post("/quotes/approve")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId: new ObjectId().toHexString() })
.expect(403);
//THEN
expect(body.message).toEqual("You don't have permission to do this.");
});
it("should fail without authentication", async () => {
await mockApp
.post("/quotes/approve")
.send({ quoteId: new ObjectId().toHexString() })
.expect(401);
});
});
describe("refuseQuote", () => {
const refuseQuoteMock = vi.spyOn(NewQuotesDal, "refuse");
beforeEach(() => {
refuseQuoteMock.mockClear();
refuseQuoteMock.mockResolvedValue();
});
it("should refuse quote", async () => {
//GIVEN
const quoteId = new ObjectId().toHexString();
//WHEN
const { body } = await mockApp
.post("/quotes/reject")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId })
.expect(200);
//THEN
expect(body).toEqual({
message: "Quote refused",
data: null,
});
expect(refuseQuoteMock).toHaveBeenCalledWith(quoteId);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/reject")
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"quoteId" Required'],
});
});
it("should fail with unknown properties", async () => {
//GIVEN
const quoteId = new ObjectId().toHexString();
//WHEN
const { body } = await mockApp
.post("/quotes/reject")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId, extra: "value" })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail if user is no quote mod", async () => {
//GIVEN
getPartialUserMock.mockClear().mockResolvedValue({} as any);
const quoteId = new ObjectId().toHexString();
//WHEN
const { body } = await mockApp
.post("/quotes/reject")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId })
.expect(403);
//THEN
expect(body.message).toEqual("You don't have permission to do this.");
});
it("should fail without authentication", async () => {
await mockApp
.post("/quotes/reject")
.send({ quoteId: new ObjectId().toHexString() })
.expect(401);
});
});
describe("getRating", () => {
const getRatingMock = vi.spyOn(QuoteRatingsDal, "get");
beforeEach(() => {
getRatingMock.mockClear();
});
it("should get", async () => {
//GIVEN
const quoteRating = {
_id: new ObjectId(),
average: 2,
language: "english",
quoteId: 23,
ratings: 100,
totalRating: 122,
};
getRatingMock.mockResolvedValue(quoteRating as any);
//WHEN
const { body } = await mockApp
.get("/quotes/rating")
.query({ quoteId: 42, language: "english" })
.set("Authorization", `Bearer ${uid}`)
.expect(200);
//THEN
expect(body).toEqual({
message: "Rating retrieved",
data: { ...quoteRating, _id: quoteRating._id.toHexString() },
});
expect(getRatingMock).toHaveBeenCalledWith(42, "english");
});
it("should fail without mandatory query parameters", async () => {
//WHEN
const { body } = await mockApp
.get("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid query schema",
validationErrors: ['"quoteId" Invalid input', '"language" Required'],
});
});
it("should fail with unknown query parameters", async () => {
//WHEN
const { body } = await mockApp
.get("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.query({ quoteId: 42, language: "english", extra: "value" })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid query schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail without authentication", async () => {
await mockApp
.get("/quotes/rating")
.query({ quoteId: 42, language: "english" })
.expect(401);
});
});
describe("submitRating", () => {
const updateQuotesRatingsMock = vi.spyOn(UserDal, "updateQuoteRatings");
const submitQuoteRating = vi.spyOn(QuoteRatingsDal, "submit");
beforeEach(() => {
getPartialUserMock
.mockClear()
.mockResolvedValue({ quoteRatings: null } as any);
updateQuotesRatingsMock.mockClear().mockResolvedValue({} as any);
submitQuoteRating.mockClear().mockResolvedValue();
});
it("should submit new rating", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.send({
quoteId: 23,
rating: 4,
language: "english",
})
.expect(200);
//THEN
expect(body).toEqual({
message: "Rating submitted",
data: null,
});
expect(submitQuoteRating).toHaveBeenCalledWith(23, "english", 4, false);
expect(updateQuotesRatingsMock).toHaveBeenCalledWith(uid, {
english: { "23": 4 },
});
});
it("should update existing rating", async () => {
//GIVEN
getPartialUserMock.mockClear().mockResolvedValue({
quoteRatings: { german: { "4": 1 }, english: { "5": 5, "23": 4 } },
} as any);
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.send({
quoteId: 23,
rating: 2,
language: "english",
})
.expect(200);
//THEN
expect(body).toEqual({
message: "Rating updated",
data: null,
});
expect(submitQuoteRating).toHaveBeenCalledWith(23, "english", -2, true);
expect(updateQuotesRatingsMock).toHaveBeenCalledWith(uid, {
german: { "4": 1 },
english: { "5": 5, "23": 2 },
});
});
it("should update existing rating with same rating", async () => {
//GIVEN
getPartialUserMock.mockClear().mockResolvedValue({
quoteRatings: { german: { "4": 1 }, english: { "5": 5, "23": 4 } },
} as any);
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.send({
quoteId: 23,
rating: 4,
language: "english",
})
.expect(200);
//THEN
expect(body).toEqual({
message: "Rating updated",
data: null,
});
expect(submitQuoteRating).toHaveBeenCalledWith(23, "english", 0, true);
expect(updateQuotesRatingsMock).toHaveBeenCalledWith(uid, {
german: { "4": 1 },
english: { "5": 5, "23": 4 },
});
});
it("should fail with missing mandatory parameter", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
'"quoteId" Invalid input',
'"language" Required',
'"rating" Required',
],
});
});
it("should fail with unknown parameter", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId: 23, language: "english", rating: 5, extra: "value" })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should fail with zero rating", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId: 23, language: "english", rating: 0 })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
'"rating" Number must be greater than or equal to 1',
],
});
});
it("should fail with rating bigger than 5", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId: 23, language: "english", rating: 6 })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"rating" Number must be less than or equal to 5'],
});
});
it("should fail with non-integer rating", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/rating")
.set("Authorization", `Bearer ${uid}`)
.send({ quoteId: 23, language: "english", rating: 2.5 })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"rating" Expected integer, received float'],
});
});
it("should fail without authentication", async () => {
await mockApp.post("/quotes/rating").expect(401);
});
});
describe("reportQuote", () => {
const verifyCaptchaMock = vi.spyOn(Captcha, "verify");
const createReportMock = vi.spyOn(ReportDal, "createReport");
beforeEach(() => {
enableQuoteReporting(true);
verifyCaptchaMock.mockClear().mockResolvedValue(true);
createReportMock.mockClear().mockResolvedValue();
});
it("should report quote", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/quotes/report")
.set("Authorization", `Bearer ${uid}`)
.send({
quoteId: "23", //quoteId is string on this endpoint
quoteLanguage: "english",
reason: "Inappropriate content",
comment: "I don't like this.",
captcha: "captcha",
});
//.expect(200);
//THEN
expect(body).toEqual({
message: "Quote reported",
data: null,
});
expect(verifyCaptchaMock).toHaveBeenCalledWith("captcha");
expect(createReportMock).toHaveBeenCalledWith(
expect.objectContaining({
type: "quote",
uid,
contentId: "english-23",
reason: "Inappropriate content",
comment: "I don't like this.",
}),
10, //configuration maxReport
20, //configuration contentReportLimit
);
});
it("should report quote without comment", async () => {
await mockApp
.post("/quotes/report")
.set("Authorization", `Bearer ${uid}`)
.send({
quoteId: "23", //quoteId is string on this endpoint
quoteLanguage: "english",
reason: "Inappropriate content",
captcha: "captcha",
})
.expect(200);
});
it("should report quote with empty comment", async () => {
await mockApp
.post("/quotes/report")
.set("Authorization", `Bearer ${uid}`)
.send({
quoteId: "23", //quoteId is string on this endpoint
quoteLanguage: "english",
reason: "Inappropriate content",
comment: "",
captcha: "captcha",
})
.expect(200);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/quotes/report")
.set("Authorization", `Bearer ${uid}`)
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
'"quoteId" Invalid input',
'"quoteLanguage" Required',
'"reason" Required',
'"captcha" Required',
],
});
});
it("should fail if feature is disabled", async () => {
//GIVEN
enableQuoteReporting(false);
//WHEN
const { body } = await mockApp
.post("/quotes/report")
.set("Authorization", `Bearer ${uid}`)
.expect(503);
//THEN
expect(body.message).toEqual("Quote reporting is unavailable.");
});
it("should fail if user cannot report", async () => {
//GIVEN
getPartialUserMock
.mockClear()
.mockResolvedValue({ canReport: false } as any);
//WHEN
const { body } = await mockApp
.post("/quotes/report")
.set("Authorization", `Bearer ${uid}`)
.expect(403);
//THEN
expect(body.message).toEqual("You don't have permission to do this.");
});
});
});
async function enableQuotes(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.quotes = { ...mockConfig.quotes, submissionsEnabled: enabled };
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}
async function enableQuoteReporting(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.quotes.reporting = {
...mockConfig.quotes.reporting,
enabled,
maxReports: 10,
contentReportLimit: 20,
};
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}

View File

@@ -0,0 +1,891 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import * as Configuration from "../../../src/init/configuration";
import * as ResultDal from "../../../src/dal/result";
import * as UserDal from "../../../src/dal/user";
import * as LogsDal from "../../../src/dal/logs";
import * as PublicDal from "../../../src/dal/public";
import { ObjectId } from "mongodb";
import { mockAuthenticateWithApeKey } from "../../__testData__/auth";
import { enableRateLimitExpects } from "../../__testData__/rate-limit";
import { DBResult } from "../../../src/utils/result";
import { omit } from "../../../src/utils/misc";
import { CompletedEvent } from "@monkeytype/schemas/results";
const { mockApp, uid, mockAuth } = setup();
const configuration = Configuration.getCachedConfiguration();
enableRateLimitExpects();
describe("result controller test", () => {
describe("getResults", () => {
const resultMock = vi.spyOn(ResultDal, "getResults");
beforeEach(async () => {
resultMock.mockResolvedValue([]);
await enablePremiumFeatures(true);
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(false);
});
afterEach(() => {
resultMock.mockClear();
});
it("should get results", async () => {
//GIVEN
const resultOne = givenDbResult(uid);
const resultTwo = givenDbResult(uid);
resultMock.mockResolvedValue([resultOne, resultTwo]);
//WHEN
const { body } = await mockApp
.get("/results")
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(body.message).toEqual("Results retrieved");
expect(body.data).toEqual([
{ ...resultOne, _id: resultOne._id.toHexString() },
{ ...resultTwo, _id: resultTwo._id.toHexString() },
]);
});
it("should get results with ape key", async () => {
//GIVEN
await acceptApeKeys(true);
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
//WHEN
await mockApp
.get("/results")
.set("Authorization", `ApeKey ${apeKey}`)
.send()
.expect(200);
});
it("should get latest 1000 results for regular user", async () => {
//WHEN
await mockApp
.get("/results")
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(resultMock).toHaveBeenCalledWith(uid, {
limit: 1000,
offset: 0,
onOrAfterTimestamp: NaN,
});
});
it("should get results filter by onOrAfterTimestamp", async () => {
//GIVEN
const now = Date.now();
//WHEN
await mockApp
.get("/results")
.query({ onOrAfterTimestamp: now })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(resultMock).toHaveBeenCalledWith(uid, {
limit: 1000,
offset: 0,
onOrAfterTimestamp: now,
});
});
it("should get with limit and offset", async () => {
//WHEN
await mockApp
.get("/results")
.query({ limit: 250, offset: 500 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(resultMock).toHaveBeenCalledWith(uid, {
limit: 250,
offset: 500,
onOrAfterTimestamp: NaN,
});
});
it("should fail exceeding max limit for regular user", async () => {
//WHEN
const { body } = await mockApp
.get("/results")
.query({ limit: 100, offset: 1000 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(422);
//THEN
expect(body.message).toEqual(
`Max results limit of ${
(await configuration).results.limits.regularUser
} exceeded.`,
);
});
it("should get with higher max limit for premium user", async () => {
//GIVEN
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
//WHEN
await mockApp
.get("/results")
.query({ limit: 800, offset: 600 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(resultMock).toHaveBeenCalledWith(uid, {
limit: 800,
offset: 600,
onOrAfterTimestamp: NaN,
});
});
it("should get results if offset/limit is partly outside the max limit", async () => {
//WHEN
await mockApp
.get("/results")
.query({ limit: 20, offset: 990 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(resultMock).toHaveBeenCalledWith(uid, {
limit: 10, //limit is reduced to stay within max limit
offset: 990,
onOrAfterTimestamp: NaN,
});
});
it("should fail exceeding 1k limit", async () => {
//GIVEN
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(false);
//WHEN
const { body } = await mockApp
.get("/results")
.query({ limit: 2000 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid query schema",
validationErrors: ['"limit" Number must be less than or equal to 1000'],
});
});
it("should fail exceeding maxlimit for premium user", async () => {
//GIVEN
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
//WHEN
const { body } = await mockApp
.get("/results")
.query({ limit: 1000, offset: 25000 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(422);
//THEN
expect(body.message).toEqual(
`Max results limit of ${
(await configuration).results.limits.premiumUser
} exceeded.`,
);
});
it("should get results within regular limits for premium users even if premium is globally disabled", async () => {
//GIVEN
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
enablePremiumFeatures(false);
//WHEN
await mockApp
.get("/results")
.query({ limit: 100, offset: 900 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(resultMock).toHaveBeenCalledWith(uid, {
limit: 100,
offset: 900,
onOrAfterTimestamp: NaN,
});
});
it("should fail exceeding max limit for premium user if premium is globally disabled", async () => {
//GIVEN
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
enablePremiumFeatures(false);
//WHEN
const { body } = await mockApp
.get("/results")
.query({ limit: 200, offset: 900 })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(503);
//THEN
expect(body.message).toEqual("Premium feature disabled.");
});
it("should get results with regular limit as default for premium users if premium is globally disabled", async () => {
//GIVEN
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
enablePremiumFeatures(false);
//WHEN
await mockApp
.get("/results")
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(resultMock).toHaveBeenCalledWith(uid, {
limit: 1000, //the default limit for regular users
offset: 0,
onOrAfterTimestamp: NaN,
});
});
it("should fail with unknown query parameters", async () => {
//WHEN
const { body } = await mockApp
.get("/results")
.query({ extra: "value" })
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid query schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
it("should be rate limited", async () => {
await expect(
mockApp.get("/results").set("Authorization", `Bearer ${uid}`),
).toBeRateLimited({ max: 60, windowMs: 60 * 60 * 1000 });
});
it("should be rate limited for ape keys", async () => {
//GIVEN
await acceptApeKeys(true);
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
//WHEN
await expect(
mockApp.get("/results").set("Authorization", `ApeKey ${apeKey}`),
).toBeRateLimited({ max: 30, windowMs: 24 * 60 * 60 * 1000 });
});
});
describe("getResultById", () => {
const getResultMock = vi.spyOn(ResultDal, "getResult");
afterEach(() => {
getResultMock.mockClear();
});
it("should get result", async () => {
//GIVEN
const result = givenDbResult(uid);
getResultMock.mockResolvedValue(result);
//WHEN
const { body } = await mockApp
.get(`/results/id/${result._id}`)
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(body.message).toEqual("Result retrieved");
expect(body.data).toEqual({ ...result, _id: result._id.toHexString() });
});
it("should get last result with ape key", async () => {
//GIVEN
await acceptApeKeys(true);
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
const result = givenDbResult(uid);
getResultMock.mockResolvedValue(result);
//WHEN
await mockApp
.get(`/results/id/${result._id}`)
.set("Authorization", `ApeKey ${apeKey}`)
.send()
.expect(200);
});
it("should rate limit get result with ape key", async () => {
//GIVEN
const result = givenDbResult(uid, {
charStats: undefined,
incorrectChars: 5,
correctChars: 12,
});
getResultMock.mockResolvedValue(result);
await acceptApeKeys(true);
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
//WHEN
await expect(
mockApp
.get(`/results/id/${result._id}`)
.set("Authorization", `ApeKey ${apeKey}`),
).toBeRateLimited({ max: 60, windowMs: 60 * 60 * 1000 });
});
});
describe("getLastResult", () => {
const getLastResultMock = vi.spyOn(ResultDal, "getLastResult");
afterEach(() => {
getLastResultMock.mockClear();
});
it("should get last result", async () => {
//GIVEN
const result = givenDbResult(uid);
getLastResultMock.mockResolvedValue(result);
//WHEN
const { body } = await mockApp
.get("/results/last")
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(body.message).toEqual("Result retrieved");
expect(body.data).toEqual({ ...result, _id: result._id.toHexString() });
});
it("should get last result with ape key", async () => {
//GIVEN
await acceptApeKeys(true);
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
const result = givenDbResult(uid);
getLastResultMock.mockResolvedValue(result);
//WHEN
await mockApp
.get("/results/last")
.set("Authorization", `ApeKey ${apeKey}`)
.send()
.expect(200);
});
it("should rate limit get last result with ape key", async () => {
//GIVEN
const result = givenDbResult(uid, {
charStats: undefined,
incorrectChars: 5,
correctChars: 12,
});
getLastResultMock.mockResolvedValue(result);
await acceptApeKeys(true);
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
//WHEN
await expect(
mockApp.get("/results/last").set("Authorization", `ApeKey ${apeKey}`),
).toBeRateLimited({ max: 30, windowMs: 60 * 1000 }); //should use defaultApeRateLimit
});
});
describe("deleteAll", () => {
const deleteAllMock = vi.spyOn(ResultDal, "deleteAll");
const logToDbMock = vi.spyOn(LogsDal, "addLog");
afterEach(() => {
deleteAllMock.mockClear();
logToDbMock.mockClear();
});
it("should delete", async () => {
//GIVEN
mockAuth.modifyToken({ iat: Date.now() - 1000 });
deleteAllMock.mockResolvedValue(undefined as any);
//WHEN
const { body } = await mockApp
.delete("/results")
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(200);
//THEN
expect(body.message).toEqual("All results deleted");
expect(body.data).toBeNull();
expect(deleteAllMock).toHaveBeenCalledWith(uid);
expect(logToDbMock).toHaveBeenCalledWith("user_results_deleted", "", uid);
});
it("should fail to delete with non-fresh token", async () => {
//GIVEN
mockAuth.modifyToken({ iat: 0 });
//WHEN/THEN
await mockApp
.delete("/results")
.set("Authorization", `Bearer ${uid}`)
.send()
.expect(401);
});
});
describe("updateTags", () => {
const getResultMock = vi.spyOn(ResultDal, "getResult");
const updateTagsMock = vi.spyOn(ResultDal, "updateTags");
const getUserPartialMock = vi.spyOn(UserDal, "getPartialUser");
const checkIfTagPbMock = vi.spyOn(UserDal, "checkIfTagPb");
afterEach(() => {
[
getResultMock,
updateTagsMock,
getUserPartialMock,
checkIfTagPbMock,
].forEach((it) => it.mockClear());
});
it("should update tags", async () => {
//GIVEN
const result = givenDbResult(uid);
const resultIdString = result._id.toHexString();
const tagIds = [
new ObjectId().toHexString(),
new ObjectId().toHexString(),
];
const partialUser = { tags: [] };
getResultMock.mockResolvedValue(result);
updateTagsMock.mockResolvedValue({} as any);
getUserPartialMock.mockResolvedValue(partialUser as any);
checkIfTagPbMock.mockResolvedValue([]);
//WHEN
const { body } = await mockApp
.patch("/results/tags")
.set("Authorization", `Bearer ${uid}`)
.send({ resultId: resultIdString, tagIds })
.expect(200);
//THEN
expect(body.message).toEqual("Result tags updated");
expect(body.data).toEqual({
tagPbs: [],
});
expect(updateTagsMock).toHaveBeenCalledWith(uid, resultIdString, tagIds);
expect(getResultMock).toHaveBeenCalledWith(uid, resultIdString);
expect(getUserPartialMock).toHaveBeenCalledWith(uid, "update tags", [
"tags",
]);
expect(checkIfTagPbMock).toHaveBeenCalledWith(uid, partialUser, result);
});
it("should apply defaults on missing data", async () => {
//GIVEN
const result = givenDbResult(uid);
const partialResult = omit(result, [
"difficulty",
"language",
"funbox",
"lazyMode",
"punctuation",
"numbers",
]);
const resultIdString = result._id.toHexString();
const tagIds = [
new ObjectId().toHexString(),
new ObjectId().toHexString(),
];
const partialUser = { tags: [] };
getResultMock.mockResolvedValue(partialResult);
updateTagsMock.mockResolvedValue({} as any);
getUserPartialMock.mockResolvedValue(partialUser as any);
checkIfTagPbMock.mockResolvedValue([]);
//WHEN
const { body } = await mockApp
.patch("/results/tags")
.set("Authorization", `Bearer ${uid}`)
.send({ resultId: resultIdString, tagIds })
.expect(200);
//THEN
expect(body.message).toEqual("Result tags updated");
expect(body.data).toEqual({
tagPbs: [],
});
expect(updateTagsMock).toHaveBeenCalledWith(uid, resultIdString, tagIds);
expect(getResultMock).toHaveBeenCalledWith(uid, resultIdString);
expect(getUserPartialMock).toHaveBeenCalledWith(uid, "update tags", [
"tags",
]);
expect(checkIfTagPbMock).toHaveBeenCalledWith(uid, partialUser, {
...result,
difficulty: "normal",
language: "english",
funbox: [],
lazyMode: false,
punctuation: false,
numbers: false,
});
});
it("should fail with missing mandatory properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.patch("/results/tags")
.set("Authorization", `Bearer ${uid}`)
.send({})
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"tagIds" Required', '"resultId" Required'],
});
});
it("should fail with unknown properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.patch("/results/tags")
.set("Authorization", `Bearer ${uid}`)
.send({ extra: "value" })
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
'"tagIds" Required',
'"resultId" Required',
"Unrecognized key(s) in object: 'extra'",
],
});
});
});
describe("addResult", () => {
//TODO improve test coverage for addResult
const insertedId = new ObjectId();
const userGetMock = vi.spyOn(UserDal, "getUser");
const userUpdateStreakMock = vi.spyOn(UserDal, "updateStreak");
const userCheckIfTagPbMock = vi.spyOn(UserDal, "checkIfTagPb");
const userCheckIfPbMock = vi.spyOn(UserDal, "checkIfPb");
const userIncrementXpMock = vi.spyOn(UserDal, "incrementXp");
const userUpdateTypingStatsMock = vi.spyOn(UserDal, "updateTypingStats");
const resultAddMock = vi.spyOn(ResultDal, "addResult");
const publicUpdateStatsMock = vi.spyOn(PublicDal, "updateStats");
beforeEach(async () => {
await enableResultsSaving(true);
await enableUsersXpGain(true);
[
userGetMock,
userUpdateStreakMock,
userCheckIfTagPbMock,
userCheckIfPbMock,
userIncrementXpMock,
userUpdateTypingStatsMock,
resultAddMock,
publicUpdateStatsMock,
].forEach((it) => it.mockClear());
userGetMock.mockResolvedValue({ name: "bob" } as any);
userUpdateStreakMock.mockResolvedValue(0);
userCheckIfTagPbMock.mockResolvedValue([]);
userCheckIfPbMock.mockResolvedValue(true);
resultAddMock.mockResolvedValue({ insertedId });
userIncrementXpMock.mockResolvedValue();
});
it("should add result", async () => {
//GIVEN
const completedEvent = buildCompletedEvent({
funbox: ["58008", "read_ahead_hard"],
});
//WHEN
const { body } = await mockApp
.post("/results")
.set("Authorization", `Bearer ${uid}`)
.send({
result: completedEvent,
})
.expect(200);
expect(body.message).toEqual("Result saved");
expect(body.data).toEqual({
isPb: true,
tagPbs: [],
xp: 0,
dailyXpBonus: false,
xpBreakdown: {
accPenalty: 28,
base: 20,
incomplete: 5,
funbox: 80,
},
streak: 0,
insertedId: insertedId.toHexString(),
});
expect(resultAddMock).toHaveBeenCalledWith(
uid,
expect.objectContaining({
acc: 86,
afkDuration: 5,
charStats: [100, 2, 3, 5],
chartData: {
err: [0, 2, 0],
burst: [50, 55, 56],
wpm: [1, 2, 3],
},
consistency: 23.5,
incompleteTestSeconds: 2,
isPb: true,
keyConsistency: 12,
keyDurationStats: {
average: 2.67,
sd: 2.05,
},
keySpacingStats: {
average: 2,
sd: 1.63,
},
mode: "time",
mode2: "15",
name: "bob",
rawWpm: 99,
restartCount: 4,
tags: ["tagOneId", "tagTwoId"],
testDuration: 15.1,
uid: uid,
wpm: 80,
}),
);
expect(publicUpdateStatsMock).toHaveBeenCalledWith(
4,
15.1 + 2 - 5, //duration + incompleteTestSeconds-afk
);
expect(userIncrementXpMock).toHaveBeenCalledWith(uid, 0);
expect(userUpdateTypingStatsMock).toHaveBeenCalledWith(
uid,
4,
15.1 + 2 - 5, //duration + incompleteTestSeconds-afk
);
});
it("should fail if result saving is disabled", async () => {
//GIVEN
await enableResultsSaving(false);
//WHEN
const { body } = await mockApp
.post("/results")
.set("Authorization", `Bearer ${uid}`)
.send({})
.expect(503);
//THEN
expect(body.message).toEqual("Results are not being saved at this time.");
});
it("should fail without mandatory properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/results")
.set("Authorization", `Bearer ${uid}`)
.send({})
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ['"result" Required'],
});
});
it("should fail with unknown properties", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/results")
.set("Authorization", `Bearer ${uid}`)
.send({
result: buildCompletedEvent({
extra2: "value",
} as any),
extra: "value",
})
.expect(422);
//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
`"result" Unrecognized key(s) in object: 'extra2'`,
"Unrecognized key(s) in object: 'extra'",
],
});
});
it("should fail wit duplicate funboxes", async () => {
//GIVEN
//WHEN
const { body } = await mockApp
.post("/results")
.set("Authorization", `Bearer ${uid}`)
.send({
result: buildCompletedEvent({
funbox: ["58008", "58008"],
}),
})
.expect(400);
//THEN
expect(body.message).toEqual("Duplicate funboxes");
});
// it("should fail invalid properties ", async () => {
//GIVEN
//WHEN
// const { body } = await mockApp
// .post("/results")
// .set("Authorization", `Bearer ${uid}`)
// //TODO add all properties
// .send({ result: { acc: 25 } })
// .expect(422);
//THEN
/*
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [
],
});
*/
// });
});
});
function buildCompletedEvent(result?: Partial<CompletedEvent>): CompletedEvent {
return {
acc: 86,
afkDuration: 5,
bailedOut: false,
blindMode: false,
charStats: [100, 2, 3, 5],
chartData: { wpm: [1, 2, 3], burst: [50, 55, 56], err: [0, 2, 0] },
consistency: 23.5,
difficulty: "normal",
funbox: [],
hash: "hash",
incompleteTestSeconds: 2,
incompleteTests: [{ acc: 75, seconds: 10 }],
keyConsistency: 12,
keyDuration: [0, 3, 5],
keySpacing: [0, 2, 4],
language: "english",
lazyMode: false,
mode: "time",
mode2: "15",
numbers: false,
punctuation: false,
rawWpm: 99,
restartCount: 4,
tags: ["tagOneId", "tagTwoId"],
testDuration: 15.1,
timestamp: 1000,
uid,
wpmConsistency: 55,
wpm: 80,
stopOnLetter: false,
//new required
charTotal: 5,
keyOverlap: 7,
lastKeyToEnd: 9,
startToFirstKey: 11,
...result,
};
}
async function enablePremiumFeatures(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.users.premium = { ...mockConfig.users.premium, enabled };
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}
function givenDbResult(uid: string, customize?: Partial<DBResult>): DBResult {
return {
_id: new ObjectId(),
wpm: Math.random() * 100,
rawWpm: Math.random() * 100,
charStats: [
Math.round(Math.random() * 10),
Math.round(Math.random() * 10),
Math.round(Math.random() * 10),
Math.round(Math.random() * 10),
],
acc: 80 + Math.random() * 20, //min accuracy is 75%
mode: "time",
mode2: "60",
timestamp: Math.round(Math.random() * 100),
testDuration: 1 + Math.random() * 100,
consistency: Math.random() * 100,
keyConsistency: Math.random() * 100,
uid,
keySpacingStats: { average: Math.random() * 100, sd: Math.random() },
keyDurationStats: { average: Math.random() * 100, sd: Math.random() },
isPb: true,
chartData: {
wpm: [Math.random() * 100],
burst: [Math.random() * 100],
err: [Math.random() * 100],
},
name: "testName",
...customize,
};
}
async function acceptApeKeys(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.apeKeys = {
...mockConfig.apeKeys,
acceptKeys: enabled,
};
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}
async function enableResultsSaving(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.results = { ...mockConfig.results, savingEnabled: enabled };
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}
async function enableUsersXpGain(enabled: boolean): Promise<void> {
const mockConfig = await configuration;
mockConfig.users.xp = { ...mockConfig.users.xp, enabled, funboxBonus: 1 };
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
mockConfig,
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setup } from "../../__testData__/controller-test";
import GeorgeQueue from "../../../src/queues/george-queue";
import crypto from "crypto";
const { mockApp } = setup();
describe("WebhooksController", () => {
describe("githubRelease", () => {
const georgeSendReleaseAnnouncementMock = vi.spyOn(
GeorgeQueue,
"sendReleaseAnnouncement",
);
const timingSafeEqualMock = vi.spyOn(crypto, "timingSafeEqual");
beforeEach(() => {
vi.stubEnv("GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET");
georgeSendReleaseAnnouncementMock.mockClear();
timingSafeEqualMock.mockClear().mockReturnValue(true);
});
it("should announce release", async () => {
//WHEN
const { body } = await mockApp
.post("/webhooks/githubRelease")
.set("x-hub-signature-256", "the-signature")
.send({ action: "published", release: { id: 1 } })
.expect(200);
//THEN
expect(body).toEqual({
message: "Added release announcement task to queue",
data: null,
});
expect(georgeSendReleaseAnnouncementMock).toHaveBeenCalledWith("1");
expect(timingSafeEqualMock).toHaveBeenCalledWith(
Buffer.from(
"sha256=ff0f3080539e9df19153f6b5b5780f66e558d61038e6cf5ecf4efdc7266a7751",
),
Buffer.from("the-signature"),
);
});
it("should ignore non-published actions", async () => {
//WHEN
const { body } = await mockApp
.post("/webhooks/githubRelease")
.set("x-hub-signature-256", "the-signature")
.send({ action: "created" })
.expect(200);
//THEN
expect(body.message).toEqual("No action taken");
expect(georgeSendReleaseAnnouncementMock).not.toHaveBeenCalled();
});
it("should ignore additional properties", async () => {
//WHEN
await mockApp
.post("/webhooks/githubRelease")
.set("x-hub-signature-256", "the-signature")
.send({
action: "published",
extra: "value",
release: { id: 1, extra2: "value" },
})
.expect(200);
});
it("should fail with missing releaseId", async () => {
//WHEN
const { body } = await mockApp
.post("/webhooks/githubRelease")
.set("x-hub-signature-256", "the-signature")
.send({ action: "published" })
.expect(422);
//THEN
expect(body.message).toEqual('Missing property "release.id".');
});
});
});

View File

@@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest";
import * as Configurations from "../../src/init/configuration";
import { Configuration } from "@monkeytype/schemas/configuration";
const mergeConfigurations = Configurations.__testing.mergeConfigurations;
describe("configurations", () => {
describe("mergeConfigurations", () => {
it("should merge configurations correctly", () => {
//GIVEN
const baseConfig: Configuration = {
maintenance: false,
dev: {
responseSlowdownMs: 5,
},
quotes: {
reporting: {
enabled: false,
maxReports: 5,
},
submissionEnabled: true,
},
} as any;
const liveConfig: Partial<Configuration> = {
maintenance: true,
quotes: {
reporting: {
enabled: true,
} as any,
maxFavorites: 10,
} as any,
};
//WHEN
mergeConfigurations(baseConfig, liveConfig);
//THEN
expect(baseConfig).toEqual({
maintenance: true,
dev: {
responseSlowdownMs: 5,
},
quotes: {
reporting: {
enabled: true,
maxReports: 5,
},
submissionEnabled: true,
},
} as any);
});
});
});

View File

@@ -0,0 +1,580 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import * as AuthUtils from "../../src/utils/auth";
import * as Auth from "../../src/middlewares/auth";
import { DecodedIdToken } from "firebase-admin/auth";
import { NextFunction, Request, Response } from "express";
import { getCachedConfiguration } from "../../src/init/configuration";
import * as ApeKeys from "../../src/dal/ape-keys";
import { ObjectId } from "mongodb";
import { hashSync } from "bcrypt";
import MonkeyError from "../../src/utils/error";
import * as Misc from "../../src/utils/misc";
import crypto from "crypto";
import {
EndpointMetadata,
RequestAuthenticationOptions,
} from "@monkeytype/contracts/util/api";
import * as Prometheus from "../../src/utils/prometheus";
import { TsRestRequestWithContext } from "../../src/api/types";
import { enableMonkeyErrorExpects } from "../__testData__/monkey-error";
enableMonkeyErrorExpects();
const mockDecodedToken: DecodedIdToken = {
uid: "123456789",
email: "newuser@mail.com",
iat: 0,
} as DecodedIdToken;
vi.spyOn(AuthUtils, "verifyIdToken").mockResolvedValue(mockDecodedToken);
const mockApeKey = {
_id: new ObjectId(),
uid: "123",
name: "test",
hash: hashSync("key", 5),
createdOn: Date.now(),
modifiedOn: Date.now(),
lastUsedOn: Date.now(),
useCount: 0,
enabled: true,
};
vi.spyOn(ApeKeys, "getApeKey").mockResolvedValue(mockApeKey);
vi.spyOn(ApeKeys, "updateLastUsedOn").mockResolvedValue();
const isDevModeMock = vi.spyOn(Misc, "isDevEnvironment");
let mockRequest: Partial<TsRestRequestWithContext>;
let mockResponse: Partial<Response>;
let nextFunction: NextFunction;
describe("middlewares/auth", () => {
beforeEach(async () => {
isDevModeMock.mockReturnValue(true);
let config = await getCachedConfiguration(true);
config.apeKeys.acceptKeys = true;
mockRequest = {
baseUrl: "/api/v1",
route: {
path: "/",
},
headers: {
authorization: "Bearer 123456789",
},
ctx: {
configuration: config,
decodedToken: {
type: "None",
uid: "",
email: "",
},
},
};
mockResponse = {
json: vi.fn(),
};
nextFunction = vi.fn((error) => {
if (error) {
throw error;
}
return "Next function called";
}) as unknown as NextFunction;
});
afterEach(() => {
isDevModeMock.mockClear();
});
describe("authenticateTsRestRequest", () => {
const prometheusRecordAuthTimeMock = vi.spyOn(Prometheus, "recordAuthTime");
const prometheusIncrementAuthMock = vi.spyOn(Prometheus, "incrementAuth");
const timingSafeEqualMock = vi.spyOn(crypto, "timingSafeEqual");
beforeEach(() => {
timingSafeEqualMock.mockClear().mockReturnValue(true);
[prometheusIncrementAuthMock, prometheusRecordAuthTimeMock].forEach(
(it) => it.mockClear(),
);
});
it("should fail if token is not fresh", async () => {
//GIVEN
Date.now = vi.fn(() => 60001);
const expectedError = new MonkeyError(
401,
"Unauthorized\nStack: This endpoint requires a fresh token",
);
//WHEN
await expect(() =>
authenticate({}, { requireFreshToken: true }),
).rejects.toMatchMonkeyError(expectedError);
//THEN
expect(nextFunction).toHaveBeenLastCalledWith(
expect.toMatchMonkeyError(expectedError),
);
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow the request if token is fresh", async () => {
//GIVEN
Date.now = vi.fn(() => 10000);
//WHEN
const result = await authenticate({}, { requireFreshToken: true });
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe(mockDecodedToken.email);
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
expect(nextFunction).toHaveBeenCalledOnce();
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("Bearer");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow the request if apeKey is supported", async () => {
//WHEN
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: true },
);
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should fail with apeKey if apeKey is not supported", async () => {
//WHEN
await expect(() =>
authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: false },
),
).rejects.toThrow("This endpoint does not accept ApeKeys");
//THEN
});
it("should fail with apeKey if apeKeys are disabled", async () => {
//GIVEN
//@ts-expect-error
mockRequest.ctx.configuration.apeKeys.acceptKeys = false;
//WHEN
await expect(() =>
authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: false },
),
).rejects.toThrow("ApeKeys are not being accepted at this time");
//THEN
});
it("should allow the request with authentation on public endpoint", async () => {
//WHEN
const result = await authenticate({}, { isPublic: true });
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe(mockDecodedToken.email);
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should allow the request without authentication on public endpoint", async () => {
//WHEN
const result = await authenticate({ headers: {} }, { isPublic: true });
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("None");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("");
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("None");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow the request with apeKey on public endpoint", async () => {
//WHEN
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ isPublic: true },
);
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow request with Uid on dev", async () => {
//WHEN
const result = await authenticate({
headers: { authorization: "Uid 123" },
});
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should allow request with Uid and email on dev", async () => {
const result = await authenticate({
headers: { authorization: "Uid 123|test@example.com" },
});
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe("test@example.com");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should fail request with Uid on non-dev", async () => {
//GIVEN
isDevModeMock.mockReturnValue(false);
//WHEN / THEN
await expect(() =>
authenticate({ headers: { authorization: "Uid 123" } }),
).rejects.toMatchMonkeyError(
new MonkeyError(401, "Bearer type uid is not supported"),
);
});
it("should fail without authentication", async () => {
await expect(() => authenticate({ headers: {} })).rejects.toThrow(
"Unauthorized\nStack: endpoint: /api/v1 no authorization header found",
);
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"None",
"failure",
expect.anything(),
expect.anything(),
);
});
it("should fail with empty authentication", async () => {
await expect(() =>
authenticate({ headers: { authorization: "" } }),
).rejects.toThrow(
"Unauthorized\nStack: endpoint: /api/v1 no authorization header found",
);
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"",
"failure",
expect.anything(),
expect.anything(),
);
});
it("should fail with missing authentication token", async () => {
await expect(() =>
authenticate({ headers: { authorization: "Bearer" } }),
).rejects.toThrow(
"Missing authentication token\nStack: authenticateWithAuthHeader",
);
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"Bearer",
"failure",
expect.anything(),
expect.anything(),
);
});
it("should fail with unknown authentication scheme", async () => {
await expect(() =>
authenticate({ headers: { authorization: "unknown format" } }),
).rejects.toThrow(
'Unknown authentication scheme\nStack: The authentication scheme "unknown" is not implemented',
);
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"unknown",
"failure",
expect.anything(),
expect.anything(),
);
});
it("should record country if provided", async () => {
const prometheusRecordRequestCountryMock = vi.spyOn(
Prometheus,
"recordRequestCountry",
);
await authenticate(
{ headers: { "cf-ipcountry": "gb" } },
{ isPublic: true },
);
//THEN
expect(prometheusRecordRequestCountryMock).toHaveBeenCalledWith(
"gb",
expect.anything(),
);
});
it("should allow the request with authentation on dev public endpoint", async () => {
//WHEN
const result = await authenticate({}, { isPublicOnDev: true });
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe(mockDecodedToken.email);
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should allow the request without authentication on dev public endpoint", async () => {
//WHEN
const result = await authenticate(
{ headers: {} },
{ isPublicOnDev: true },
);
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("None");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("");
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("None");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow the request with apeKey on dev public endpoint", async () => {
//WHEN
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: true, isPublicOnDev: true },
);
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow with apeKey if apeKeys are disabled on dev public endpoint", async () => {
//GIVEN
//@ts-expect-error
mockRequest.ctx.configuration.apeKeys.acceptKeys = false;
//WHEN
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: true, isPublicOnDev: true },
);
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow the request with authentation on dev public endpoint in production", async () => {
//WHEN
isDevModeMock.mockReturnValue(false);
const result = await authenticate({}, { isPublicOnDev: true });
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("Bearer");
expect(decodedToken?.email).toBe(mockDecodedToken.email);
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
expect(nextFunction).toHaveBeenCalledTimes(1);
});
it("should fail without authentication on dev public endpoint in production", async () => {
//WHEN
isDevModeMock.mockReturnValue(false);
//THEN
await expect(() =>
authenticate({ headers: {} }, { isPublicOnDev: true }),
).rejects.toThrow("Unauthorized");
});
it("should allow with apeKey on dev public endpoint in production", async () => {
//WHEN
isDevModeMock.mockReturnValue(false);
const result = await authenticate(
{ headers: { authorization: "ApeKey aWQua2V5" } },
{ acceptApeKeys: true, isPublicOnDev: true },
);
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("ApeKey");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("123");
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
});
it("should allow githubwebhook with header", async () => {
vi.stubEnv("GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET");
//WHEN
const result = await authenticate(
{
headers: { "x-hub-signature-256": "the-signature" },
body: { action: "published", release: { id: 1 } },
},
{ isGithubWebhook: true },
);
//THEN
const decodedToken = result.decodedToken;
expect(decodedToken?.type).toBe("GithubWebhook");
expect(decodedToken?.email).toBe("");
expect(decodedToken?.uid).toBe("");
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("GithubWebhook");
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
expect(timingSafeEqualMock).toHaveBeenCalledWith(
Buffer.from(
"sha256=ff0f3080539e9df19153f6b5b5780f66e558d61038e6cf5ecf4efdc7266a7751",
),
Buffer.from("the-signature"),
);
});
it("should fail githubwebhook with mismatched signature", async () => {
vi.stubEnv("GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET");
timingSafeEqualMock.mockReturnValue(false);
await expect(() =>
authenticate(
{
headers: { "x-hub-signature-256": "the-signature" },
body: { action: "published", release: { id: 1 } },
},
{ isGithubWebhook: true },
),
).rejects.toThrow("Github webhook signature invalid");
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"None",
"failure",
expect.anything(),
expect.anything(),
);
});
it("should fail without header when endpoint is using githubwebhook", async () => {
vi.stubEnv("GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET");
await expect(() =>
authenticate(
{
headers: {},
body: { action: "published", release: { id: 1 } },
},
{ isGithubWebhook: true },
),
).rejects.toThrow("Missing Github signature header");
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"None",
"failure",
expect.anything(),
expect.anything(),
);
});
it("should fail with missing GITHUB_WEBHOOK_SECRET when endpoint is using githubwebhook", async () => {
vi.stubEnv("GITHUB_WEBHOOK_SECRET", "");
await expect(() =>
authenticate(
{
headers: { "x-hub-signature-256": "the-signature" },
body: { action: "published", release: { id: 1 } },
},
{ isGithubWebhook: true },
),
).rejects.toThrow("Missing Github Webhook Secret");
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"None",
"failure",
expect.anything(),
expect.anything(),
);
});
it("should throw 500 if something went wrong when validating the signature when endpoint is using githubwebhook", async () => {
vi.stubEnv("GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET");
timingSafeEqualMock.mockImplementation(() => {
throw new Error("could not validate");
});
await expect(() =>
authenticate(
{
headers: { "x-hub-signature-256": "the-signature" },
body: { action: "published", release: { id: 1 } },
},
{ isGithubWebhook: true },
),
).rejects.toThrow(
"Failed to authenticate Github webhook: could not validate",
);
//THEH
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
"None",
"failure",
expect.anything(),
expect.anything(),
);
});
});
});
async function authenticate(
request: Partial<Request>,
authenticationOptions?: RequestAuthenticationOptions,
): Promise<{ decodedToken: Auth.DecodedToken }> {
const mergedRequest = {
...mockRequest,
...request,
tsRestRoute: {
metadata: { authenticationOptions } as EndpointMetadata,
},
} as any;
await Auth.authenticateTsRestRequest()(
mergedRequest,
mockResponse as Response,
nextFunction,
);
return { decodedToken: mergedRequest.ctx.decodedToken };
}

View File

@@ -0,0 +1,201 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { RequireConfiguration } from "@monkeytype/contracts/require-configuration/index";
import { verifyRequiredConfiguration } from "../../src/middlewares/configuration";
import { Configuration } from "@monkeytype/schemas/configuration";
import { Response } from "express";
import MonkeyError from "../../src/utils/error";
import { TsRestRequest } from "../../src/api/types";
import { enableMonkeyErrorExpects } from "../__testData__/monkey-error";
enableMonkeyErrorExpects();
describe("configuration middleware", () => {
const handler = verifyRequiredConfiguration();
const res: Response = {} as any;
const next = vi.fn();
beforeEach(() => {
next.mockClear();
});
afterEach(() => {
//next function must only be called once
expect(next).toHaveBeenCalledOnce();
});
it("should pass without requireConfiguration", async () => {
//GIVEN
const req = { tsRestRoute: { metadata: {} } } as any;
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should pass for enabled configuration", async () => {
//GIVEN
const req = givenRequest({ path: "maintenance" }, { maintenance: true });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should pass for enabled configuration with complex path", async () => {
//GIVEN
const req = givenRequest(
{ path: "users.xp.streak.enabled" },
{ users: { xp: { streak: { enabled: true } as any } as any } as any },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should fail for disabled configuration", async () => {
//GIVEN
const req = givenRequest({ path: "maintenance" }, { maintenance: false });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(503, "This endpoint is currently unavailable."),
),
);
});
it("should fail for disabled configuration and custom message", async () => {
//GIVEN
const req = givenRequest(
{ path: "maintenance", invalidMessage: "Feature not enabled." },
{ maintenance: false },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(new MonkeyError(503, "Feature not enabled.")),
);
});
it("should fail for invalid path", async () => {
//GIVEN
const req = givenRequest({ path: "invalid.path" as any }, {});
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(500, 'Invalid configuration path: "invalid.path"'),
),
);
});
it("should fail for undefined value", async () => {
//GIVEN
const req = givenRequest(
{ path: "admin.endpointsEnabled" },
{ admin: {} as any },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(
500,
'Required configuration doesnt exist: "admin.endpointsEnabled"',
),
),
);
});
it("should fail for null value", async () => {
//GIVEN
const req = givenRequest(
{ path: "admin.endpointsEnabled" },
{ admin: { endpointsEnabled: null as any } },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(
500,
'Required configuration doesnt exist: "admin.endpointsEnabled"',
),
),
);
});
it("should fail for non booean value", async () => {
//GIVEN
const req = givenRequest(
{ path: "admin.endpointsEnabled" },
{ admin: { endpointsEnabled: "disabled" as any } },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(
500,
'Required configuration is not a boolean: "admin.endpointsEnabled"',
),
),
);
});
it("should pass for multiple configurations", async () => {
//GIVEN
const req = givenRequest(
[{ path: "maintenance" }, { path: "admin.endpointsEnabled" }],
{ maintenance: true, admin: { endpointsEnabled: true } },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should fail for multiple configurations", async () => {
//GIVEN
const req = givenRequest(
[
{ path: "maintenance", invalidMessage: "maintenance mode" },
{ path: "admin.endpointsEnabled", invalidMessage: "admin disabled" },
],
{ maintenance: true, admin: { endpointsEnabled: false } },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(new MonkeyError(503, "admin disabled")),
);
});
});
function givenRequest(
requireConfiguration: RequireConfiguration | RequireConfiguration[],
configuration: Partial<Configuration>,
): TsRestRequest {
return {
tsRestRoute: { metadata: { requireConfiguration } },
ctx: { configuration },
} as any;
}

View File

@@ -0,0 +1,338 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Response } from "express";
import { verifyPermissions } from "../../src/middlewares/permission";
import { EndpointMetadata } from "@monkeytype/contracts/util/api";
import * as Misc from "../../src/utils/misc";
import * as AdminUids from "../../src/dal/admin-uids";
import * as UserDal from "../../src/dal/user";
import MonkeyError from "../../src/utils/error";
import { DecodedToken } from "../../src/middlewares/auth";
import { TsRestRequest } from "../../src/api/types";
import { enableMonkeyErrorExpects } from "../__testData__/monkey-error";
enableMonkeyErrorExpects();
const uid = "123456789";
describe("permission middleware", () => {
const handler = verifyPermissions();
const res: Response = {} as any;
const next = vi.fn();
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
const isAdminMock = vi.spyOn(AdminUids, "isAdmin");
const isDevMock = vi.spyOn(Misc, "isDevEnvironment");
beforeEach(() => {
next.mockClear();
getPartialUserMock.mockClear().mockResolvedValue({} as any);
isDevMock.mockClear().mockReturnValue(false);
isAdminMock.mockClear().mockResolvedValue(false);
});
afterEach(() => {
//next function must only be called once
expect(next).toHaveBeenCalledOnce();
});
it("should bypass without requiredPermission", async () => {
//GIVEN
const req = givenRequest({});
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should bypass with empty requiredPermission", async () => {
//GIVEN
const req = givenRequest({ requirePermission: [] });
//WHEN
await handler(req, res, next);
//THE
expect(next).toHaveBeenCalledWith();
});
describe("admin check", () => {
const requireAdminPermission: EndpointMetadata = {
requirePermission: "admin",
};
it("should fail without authentication", async () => {
//GIVEN
const req = givenRequest(requireAdminPermission);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(403, "You don't have permission to do this."),
),
);
});
it("should pass without authentication if publicOnDev on dev", async () => {
//GIVEN
isDevMock.mockReturnValue(true);
const req = givenRequest(
{
...requireAdminPermission,
authenticationOptions: { isPublicOnDev: true },
},
{ uid },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should fail without authentication if publicOnDev on prod ", async () => {
//GIVEN
const req = givenRequest(
{
...requireAdminPermission,
authenticationOptions: { isPublicOnDev: true },
},
{ uid },
);
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(403, "You don't have permission to do this."),
),
);
});
it("should fail without admin permissions", async () => {
//GIVEN
const req = givenRequest(requireAdminPermission, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(403, "You don't have permission to do this."),
),
);
expect(isAdminMock).toHaveBeenCalledWith(uid);
});
});
describe("user checks", () => {
it("should fetch user only once", async () => {
//GIVEN
const req = givenRequest(
{
requirePermission: ["canReport", "canManageApeKeys"],
},
{ uid },
);
//WHEN
await handler(req, res, next);
//THEN
expect(getPartialUserMock).toHaveBeenCalledOnce();
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["canReport", "canManageApeKeys"],
);
});
it("should fail if authentication is missing", async () => {
//GIVEN
const req = givenRequest({
requirePermission: ["canReport", "canManageApeKeys"],
});
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(
403,
"Failed to check permissions, authentication required.",
),
),
);
});
});
describe("quoteMod check", () => {
const requireQuoteMod: EndpointMetadata = {
requirePermission: "quoteMod",
};
it("should pass for quoteAdmin", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ quoteMod: true } as any);
const req = givenRequest(requireQuoteMod, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["quoteMod"],
);
});
it("should pass for specific language", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ quoteMod: "english" } as any);
const req = givenRequest(requireQuoteMod, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["quoteMod"],
);
});
it("should fail for empty string", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ quoteMod: "" } as any);
const req = givenRequest(requireQuoteMod, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(403, "You don't have permission to do this."),
),
);
});
it("should fail for missing quoteMod", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({} as any);
const req = givenRequest(requireQuoteMod, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(403, "You don't have permission to do this."),
),
);
});
});
describe("canReport check", () => {
const requireCanReport: EndpointMetadata = {
requirePermission: "canReport",
};
it("should fail if user cannot report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canReport: false } as any);
const req = givenRequest(requireCanReport, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(403, "You don't have permission to do this."),
),
);
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["canReport"],
);
});
it("should pass if user can report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canReport: true } as any);
const req = givenRequest(requireCanReport, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should pass if canReport is not set", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({} as any);
const req = givenRequest(requireCanReport, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
});
describe("canManageApeKeys check", () => {
const requireCanReport: EndpointMetadata = {
requirePermission: "canManageApeKeys",
};
it("should fail if user cannot report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canManageApeKeys: false } as any);
const req = givenRequest(requireCanReport, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith(
expect.toMatchMonkeyError(
new MonkeyError(
403,
"You have lost access to ape keys, please contact support",
),
),
);
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["canManageApeKeys"],
);
});
it("should pass if user can report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canManageApeKeys: true } as any);
const req = givenRequest(requireCanReport, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
it("should pass if canManageApeKeys is not set", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({} as any);
const req = givenRequest(requireCanReport, { uid });
//WHEN
await handler(req, res, next);
//THEN
expect(next).toHaveBeenCalledWith();
});
});
});
function givenRequest(
metadata: EndpointMetadata,
decodedToken?: Partial<DecodedToken>,
): TsRestRequest {
return { tsRestRoute: { metadata }, ctx: { decodedToken } } as any;
}

View File

@@ -0,0 +1,42 @@
import { vi } from "vitest";
export function setupCommonMocks() {
vi.mock("../src/utils/logger", () => ({
__esModule: true,
default: {
error: console.error,
warning: console.warn,
info: console.info,
success: console.info,
logToDb: console.info,
},
}));
vi.mock("swagger-stats", () => ({
getMiddleware:
() =>
(_: unknown, __: unknown, next: () => unknown): void => {
next();
},
}));
// TODO: better approach for this when needed
// https://firebase.google.com/docs/rules/unit-tests#run_local_unit_tests_with_the_version_9_javascript_sdk
vi.mock("firebase-admin", () => ({
__esModule: true,
default: {
auth: (): unknown => ({
verifyIdToken: (
_token: string,
_checkRevoked: boolean,
): unknown /* Promise<DecodedIdToken> */ =>
Promise.resolve({
aud: "mockFirebaseProjectId",
auth_time: 123,
exp: 1000,
uid: "mockUid",
}),
}),
},
}));
}

View File

@@ -0,0 +1,40 @@
import { afterAll, beforeAll, afterEach, vi } from "vitest";
import { BASE_CONFIGURATION } from "../src/constants/base-configuration";
import { setupCommonMocks } from "./setup-common-mocks";
import { __testing } from "../src/init/configuration";
process.env["MODE"] = "dev";
process.env.TZ = "UTC";
beforeAll(async () => {
//don't add any configuration here, add to global-setup.ts instead.
vi.mock("../src/init/configuration", async (importOriginal) => {
const orig = (await importOriginal()) as { __testing: typeof __testing };
return {
__testing: orig.__testing,
getLiveConfiguration: () => BASE_CONFIGURATION,
getCachedConfiguration: () => BASE_CONFIGURATION,
patchConfiguration: vi.fn(),
};
});
vi.mock("../src/init/db", () => ({
__esModule: true,
getDb: () => undefined,
collection: () => undefined,
close: () => {
//
},
}));
setupCommonMocks();
});
afterEach(async () => {
//nothing
});
afterAll(async () => {
vi.resetAllMocks();
});

View File

@@ -0,0 +1,12 @@
{
"extends": "@monkeytype/typescript-config/base.json",
"compilerOptions": {
"noEmit": true,
"noImplicitAny": false,
"strictFunctionTypes": false,
"useUnknownInCatchVariables": false,
"strictPropertyInitialization": false
},
"files": ["vitest.d.ts"],
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
}

View File

@@ -0,0 +1,498 @@
import { describe, it, expect, afterAll, vi } from "vitest";
import * as Misc from "../../src/utils/misc";
import { ObjectId } from "mongodb";
describe("Misc Utils", () => {
afterAll(() => {
vi.useRealTimers();
});
describe("matchesAPattern", () => {
const testCases = [
{
pattern: "eng.*",
cases: ["english", "aenglish", "en", "eng"],
expected: [true, false, false, true],
},
{
pattern: "\\d+",
cases: ["b", "2", "331", "1a"],
expected: [false, true, true, false],
},
{
pattern: "(hi|hello)",
cases: ["hello", "hi", "hillo", "hi hello"],
expected: [true, true, false, false],
},
{
pattern: ".+",
cases: ["a2", "b2", "c1", ""],
expected: [true, true, true, false],
},
];
it.each(testCases)(
"matchesAPattern with $pattern",
({ pattern, cases, expected }) => {
cases.forEach((caseValue, index) => {
expect(Misc.matchesAPattern(caseValue, pattern)).toBe(
expected[index],
);
});
},
);
});
describe("kogascore", () => {
const testCases = [
{
wpm: 214.8,
acc: 93.04,
timestamp: 1653586489000,
expectedScore: 1214800930423111,
},
{
wpm: 214.8,
acc: 93.04,
timestamp: 1653601763000,
expectedScore: 1214800930407837,
},
{
wpm: 199.37,
acc: 97.69,
timestamp: 1653588809000,
expectedScore: 1199370976920791,
},
{
wpm: 196.2,
acc: 96.07,
timestamp: 1653591901000,
expectedScore: 1196200960717699,
},
{
wpm: 196.205,
acc: 96.075,
timestamp: 1653591901000,
expectedScore: 1196210960817699,
},
{
// this one is particularly important - in JS 154.39 * 100 is equal to 15438.999999999998
// thanks floating point errors!
wpm: 154.39,
acc: 96.14,
timestamp: 1740333827000,
expectedScore: 1154390961421373,
},
];
it.each(testCases)(
"kogascore with wpm:$wpm, acc:$acc, timestamp:$timestamp = $expectedScore",
({ wpm, acc, timestamp, expectedScore }) => {
expect(Misc.kogascore(wpm, acc, timestamp)).toBe(expectedScore);
},
);
});
describe("identity", () => {
const testCases = [
{
input: "",
expected: "string",
},
{
input: {},
expected: "object",
},
{
input: 0,
expected: "number",
},
{
input: null,
expected: "null",
},
{
input: undefined,
expected: "undefined",
},
];
it.each(testCases)(
"identity with $input = $expected",
({ input, expected }) => {
expect(Misc.identity(input)).toBe(expected);
},
);
});
describe("flattenObjectDeep", () => {
const testCases = [
{
obj: {
a: {
b: {
c: 1,
},
},
d: 2,
e: [],
},
expected: {
"a.b.c": 1,
d: 2,
e: [],
},
},
{
obj: {
a: {
b: {
c: 1,
},
},
d: {
e: {
f: 2,
g: 3,
},
},
},
expected: {
"a.b.c": 1,
"d.e.f": 2,
"d.e.g": 3,
},
},
{
obj: {
a: {
b: {
c: 1,
d: {
e: 2,
f: 3,
g: {},
},
},
},
},
expected: {
"a.b.c": 1,
"a.b.d.e": 2,
"a.b.d.f": 3,
"a.b.d.g": {},
},
},
{
obj: {},
expected: {},
},
];
it.each(testCases)(
"flattenObjectDeep with $obj = $expected",
({ obj, expected }) => {
expect(Misc.flattenObjectDeep(obj)).toEqual(expected);
},
);
});
it("sanitizeString", () => {
const testCases = [
{
input: "h̶̼͔̭͈̏́̀́͋͜ͅe̵̺̞̦̫̫͔̋́̅̅̃̀͝͝ļ̶̬̯͚͇̺͍̞̫̟͖͋̓͛̆̒̓͜ĺ̴̗̘͇̬̆͂͌̈͊͝͝ỡ̴̡̦̩̠̞̐̃͆̚͠͝",
expected: "hello",
},
{
input: "hello",
expected: "hello",
},
{
input: "hel lo",
expected: "hel lo",
},
{
input: " hel lo ",
expected: "hel lo",
},
{
input: "",
expected: "",
},
{
input: " \n\n\n",
expected: "",
},
{
input: undefined,
expected: undefined,
},
];
testCases.forEach(({ input, expected }) => {
expect(Misc.sanitizeString(input)).toEqual(expected);
});
});
it("getOrdinalNumberString", () => {
const testCases = [
{
input: 0,
output: "0th",
},
{
input: 1,
output: "1st",
},
{
input: 2,
output: "2nd",
},
{
input: 3,
output: "3rd",
},
{
input: 4,
output: "4th",
},
{
input: 10,
output: "10th",
},
{
input: 11,
output: "11th",
},
{
input: 12,
output: "12th",
},
{
input: 13,
output: "13th",
},
{
input: 100,
output: "100th",
},
{
input: 101,
output: "101st",
},
{
input: 102,
output: "102nd",
},
{
input: 103,
output: "103rd",
},
{
input: 104,
output: "104th",
},
{
input: 93589423,
output: "93589423rd",
},
];
testCases.forEach(({ input, output }) => {
expect(Misc.getOrdinalNumberString(input)).toEqual(output);
});
});
it("formatSeconds", () => {
const testCases = [
{
seconds: 5,
expected: "5 seconds",
},
{
seconds: 65,
expected: "1.08 minutes",
},
{
seconds: Misc.HOUR_IN_SECONDS,
expected: "1 hour",
},
{
seconds: Misc.DAY_IN_SECONDS,
expected: "1 day",
},
{
seconds: Misc.WEEK_IN_SECONDS,
expected: "1 week",
},
{
seconds: Misc.YEAR_IN_SECONDS,
expected: "1 year",
},
{
seconds: 2 * Misc.YEAR_IN_SECONDS,
expected: "2 years",
},
{
seconds: 4 * Misc.YEAR_IN_SECONDS,
expected: "4 years",
},
{
seconds: 3 * Misc.WEEK_IN_SECONDS,
expected: "3 weeks",
},
{
seconds: Misc.MONTH_IN_SECONDS * 4,
expected: "4 months",
},
{
seconds: Misc.MONTH_IN_SECONDS * 11,
expected: "11 months",
},
];
testCases.forEach(({ seconds, expected }) => {
expect(Misc.formatSeconds(seconds)).toBe(expected);
});
});
describe("replaceObjectId", () => {
it("replaces objecId with string", () => {
const fromDatabase = {
_id: new ObjectId(),
test: "test",
number: 1,
};
expect(Misc.replaceObjectId(fromDatabase)).toStrictEqual({
_id: fromDatabase._id.toHexString(),
test: "test",
number: 1,
});
});
it("ignores null values", () => {
expect(Misc.replaceObjectId(null)).toBeNull();
});
});
describe("replaceObjectIds", () => {
it("replaces objecIds with string", () => {
const fromDatabase = {
_id: new ObjectId(),
test: "test",
number: 1,
};
const fromDatabase2 = {
_id: new ObjectId(),
test: "bob",
number: 2,
};
expect(
Misc.replaceObjectIds([fromDatabase, fromDatabase2]),
).toStrictEqual([
{
_id: fromDatabase._id.toHexString(),
test: "test",
number: 1,
},
{
_id: fromDatabase2._id.toHexString(),
test: "bob",
number: 2,
},
]);
});
it("handles undefined", () => {
expect(Misc.replaceObjectIds(undefined as any)).toBeUndefined();
});
});
describe("omit()", () => {
it("should omit a single key", () => {
const input = { a: 1, b: 2, c: 3 };
const result = Misc.omit(input, ["b"]);
expect(result).toEqual({ a: 1, c: 3 });
});
it("should omit multiple keys", () => {
const input = { a: 1, b: 2, c: 3, d: 4 };
const result = Misc.omit(input, ["a", "d"]);
expect(result).toEqual({ b: 2, c: 3 });
});
it("should return the same object if no keys are omitted", () => {
const input = { x: 1, y: 2 };
const result = Misc.omit(input, []);
expect(result).toEqual({ x: 1, y: 2 });
});
it("should not mutate the original object", () => {
const input = { foo: "bar", baz: "qux" };
const copy = { ...input };
Misc.omit(input, ["baz"]);
expect(input).toEqual(copy);
});
it("should ignore keys that do not exist", () => {
const input = { a: 1, b: 2 };
const result = Misc.omit(input, "c" as any); // allow a non-existing key
expect(result).toEqual({ a: 1, b: 2 });
});
it("should work with different value types", () => {
const input = {
str: "hello",
num: 123,
bool: true,
obj: { x: 1 },
arr: [1, 2, 3],
};
const result = Misc.omit(input, ["bool", "arr"]);
expect(result).toEqual({
str: "hello",
num: 123,
obj: { x: 1 },
});
});
});
describe("isPlainObject", () => {
it("should return true for plain objects", () => {
expect(Misc.isPlainObject({})).toBe(true);
expect(Misc.isPlainObject({ a: 1, b: 2 })).toBe(true);
expect(Misc.isPlainObject(Object.create(Object.prototype))).toBe(true);
});
it("should return false for arrays", () => {
expect(Misc.isPlainObject([])).toBe(false);
expect(Misc.isPlainObject([1, 2, 3])).toBe(false);
});
it("should return false for null", () => {
expect(Misc.isPlainObject(null)).toBe(false);
});
it("should return false for primitives", () => {
expect(Misc.isPlainObject(123)).toBe(false);
expect(Misc.isPlainObject("string")).toBe(false);
expect(Misc.isPlainObject(true)).toBe(false);
expect(Misc.isPlainObject(undefined)).toBe(false);
expect(Misc.isPlainObject(Symbol("sym"))).toBe(false);
});
it("should return false for objects with different prototypes", () => {
// oxlint-disable-next-line no-extraneous-class
class MyClass {}
expect(Misc.isPlainObject(new MyClass())).toBe(false);
expect(Misc.isPlainObject(Object.create(null))).toBe(false);
expect(Misc.isPlainObject(new Date())).toBe(false);
expect(Misc.isPlainObject(new Map())).toBe(false);
expect(Misc.isPlainObject(new Set())).toBe(false);
});
it("should return false for functions", () => {
// oxlint-disable-next-line no-empty-function
expect(Misc.isPlainObject(function () {})).toBe(false);
// oxlint-disable-next-line no-empty-function
expect(Misc.isPlainObject(() => {})).toBe(false);
});
});
});

View File

@@ -0,0 +1,21 @@
import { describe, it, expect } from "vitest";
import { buildMonkeyMail } from "../../src/utils/monkey-mail";
describe("Monkey Mail", () => {
it("should properly create a mail object", () => {
const mailConfig = {
subject: "",
body: "",
timestamp: Date.now(),
};
const mail = buildMonkeyMail(mailConfig) as any;
expect(mail.id).toBeDefined();
expect(mail.subject).toBe("");
expect(mail.body).toBe("");
expect(mail.timestamp).toBeDefined();
expect(mail.read).toBe(false);
expect(mail.rewards).toEqual([]);
});
});

View File

@@ -0,0 +1,213 @@
import { describe, it, expect } from "vitest";
import * as pb from "../../src/utils/pb";
import { Mode, PersonalBests } from "@monkeytype/schemas/shared";
import { Result } from "@monkeytype/schemas/results";
import { FunboxName } from "@monkeytype/schemas/configs";
describe("Pb Utils", () => {
describe("funboxCatGetPb", () => {
const testCases: { funbox: FunboxName[] | undefined; expected: boolean }[] =
[
{
funbox: ["plus_one"],
expected: true,
},
{
funbox: [],
expected: true,
},
{
funbox: undefined,
expected: true,
},
{
funbox: ["nausea", "plus_one"],
expected: true,
},
{
funbox: ["arrows"],
expected: false,
},
];
it.each(testCases)(
"canFunboxGetPb with $funbox = $expected",
({ funbox, expected }) => {
const result = pb.canFunboxGetPb({ funbox } as any);
expect(result).toBe(expected);
},
);
});
describe("checkAndUpdatePb", () => {
it("should update personal best", () => {
const userPbs: PersonalBests = {
time: {},
words: {},
custom: {},
quote: {},
zen: {},
};
const result = {
difficulty: "normal",
language: "english",
punctuation: false,
lazyMode: false,
acc: 100,
consistency: 100,
rawWpm: 100,
wpm: 110,
numbers: false,
mode: "time",
mode2: "15",
} as unknown as Result<Mode>;
const run = pb.checkAndUpdatePb(
userPbs,
{} as pb.LbPersonalBests,
result,
);
expect(run.isPb).toBe(true);
expect(run.personalBests.time?.["15"]?.[0]).not.toBe(undefined);
expect(run.lbPersonalBests).not.toBe({});
});
it("should not override default pb when saving numbers test", () => {
const userPbs: PersonalBests = {
time: {
"15": [
{
acc: 100,
consistency: 100,
difficulty: "normal",
lazyMode: false,
language: "english",
numbers: false,
punctuation: false,
raw: 100,
timestamp: 0,
wpm: 100,
},
],
},
words: {},
custom: {},
quote: {},
zen: {},
};
const result = {
difficulty: "normal",
language: "english",
punctuation: false,
lazyMode: false,
acc: 100,
consistency: 100,
rawWpm: 100,
wpm: 110,
numbers: true,
mode: "time",
mode2: "15",
} as unknown as Result<Mode>;
const run = pb.checkAndUpdatePb(userPbs, undefined, result);
expect(run.isPb).toBe(true);
expect(run.personalBests.time?.["15"]).toEqual(
expect.arrayContaining([
expect.objectContaining({ numbers: false, wpm: 100 }),
expect.objectContaining({ numbers: true, wpm: 110 }),
]),
);
});
});
describe("updateLeaderboardPersonalBests", () => {
const userPbs: PersonalBests = {
time: {
"15": [
{
acc: 100,
consistency: 100,
difficulty: "normal",
lazyMode: false,
language: "english",
numbers: false,
punctuation: false,
raw: 100,
timestamp: 0,
wpm: 100,
},
{
acc: 100,
consistency: 100,
difficulty: "normal",
lazyMode: false,
language: "spanish",
numbers: false,
punctuation: false,
raw: 100,
timestamp: 0,
wpm: 100,
},
],
},
words: {},
custom: {},
quote: {},
zen: {},
};
it("should update leaderboard personal bests if they dont exist or the structure is incomplete", () => {
const lbpbstartingvalues = [
undefined,
{},
{ time: {} },
{ time: { "15": {} } },
{ time: { "15": { english: {} } } },
];
const result15 = {
mode: "time",
mode2: "15",
} as unknown as Result<Mode>;
for (const lbPb of lbpbstartingvalues) {
const lbPbPb = pb.updateLeaderboardPersonalBests(
userPbs,
structuredClone(lbPb) as pb.LbPersonalBests,
result15,
);
expect(lbPbPb).toEqual({
time: {
"15": {
english: {
acc: 100,
consistency: 100,
difficulty: "normal",
lazyMode: false,
language: "english",
numbers: false,
punctuation: false,
raw: 100,
timestamp: 0,
wpm: 100,
},
spanish: {
acc: 100,
consistency: 100,
difficulty: "normal",
lazyMode: false,
language: "spanish",
numbers: false,
punctuation: false,
raw: 100,
timestamp: 0,
wpm: 100,
},
},
},
});
}
});
});
});

View File

@@ -0,0 +1,220 @@
import { describe, it, expect } from "vitest";
import { replaceLegacyValues, DBResult } from "../../src/utils/result";
describe("Result Utils", () => {
describe("replaceLegacyValues", () => {
describe("legacy charStats conversion", () => {
it.each([
{
description:
"should convert correctChars and incorrectChars to charStats",
correctChars: 95,
incorrectChars: 5,
expectedCharStats: [95, 5, 0, 0],
},
{
description: "should handle zero values for legacy chars",
correctChars: 0,
incorrectChars: 0,
expectedCharStats: [0, 0, 0, 0],
},
{
description: "should handle large values for legacy chars",
correctChars: 9999,
incorrectChars: 1234,
expectedCharStats: [9999, 1234, 0, 0],
},
])(
"$description",
({ correctChars, incorrectChars, expectedCharStats }) => {
const resultWithLegacyChars: DBResult = {
correctChars,
incorrectChars,
} as any;
const result = replaceLegacyValues(resultWithLegacyChars);
expect(result.charStats).toEqual(expectedCharStats);
expect(result.correctChars).toBeUndefined();
expect(result.incorrectChars).toBeUndefined();
},
);
it("should prioritise charStats when legacy data exists", () => {
const resultWithBothFormats: DBResult = {
charStats: [80, 4, 2, 1],
correctChars: 95,
incorrectChars: 5,
} as any;
const result = replaceLegacyValues(resultWithBothFormats);
// Should convert legacy values and overwrite existing charStats
expect(result.charStats).toEqual([80, 4, 2, 1]);
// Legacy values should be removed after conversion
expect(result.correctChars).toBeUndefined();
expect(result.incorrectChars).toBeUndefined();
});
it.each([
{
description:
"should not convert when only one legacy property is present",
input: { correctChars: 95 },
expectedCharStats: undefined,
expectedCorrectChars: 95,
expectedIncorrectChars: undefined,
},
{
description: "should not convert when only incorrectChars is present",
input: { incorrectChars: 5 },
expectedCharStats: undefined,
expectedCorrectChars: undefined,
expectedIncorrectChars: 5,
},
])(
"$description",
({
input,
expectedCharStats,
expectedCorrectChars,
expectedIncorrectChars,
}) => {
const result = replaceLegacyValues(input as any);
// Should not convert since both properties are required
expect(result.charStats).toBe(expectedCharStats);
expect(result.correctChars).toBe(expectedCorrectChars);
expect(result.incorrectChars).toBe(expectedIncorrectChars);
},
);
});
describe("legacy funbox conversion", () => {
it.each([
{
description: "should convert string funbox to array",
input: "memory#mirror",
expected: ["memory", "mirror"],
},
{
description: "should convert single funbox string to array",
input: "memory",
expected: ["memory"],
},
{
description: "should convert 'none' funbox to empty array",
input: "none",
expected: [],
},
{
description: "should handle complex funbox combinations",
input: "memory#mirror#arrows#58008",
expected: ["memory", "mirror", "arrows", "58008"],
},
])("$description", ({ input, expected }) => {
const resultWithStringFunbox: DBResult = {
funbox: input as any,
} as any;
const result = replaceLegacyValues(resultWithStringFunbox);
expect(result.funbox).toEqual(expected);
});
});
describe("legacy chartData conversion", () => {
it("should convert chartData with 'raw' property to 'burst'", () => {
const resultWithLegacyChartData: DBResult = {
chartData: {
wpm: [50, 55, 60],
raw: [52, 57, 62],
err: [1, 0, 2],
} as any,
} as any;
const result = replaceLegacyValues(resultWithLegacyChartData);
expect(result.chartData).toEqual({
wpm: [50, 55, 60],
burst: [52, 57, 62],
err: [1, 0, 2],
});
});
it("should not convert chartData when it's 'toolong'", () => {
const resultWithToolongChartData: DBResult = {
chartData: "toolong",
} as any;
const result = replaceLegacyValues(resultWithToolongChartData);
expect(result.chartData).toBe("toolong");
});
it("should not convert chartData when it doesn't have 'raw' property", () => {
const resultWithModernChartData: DBResult = {
chartData: {
wpm: [50, 55, 60],
burst: [52, 57, 62],
err: [1, 0, 2],
},
} as any;
const result = replaceLegacyValues(resultWithModernChartData);
expect(result.chartData).toEqual({
wpm: [50, 55, 60],
burst: [52, 57, 62],
err: [1, 0, 2],
});
});
it("should not convert chartData when it's undefined", () => {
const resultWithoutChartData: DBResult = {} as any;
const result = replaceLegacyValues(resultWithoutChartData);
expect(result.chartData).toBeUndefined();
});
});
it("should convert all legacy data at once", () => {
const resultWithAllLegacy: DBResult = {
correctChars: 100,
incorrectChars: 8,
funbox: "memory#mirror" as any,
chartData: {
wpm: [50, 55, 60],
raw: [52, 57, 62],
err: [1, 0, 2],
} as any,
} as any;
const result = replaceLegacyValues(resultWithAllLegacy);
expect(result.charStats).toEqual([100, 8, 0, 0]);
expect(result.correctChars).toBeUndefined();
expect(result.incorrectChars).toBeUndefined();
expect(result.funbox).toEqual(["memory", "mirror"]);
expect(result.chartData).toEqual({
wpm: [50, 55, 60],
burst: [52, 57, 62],
err: [1, 0, 2],
});
});
describe("no legacy values", () => {
it("should return result unchanged when no legacy values present", () => {
const modernResult: DBResult = {
charStats: [95, 5, 2, 1],
funbox: ["memory"],
} as any;
const result = replaceLegacyValues(modernResult);
expect(result).toEqual(modernResult);
});
});
});
});

View File

@@ -0,0 +1,55 @@
import { describe, it, expect } from "vitest";
import * as Validation from "../../src/utils/validation";
describe("Validation", () => {
it("isTestTooShort", () => {
const testCases = [
{
result: {
mode: "time",
mode2: 10,
customText: undefined,
testDuration: 10,
bailedOut: false,
},
expected: true,
},
{
result: {
mode: "time",
mode2: 15,
customText: undefined,
testDuration: 15,
bailedOut: false,
},
expected: false,
},
{
result: {
mode: "time",
mode2: 0,
customText: undefined,
testDuration: 20,
bailedOut: false,
},
expected: false,
},
{
result: {
mode: "time",
mode2: 0,
customText: undefined,
testDuration: 2,
bailedOut: false,
},
expected: true,
},
];
testCases.forEach((testCase) => {
expect(Validation.isTestTooShort(testCase.result as any)).toBe(
testCase.expected,
);
});
});
});

32
backend/__tests__/vitest.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
import type { Assertion, AsymmetricMatchersContaining } from "vitest";
import type { Test as SuperTest } from "supertest";
import MonkeyError from "../src/utils/error";
type ExpectedRateLimit = {
/** max calls */
max: number;
/** window in milliseconds. Needs to be within 2500ms */
windowMs: number;
};
interface RestRequestMatcher<R = Supertest> {
toBeRateLimited: (expected: ExpectedRateLimit) => RestRequestMatcher<R>;
}
interface ThrowMatcher {
toMatchMonkeyError: (expected: {
status: number;
message: string;
}) => MatcherResult;
}
declare module "vitest" {
interface Assertion<T = any> extends RestRequestMatcher<T>, ThrowMatcher {}
interface AsymmetricMatchersContaining
extends RestRequestMatcher, ThrowMatcher {}
}
interface MatcherResult {
pass: boolean;
message: () => string;
actual?: unknown;
expected?: unknown;
}

View File

@@ -0,0 +1,33 @@
import { describe, it, expect } from "vitest";
import * as LaterWorker from "../../src/workers/later-worker";
const calculateXpReward = LaterWorker.__testing.calculateXpReward;
describe("later-worker", () => {
describe("calculateXpReward", () => {
it("should return the correct XP reward for a given rank", () => {
//GIVEN
const xpRewardBrackets = [
{ minRank: 1, maxRank: 1, minReward: 100, maxReward: 100 },
{ minRank: 2, maxRank: 10, minReward: 50, maxReward: 90 },
];
//WHEN / THEN
expect(calculateXpReward(xpRewardBrackets, 5)).toBe(75);
expect(calculateXpReward(xpRewardBrackets, 11)).toBeUndefined();
});
it("should return the highest XP reward if brackets overlap", () => {
//GIVEN
const xpRewardBrackets = [
{ minRank: 1, maxRank: 5, minReward: 900, maxReward: 1000 },
{ minRank: 2, maxRank: 20, minReward: 50, maxReward: 90 },
];
//WHEN
const reward = calculateXpReward(xpRewardBrackets, 5);
//THEN
expect(reward).toBe(900);
});
});
});