This commit is contained in:
580
backend/__tests__/api/controllers/admin.spec.ts
Normal file
580
backend/__tests__/api/controllers/admin.spec.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
373
backend/__tests__/api/controllers/ape-key.spec.ts
Normal file
373
backend/__tests__/api/controllers/ape-key.spec.ts
Normal 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;
|
||||
}
|
||||
133
backend/__tests__/api/controllers/config.spec.ts
Normal file
133
backend/__tests__/api/controllers/config.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
183
backend/__tests__/api/controllers/configuration.spec.ts
Normal file
183
backend/__tests__/api/controllers/configuration.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
397
backend/__tests__/api/controllers/connections.spec.ts
Normal file
397
backend/__tests__/api/controllers/connections.spec.ts
Normal 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.");
|
||||
}
|
||||
56
backend/__tests__/api/controllers/dev.spec.ts
Normal file
56
backend/__tests__/api/controllers/dev.spec.ts
Normal 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'"],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1491
backend/__tests__/api/controllers/leaderboard.spec.ts
Normal file
1491
backend/__tests__/api/controllers/leaderboard.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
501
backend/__tests__/api/controllers/preset.spec.ts
Normal file
501
backend/__tests__/api/controllers/preset.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
81
backend/__tests__/api/controllers/psa.spec.ts
Normal file
81
backend/__tests__/api/controllers/psa.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
144
backend/__tests__/api/controllers/public.spec.ts
Normal file
144
backend/__tests__/api/controllers/public.spec.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
896
backend/__tests__/api/controllers/quotes.spec.ts
Normal file
896
backend/__tests__/api/controllers/quotes.spec.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
891
backend/__tests__/api/controllers/result.spec.ts
Normal file
891
backend/__tests__/api/controllers/result.spec.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
4056
backend/__tests__/api/controllers/user.spec.ts
Normal file
4056
backend/__tests__/api/controllers/user.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
81
backend/__tests__/api/controllers/webhooks.spec.ts
Normal file
81
backend/__tests__/api/controllers/webhooks.spec.ts
Normal 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".');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user