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 { const mockConfig = await configuration; mockConfig.quotes = { ...mockConfig.quotes, submissionsEnabled: enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig, ); } async function enableQuoteReporting(enabled: boolean): Promise { const mockConfig = await configuration; mockConfig.quotes.reporting = { ...mockConfig.quotes.reporting, enabled, maxReports: 10, contentReportLimit: 20, }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig, ); }