diff --git a/backend/__tests__/dal/user.spec.ts b/backend/__tests__/dal/user.spec.ts index 21243c056..4ce193a1c 100644 --- a/backend/__tests__/dal/user.spec.ts +++ b/backend/__tests__/dal/user.spec.ts @@ -1,7 +1,7 @@ import _ from "lodash"; -import { updateStreak } from "../../src/dal/user"; import * as UserDAL from "../../src/dal/user"; import * as UserTestData from "../__testData__/users"; +import { ObjectId } from "mongodb"; const mockPersonalBest = { acc: 1, @@ -80,6 +80,8 @@ const mockResultFilter: SharedTypes.ResultFilters = { }, }; +const mockDbResultFilter = { ...mockResultFilter, _id: new ObjectId() }; + describe("UserDal", () => { it("should be able to insert users", async () => { // given @@ -350,43 +352,335 @@ describe("UserDal", () => { expect(updatedUser.autoBanTimestamps).toEqual([36000000]); }); - it("addResultFilterPreset should return error if uuid not found", async () => { - // given - await UserDAL.addUser("test name", "test email", "TestID"); + 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" + ); + }); - // when, then - await expect( - UserDAL.addResultFilterPreset("non existing uid", mockResultFilter, 5) - ).rejects.toThrow("User not found"); + 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); + }); }); - it("UserDAL.addResultFilterPreset should return error if user has reached maximum", async () => { - // given - await UserDAL.addUser("test name", "test email", "TestID"); - await UserDAL.addResultFilterPreset("TestID", mockResultFilter, 1); + 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"); + }); - // when, then - await expect( - UserDAL.addResultFilterPreset("TestID", mockResultFilter, 1) - ).rejects.toThrow("Maximum number of custom filters reached for user."); + 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]); + }); }); - it("addResultFilterPreset success", async () => { - // given - await UserDAL.addUser("test name", "test email", "TestID"); + 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"); + }); - // when - const result = await UserDAL.addResultFilterPreset( - "TestID", - mockResultFilter, - 1 - ); + 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, + })), + }); - // then - const user = await UserDAL.getUser("TestID", "test add result filters"); - const createdFilter = user.resultFilterPresets ?? []; + // when, then + await expect(UserDAL.addTag(uid, "new")).rejects.toThrow( + "Maximum number of tags reached\nStack: add tag" + ); + }); - expect(result).toStrictEqual(createdFilter[0]?._id); + it("addTag success", async () => { + // given + const emptyPb: SharedTypes.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: MonkeyTypes.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: MonkeyTypes.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: MonkeyTypes.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: MonkeyTypes.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 SharedTypes.PersonalBests, + }; + const tagTwo = { + _id: new ObjectId(), + name: "tagTwo", + personalBests: { + custom: { custom: [mockPersonalBest] }, + } as SharedTypes.PersonalBests, + }; + const tagThree = { + _id: new ObjectId(), + name: "tagThree", + personalBests: { + custom: { custom: [mockPersonalBest] }, + } as SharedTypes.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, + ]); + }); }); it("updateProfile should appropriately handle multiple profile updates", async () => { @@ -506,7 +800,7 @@ describe("UserDal", () => { } ); - await UserDAL.incrementBananas("TestID", "100"); + await UserDAL.incrementBananas("TestID", 100); await UserDAL.incrementXp("TestID", 15); await UserDAL.resetUser("TestID"); @@ -645,145 +939,155 @@ describe("UserDal", () => { ]); }); - it("updateStreak should update streak", async () => { - await UserDAL.addUser("testStack", "test email", "TestID"); + 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" + ); + }); - 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, - }, - ]; + it("updateStreak should update streak", async () => { + const { uid } = await UserTestData.createUser(); - for (const { date, expectedStreak } of testSteps) { - const milis = new Date(date).getTime(); - Date.now = vi.fn(() => milis); + 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, + }, + ]; - const streak = await updateStreak("TestID", milis); + for (const { date, expectedStreak } of testSteps) { + const milis = new Date(date).getTime(); + Date.now = vi.fn(() => milis); - await expect(streak).toBe(expectedStreak); - } + const streak = await UserDAL.updateStreak(uid, milis); + + await 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); + + await 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); + + await expect(streak).toBe(expectedStreak); + } + }); }); - it("positive streak offset should award streak correctly", async () => { - await UserDAL.addUser("testStack", "test email", "TestID"); - - await UserDAL.setStreakHourOffset("TestID", 10); - - 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 updateStreak("TestID", milis); - - await expect(streak).toBe(expectedStreak); - } - }); - - it("negative streak offset should award streak correctly", async () => { - await UserDAL.addUser("testStack", "test email", "TestID"); - - await UserDAL.setStreakHourOffset("TestID", -4); - - 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 updateStreak("TestID", milis); - - await expect(streak).toBe(expectedStreak); - } - }); describe("incrementTestActivity", () => { it("ignores user without migration", async () => { // given @@ -887,9 +1191,90 @@ describe("UserDal", () => { describe("updateEmail", () => { it("throws for nonexisting user", async () => { expect(async () => - UserDAL.updateEmail(123, "test@example.com") + UserDAL.updateEmail("unknown", "test@example.com") ).rejects.toThrowError("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 () => { + expect(async () => UserDAL.resetPb("unknown")).rejects.toThrowError( + "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 () => { + expect(async () => + UserDAL.linkDiscord("unknown", "", "") + ).rejects.toThrowError("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"); + }); + }); + describe("unlinkDiscord", () => { + it("throws for nonexisting user", async () => { + expect(async () => UserDAL.unlinkDiscord("unknown")).rejects.toThrowError( + "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 () => { @@ -932,6 +1317,7 @@ describe("UserDal", () => { }; let user = await UserTestData.createUser({ + name: "bob", xp: 100, inbox: [rewardOne, rewardTwo, rewardThree, rewardFour], }); @@ -1162,4 +1548,489 @@ describe("UserDal", () => { expect(xp).toEqual(3100); }); }); + describe("isDiscordIdAvailable", () => { + it("should return true for available discordId", async () => { + await expect(UserDAL.isDiscordIdAvailable("myId")).resolves.toBe(true); + }); + + it("should return false if discordId is taken", async () => { + // given + await UserTestData.createUser({ + discordId: "myId", + }); + + // when, then + await expect(UserDAL.isDiscordIdAvailable("myId")).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 { uid } = await UserTestData.createUser({ + name: "bob", + bananas: 1, + personalBests: { + time: { + "60": [ + { wpm: 100 } as SharedTypes.PersonalBest, + { wpm: 30 } as SharedTypes.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("bob"); + + //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({ + name: "bob", + 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({ + name: "bob", + 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({ + name: "bob", + 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({ + name: "bob", + 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({ + name: "bob", + 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: [] }) + ).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(10).fill(0).map(() => ({ + _id: new ObjectId(), + name: "any", + colors: [], + })), + }); + + // when, then + await expect( + UserDAL.addTheme(uid, { name: "new", colors: [] }) + ).rejects.toThrow( + "Maximum number of custom themes reached\nStack: add theme" + ); + }); + + it("addTheme success", async () => { + // given + const themeOne = { + _id: new ObjectId(), + name: "first", + colors: ["green", "white", "red"], + }; + const { uid } = await UserTestData.createUser({ + customThemes: [themeOne], + }); + + // when + await UserDAL.addTheme(uid, { + name: "newTheme", + colors: ["red", "white", "blue"], + }); + + // then + const read = await UserDAL.getUser(uid, "read"); + expect(read.customThemes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "first", + colors: ["green", "white", "red"], + }), + expect.objectContaining({ + name: "newTheme", + colors: ["red", "white", "blue"], + }), + ]) + ); + }); + }); + + 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: [], + }) + ).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"], + }; + const { uid } = await UserTestData.createUser({ + customThemes: [themeOne], + }); + + // when, then + await expect( + UserDAL.editTheme(uid, new ObjectId().toHexString(), { + name: "newName", + colors: [], + }) + ).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"], + }; + const { uid } = await UserTestData.createUser({ + customThemes: [themeOne], + }); + // when + await UserDAL.editTheme(uid, themeOne._id.toHexString(), { + name: "newThemeName", + colors: ["red", "white", "blue"], + }); + + // 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"], + }; + 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: [], + }; + const themeTwo = { + _id: new ObjectId(), + name: "second", + colors: [], + }; + + const themeThree = { + _id: new ObjectId(), + name: "third", + colors: [], + }; + + 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({ + name: "bob", + 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.name).toEqual("bob"); + expect(read).not.toHaveProperty("tmp"); + + 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({ + name: "bob", + 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.name).toEqual("bob"); + expect(read).not.toHaveProperty("tmp"); + + 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"], + }); + }); + }); }); diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index e3dc452e0..81f3e553f 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -3,7 +3,7 @@ import { containsProfanity, isUsernameValid } from "../utils/validation"; import { canFunboxGetPb, checkAndUpdatePb } from "../utils/pb"; import * as db from "../init/db"; import MonkeyError from "../utils/error"; -import { Collection, ObjectId, Long, UpdateFilter } from "mongodb"; +import { Collection, ObjectId, Long, UpdateFilter, Filter } from "mongodb"; import Logger from "../utils/logger"; import { flattenObjectDeep, isToday, isYesterday } from "../utils/misc"; import { getCachedConfiguration } from "../init/configuration"; @@ -179,7 +179,11 @@ export async function updateQuoteRatings( uid: string, quoteRatings: SharedTypes.UserQuoteRatings ): Promise { - await updateUser("update quote ratings", { uid }, { $set: { quoteRatings } }); + await updateUser( + { uid }, + { $set: { quoteRatings } }, + { stack: "update quote ratings" } + ); return true; } @@ -187,7 +191,7 @@ export async function updateEmail( uid: string, email: string ): Promise { - await updateUser("update email", { uid }, { $set: { email } }); + await updateUser({ uid }, { $set: { email } }, { stack: "update email" }); return true; } @@ -255,33 +259,40 @@ export async function getUserByName( export async function isDiscordIdAvailable( discordId: string ): Promise { - const user = await getUsersCollection().findOne({ discordId }); - return _.isNil(user); + const user = await getUsersCollection().findOne( + { discordId }, + { projection: { _id: 1 } } + ); + return user === null; } export async function addResultFilterPreset( uid: string, - filter: SharedTypes.ResultFilters, + resultFilter: SharedTypes.ResultFilters, maxFiltersPerUser: number ): Promise { - // ensure limit not reached - const filtersCount = ( - (await getPartialUser(uid, "Add Result filter", ["resultFilterPresets"])) - .resultFilterPresets ?? [] - ).length; - - if (filtersCount >= maxFiltersPerUser) { + if (maxFiltersPerUser === 0) { throw new MonkeyError( 409, - "Maximum number of custom filters reached for user." + "Maximum number of custom filters reached", + "add result filter preset" ); } const _id = new ObjectId(); - await getUsersCollection().updateOne( - { uid }, - { $push: { resultFilterPresets: { ...filter, _id } } } + const filter = { uid }; + filter[`resultFilterPresets.${maxFiltersPerUser - 1}`] = { $exists: false }; + + await updateUser( + filter, + { $push: { resultFilterPresets: { ...resultFilter, _id } } }, + { + statusCode: 409, + message: "Maximum number of custom filters reached", + stack: "add result filter preset", + } ); + return _id; } @@ -289,24 +300,16 @@ export async function removeResultFilterPreset( uid: string, _id: string ): Promise { - const user = await getPartialUser(uid, "remove result filter", [ - "resultFilterPresets", - ]); - const filterId = new ObjectId(_id); - if ( - user.resultFilterPresets === undefined || - user.resultFilterPresets.filter((t) => t._id.toString() === _id).length === - 0 - ) { - throw new MonkeyError(404, "Custom filter not found"); - } + const presetId = new ObjectId(_id); - await getUsersCollection().updateOne( + await updateUser( + { uid, "resultFilterPresets._id": presetId }, + { $pull: { resultFilterPresets: { _id: presetId } } }, { - uid, - "resultFilterPresets._id": filterId, - }, - { $pull: { resultFilterPresets: { _id: filterId } } } + statusCode: 404, + message: "Custom filter not found", + stack: "remove result filter preset", + } ); } @@ -314,15 +317,8 @@ export async function addTag( uid: string, name: string ): Promise { - const user = await getPartialUser(uid, "add tag", ["tags"]); - - if ((user?.tags?.length ?? 0) >= 15) { - throw new MonkeyError(400, "You can only have up to 15 tags"); - } - - const _id = new ObjectId(); const toPush = { - _id, + _id: new ObjectId(), name, personalBests: { time: {}, @@ -333,14 +329,16 @@ export async function addTag( }, }; - await getUsersCollection().updateOne( - { uid }, + await updateUser( + { uid, "tags.14": { $exists: false } }, + { $push: { tags: toPush } }, { - $push: { - tags: toPush, - }, + statusCode: 400, + message: "Maximum number of tags reached", + stack: "add tag", } ); + return toPush; } @@ -355,52 +353,30 @@ export async function editTag( _id: string, name: string ): Promise { - const user = await getPartialUser(uid, "edit tag", ["tags"]); - if ( - user.tags === undefined || - user.tags.filter((t) => t._id.toHexString() === _id).length === 0 - ) { - throw new MonkeyError(404, "Tag not found"); - } - await getUsersCollection().updateOne( - { - uid: uid, - "tags._id": new ObjectId(_id), - }, - { $set: { "tags.$.name": name } } + const tagId = new ObjectId(_id); + + await updateUser( + { uid, "tags._id": tagId }, + { $set: { "tags.$.name": name } }, + { statusCode: 404, message: "Tag not found", stack: "edit tag" } ); } export async function removeTag(uid: string, _id: string): Promise { - const user = await getPartialUser(uid, "remove tag", ["tags"]); - if ( - user.tags === undefined || - user.tags.filter((t) => t._id.toHexString() === _id).length === 0 - ) { - throw new MonkeyError(404, "Tag not found"); - } - await getUsersCollection().updateOne( - { - uid: uid, - "tags._id": new ObjectId(_id), - }, - { $pull: { tags: { _id: new ObjectId(_id) } } } + const tagId = new ObjectId(_id); + + await updateUser( + { uid, "tags._id": tagId }, + { $pull: { tags: { _id: tagId } } }, + { statusCode: 404, message: "Tag not found", stack: "remove tag" } ); } export async function removeTagPb(uid: string, _id: string): Promise { - const user = await getPartialUser(uid, "remove tag pb", ["tags"]); - if ( - user.tags === undefined || - user.tags.filter((t) => t._id.toHexString() === _id).length === 0 - ) { - throw new MonkeyError(404, "Tag not found"); - } - await getUsersCollection().updateOne( - { - uid: uid, - "tags._id": new ObjectId(_id), - }, + const tagId = new ObjectId(_id); + + await updateUser( + { uid, "tags._id": tagId }, { $set: { "tags.$.personalBests": { @@ -411,7 +387,8 @@ export async function removeTagPb(uid: string, _id: string): Promise { custom: {}, }, }, - } + }, + { statusCode: 404, message: "Tag not found", stack: "remove tag pb" } ); } @@ -422,20 +399,13 @@ export async function updateLbMemory( language: string, rank: number ): Promise { - const user = await getPartialUser(uid, "update lb memory", ["lbMemory"]); - if (user.lbMemory === undefined) user.lbMemory = {}; - if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {}; - if (user.lbMemory[mode]?.[mode2] === undefined) { - //@ts-expect-error guarded above - user.lbMemory[mode][mode2] = {}; - } - //@ts-expect-error guarded above - user.lbMemory[mode][mode2][language] = rank; - await getUsersCollection().updateOne( + const partialUpdate = {}; + partialUpdate[`lbMemory.${mode}.${mode2}.${language}`] = rank; + + await updateUser( { uid }, - { - $set: { lbMemory: user.lbMemory }, - } + { $set: partialUpdate }, + { stack: "update lb memory" } ); } @@ -534,7 +504,6 @@ export async function checkIfTagPb( export async function resetPb(uid: string): Promise { await updateUser( - "reset pb", { uid }, { $set: { @@ -546,7 +515,8 @@ export async function resetPb(uid: string): Promise { custom: {}, }, }, - } + }, + { stack: "reset pb" } ); } @@ -590,40 +560,55 @@ export async function linkDiscord( { discordId, discordAvatar }, _.identity ); - const result = await getUsersCollection().updateOne( - { uid }, - { $set: updates } - ); - - if (result.matchedCount === 0) { - throw new MonkeyError(404, "User not found"); - } + await updateUser({ uid }, { $set: updates }, { stack: "link discord" }); } export async function unlinkDiscord(uid: string): Promise { await updateUser( - "unlink discord", { uid }, - { $unset: { discordId: "", discordAvatar: "" } } + { $unset: { discordId: "", discordAvatar: "" } }, + { stack: "unlink discord" } ); } -export async function incrementBananas(uid: string, wpm): Promise { - const user = await getPartialUser(uid, "increment bananas", [ - "personalBests", - ]); - - let best60: number | undefined; - const personalBests60 = user.personalBests?.time["60"]; - - if (personalBests60) { - best60 = Math.max(...personalBests60.map((best) => best.wpm)); - } - - if (best60 === undefined || wpm >= best60 - best60 * 0.25) { - //increment when no record found or wpm is within 25% of the record - await getUsersCollection().updateOne({ uid }, { $inc: { bananas: 1 } }); - } +export async function incrementBananas( + uid: string, + wpm: number +): Promise { + //don't throw on missing user + await getUsersCollection().updateOne( + { + uid, + "personalBests.time.60": { $exists: true, $not: { $size: 0 } }, + $expr: { + // wpm needs to be >= 75% of the the highest time 60 PB + $gte: [ + wpm, + { + $multiply: [ + //highest wpm with 0.75 + { + $reduce: { + //find highest wpm from time 60 PBs + input: "$personalBests.time.60", + initialValue: 0, + in: { + $cond: [ + { $gte: ["$$this.wpm", "$$value"] }, + "$$this.wpm", + "$$value", + ], + }, + }, + }, + 0.75, + ], + }, + ], + }, + }, + { $inc: { bananas: 1 } } + ); } export async function incrementXp(uid: string, xp: number): Promise { @@ -657,76 +642,65 @@ export async function incrementTestActivity( ); } -export function themeDoesNotExist(customThemes, id): boolean { - return ( - (customThemes ?? []).filter((t) => t._id.toString() === id).length === 0 - ); -} - export async function addTheme( uid: string, - theme + { name, colors }: Omit ): Promise<{ _id: ObjectId; name: string }> { - const user = await getPartialUser(uid, "add theme", ["customThemes"]); - - if ((user.customThemes ?? []).length >= 10) { - throw new MonkeyError(409, "Too many custom themes"); - } - const _id = new ObjectId(); - await getUsersCollection().updateOne( - { uid }, + + await updateUser( + { uid, "customThemes.9": { $exists: false } }, { $push: { customThemes: { _id, - name: theme.name, - colors: theme.colors, + name: name, + colors: colors, }, }, + }, + { + statusCode: 409, + message: "Maximum number of custom themes reached", + stack: "add theme", } ); return { _id, - name: theme.name, + name, }; } -export async function removeTheme(uid: string, _id): Promise { - const user = await getPartialUser(uid, "remove theme", ["customThemes"]); - - if (themeDoesNotExist(user.customThemes, _id)) { - throw new MonkeyError(404, "Custom theme not found"); - } - - await getUsersCollection().updateOne( +export async function removeTheme(uid: string, id: string): Promise { + const themeId = new ObjectId(id); + await updateUser( + { uid, "customThemes._id": themeId }, + { $pull: { customThemes: { _id: themeId } } }, { - uid: uid, - "customThemes._id": new ObjectId(_id), - }, - { $pull: { customThemes: { _id: new ObjectId(_id) } } } + statusCode: 404, + message: "Custom theme not found", + stack: "remove theme", + } ); } -export async function editTheme(uid: string, _id, theme): Promise { - const user = await getPartialUser(uid, "edit theme", ["customThemes"]); +export async function editTheme( + uid: string, + id: string, + { name, colors }: Omit +): Promise { + const themeId = new ObjectId(id); - if (themeDoesNotExist(user.customThemes, _id)) { - throw new MonkeyError(404, "Custom Theme not found"); - } - - await getUsersCollection().updateOne( - { - uid: uid, - "customThemes._id": new ObjectId(_id), - }, + await updateUser( + { uid, "customThemes._id": themeId }, { $set: { - "customThemes.$.name": theme.name, - "customThemes.$.colors": theme.colors, + "customThemes.$.name": name, + "customThemes.$.colors": colors, }, - } + }, + { statusCode: 404, message: "Custom theme not found", stack: "edit theme" } ); } @@ -755,18 +729,16 @@ export async function getPersonalBests( export async function getStats( uid: string -): Promise> { +): Promise< + Pick +> { const user = await getPartialUser(uid, "get stats", [ "startedTests", "completedTests", "timeTyping", ]); - return { - startedTests: user.startedTests, - completedTests: user.completedTests, - timeTyping: user.timeTyping, - }; + return user; } export async function getFavoriteQuotes( @@ -785,35 +757,33 @@ export async function addFavoriteQuote( quoteId: string, maxQuotes: number ): Promise { - const user = await getPartialUser(uid, "add favorite quote", [ - "favoriteQuotes", - ]); - - if (user.favoriteQuotes) { - if (user.favoriteQuotes[language]?.includes(quoteId)) { - return; - } - - const quotesLength = _.sumBy( - Object.values(user.favoriteQuotes), - (favQuotes) => favQuotes.length - ); - - if (quotesLength >= maxQuotes) { - throw new MonkeyError( - 409, - "Too many favorite quotes", - "addFavoriteQuote" - ); - } - } - - await getUsersCollection().updateOne( - { uid }, + await updateUser( { - $push: { + uid, + $expr: { + //total amount of quotes need to be lower than maxQuotes + $lt: [ + { + $reduce: { + input: { $objectToArray: "$favoriteQuotes" }, + initialValue: 0, + in: { $add: ["$$value", { $size: "$$this.v" }] }, + }, + }, + maxQuotes, + ], + }, + }, + { + $addToSet: { + //ensure quoteId is unique in the array [`favoriteQuotes.${language}`]: quoteId, }, + }, + { + statusCode: 409, + message: "Maximum number of favorite quotes reached", + stack: "add favorite quote", } ); } @@ -823,17 +793,10 @@ export async function removeFavoriteQuote( language: string, quoteId: string ): Promise { - const user = await getPartialUser(uid, "remove favorite quote", [ - "favoriteQuotes", - ]); - - if (!user.favoriteQuotes?.[language]?.includes(quoteId)) { - return; - } - - await getUsersCollection().updateOne( + await updateUser( { uid }, - { $pull: { [`favoriteQuotes.${language}`]: quoteId } } + { $pull: { [`favoriteQuotes.${language}`]: quoteId } }, + { stack: "remove favorite quote" } ); } @@ -989,80 +952,80 @@ export async function updateInbox( (it) => deleteSet.includes(it) === false ); - console.log({ deleteSet, readSet }); - const update = await getUsersCollection().updateOne({ uid }, [ { $addFields: { tmp: { $function: { lang: "js", - args: ["$_id", "$inbox", "$xp", "$inventory"], - body: ` - function(_id, inbox, xp, inventory) { + args: ["$inbox", "$xp", "$inventory", deleteSet, readSet], + body: function ( + inbox: SharedTypes.MonkeyMail[], + xp: number, + inventory: SharedTypes.UserInventory, + deletedIds: string[], + readIds: string[] + ): Pick { + const toBeDeleted = inbox.filter((it) => + deletedIds.includes(it.id) + ); - var toBeDeleted = inbox.filter(it => ${JSON.stringify( - deleteSet - )}.includes(it.id) === true); - - var toBeRead = inbox.filter(it => ${JSON.stringify( - readSet - )}.includes(it.id) === true && it.read === false); + const toBeRead = inbox.filter( + (it) => readIds.includes(it.id) && it.read === false + ); //flatMap rewards - var rewards = [...toBeRead, ...toBeDeleted] - .filter(it => it.read === false) - .reduce((arr, current) => { - return arr.concat(current.rewards); - }, []); + const rewards: SharedTypes.AllRewards[] = [ + ...toBeRead, + ...toBeDeleted, + ] + .filter((it) => it.read === false) + .reduce((arr, current) => { + return [...arr, ...current.rewards]; + }, []); - var xpGain = rewards - .filter(it => it.type === "xp") - .map(it => it.item) - .reduce((s, a) => s + a, 0); + const xpGain = rewards + .filter((it) => it.type === "xp") + .map((it) => it.item as number) + .reduce((s, a) => s + a, 0); - var badgesToClaim = rewards - .filter(it => it.type === "badge") - .map(it => it.item); + const badgesToClaim = rewards + .filter((it) => it.type === "badge") + .map((it) => it.item as SharedTypes.Badge); - if (inventory === null) inventory = { - badges: null - }; + if (inventory === null) + inventory = { + badges: [], + }; if (inventory.badges === null) inventory.badges = []; - - const uniqueBadgeIds = new Set(); - const newBadges = []; - for(badge of [...inventory.badges, ...badgesToClaim]){ - if(uniqueBadgeIds.has(badge.id))continue; - uniqueBadgeIds.add(badge.id); - newBadges.push(badge); + const uniqueBadgeIds = new Set(); + const newBadges: SharedTypes.Badge[] = []; + + for (const badge of [...inventory.badges, ...badgesToClaim]) { + if (uniqueBadgeIds.has(badge.id)) continue; + uniqueBadgeIds.add(badge.id); + newBadges.push(badge); } inventory.badges = newBadges; - + //remove deleted mail from inbox, sort by timestamp descending - var inboxUpdate = inbox - .filter(it => ${JSON.stringify( - deleteSet - )}.includes(it.id) === false) - .sort((a, b) => b.timestamp - a.timestamp); + const inboxUpdate = inbox + .filter((it) => !deletedIds.includes(it.id)) + .sort((a, b) => b.timestamp - a.timestamp); //mark read mail as read, remove rewards - toBeRead.forEach(it => { - it.read = true; - it.rewards = []; + toBeRead.forEach((it) => { + it.read = true; + it.rewards = []; }); - - return { - _id, - xp: xp + xpGain, - inbox: inboxUpdate, - inventory: inventory, + xp: xp + xpGain, + inbox: inboxUpdate, + inventory: inventory, }; - } - `, + }.toString(), }, }, }, @@ -1179,17 +1142,22 @@ export async function logIpAddress( /** * Update user document. Requires the user to exist - * @param stack stack description used in the error * @param filter user filter * @param update update document + * @param error stack description used in the error or statusCode and message of the error * @throws MonkeyError if user does not exist */ async function updateUser( - stack: string, - filter: { uid: string }, - update: UpdateFilter + filter: Filter, + update: UpdateFilter, + error: { stack: string; statusCode?: number; message?: string } ): Promise { const result = await getUsersCollection().updateOne(filter, update); + if (result.matchedCount !== 1) - throw new MonkeyError(404, "User not found", stack); + throw new MonkeyError( + error.statusCode ?? 404, + error.message ?? "User not found", + error.stack + ); } diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index 97a519669..5d6564f28 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -28,7 +28,7 @@ declare namespace MonkeyTypes { | "testActivity" > & { _id: ObjectId; - resultFilterPresets?: WithObjectIdArray; + resultFilterPresets?: WithObjectId[]; tags?: DBUserTag[]; lbPersonalBests?: LbPersonalBests; customThemes?: DBCustomTheme[]; @@ -54,11 +54,6 @@ declare namespace MonkeyTypes { _id: ObjectId; }; - type WithObjectIdArray = Omit & - { - _id: ObjectId; - }[]; - type ApeKeyDB = SharedTypes.ApeKey & { _id: ObjectId; uid: string;