This commit is contained in:
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user