Files
test/backend/__tests__/api/controllers/quotes.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

897 lines
24 KiB
TypeScript

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