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