import { describe, expect, it, vi } from "vitest"; import { CustomThemeColors } from "@monkeytype/schemas/configs"; import { PersonalBest, PersonalBests } from "@monkeytype/schemas/shared"; import { MonkeyMail, ResultFilters } from "@monkeytype/schemas/users"; import { ObjectId } from "mongodb"; import * as UserDAL from "../../../src/dal/user"; import { createConnection as createFriend } from "../../__testData__/connections"; import * as UserTestData from "../../__testData__/users"; const mockPersonalBest: PersonalBest = { acc: 1, consistency: 1, difficulty: "normal" as const, lazyMode: true, language: "polish", punctuation: false, raw: 230, wpm: 215, timestamp: 13123123, }; const mockResultFilter: ResultFilters = { _id: "id", name: "sfdkjhgdf", pb: { no: true, yes: true, }, difficulty: { normal: true, expert: false, master: false, }, mode: { words: false, time: true, quote: false, zen: false, custom: false, }, words: { "10": false, "25": false, "50": false, "100": false, custom: false, }, time: { "15": false, "30": true, "60": false, "120": false, custom: false, }, quoteLength: { short: false, medium: false, long: false, thicc: false, }, punctuation: { on: false, off: true, }, numbers: { on: false, off: true, }, date: { last_day: false, last_week: false, last_month: false, last_3months: false, all: true, }, tags: { none: true, }, language: { english: true, }, funbox: { none: true, }, }; const mockDbResultFilter = { ...mockResultFilter, _id: new ObjectId() }; describe("UserDal", () => { it("should be able to insert users", async () => { // given const uid = new ObjectId().toHexString(); const newUser = { name: "Test", email: "mockemail@email.com", uid, }; // when await UserDAL.addUser(newUser.name, newUser.email, newUser.uid); const insertedUser = await UserDAL.getUser(newUser.uid, "test"); // then expect(insertedUser.email).toBe(newUser.email); expect(insertedUser.uid).toBe(newUser.uid); expect(insertedUser.name).toBe(newUser.name); }); it("should error if the user already exists", async () => { // given const uid = new ObjectId().toHexString(); const newUser = { name: "Test", email: "mockemail@email.com", uid: uid, }; // when await UserDAL.addUser(newUser.name, newUser.email, newUser.uid); // then // should error because user already exists await expect( UserDAL.addUser(newUser.name, newUser.email, newUser.uid), ).rejects.toThrow("User document already exists"); }); it("isNameAvailable should correctly check if a username is available", async () => { // given const name1 = "user" + new ObjectId().toHexString(); const name2 = "user" + new ObjectId().toHexString(); const { uid: user1 } = await UserTestData.createUser({ name: name1 }); await UserTestData.createUser({ name: name2 }); const testCases = [ { name: name1, whosChecking: user1, expected: true, }, { name: name1.toUpperCase(), whosChecking: user1, expected: true, }, { name: name2, whosChecking: user1, expected: false, }, ]; // when, then for (const { name, expected, whosChecking } of testCases) { const isAvailable = await UserDAL.isNameAvailable(name, whosChecking); expect(isAvailable).toBe(expected); } }); it("updatename should not allow unavailable usernames", async () => { // given const name1 = "user" + new ObjectId().toHexString(); const name2 = "user" + new ObjectId().toHexString(); const user1 = await UserTestData.createUser({ name: name1 }); const user2 = await UserTestData.createUser({ name: name2 }); const _decoy = await UserTestData.createUser(); // when, then await expect( UserDAL.updateName(user1.uid, user2.name, user1.name), ).rejects.toThrow("Username already taken"); }); it("same usernames (different casing) should be available only for the same user", async () => { const name1 = "user" + new ObjectId().toHexString(); const name2 = "user" + new ObjectId().toHexString(); const user1 = await UserTestData.createUser({ name: name1 }); const user2 = await UserTestData.createUser({ name: name2 }); await UserDAL.updateName(user1.uid, name1.toUpperCase(), user1.name); const updatedUser1 = await UserDAL.getUser(user1.uid, "test"); // when, then expect(updatedUser1.name).toBe(name1.toUpperCase()); await expect( UserDAL.updateName(user2.uid, name1, user2.name), ).rejects.toThrow("Username already taken"); }); it("UserDAL.updateName should change the name of a user", async () => { // given const name = "user" + new ObjectId().toHexString(); const renamed = "renamed" + new ObjectId().toHexString(); const testUser = await UserTestData.createUser({ name: name }); // when await UserDAL.updateName(testUser.uid, renamed, testUser.name); // then const updatedUser = await UserDAL.getUser(testUser.uid, "test"); expect(updatedUser.name).toBe(renamed); }); it("clearPb should clear the personalBests of a user", async () => { // given const testUser = await UserTestData.createUser(); await UserDAL.getUsersCollection().updateOne( { uid: testUser.uid }, { $set: { personalBests: { time: { 20: [mockPersonalBest] }, words: {}, quote: {}, zen: {}, custom: {}, }, }, }, ); const { personalBests } = (await UserDAL.getUser(testUser.uid, "test")) ?? {}; expect(personalBests).toStrictEqual({ time: { 20: [mockPersonalBest] }, words: {}, quote: {}, custom: {}, zen: {}, }); // when await UserDAL.clearPb(testUser.uid); // then const updatedUser = (await UserDAL.getUser(testUser.uid, "test")) ?? {}; expect(updatedUser.personalBests).toStrictEqual({ time: {}, words: {}, quote: {}, custom: {}, zen: {}, }); }); it("autoBan should automatically ban after configured anticheat triggers", async () => { // given const testUser = await UserTestData.createUser(); // when Date.now = vi.fn(() => 0); await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1); await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1); await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1); // then const updatedUser = await UserDAL.getUser(testUser.uid, "test"); expect(updatedUser.banned).toBe(true); expect(updatedUser.autoBanTimestamps).toEqual([0, 0, 0]); }); it("autoBan should not ban ban if triggered once", async () => { // given const testUser = await UserTestData.createUser(); // when Date.now = vi.fn(() => 0); await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1); // then const updatedUser = await UserDAL.getUser(testUser.uid, "test"); expect(updatedUser.banned).toBe(undefined); expect(updatedUser.autoBanTimestamps).toEqual([0]); }); it("autoBan should correctly remove old anticheat triggers", async () => { // given const testUser = await UserTestData.createUser(); // when Date.now = vi.fn(() => 0); await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1); await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1); Date.now = vi.fn(() => 36000000); await UserDAL.recordAutoBanEvent(testUser.uid, 2, 1); // then const updatedUser = await UserDAL.getUser(testUser.uid, "test"); expect(updatedUser.banned).toBe(undefined); expect(updatedUser.autoBanTimestamps).toEqual([36000000]); }); describe("addResultFilterPreset", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.addResultFilterPreset("non existing uid", mockResultFilter, 5), ).rejects.toThrow( "Maximum number of custom filters reached\nStack: add result filter preset", ); }); it("should return error if user has reached maximum", async () => { // given const { uid } = await UserTestData.createUser({ resultFilterPresets: [mockDbResultFilter], }); // when, then await expect( UserDAL.addResultFilterPreset(uid, mockResultFilter, 1), ).rejects.toThrow( "Maximum number of custom filters reached\nStack: add result filter preset", ); }); it("should handle zero maximum", async () => { // given const { uid } = await UserTestData.createUser(); // when, then await expect( UserDAL.addResultFilterPreset(uid, mockResultFilter, 0), ).rejects.toThrow( "Maximum number of custom filters reached\nStack: add result filter preset", ); }); it("addResultFilterPreset success", async () => { // given const { uid } = await UserTestData.createUser({ resultFilterPresets: [mockDbResultFilter], }); // when const result = await UserDAL.addResultFilterPreset( uid, { ...mockResultFilter }, 2, ); // then const read = await UserDAL.getUser(uid, "read"); const createdFilter = read.resultFilterPresets ?? []; expect(result).toStrictEqual(createdFilter[1]?._id); }); }); describe("removeResultFilterPreset", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.removeResultFilterPreset( "non existing uid", new ObjectId().toHexString(), ), ).rejects.toThrow("Custom filter not found\nStack: remove result filter"); }); it("should return error if filter is unknown", async () => { // given const { uid } = await UserTestData.createUser({ resultFilterPresets: [mockDbResultFilter], }); // when, then await expect( UserDAL.removeResultFilterPreset(uid, new ObjectId().toHexString()), ).rejects.toThrow("Custom filter not found\nStack: remove result filter"); }); it("should remove filter", async () => { // given const filterOne = { ...mockDbResultFilter, _id: new ObjectId() }; const filterTwo = { ...mockDbResultFilter, _id: new ObjectId() }; const filterThree = { ...mockDbResultFilter, _id: new ObjectId() }; const { uid } = await UserTestData.createUser({ resultFilterPresets: [filterOne, filterTwo, filterThree], }); // when, then await UserDAL.removeResultFilterPreset(uid, filterTwo._id.toHexString()); const read = await UserDAL.getUser(uid, "read"); expect(read.resultFilterPresets).toStrictEqual([filterOne, filterThree]); }); }); describe("addTag", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.addTag("non existing uid", "tagName"), ).rejects.toThrow("Maximum number of tags reached\nStack: add tag"); }); it("should return error if user has reached maximum", async () => { // given const { uid } = await UserTestData.createUser({ tags: new Array(15).fill(0).map(() => ({ _id: new ObjectId(), name: "any", personalBests: {} as any, })), }); // when, then await expect(UserDAL.addTag(uid, "new")).rejects.toThrow( "Maximum number of tags reached\nStack: add tag", ); }); it("addTag success", async () => { // given const emptyPb: PersonalBests = { time: {}, words: {}, quote: {}, zen: {}, custom: {}, }; const { uid } = await UserTestData.createUser({ tags: [ { _id: new ObjectId(), name: "first", personalBests: emptyPb, }, ], }); // when await UserDAL.addTag(uid, "newTag"); // then const read = await UserDAL.getUser(uid, "read"); expect(read.tags).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "first", personalBests: emptyPb }), expect.objectContaining({ name: "newTag", personalBests: emptyPb }), ]), ); }); }); describe("editTag", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.editTag( "non existing uid", new ObjectId().toHexString(), "newName", ), ).rejects.toThrow("Tag not found\nStack: edit tag"); }); it("should fail if tag not found", async () => { // given const tagOne: UserDAL.DBUserTag = { _id: new ObjectId(), name: "one", personalBests: {} as any, }; const { uid } = await UserTestData.createUser({ tags: [tagOne], }); // when, then await expect( UserDAL.editTag(uid, new ObjectId().toHexString(), "newName"), ).rejects.toThrow("Tag not found\nStack: edit tag"); }); it("editTag success", async () => { // given const tagOne: UserDAL.DBUserTag = { _id: new ObjectId(), name: "one", personalBests: {} as any, }; const { uid } = await UserTestData.createUser({ tags: [tagOne], }); // when await UserDAL.editTag(uid, tagOne._id.toHexString(), "newTagName"); // then const read = await UserDAL.getUser(uid, "read"); expect(read.tags ?? [][0]).toStrictEqual([ { ...tagOne, name: "newTagName" }, ]); }); }); describe("removeTag", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.removeTag("non existing uid", new ObjectId().toHexString()), ).rejects.toThrow("Tag not found\nStack: remove tag"); }); it("should return error if tag is unknown", async () => { // given const tagOne: UserDAL.DBUserTag = { _id: new ObjectId(), name: "one", personalBests: {} as any, }; const { uid } = await UserTestData.createUser({ tags: [tagOne], }); // when, then await expect( UserDAL.removeTag(uid, new ObjectId().toHexString()), ).rejects.toThrow("Tag not found\nStack: remove tag"); }); it("should remove tag", async () => { // given const tagOne = { _id: new ObjectId(), name: "tagOne", personalBests: {} as any, }; const tagTwo = { _id: new ObjectId(), name: "tagTwo", personalBests: {} as any, }; const tagThree = { _id: new ObjectId(), name: "tagThree", personalBests: {} as any, }; const { uid } = await UserTestData.createUser({ tags: [tagOne, tagTwo, tagThree], }); // when, then await UserDAL.removeTag(uid, tagTwo._id.toHexString()); const read = await UserDAL.getUser(uid, "read"); expect(read.tags).toStrictEqual([tagOne, tagThree]); }); }); describe("removeTagPb", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.removeTagPb("non existing uid", new ObjectId().toHexString()), ).rejects.toThrow("Tag not found\nStack: remove tag pb"); }); it("should return error if tag is unknown", async () => { // given const tagOne: UserDAL.DBUserTag = { _id: new ObjectId(), name: "one", personalBests: {} as any, }; const { uid } = await UserTestData.createUser({ tags: [tagOne], }); // when, then await expect( UserDAL.removeTagPb(uid, new ObjectId().toHexString()), ).rejects.toThrow("Tag not found\nStack: remove tag pb"); }); it("should remove tag pb", async () => { // given const tagOne = { _id: new ObjectId(), name: "tagOne", personalBests: { custom: { custom: [mockPersonalBest] }, } as PersonalBests, }; const tagTwo = { _id: new ObjectId(), name: "tagTwo", personalBests: { custom: { custom: [mockPersonalBest] }, } as PersonalBests, }; const tagThree = { _id: new ObjectId(), name: "tagThree", personalBests: { custom: { custom: [mockPersonalBest] }, } as PersonalBests, }; const { uid } = await UserTestData.createUser({ tags: [tagOne, tagTwo, tagThree], }); // when, then await UserDAL.removeTagPb(uid, tagTwo._id.toHexString()); const read = await UserDAL.getUser(uid, "read"); expect(read.tags).toStrictEqual([ tagOne, { ...tagTwo, personalBests: { time: {}, words: {}, quote: {}, zen: {}, custom: {}, }, }, tagThree, ]); }); }); describe("updateProfile", () => { it("updateProfile should appropriately handle multiple profile updates", async () => { const uid = new ObjectId().toHexString(); await UserDAL.addUser("test name", "test email", uid); await UserDAL.updateProfile( uid, { bio: "test bio", }, { badges: [], }, ); const user = await UserDAL.getUser(uid, "test add result filters"); expect(user.profileDetails).toStrictEqual({ bio: "test bio", }); expect(user.inventory).toStrictEqual({ badges: [], }); await UserDAL.updateProfile( uid, { keyboard: "test keyboard", socialProfiles: { twitter: "test twitter", }, }, { badges: [ { id: 1, selected: true, }, ], }, ); const updatedUser = await UserDAL.getUser(uid, "test add result filters"); expect(updatedUser.profileDetails).toStrictEqual({ bio: "test bio", keyboard: "test keyboard", socialProfiles: { twitter: "test twitter", }, }); expect(updatedUser.inventory).toStrictEqual({ badges: [ { id: 1, selected: true, }, ], }); await UserDAL.updateProfile( uid, { bio: "test bio 2", socialProfiles: { github: "test github", website: "test website", }, }, { badges: [ { id: 1, }, ], }, ); const updatedUser2 = await UserDAL.getUser( uid, "test add result filters", ); expect(updatedUser2.profileDetails).toStrictEqual({ bio: "test bio 2", keyboard: "test keyboard", socialProfiles: { twitter: "test twitter", github: "test github", website: "test website", }, }); expect(updatedUser2.inventory).toStrictEqual({ badges: [ { id: 1, }, ], }); }); it("should omit undefined or empty object values", async () => { //GIVEN const givenUser = await UserTestData.createUser({ profileDetails: { bio: "test bio", keyboard: "test keyboard", socialProfiles: { twitter: "test twitter", github: "test github", }, }, }); //WHEN await UserDAL.updateProfile(givenUser.uid, { bio: undefined, //ignored keyboard: "updates", socialProfiles: {}, //ignored }); //THEN const read = await UserDAL.getUser(givenUser.uid, "read"); expect(read.profileDetails).toStrictEqual({ ...givenUser.profileDetails, keyboard: "updates", }); }); }); it("resetUser should reset user", async () => { const uid = new ObjectId().toHexString(); await UserDAL.addUser("test name", "test email", uid); await UserDAL.updateProfile( uid, { bio: "test bio", keyboard: "test keyboard", socialProfiles: { twitter: "test twitter", github: "test github", }, }, { badges: [], }, ); await UserDAL.incrementBananas(uid, 100); await UserDAL.incrementXp(uid, 15); await UserDAL.resetUser(uid); const resetUser = await UserDAL.getUser(uid, "test add result filters"); expect(resetUser.profileDetails).toStrictEqual({ bio: "", keyboard: "", socialProfiles: {}, }); expect(resetUser.inventory).toStrictEqual({ badges: [], }); expect(resetUser.bananas).toStrictEqual(0); expect(resetUser.xp).toStrictEqual(0); expect(resetUser.streak).toStrictEqual({ length: 0, lastResultTimestamp: 0, maxLength: 0, }); }); it("getInbox should return the user's inbox", async () => { const uid = new ObjectId().toHexString(); await UserDAL.addUser("test name", "test email", uid); const emptyInbox = await UserDAL.getInbox(uid); expect(emptyInbox).toStrictEqual([]); await UserDAL.addToInbox( uid, [ { subject: `Hello!`, } as any, ], { enabled: true, maxMail: 100, }, ); const inbox = await UserDAL.getInbox(uid); expect(inbox).toStrictEqual([ { subject: "Hello!", }, ]); }); it("addToInbox discards mail if inbox is full", async () => { const uid = new ObjectId().toHexString(); await UserDAL.addUser("test name", "test email", uid); const config = { enabled: true, maxMail: 1, }; await UserDAL.addToInbox( uid, [ { subject: "Hello 1!", } as any, ], config, ); await UserDAL.addToInbox( uid, [ { subject: "Hello 2!", } as any, ], config, ); const inbox = await UserDAL.getInbox(uid); expect(inbox).toStrictEqual([ { subject: "Hello 2!", }, ]); }); it("addToInboxBulk should add mail to multiple users", async () => { const { uid: user1 } = await UserTestData.createUser(); const { uid: user2 } = await UserTestData.createUser(); await UserDAL.addToInboxBulk( [ { uid: user1, mail: [ { subject: `Hello!`, } as any, ], }, { uid: user2, mail: [ { subject: `Hello 2!`, } as any, ], }, ], { enabled: true, maxMail: 100, }, ); const inbox = await UserDAL.getInbox(user1); const inbox2 = await UserDAL.getInbox(user2); expect(inbox).toStrictEqual([ { subject: "Hello!", }, ]); expect(inbox2).toStrictEqual([ { subject: "Hello 2!", }, ]); }); describe("updateStreak", () => { it("should return error if uid not found", async () => { // when, then await expect(UserDAL.updateStreak("non existing uid", 0)).rejects.toThrow( "User not found\nStack: calculate streak", ); }); it("updateStreak should update streak", async () => { const { uid } = await UserTestData.createUser(); const testSteps = [ { date: "2023/06/07 21:00:00 UTC", expectedStreak: 1, }, { date: "2023/06/07 23:00:00 UTC", expectedStreak: 1, }, { date: "2023/06/08 00:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/08 23:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/09 00:00:00 UTC", expectedStreak: 3, }, { date: "2023/06/11 00:00:00 UTC", expectedStreak: 1, }, ]; for (const { date, expectedStreak } of testSteps) { const milis = new Date(date).getTime(); Date.now = vi.fn(() => milis); const streak = await UserDAL.updateStreak(uid, milis); expect(streak).toBe(expectedStreak); } }); it("positive streak offset should award streak correctly", async () => { const { uid } = await UserTestData.createUser({ streak: { hourOffset: 10 } as any, }); const testSteps = [ { date: "2023/06/06 21:00:00 UTC", expectedStreak: 1, }, { date: "2023/06/07 01:00:00 UTC", expectedStreak: 1, }, { date: "2023/06/07 09:00:00 UTC", expectedStreak: 1, }, { date: "2023/06/07 10:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/07 23:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/08 00:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/08 01:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/08 09:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/08 10:00:00 UTC", expectedStreak: 3, }, { date: "2023/06/10 10:00:00 UTC", expectedStreak: 1, }, ]; for (const { date, expectedStreak } of testSteps) { const milis = new Date(date).getTime(); Date.now = vi.fn(() => milis); const streak = await UserDAL.updateStreak(uid, milis); expect(streak).toBe(expectedStreak); } }); it("negative streak offset should award streak correctly", async () => { const { uid } = await UserTestData.createUser({ streak: { hourOffset: -4 } as any, }); const testSteps = [ { date: "2023/06/06 19:00:00 UTC", expectedStreak: 1, }, { date: "2023/06/06 20:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/07 01:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/07 19:00:00 UTC", expectedStreak: 2, }, { date: "2023/06/07 20:00:00 UTC", expectedStreak: 3, }, { date: "2023/06/09 23:00:00 UTC", expectedStreak: 1, }, ]; for (const { date, expectedStreak } of testSteps) { const milis = new Date(date).getTime(); Date.now = vi.fn(() => milis); const streak = await UserDAL.updateStreak(uid, milis); expect(streak).toBe(expectedStreak); } }); }); describe("incrementTestActivity", () => { it("ignores user without migration", async () => { // given const user = await UserTestData.createUserWithoutMigration(); //when await UserDAL.incrementTestActivity(user, 1712102400000); //then const read = await UserDAL.getUser(user.uid, ""); expect(read.testActivity).toBeUndefined(); }); it("increments for new year", async () => { // given const user = await UserTestData.createUser({ testActivity: { "2023": [null, 1] }, }); //when await UserDAL.incrementTestActivity(user, 1712102400000); //then const read = (await UserDAL.getUser(user.uid, "")).testActivity || {}; expect(read).toHaveProperty("2024"); const year2024 = read["2024"] as any; expect(year2024).toHaveLength(94); //fill previous days with null expect(year2024.slice(0, 93)).toEqual(new Array(93).fill(null)); expect(year2024[93]).toEqual(1); }); it("increments for existing year", async () => { // given const user = await UserTestData.createUser({ testActivity: { "2024": [null, 5] }, }); //when await UserDAL.incrementTestActivity(user, 1712102400000); //then const read = (await UserDAL.getUser(user.uid, "")).testActivity || {}; expect(read).toHaveProperty("2024"); const year2024 = read["2024"] as any; expect(year2024).toHaveLength(94); expect(year2024[0]).toBeNull(); expect(year2024[1]).toEqual(5); expect(year2024.slice(2, 91)).toEqual(new Array(89).fill(null)); expect(year2024[93]).toEqual(1); }); it("increments for existing day", async () => { // given let user = await UserTestData.createUser({ testActivity: {} }); await UserDAL.incrementTestActivity(user, 1712102400000); user = await UserDAL.getUser(user.uid, ""); //when await UserDAL.incrementTestActivity(user, 1712102400000); //then const read = (await UserDAL.getUser(user.uid, "")).testActivity || {}; const year2024 = read["2024"] as any; expect(year2024[93]).toEqual(2); }); }); describe("getUser", () => { it("should get with missing personalBests", async () => { //GIVEN let user = await UserTestData.createUser({ personalBests: undefined }); //WHEN const read = await UserDAL.getUser(user.uid, "read"); expect(read.personalBests).toEqual({ custom: {}, quote: {}, time: {}, words: {}, zen: {}, }); }); }); describe("getUserByName", () => { it("should get with missing personalBests", async () => { //GIVEN let user = await UserTestData.createUser({ personalBests: undefined }); //WHEN const read = await UserDAL.getUserByName(user.name, "read"); expect(read.personalBests).toEqual({ custom: {}, quote: {}, time: {}, words: {}, zen: {}, }); }); }); describe("getPersonalBests", () => { it("should get with missing personalBests", async () => { //GIVEN let user = await UserTestData.createUser({ personalBests: undefined }); //WHEN const read = await UserDAL.getPersonalBests(user.uid, "time", "15"); expect(read).toBeUndefined(); }); }); describe("getPartialUser", () => { it("should throw for unknown user", async () => { await expect(async () => UserDAL.getPartialUser("1234", "stack", []), ).rejects.toThrow("User not found\nStack: stack"); }); it("should get streak", async () => { //GIVEN let user = await UserTestData.createUser({ streak: { hourOffset: 1, length: 5, lastResultTimestamp: 4711, maxLength: 23, }, }); //WHEN const partial = await UserDAL.getPartialUser(user.uid, "streak", [ "streak", ]); //THEN expect(partial).toStrictEqual({ _id: user._id, streak: { hourOffset: 1, length: 5, lastResultTimestamp: 4711, maxLength: 23, }, }); }); it("should get with missing personalBests", async () => { //GIVEN let user = await UserTestData.createUser({ personalBests: undefined }); //WHEN const read = await UserDAL.getPartialUser(user.uid, "read", [ "uid", "personalBests", ]); expect(read.personalBests).toEqual({ custom: {}, quote: {}, time: {}, words: {}, zen: {}, }); }); }); describe("updateEmail", () => { it("throws for nonexisting user", async () => { await expect(async () => UserDAL.updateEmail("unknown", "test@example.com"), ).rejects.toThrow("User not found\nStack: update email"); }); it("should update", async () => { //given const { uid } = await UserTestData.createUser({ email: "init" }); //when await expect(UserDAL.updateEmail(uid, "next")).resolves.toBe(true); //then const read = await UserDAL.getUser(uid, "read"); expect(read.email).toEqual("next"); }); }); describe("resetPb", () => { it("throws for nonexisting user", async () => { await expect(async () => UserDAL.resetPb("unknown")).rejects.toThrow( "User not found\nStack: reset pb", ); }); it("should reset", async () => { //given const { uid } = await UserTestData.createUser({ personalBests: { custom: { custom: [{ acc: 1 } as any] } } as any, }); //when await UserDAL.resetPb(uid); //then const read = await UserDAL.getUser(uid, "read"); expect(read.personalBests).toStrictEqual({ time: {}, words: {}, quote: {}, zen: {}, custom: {}, }); }); }); describe("linkDiscord", () => { it("throws for nonexisting user", async () => { await expect(async () => UserDAL.linkDiscord("unknown", "", ""), ).rejects.toThrow("User not found\nStack: link discord"); }); it("should update", async () => { //given const { uid } = await UserTestData.createUser({ discordId: "discordId", discordAvatar: "discordAvatar", }); //when await UserDAL.linkDiscord(uid, "newId", "newAvatar"); //then const read = await UserDAL.getUser(uid, "read"); expect(read.discordId).toEqual("newId"); expect(read.discordAvatar).toEqual("newAvatar"); }); it("should update without avatar", async () => { //given const { uid } = await UserTestData.createUser({ discordId: "discordId", discordAvatar: "discordAvatar", }); //when await UserDAL.linkDiscord(uid, "newId"); //then const read = await UserDAL.getUser(uid, "read"); expect(read.discordId).toEqual("newId"); expect(read.discordAvatar).toEqual("discordAvatar"); }); }); describe("unlinkDiscord", () => { it("throws for nonexisting user", async () => { await expect(async () => UserDAL.unlinkDiscord("unknown"), ).rejects.toThrow("User not found\nStack: unlink discord"); }); it("should update", async () => { //given const { uid } = await UserTestData.createUser({ discordId: "discordId", discordAvatar: "discordAvatar", }); //when await UserDAL.unlinkDiscord(uid); //then const read = await UserDAL.getUser(uid, "read"); expect(read.discordId).toBeUndefined(); expect(read.discordAvatar).toBeUndefined(); }); }); describe("updateInbox", () => { it("claims rewards on read", async () => { //GIVEN const rewardOne: MonkeyMail = { id: "b5866d4c-0749-41b6-b101-3656249d39b9", body: "test", subject: "reward one", timestamp: 1, read: false, rewards: [ { type: "xp", item: 400 }, { type: "xp", item: 600 }, { type: "badge", item: { id: 4 } }, ], }; const rewardTwo: MonkeyMail = { id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a", body: "test", subject: "reward two", timestamp: 2, read: false, rewards: [{ type: "xp", item: 2000 }], }; const rewardThree: MonkeyMail = { id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a", body: "test", subject: "reward three", timestamp: 3, read: true, rewards: [{ type: "xp", item: 3000 }], }; const rewardFour: MonkeyMail = { id: "d852d2cf-1802-4cd0-9fb4-336650fc470a", body: "test", subject: "reward four", timestamp: 4, read: false, rewards: [{ type: "xp", item: 4000 }], }; let user = await UserTestData.createUser({ xp: 100, inbox: [rewardOne, rewardTwo, rewardThree, rewardFour], }); //WNEN await UserDAL.updateInbox( user.uid, [rewardOne.id, rewardTwo.id, rewardThree.id], [], ); //THEN const read = await UserDAL.getUser(user.uid, ""); expect(read).not.toHaveProperty("tmp"); const { xp, inbox } = read; expect(xp).toEqual(3100); //100 existing + 1000 from rewardOne, 2000 from rewardTwo //inbox is sorted by timestamp expect(inbox).toStrictEqual([ { ...rewardFour }, { ...rewardThree }, { ...rewardTwo, read: true, rewards: [] }, { ...rewardOne, read: true, rewards: [] }, ]); }); it("claims rewards on delete", async () => { //GIVEN //GIVEN const rewardOne: MonkeyMail = { id: "b5866d4c-0749-41b6-b101-3656249d39b9", body: "test", subject: "reward one", timestamp: 1, read: false, rewards: [ { type: "xp", item: 400 }, { type: "xp", item: 600 }, { type: "badge", item: { id: 4 } }, ], }; const rewardTwo: MonkeyMail = { id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a", body: "test", subject: "reward two", timestamp: 2, read: true, rewards: [{ type: "xp", item: 2000 }], }; const rewardThree: MonkeyMail = { id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a", body: "test", subject: "reward three", timestamp: 4, read: false, rewards: [{ type: "xp", item: 3000 }], }; let user = await UserTestData.createUser({ xp: 100, inbox: [rewardOne, rewardTwo, rewardThree], }); //WNEN await UserDAL.updateInbox(user.uid, [], [rewardOne.id, rewardTwo.id]); //THEN const { xp, inbox } = await UserDAL.getUser(user.uid, ""); expect(xp).toBe(1100); expect(inbox).toStrictEqual([rewardThree]); }); it("updates badge", async () => { //GIVEN const rewardOne: MonkeyMail = { id: "b5866d4c-0749-41b6-b101-3656249d39b9", body: "test", subject: "reward one", timestamp: 2, read: false, rewards: [ { type: "xp", item: 400 }, { type: "badge", item: { id: 4 } }, ], }; const rewardTwo: MonkeyMail = { id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a", body: "test", subject: "reward two", timestamp: 1, read: false, rewards: [ { type: "badge", item: { id: 3 } }, { type: "badge", item: { id: 4 } }, { type: "badge", item: { id: 5 } }, ], }; const rewardThree: MonkeyMail = { id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a", body: "test", subject: "reward three", timestamp: 0, read: true, rewards: [{ type: "badge", item: { id: 6 } }], }; let user = await UserTestData.createUser({ inbox: [rewardOne, rewardTwo, rewardThree], inventory: { badges: [ { id: 1, selected: true }, { id: 3, selected: false }, ], }, }); //WNEN await UserDAL.updateInbox( user.uid, [rewardOne.id, rewardTwo.id, rewardThree.id, rewardOne.id], [], ); //THEN const { inbox, inventory } = await UserDAL.getUser(user.uid, ""); expect(inbox).toStrictEqual([ { ...rewardOne, read: true, rewards: [] }, { ...rewardTwo, read: true, rewards: [] }, { ...rewardThree }, ]); expect(inventory?.badges).toStrictEqual([ { id: 1, selected: true }, //previously owned { id: 3, selected: false }, // previously owned, no duplicate { id: 4 }, // gets only added once { id: 5 }, ]); }); it("read and delete the same message does not claim reward twice", async () => { //GIVEN const rewardOne: MonkeyMail = { id: "b5866d4c-0749-41b6-b101-3656249d39b9", body: "test", subject: "reward one", timestamp: 0, read: false, rewards: [{ type: "xp", item: 1000 }], }; const rewardTwo: MonkeyMail = { id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a", body: "test", subject: "reward two", timestamp: 0, read: false, rewards: [{ type: "xp", item: 2000 }], }; let user = await UserTestData.createUser({ xp: 100, inbox: [rewardOne, rewardTwo], }); await UserDAL.updateInbox( user.uid, [rewardOne.id, rewardTwo.id], [rewardOne.id, rewardTwo.id], ); //THEN const { xp } = await UserDAL.getUser(user.uid, ""); expect(xp).toEqual(3100); }); it("concurrent calls dont claim a reward multiple times", async () => { //GIVEN const rewardOne: MonkeyMail = { id: "b5866d4c-0749-41b6-b101-3656249d39b9", body: "test", subject: "reward one", timestamp: 0, read: false, rewards: [ { type: "xp", item: 400 }, { type: "xp", item: 600 }, { type: "badge", item: { id: 4 } }, ], }; const rewardTwo: MonkeyMail = { id: "3692b9f5-84fb-4d9b-bd39-9a3217b3a33a", body: "test", subject: "reward two", timestamp: 0, read: false, rewards: [{ type: "xp", item: 2000 }], }; const rewardThree: MonkeyMail = { id: "0d73b3e0-dc79-4abb-bcaf-66fa6b09a58a", body: "test", subject: "reward three", timestamp: 0, read: true, rewards: [{ type: "xp", item: 2000 }], }; let user = await UserTestData.createUser({ xp: 100, inbox: [rewardOne, rewardTwo, rewardThree], }); const count = 100; const calls = new Array(count) .fill(0) .map(() => UserDAL.updateInbox( user.uid, [rewardOne.id, rewardTwo.id, rewardThree.id], [], ), ); await Promise.all(calls); //THEN const { xp } = await UserDAL.getUser(user.uid, ""); expect(xp).toEqual(3100); }); }); describe("isDiscordIdAvailable", () => { it("should return true for available discordId", async () => { const discordId = new ObjectId().toHexString(); await expect(UserDAL.isDiscordIdAvailable(discordId)).resolves.toBe(true); }); it("should return false if discordId is taken", async () => { // given const discordId = new ObjectId().toHexString(); await UserTestData.createUser({ discordId: discordId, }); // when, then await expect(UserDAL.isDiscordIdAvailable(discordId)).resolves.toBe( false, ); }); }); describe("updateLbMemory", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.updateLbMemory( "non existing uid", "time", "15", "english", 4711, ), ).rejects.toThrow("User not found\nStack: update lb memory"); }); it("updates on empty lbMemory", async () => { //GIVEN const { uid } = await UserTestData.createUser({}); //WHEN await UserDAL.updateLbMemory(uid, "time", "15", "english", 4711); //THEN const read = await UserDAL.getUser(uid, "read"); expect(read.lbMemory).toStrictEqual({ time: { "15": { english: 4711, }, }, }); }); it("updates on empty lbMemory.mode", async () => { //GIVEN const { uid } = await UserTestData.createUser({ lbMemory: { custom: {} }, }); //WHEN await UserDAL.updateLbMemory(uid, "time", "15", "english", 4711); //THEN const read = await UserDAL.getUser(uid, "read"); expect(read.lbMemory).toStrictEqual({ custom: {}, time: { "15": { english: 4711, }, }, }); }); it("updates on empty lbMemory.mode.mode2", async () => { //GIVEN const { uid } = await UserTestData.createUser({ lbMemory: { time: { "30": {} } }, }); //WHEN await UserDAL.updateLbMemory(uid, "time", "15", "english", 4711); //THEN const read = await UserDAL.getUser(uid, "read"); expect(read.lbMemory).toStrictEqual({ time: { "15": { english: 4711, }, "30": {}, }, }); }); }); describe("incrementBananas", () => { it("should not return error if uuid not found", async () => { // when, then await UserDAL.incrementBananas("non existing uid", 60); }); it("increments bananas", async () => { //GIVEN const name = "user" + new ObjectId().toHexString(); const { uid } = await UserTestData.createUser({ name, bananas: 1, personalBests: { time: { "60": [ { wpm: 100 } as PersonalBest, { wpm: 30 } as PersonalBest, //highest PB should be used ], }, } as any, }); //within 25% of PB await UserDAL.incrementBananas(uid, 75); const read = await UserDAL.getUser(uid, "read"); expect(read.bananas).toEqual(2); expect(read.name).toEqual(name); //NOT within 25% of PB await UserDAL.incrementBananas(uid, 74); expect((await UserDAL.getUser(uid, "read")).bananas).toEqual(2); }); it("ignores missing personalBests", async () => { //GIVEN const { uid } = await UserTestData.createUser({ bananas: 1, }); //WHEN await UserDAL.incrementBananas(uid, 75); //THEM expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1); }); it("ignores missing personalBests time", async () => { //GIVEN const { uid } = await UserTestData.createUser({ bananas: 1, personalBests: {} as any, }); //WHEN await UserDAL.incrementBananas(uid, 75); //THEM expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1); }); it("ignores missing personalBests time 60", async () => { //GIVEN const { uid } = await UserTestData.createUser({ bananas: 1, personalBests: { time: {} } as any, }); //WHEN await UserDAL.incrementBananas(uid, 75); //THEM expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1); }); it("ignores empty personalBests time 60", async () => { //GIVEN const { uid } = await UserTestData.createUser({ bananas: 1, personalBests: { time: { "60": [] } } as any, }); //WHEN await UserDAL.incrementBananas(uid, 75); //THEM expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1); }); it("should increment missing bananas", async () => { //GIVEN const { uid } = await UserTestData.createUser({ personalBests: { time: { "60": [{ wpm: 100 }] } } as any, }); //WHEN await UserDAL.incrementBananas(uid, 75); //THEM expect((await UserDAL.getUser(uid, "read")).bananas).toBe(1); }); }); describe("addTheme", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.addTheme("non existing uid", { name: "new", colors: [] as any, }), ).rejects.toThrow( "Maximum number of custom themes reached\nStack: add theme", ); }); it("should return error if user has reached maximum", async () => { // given const { uid } = await UserTestData.createUser({ customThemes: new Array(20).fill(0).map(() => ({ _id: new ObjectId(), name: "any", colors: [] as any, })), }); // when, then await expect( UserDAL.addTheme(uid, { name: "new", colors: [] as any }), ).rejects.toThrow( "Maximum number of custom themes reached\nStack: add theme", ); }); it("addTheme success", async () => { // given const themeOne = { _id: new ObjectId(), name: "first", colors: new Array(10).fill("#123456") as CustomThemeColors, }; const { uid } = await UserTestData.createUser({ customThemes: [themeOne], }); const newTheme = { name: "newTheme", colors: new Array(10).fill("#000000") as CustomThemeColors, }; // when await UserDAL.addTheme(uid, { ...newTheme }); // then const read = await UserDAL.getUser(uid, "read"); expect(read.customThemes).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "first", colors: themeOne.colors, }), expect.objectContaining({ name: "newTheme", colors: newTheme.colors, }), ]), ); }); }); describe("editTheme", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.editTheme("non existing uid", new ObjectId().toHexString(), { name: "newName", colors: [] as any, }), ).rejects.toThrow("Custom theme not found\nStack: edit theme"); }); it("should fail if theme not found", async () => { // given const themeOne = { _id: new ObjectId(), name: "first", colors: ["green", "white", "red"] as any, }; const { uid } = await UserTestData.createUser({ customThemes: [themeOne], }); // when, then await expect( UserDAL.editTheme(uid, new ObjectId().toHexString(), { name: "newName", colors: [] as any, }), ).rejects.toThrow("Custom theme not found\nStack: edit theme"); }); it("editTheme success", async () => { // given const themeOne = { _id: new ObjectId(), name: "first", colors: ["green", "white", "red"] as any, }; const { uid } = await UserTestData.createUser({ customThemes: [themeOne], }); // when await UserDAL.editTheme(uid, themeOne._id.toHexString(), { name: "newThemeName", colors: ["red", "white", "blue"] as any, }); // then const read = await UserDAL.getUser(uid, "read"); expect(read.customThemes ?? [][0]).toStrictEqual([ { ...themeOne, name: "newThemeName", colors: ["red", "white", "blue"] }, ]); }); }); describe("removeTheme", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.removeTheme("non existing uid", new ObjectId().toHexString()), ).rejects.toThrow("Custom theme not found\nStack: remove theme"); }); it("should return error if theme is unknown", async () => { // given const themeOne = { _id: new ObjectId(), name: "first", colors: ["green", "white", "red"] as any, }; const { uid } = await UserTestData.createUser({ customThemes: [themeOne], }); // when, then await expect( UserDAL.removeTheme(uid, new ObjectId().toHexString()), ).rejects.toThrow("Custom theme not found\nStack: remove theme"); }); it("should remove theme", async () => { // given const themeOne = { _id: new ObjectId(), name: "first", colors: [] as any, }; const themeTwo = { _id: new ObjectId(), name: "second", colors: [] as any, }; const themeThree = { _id: new ObjectId(), name: "third", colors: [] as any, }; const { uid } = await UserTestData.createUser({ customThemes: [themeOne, themeTwo, themeThree], }); // when, then await UserDAL.removeTheme(uid, themeTwo._id.toHexString()); const read = await UserDAL.getUser(uid, "read"); expect(read.customThemes).toStrictEqual([themeOne, themeThree]); }); }); describe("addFavoriteQuote", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.addFavoriteQuote("non existing uid", "english", "1", 5), ).rejects.toThrow( "Maximum number of favorite quotes reached\nStack: add favorite quote", ); }); it("should return error if user has reached maximum", async () => { // given const { uid } = await UserTestData.createUser({ favoriteQuotes: { english: ["1", "2"], german: ["3", "4"], polish: ["5"], }, }); // when, then await expect( UserDAL.addFavoriteQuote(uid, "polish", "6", 5), ).rejects.toThrow( "Maximum number of favorite quotes reached\nStack: add favorite quote", ); }); it("addFavoriteQuote success", async () => { // given const { uid } = await UserTestData.createUser({ favoriteQuotes: { english: ["1"], german: ["2"], polish: ["3"], }, }); // when await UserDAL.addFavoriteQuote(uid, "english", "4", 5); // then const read = await UserDAL.getUser(uid, "read"); expect(read.favoriteQuotes).toStrictEqual({ english: ["1", "4"], german: ["2"], polish: ["3"], }); }); it("should not add a quote twice", async () => { // given const { uid } = await UserTestData.createUser({ favoriteQuotes: { english: ["1", "3", "4"], german: ["2"], }, }); // when await UserDAL.addFavoriteQuote(uid, "english", "4", 5); // then const read = await UserDAL.getUser(uid, "read"); expect(read.favoriteQuotes).toStrictEqual({ english: ["1", "3", "4"], german: ["2"], }); }); }); describe("removeFavoriteQuote", () => { it("should return error if uid not found", async () => { // when, then await expect( UserDAL.removeFavoriteQuote("non existing uid", "english", "0"), ).rejects.toThrow("User not found\nStack: remove favorite quote"); }); it("should not fail if quote is not favorite", async () => { // given const { uid } = await UserTestData.createUser({ favoriteQuotes: { english: ["1", "2"], }, }); // when await UserDAL.removeFavoriteQuote(uid, "english", "3"); await UserDAL.removeFavoriteQuote(uid, "german", "1"); //then const read = await UserDAL.getUser(uid, "read"); expect(read.favoriteQuotes).toStrictEqual({ english: ["1", "2"], }); }); it("should remove", async () => { // given const { uid } = await UserTestData.createUser({ favoriteQuotes: { english: ["1", "2", "3"], }, }); // when await UserDAL.removeFavoriteQuote(uid, "english", "2"); //%hen const read = await UserDAL.getUser(uid, "read"); expect(read.favoriteQuotes).toStrictEqual({ english: ["1", "3"], }); }); }); describe("clearStreakHourOffset", () => { it("should clear streak hour offset", async () => { // given const { uid } = await UserTestData.createUser({ //@ts-expect-error streak: { hourOffset: 1, }, }); // when await UserDAL.clearStreakHourOffset(uid); //then const read = await UserDAL.getUser(uid, "read"); expect(read.streak?.hourOffset).toBeUndefined(); }); }); describe("getFriends", () => { it("get list of friends", async () => { //GIVEN const me = await UserTestData.createUser({ name: "Me" }); const uid = me.uid; const friendOne = await UserTestData.createUser({ name: "One", personalBests: { time: { "15": [UserTestData.pb(100)], "60": [UserTestData.pb(85), UserTestData.pb(90)], }, } as any, inventory: { badges: [{ id: 42, selected: true }, { id: 23 }, { id: 5 }], }, banned: true, lbOptOut: true, premium: { expirationTimestamp: -1 } as any, }); const friendOneRequest = await createFriend({ initiatorUid: uid, receiverUid: friendOne.uid, status: "accepted", lastModified: 100, }); const friendTwo = await UserTestData.createUser({ name: "Two", discordId: "discordId", discordAvatar: "discordAvatar", timeTyping: 600, startedTests: 150, completedTests: 125, streak: { length: 10, maxLength: 50, lastResultTimestamp: 0, hourOffset: -1, } as any, xp: 42, inventory: { badges: [{ id: 23 }, { id: 5 }], }, premium: { expirationTimestamp: vi.getRealSystemTime() + 5000, } as any, }); const friendTwoRequest = await createFriend({ initiatorUid: uid, receiverUid: friendTwo.uid, status: "accepted", lastModified: 200, }); const friendThree = await UserTestData.createUser({ name: "Three" }); const friendThreeRequest = await createFriend({ receiverUid: uid, initiatorUid: friendThree.uid, status: "accepted", lastModified: 300, }); //non accepted await createFriend({ receiverUid: uid, status: "pending" }); await createFriend({ initiatorUid: uid, status: "blocked" }); //WHEN const friends = await UserDAL.getFriends(uid); //THEN expect(friends).toEqual([ { uid: friendOne.uid, name: "One", lastModified: 100, connectionId: friendOneRequest._id, // oxlint-disable-next-line no-non-null-assertion top15: friendOne.personalBests.time["15"]![0] as any, // oxlint-disable-next-line no-non-null-assertion top60: friendOne.personalBests.time["60"]![1] as any, badgeId: 42, banned: true, lbOptOut: true, isPremium: true, }, { uid: friendTwo.uid, name: "Two", lastModified: 200, connectionId: friendTwoRequest._id, discordId: friendTwo.discordId, discordAvatar: friendTwo.discordAvatar, timeTyping: friendTwo.timeTyping, startedTests: friendTwo.startedTests, completedTests: friendTwo.completedTests, streak: { length: friendTwo.streak?.length, maxLength: friendTwo.streak?.maxLength, }, xp: friendTwo.xp, isPremium: true, }, { uid: friendThree.uid, name: "Three", lastModified: 300, connectionId: friendThreeRequest._id, }, { uid: me.uid, name: "Me", }, ]); }); }); });