Files
test/backend/__tests__/api/controllers/admin.spec.ts
Benjamin Falch 2bc741fb78
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled
adding monkeytype
2026-04-23 13:53:44 +02:00

581 lines
17 KiB
TypeScript

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