diff --git a/backend/__tests__/__integration__/dal/ape-keys.spec.ts b/backend/__tests__/__integration__/dal/ape-keys.spec.ts index 153a857abe42..f2506cc8afd2 100644 --- a/backend/__tests__/__integration__/dal/ape-keys.spec.ts +++ b/backend/__tests__/__integration__/dal/ape-keys.spec.ts @@ -1,23 +1,107 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { ObjectId } from "mongodb"; -import { addApeKey } from "../../../src/dal/ape-keys"; +import { + addApeKey, + DBApeKey, + editApeKey, + getApeKey, + updateLastUsedOn, +} from "../../../src/dal/ape-keys"; describe("ApeKeysDal", () => { - it("should be able to add a new ape key", async () => { - const apeKey = { - _id: new ObjectId(), - uid: "123", - name: "test", - hash: "12345", - createdOn: Date.now(), - modifiedOn: Date.now(), - lastUsedOn: Date.now(), - useCount: 0, - enabled: true, - }; - - const apeKeyId = await addApeKey(apeKey); - - expect(apeKeyId).toBe(apeKey._id.toHexString()); + beforeEach(() => { + vi.useFakeTimers(); + }); + + describe("addApeKey", () => { + it("should be able to add a new ape key", async () => { + const apeKey = buildApeKey(); + + const apeKeyId = await addApeKey(apeKey); + + expect(apeKeyId).toBe(apeKey._id.toHexString()); + + const read = await getApeKey(apeKeyId); + expect(read).toEqual({ + ...apeKey, + }); + }); + }); + + describe("editApeKey", () => { + it("should edit name of an existing ape key", async () => { + //GIVEN + const apeKey = buildApeKey({ useCount: 5, enabled: true }); + const apeKeyId = await addApeKey(apeKey); + + //WHEN + const newName = "new name"; + await editApeKey(apeKey.uid, apeKeyId, newName, undefined); + + //THENa + const readAfterEdit = (await getApeKey(apeKeyId)) as DBApeKey; + expect(readAfterEdit).toEqual({ + ...apeKey, + name: newName, + modifiedOn: Date.now(), + }); + }); + + it("should edit enabled of an existing ape key", async () => { + //GIVEN + const apeKey = buildApeKey({ useCount: 5, enabled: true }); + const apeKeyId = await addApeKey(apeKey); + + //WHEN + + await editApeKey(apeKey.uid, apeKeyId, undefined, false); + + //THEN + const readAfterEdit = (await getApeKey(apeKeyId)) as DBApeKey; + expect(readAfterEdit).toEqual({ + ...apeKey, + enabled: false, + modifiedOn: Date.now(), + }); + }); + }); + + describe("updateLastUsedOn", () => { + it("should update lastUsedOn and increment useCount when editing with lastUsedOn", async () => { + //GIVEN + const apeKey = buildApeKey({ + useCount: 5, + lastUsedOn: 42, + }); + const apeKeyId = await addApeKey(apeKey); + + //WHEN + await updateLastUsedOn(apeKey.uid, apeKeyId); + await updateLastUsedOn(apeKey.uid, apeKeyId); + + //THENa + const readAfterEdit = (await getApeKey(apeKeyId)) as DBApeKey; + expect(readAfterEdit).toEqual({ + ...apeKey, + modifiedOn: readAfterEdit.modifiedOn, + lastUsedOn: Date.now(), + useCount: 5 + 2, + }); + }); }); }); + +function buildApeKey(overrides: Partial = {}): DBApeKey { + return { + _id: new ObjectId(), + uid: "123", + name: "test", + hash: "12345", + createdOn: Date.now(), + modifiedOn: Date.now(), + lastUsedOn: Date.now(), + useCount: 0, + enabled: true, + ...overrides, + }; +} diff --git a/backend/__tests__/__integration__/dal/config.spec.ts b/backend/__tests__/__integration__/dal/config.spec.ts new file mode 100644 index 000000000000..bab22b49f96c --- /dev/null +++ b/backend/__tests__/__integration__/dal/config.spec.ts @@ -0,0 +1,42 @@ +import { ObjectId } from "mongodb"; +import { describe, expect, it } from "vitest"; +import * as ConfigDal from "../../../src/dal/config"; + +const getConfigCollection = ConfigDal.__testing.getConfigCollection; + +describe("ConfigDal", () => { + describe("saveConfig", () => { + it("should save and update user configuration correctly", async () => { + //GIVEN + const uid = new ObjectId().toString(); + await getConfigCollection().insertOne({ + uid, + config: { + ads: "on", + time: 60, + quickTab: true, //legacy value + }, + } as any); + + //WHEN + await ConfigDal.saveConfig(uid, { + ads: "on", + difficulty: "normal", + } as any); + + //WHEN + await ConfigDal.saveConfig(uid, { ads: "off" }); + + //THEN + const savedConfig = (await ConfigDal.getConfig( + uid + )) as ConfigDal.DBConfig; + + expect(savedConfig.config.ads).toBe("off"); + expect(savedConfig.config.time).toBe(60); + + //should remove legacy values + expect((savedConfig.config as any)["quickTab"]).toBeUndefined(); + }); + }); +}); diff --git a/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts b/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts index afe64f556787..82e240b7f7bb 100644 --- a/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts +++ b/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts @@ -1,5 +1,4 @@ import { describe, it, expect, afterEach, vi } from "vitest"; -import _ from "lodash"; import { ObjectId } from "mongodb"; import * as UserDal from "../../../src/dal/user"; import * as LeaderboardsDal from "../../../src/dal/leaderboards"; @@ -11,6 +10,7 @@ import * as DB from "../../../src/init/db"; import { LbPersonalBests } from "../../../src/utils/pb"; import { pb } from "../../__testData__/users"; +import { omit } from "es-toolkit"; describe("LeaderboardsDal", () => { afterEach(async () => { @@ -59,7 +59,7 @@ describe("LeaderboardsDal", () => { )) as DBLeaderboardEntry[]; //THEN - const lb = result.map((it) => _.omit(it, ["_id"])); + const lb = result.map((it) => omit(it, ["_id"])); expect(lb).toEqual([ expectedLbEntry("15", { rank: 1, user: rank1 }), @@ -86,7 +86,7 @@ describe("LeaderboardsDal", () => { )) as LeaderboardsDal.DBLeaderboardEntry[]; //THEN - const lb = result.map((it) => _.omit(it, ["_id"])); + const lb = result.map((it) => omit(it, ["_id"])); expect(lb).toEqual([ expectedLbEntry("60", { rank: 1, user: rank1 }), @@ -200,7 +200,7 @@ describe("LeaderboardsDal", () => { )) as DBLeaderboardEntry[]; //THEN - const lb = result.map((it) => _.omit(it, ["_id"])); + const lb = result.map((it) => omit(it, ["_id"])); expect(lb).toEqual([ expectedLbEntry("15", { rank: 1, user: noBadge }), @@ -239,7 +239,7 @@ describe("LeaderboardsDal", () => { )) as DBLeaderboardEntry[]; //THEN - const lb = result.map((it) => _.omit(it, ["_id"])); + const lb = result.map((it) => omit(it, ["_id"])); expect(lb).toEqual([ expectedLbEntry("15", { rank: 1, user: noPremium }), diff --git a/backend/__tests__/__integration__/dal/preset.spec.ts b/backend/__tests__/__integration__/dal/preset.spec.ts index bffa5160f3cc..db31eb6581e5 100644 --- a/backend/__tests__/__integration__/dal/preset.spec.ts +++ b/backend/__tests__/__integration__/dal/preset.spec.ts @@ -1,7 +1,6 @@ import { describe, it, expect } from "vitest"; import { ObjectId } from "mongodb"; import * as PresetDal from "../../../src/dal/preset"; -import _ from "lodash"; describe("PresetDal", () => { describe("readPreset", () => { diff --git a/backend/__tests__/__integration__/dal/result.spec.ts b/backend/__tests__/__integration__/dal/result.spec.ts index 4d893656d307..c3bf95760fd8 100644 --- a/backend/__tests__/__integration__/dal/result.spec.ts +++ b/backend/__tests__/__integration__/dal/result.spec.ts @@ -50,7 +50,7 @@ async function createDummyData( tags: [], consistency: 100, keyConsistency: 100, - chartData: { wpm: [], raw: [], err: [] }, + chartData: { wpm: [], burst: [], err: [] }, uid, keySpacingStats: { average: 0, sd: 0 }, keyDurationStats: { average: 0, sd: 0 }, diff --git a/backend/__tests__/__integration__/dal/user.spec.ts b/backend/__tests__/__integration__/dal/user.spec.ts index c42b31d86ef9..baa9b78d8546 100644 --- a/backend/__tests__/__integration__/dal/user.spec.ts +++ b/backend/__tests__/__integration__/dal/user.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from "vitest"; -import _ from "lodash"; + import * as UserDAL from "../../../src/dal/user"; import * as UserTestData from "../../__testData__/users"; import { ObjectId } from "mongodb"; @@ -235,9 +235,13 @@ describe("UserDal", () => { // then const updatedUser = (await UserDAL.getUser(testUser.uid, "test")) ?? {}; - expect(_.values(updatedUser.personalBests).filter(_.isEmpty)).toHaveLength( - 5 - ); + expect(updatedUser.personalBests).toStrictEqual({ + time: {}, + words: {}, + quote: {}, + custom: {}, + zen: {}, + }); }); it("autoBan should automatically ban after configured anticheat triggers", async () => { @@ -620,97 +624,129 @@ describe("UserDal", () => { }); }); - it("updateProfile should appropriately handle multiple profile updates", async () => { - const uid = new ObjectId().toHexString(); - await UserDAL.addUser("test name", "test email", uid); + 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, - { + 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: [], - } - ); + }); - 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, + }, + ], + } + ); - await UserDAL.updateProfile( - uid, - { + 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, }, ], - } - ); + }); - 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: [ + await UserDAL.updateProfile( + uid, { - id: 1, - selected: true, + bio: "test bio 2", + socialProfiles: { + github: "test github", + website: "test website", + }, }, - ], - }); + { + badges: [ + { + id: 1, + }, + ], + } + ); - await UserDAL.updateProfile( - uid, - { + 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, }, ], - } - ); - - 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", + }); }); }); @@ -1176,7 +1212,6 @@ describe("UserDal", () => { discordId: "discordId", discordAvatar: "discordAvatar", }); - //when await UserDAL.linkDiscord(uid, "newId", "newAvatar"); @@ -1185,6 +1220,21 @@ describe("UserDal", () => { 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 () => { diff --git a/backend/__tests__/api/controllers/admin.spec.ts b/backend/__tests__/api/controllers/admin.spec.ts index 2db8391c6458..b1b04c3dd39b 100644 --- a/backend/__tests__/api/controllers/admin.spec.ts +++ b/backend/__tests__/api/controllers/admin.spec.ts @@ -8,7 +8,7 @@ import * as ReportDal from "../../../src/dal/report"; import * as LogsDal from "../../../src/dal/logs"; import GeorgeQueue from "../../../src/queues/george-queue"; import * as AuthUtil from "../../../src/utils/auth"; -import _ from "lodash"; + import { enableRateLimitExpects } from "../../__testData__/rate-limit"; const { mockApp, uid } = setup(); @@ -570,9 +570,8 @@ describe("AdminController", () => { } }); async function enableAdminEndpoints(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - admin: { endpointsEnabled: enabled }, - }); + const mockConfig = await configuration; + mockConfig.admin = { ...mockConfig.admin, endpointsEnabled: enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig diff --git a/backend/__tests__/api/controllers/ape-key.spec.ts b/backend/__tests__/api/controllers/ape-key.spec.ts index 60aff1724e32..330ae4832576 100644 --- a/backend/__tests__/api/controllers/ape-key.spec.ts +++ b/backend/__tests__/api/controllers/ape-key.spec.ts @@ -5,7 +5,6 @@ import * as ApeKeyDal from "../../../src/dal/ape-keys"; import { ObjectId } from "mongodb"; import * as Configuration from "../../../src/init/configuration"; import * as UserDal from "../../../src/dal/user"; -import _ from "lodash"; const { mockApp, uid } = setup(); const configuration = Configuration.getCachedConfiguration(); @@ -354,9 +353,12 @@ function apeKeyDb( } async function enableApeKeysEndpoints(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - apeKeys: { endpointsEnabled: enabled, maxKeysPerUser: 1 }, - }); + const mockConfig = await configuration; + mockConfig.apeKeys = { + ...mockConfig.apeKeys, + endpointsEnabled: enabled, + maxKeysPerUser: 1, + }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig diff --git a/backend/__tests__/api/controllers/leaderboard.spec.ts b/backend/__tests__/api/controllers/leaderboard.spec.ts index fb42f52b73c5..ff382f4d2ea0 100644 --- a/backend/__tests__/api/controllers/leaderboard.spec.ts +++ b/backend/__tests__/api/controllers/leaderboard.spec.ts @@ -1,6 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { setup } from "../../__testData__/controller-test"; -import _ from "lodash"; import { ObjectId } from "mongodb"; import * as LeaderboardDal from "../../../src/dal/leaderboards"; import * as DailyLeaderboards from "../../../src/utils/daily-leaderboards"; @@ -1217,9 +1216,8 @@ describe("Loaderboard Controller", () => { }); async function acceptApeKeys(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - apeKeys: { acceptKeys: enabled }, - }); + const mockConfig = await configuration; + mockConfig.apeKeys = { ...mockConfig.apeKeys, acceptKeys: enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -1227,18 +1225,22 @@ async function acceptApeKeys(enabled: boolean): Promise { } async function dailyLeaderboardEnabled(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - dailyLeaderboards: { enabled: enabled }, - }); + const mockConfig = await configuration; + mockConfig.dailyLeaderboards = { + ...mockConfig.dailyLeaderboards, + enabled: enabled, + }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig ); } async function weeklyLeaderboardEnabled(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - leaderboards: { weeklyXp: { enabled } }, - }); + const mockConfig = await configuration; + mockConfig.leaderboards.weeklyXp = { + ...mockConfig.leaderboards.weeklyXp, + enabled, + }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig diff --git a/backend/__tests__/api/controllers/quotes.spec.ts b/backend/__tests__/api/controllers/quotes.spec.ts index e2663ebdd334..1f9d6c0ebff0 100644 --- a/backend/__tests__/api/controllers/quotes.spec.ts +++ b/backend/__tests__/api/controllers/quotes.spec.ts @@ -9,7 +9,6 @@ import * as ReportDal from "../../../src/dal/report"; import * as LogsDal from "../../../src/dal/logs"; import * as Captcha from "../../../src/utils/captcha"; import { ObjectId } from "mongodb"; -import _ from "lodash"; import { ApproveQuote } from "@monkeytype/schemas/quotes"; const { mockApp, uid } = setup(); @@ -874,9 +873,8 @@ describe("QuotesController", () => { }); async function enableQuotes(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - quotes: { submissionsEnabled: enabled }, - }); + const mockConfig = await configuration; + mockConfig.quotes = { ...mockConfig.quotes, submissionsEnabled: enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -884,9 +882,13 @@ async function enableQuotes(enabled: boolean): Promise { } async function enableQuoteReporting(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - quotes: { reporting: { enabled, maxReports: 10, contentReportLimit: 20 } }, - }); + const mockConfig = await configuration; + mockConfig.quotes.reporting = { + ...mockConfig.quotes.reporting, + enabled, + maxReports: 10, + contentReportLimit: 20, + }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig diff --git a/backend/__tests__/api/controllers/result.spec.ts b/backend/__tests__/api/controllers/result.spec.ts index 758fc6c9ce7d..deba6ffbd256 100644 --- a/backend/__tests__/api/controllers/result.spec.ts +++ b/backend/__tests__/api/controllers/result.spec.ts @@ -1,6 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { setup } from "../../__testData__/controller-test"; -import _, { omit } from "lodash"; import * as Configuration from "../../../src/init/configuration"; import * as ResultDal from "../../../src/dal/result"; import * as UserDal from "../../../src/dal/user"; @@ -10,6 +9,8 @@ import { ObjectId } from "mongodb"; import { mockAuthenticateWithApeKey } from "../../__testData__/auth"; import { enableRateLimitExpects } from "../../__testData__/rate-limit"; import { DBResult } from "../../../src/utils/result"; +import { omit } from "es-toolkit"; +import { CompletedEvent } from "@monkeytype/schemas/results"; const { mockApp, uid, mockAuth } = setup(); const configuration = Configuration.getCachedConfiguration(); @@ -488,15 +489,14 @@ describe("result controller test", () => { it("should apply defaults on missing data", async () => { //GIVEN const result = givenDbResult(uid); - const partialResult = omit( - result, + const partialResult = omit(result, [ "difficulty", "language", "funbox", "lazyMode", "punctuation", - "numbers" - ); + "numbers", + ]); const resultIdString = result._id.toHexString(); const tagIds = [ @@ -588,6 +588,7 @@ describe("result controller test", () => { beforeEach(async () => { await enableResultsSaving(true); + await enableUsersXpGain(true); [ userGetMock, @@ -611,48 +612,15 @@ describe("result controller test", () => { it("should add result", async () => { //GIVEN + const completedEvent = buildCompletedEvent({ + funbox: ["58008", "read_ahead_hard"], + }); //WHEN const { body } = await mockApp .post("/results") .set("Authorization", `Bearer ${uid}`) .send({ - result: { - acc: 86, - afkDuration: 5, - bailedOut: false, - blindMode: false, - charStats: [100, 2, 3, 5], - chartData: { wpm: [1, 2, 3], burst: [50, 55, 56], err: [0, 2, 0] }, - consistency: 23.5, - difficulty: "normal", - funbox: [], - hash: "hash", - incompleteTestSeconds: 2, - incompleteTests: [{ acc: 75, seconds: 10 }], - keyConsistency: 12, - keyDuration: [0, 3, 5], - keySpacing: [0, 2, 4], - language: "english", - lazyMode: false, - mode: "time", - mode2: "15", - numbers: false, - punctuation: false, - rawWpm: 99, - restartCount: 4, - tags: ["tagOneId", "tagTwoId"], - testDuration: 15.1, - timestamp: 1000, - uid, - wpmConsistency: 55, - wpm: 80, - stopOnLetter: false, - //new required - charTotal: 5, - keyOverlap: 7, - lastKeyToEnd: 9, - startToFirstKey: 11, - }, + result: completedEvent, }) .expect(200); @@ -662,7 +630,12 @@ describe("result controller test", () => { tagPbs: [], xp: 0, dailyXpBonus: false, - xpBreakdown: {}, + xpBreakdown: { + accPenalty: 28, + base: 20, + incomplete: 5, + funbox: 80, + }, streak: 0, insertedId: insertedId.toHexString(), }); @@ -751,44 +724,9 @@ describe("result controller test", () => { .post("/results") .set("Authorization", `Bearer ${uid}`) .send({ - result: { - acc: 86, - afkDuration: 5, - bailedOut: false, - blindMode: false, - charStats: [100, 2, 3, 5], - chartData: { wpm: [1, 2, 3], burst: [50, 55, 56], err: [0, 2, 0] }, - consistency: 23.5, - difficulty: "normal", - funbox: [], - hash: "hash", - incompleteTestSeconds: 2, - incompleteTests: [{ acc: 75, seconds: 10 }], - keyConsistency: 12, - keyDuration: [0, 3, 5], - keySpacing: [0, 2, 4], - language: "english", - lazyMode: false, - mode: "time", - mode2: "15", - numbers: false, - punctuation: false, - rawWpm: 99, - restartCount: 4, - tags: ["tagOneId", "tagTwoId"], - testDuration: 15.1, - timestamp: 1000, - uid, - wpmConsistency: 55, - wpm: 80, - stopOnLetter: false, - //new required - charTotal: 5, - keyOverlap: 7, - lastKeyToEnd: 9, - startToFirstKey: 11, + result: buildCompletedEvent({ extra2: "value", - }, + } as any), extra: "value", }) .expect(422); @@ -803,6 +741,24 @@ describe("result controller test", () => { }); }); + it("should fail wit duplicate funboxes", async () => { + //GIVEN + + //WHEN + const { body } = await mockApp + .post("/results") + .set("Authorization", `Bearer ${uid}`) + .send({ + result: buildCompletedEvent({ + funbox: ["58008", "58008"], + }), + }) + .expect(400); + + //THEN + expect(body.message).toEqual("Duplicate funboxes"); + }); + // it("should fail invalid properties ", async () => { //GIVEN //WHEN @@ -824,10 +780,50 @@ describe("result controller test", () => { }); }); -async function enablePremiumFeatures(premium: boolean): Promise { - const mockConfig = _.merge(await configuration, { - users: { premium: { enabled: premium } }, - }); +function buildCompletedEvent(result?: Partial): CompletedEvent { + return { + acc: 86, + afkDuration: 5, + bailedOut: false, + blindMode: false, + charStats: [100, 2, 3, 5], + chartData: { wpm: [1, 2, 3], burst: [50, 55, 56], err: [0, 2, 0] }, + consistency: 23.5, + difficulty: "normal", + funbox: [], + hash: "hash", + incompleteTestSeconds: 2, + incompleteTests: [{ acc: 75, seconds: 10 }], + keyConsistency: 12, + keyDuration: [0, 3, 5], + keySpacing: [0, 2, 4], + language: "english", + lazyMode: false, + mode: "time", + mode2: "15", + numbers: false, + punctuation: false, + rawWpm: 99, + restartCount: 4, + tags: ["tagOneId", "tagTwoId"], + testDuration: 15.1, + timestamp: 1000, + uid, + wpmConsistency: 55, + wpm: 80, + stopOnLetter: false, + //new required + charTotal: 5, + keyOverlap: 7, + lastKeyToEnd: 9, + startToFirstKey: 11, + ...result, + }; +} + +async function enablePremiumFeatures(enabled: boolean): Promise { + const mockConfig = await configuration; + mockConfig.users.premium = { ...mockConfig.users.premium, enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -857,7 +853,7 @@ function givenDbResult(uid: string, customize?: Partial): DBResult { isPb: true, chartData: { wpm: [Math.random() * 100], - raw: [Math.random() * 100], + burst: [Math.random() * 100], err: [Math.random() * 100], }, name: "testName", @@ -866,9 +862,11 @@ function givenDbResult(uid: string, customize?: Partial): DBResult { } async function acceptApeKeys(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - apeKeys: { acceptKeys: enabled }, - }); + const mockConfig = await configuration; + mockConfig.apeKeys = { + ...mockConfig.apeKeys, + acceptKeys: enabled, + }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -876,9 +874,16 @@ async function acceptApeKeys(enabled: boolean): Promise { } async function enableResultsSaving(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - results: { savingEnabled: enabled }, - }); + const mockConfig = await configuration; + mockConfig.results = { ...mockConfig.results, savingEnabled: enabled }; + + vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( + mockConfig + ); +} +async function enableUsersXpGain(enabled: boolean): Promise { + const mockConfig = await configuration; + mockConfig.users.xp = { ...mockConfig.users.xp, enabled, funboxBonus: 1 }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index 1a7634270b12..c25bebdba590 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -31,7 +31,6 @@ import { ObjectId } from "mongodb"; import { PersonalBest } from "@monkeytype/schemas/shared"; import { mockAuthenticateWithApeKey } from "../../__testData__/auth"; import { randomUUID } from "node:crypto"; -import _ from "lodash"; import { MonkeyMail, UserStreak } from "@monkeytype/schemas/users"; import MonkeyError, { isFirebaseError } from "../../../src/utils/error"; import { LeaderboardEntry } from "@monkeytype/schemas/leaderboards"; @@ -3389,7 +3388,7 @@ describe("user controller test", () => { await enableInbox(true); }); - it("shold get inbox", async () => { + it("should get inbox", async () => { //GIVEN const mailOne: MonkeyMail = { id: randomUUID(), @@ -3885,31 +3884,18 @@ function fillYearWithDay(days: number): number[] { return result; } -async function enablePremiumFeatures(premium: boolean): Promise { - const mockConfig = _.merge(await configuration, { - users: { premium: { enabled: premium } }, - }); +async function enablePremiumFeatures(enabled: boolean): Promise { + const mockConfig = await configuration; + mockConfig.users.premium = { ...mockConfig.users.premium, enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig ); } -// eslint-disable-next-line no-unused-vars -async function enableAdminFeatures(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - admin: { endpointsEnabled: enabled }, - }); - - vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( - mockConfig - ); -} - -async function enableSignup(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - users: { signUp: enabled }, - }); +async function enableSignup(signUp: boolean): Promise { + const mockConfig = await configuration; + mockConfig.users = { ...mockConfig.users, signUp }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -3917,9 +3903,11 @@ async function enableSignup(enabled: boolean): Promise { } async function enableDiscordIntegration(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - users: { discordIntegration: { enabled } }, - }); + const mockConfig = await configuration; + mockConfig.users.discordIntegration = { + ...mockConfig.users.discordIntegration, + enabled, + }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -3927,19 +3915,20 @@ async function enableDiscordIntegration(enabled: boolean): Promise { } async function enableResultFilterPresets(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - results: { filterPresets: { enabled } }, - }); + const mockConfig = await configuration; + mockConfig.results.filterPresets = { + ...mockConfig.results.filterPresets, + enabled, + }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig ); } -async function acceptApeKeys(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - apeKeys: { acceptKeys: enabled }, - }); +async function acceptApeKeys(acceptKeys: boolean): Promise { + const mockConfig = await configuration; + mockConfig.apeKeys = { ...mockConfig.apeKeys, acceptKeys }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -3947,18 +3936,16 @@ async function acceptApeKeys(enabled: boolean): Promise { } async function enableProfiles(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - users: { profiles: { enabled } }, - }); + const mockConfig = await configuration; + mockConfig.users.profiles = { ...mockConfig.users.profiles, enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig ); } async function enableInbox(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - users: { inbox: { enabled } }, - }); + const mockConfig = await configuration; + mockConfig.users.inbox = { ...mockConfig.users.inbox, enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig @@ -3966,9 +3953,8 @@ async function enableInbox(enabled: boolean): Promise { } async function enableReporting(enabled: boolean): Promise { - const mockConfig = _.merge(await configuration, { - quotes: { reporting: { enabled } }, - }); + const mockConfig = await configuration; + mockConfig.quotes.reporting = { ...mockConfig.quotes.reporting, enabled }; vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( mockConfig diff --git a/backend/__tests__/init/configurations.spec.ts b/backend/__tests__/init/configurations.spec.ts new file mode 100644 index 000000000000..949171f75fb0 --- /dev/null +++ b/backend/__tests__/init/configurations.spec.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import * as Configurations from "../../src/init/configuration"; + +import { Configuration } from "@monkeytype/schemas/configuration"; +const mergeConfigurations = Configurations.__testing.mergeConfigurations; + +describe("configurations", () => { + describe("mergeConfigurations", () => { + it("should merge configurations correctly", () => { + //GIVEN + const baseConfig: Configuration = { + maintenance: false, + dev: { + responseSlowdownMs: 5, + }, + quotes: { + reporting: { + enabled: false, + maxReports: 5, + }, + submissionEnabled: true, + }, + } as any; + const liveConfig: Partial = { + maintenance: true, + quotes: { + reporting: { + enabled: true, + } as any, + maxFavorites: 10, + } as any, + }; + + //WHEN + mergeConfigurations(baseConfig, liveConfig); + + //THEN + expect(baseConfig).toEqual({ + maintenance: true, + dev: { + responseSlowdownMs: 5, + }, + quotes: { + reporting: { + enabled: true, + maxReports: 5, + }, + submissionEnabled: true, + }, + } as any); + }); + }); +}); diff --git a/backend/__tests__/setup-tests.ts b/backend/__tests__/setup-tests.ts index 183a1c377032..0c9c8b7c514f 100644 --- a/backend/__tests__/setup-tests.ts +++ b/backend/__tests__/setup-tests.ts @@ -1,17 +1,23 @@ import { afterAll, beforeAll, afterEach, vi } from "vitest"; import { BASE_CONFIGURATION } from "../src/constants/base-configuration"; import { setupCommonMocks } from "./setup-common-mocks"; +import { __testing } from "../src/init/configuration"; process.env["MODE"] = "dev"; process.env.TZ = "UTC"; beforeAll(async () => { //don't add any configuration here, add to global-setup.ts instead. - vi.mock("../src/init/configuration", () => ({ - getLiveConfiguration: () => BASE_CONFIGURATION, - getCachedConfiguration: () => BASE_CONFIGURATION, - patchConfiguration: vi.fn(), - })); + vi.mock("../src/init/configuration", async (importOriginal) => { + const orig = (await importOriginal()) as { __testing: typeof __testing }; + + return { + __testing: orig.__testing, + getLiveConfiguration: () => BASE_CONFIGURATION, + getCachedConfiguration: () => BASE_CONFIGURATION, + patchConfiguration: vi.fn(), + }; + }); vi.mock("../src/init/db", () => ({ __esModule: true, diff --git a/backend/__tests__/utils/misc.spec.ts b/backend/__tests__/utils/misc.spec.ts index 5e03db98b6f0..dc7a097c01ff 100644 --- a/backend/__tests__/utils/misc.spec.ts +++ b/backend/__tests__/utils/misc.spec.ts @@ -1,6 +1,5 @@ import { describe, it, expect, afterAll, vi } from "vitest"; -import _ from "lodash"; -import * as misc from "../../src/utils/misc"; +import * as Misc from "../../src/utils/misc"; import { ObjectId } from "mongodb"; describe("Misc Utils", () => { @@ -8,36 +7,44 @@ describe("Misc Utils", () => { vi.useRealTimers(); }); - it("matchesAPattern", () => { - const testCases = { - "eng.*": { + describe("matchesAPattern", () => { + const testCases = [ + { + pattern: "eng.*", cases: ["english", "aenglish", "en", "eng"], expected: [true, false, false, true], }, - "\\d+": { + { + pattern: "\\d+", cases: ["b", "2", "331", "1a"], expected: [false, true, true, false], }, - "(hi|hello)": { + { + pattern: "(hi|hello)", cases: ["hello", "hi", "hillo", "hi hello"], expected: [true, true, false, false], }, - ".+": { + { + pattern: ".+", cases: ["a2", "b2", "c1", ""], expected: [true, true, true, false], }, - }; + ]; - _.each(testCases, (testCase, pattern) => { - const { cases, expected } = testCase; - _.each(cases, (caseValue, index) => { - expect(misc.matchesAPattern(caseValue, pattern)).toBe(expected[index]); - }); - }); + it.each(testCases)( + "matchesAPattern with $pattern", + ({ pattern, cases, expected }) => { + cases.forEach((caseValue, index) => { + expect(Misc.matchesAPattern(caseValue, pattern)).toBe( + expected[index] + ); + }); + } + ); }); - it("kogascore", () => { + describe("kogascore", () => { const testCases = [ { wpm: 214.8, @@ -79,12 +86,15 @@ describe("Misc Utils", () => { }, ]; - _.each(testCases, ({ wpm, acc, timestamp, expectedScore }) => { - expect(misc.kogascore(wpm, acc, timestamp)).toBe(expectedScore); - }); + it.each(testCases)( + "kogascore with wpm:$wpm, acc:$acc, timestamp:$timestamp = $expectedScore", + ({ wpm, acc, timestamp, expectedScore }) => { + expect(Misc.kogascore(wpm, acc, timestamp)).toBe(expectedScore); + } + ); }); - it("identity", () => { + describe("identity", () => { const testCases = [ { input: "", @@ -107,13 +117,15 @@ describe("Misc Utils", () => { expected: "undefined", }, ]; - - _.each(testCases, ({ input, expected }) => { - expect(misc.identity(input)).toEqual(expected); - }); + it.each(testCases)( + "identity with $input = $expected", + ({ input, expected }) => { + expect(Misc.identity(input)).toBe(expected); + } + ); }); - it("flattenObjectDeep", () => { + describe("flattenObjectDeep", () => { const testCases = [ { obj: { @@ -177,9 +189,12 @@ describe("Misc Utils", () => { }, ]; - _.each(testCases, ({ obj, expected }) => { - expect(misc.flattenObjectDeep(obj)).toEqual(expected); - }); + it.each(testCases)( + "flattenObjectDeep with $obj = $expected", + ({ obj, expected }) => { + expect(Misc.flattenObjectDeep(obj)).toEqual(expected); + } + ); }); it("sanitizeString", () => { @@ -215,7 +230,7 @@ describe("Misc Utils", () => { ]; testCases.forEach(({ input, expected }) => { - expect(misc.sanitizeString(input)).toEqual(expected); + expect(Misc.sanitizeString(input)).toEqual(expected); }); }); @@ -284,7 +299,7 @@ describe("Misc Utils", () => { ]; testCases.forEach(({ input, output }) => { - expect(misc.getOrdinalNumberString(input)).toEqual(output); + expect(Misc.getOrdinalNumberString(input)).toEqual(output); }); }); it("formatSeconds", () => { @@ -298,45 +313,45 @@ describe("Misc Utils", () => { expected: "1.08 minutes", }, { - seconds: misc.HOUR_IN_SECONDS, + seconds: Misc.HOUR_IN_SECONDS, expected: "1 hour", }, { - seconds: misc.DAY_IN_SECONDS, + seconds: Misc.DAY_IN_SECONDS, expected: "1 day", }, { - seconds: misc.WEEK_IN_SECONDS, + seconds: Misc.WEEK_IN_SECONDS, expected: "1 week", }, { - seconds: misc.YEAR_IN_SECONDS, + seconds: Misc.YEAR_IN_SECONDS, expected: "1 year", }, { - seconds: 2 * misc.YEAR_IN_SECONDS, + seconds: 2 * Misc.YEAR_IN_SECONDS, expected: "2 years", }, { - seconds: 4 * misc.YEAR_IN_SECONDS, + seconds: 4 * Misc.YEAR_IN_SECONDS, expected: "4 years", }, { - seconds: 3 * misc.WEEK_IN_SECONDS, + seconds: 3 * Misc.WEEK_IN_SECONDS, expected: "3 weeks", }, { - seconds: misc.MONTH_IN_SECONDS * 4, + seconds: Misc.MONTH_IN_SECONDS * 4, expected: "4 months", }, { - seconds: misc.MONTH_IN_SECONDS * 11, + seconds: Misc.MONTH_IN_SECONDS * 11, expected: "11 months", }, ]; testCases.forEach(({ seconds, expected }) => { - expect(misc.formatSeconds(seconds)).toBe(expected); + expect(Misc.formatSeconds(seconds)).toBe(expected); }); }); @@ -347,14 +362,14 @@ describe("Misc Utils", () => { test: "test", number: 1, }; - expect(misc.replaceObjectId(fromDatabase)).toStrictEqual({ + expect(Misc.replaceObjectId(fromDatabase)).toStrictEqual({ _id: fromDatabase._id.toHexString(), test: "test", number: 1, }); }); it("ignores null values", () => { - expect(misc.replaceObjectId(null)).toBeNull(); + expect(Misc.replaceObjectId(null)).toBeNull(); }); }); @@ -371,7 +386,7 @@ describe("Misc Utils", () => { number: 2, }; expect( - misc.replaceObjectIds([fromDatabase, fromDatabase2]) + Misc.replaceObjectIds([fromDatabase, fromDatabase2]) ).toStrictEqual([ { _id: fromDatabase._id.toHexString(), @@ -386,7 +401,7 @@ describe("Misc Utils", () => { ]); }); it("handles undefined", () => { - expect(misc.replaceObjectIds(undefined as any)).toBeUndefined(); + expect(Misc.replaceObjectIds(undefined as any)).toBeUndefined(); }); }); }); diff --git a/backend/__tests__/utils/pb.spec.ts b/backend/__tests__/utils/pb.spec.ts index 3d969e4e1879..b8400368d465 100644 --- a/backend/__tests__/utils/pb.spec.ts +++ b/backend/__tests__/utils/pb.spec.ts @@ -1,12 +1,11 @@ import { describe, it, expect } from "vitest"; -import _ from "lodash"; import * as pb from "../../src/utils/pb"; import { Mode, PersonalBests } from "@monkeytype/schemas/shared"; import { Result } from "@monkeytype/schemas/results"; import { FunboxName } from "@monkeytype/schemas/configs"; describe("Pb Utils", () => { - it("funboxCatGetPb", () => { + describe("funboxCatGetPb", () => { const testCases: { funbox: FunboxName[] | undefined; expected: boolean }[] = [ { @@ -31,16 +30,15 @@ describe("Pb Utils", () => { }, ]; - _.each(testCases, (testCase) => { - const { funbox, expected } = testCase; - //@ts-ignore ignore because this expects a whole result object - const result = pb.canFunboxGetPb({ - funbox, - }); - - expect(result).toBe(expected); - }); + it.each(testCases)( + "canFunboxGetPb with $funbox = $expected", + ({ funbox, expected }) => { + const result = pb.canFunboxGetPb({ funbox } as any); + expect(result).toBe(expected); + } + ); }); + describe("checkAndUpdatePb", () => { it("should update personal best", () => { const userPbs: PersonalBests = { @@ -175,7 +173,7 @@ describe("Pb Utils", () => { for (const lbPb of lbpbstartingvalues) { const lbPbPb = pb.updateLeaderboardPersonalBests( userPbs, - _.cloneDeep(lbPb) as pb.LbPersonalBests, + structuredClone(lbPb) as pb.LbPersonalBests, result15 ); diff --git a/backend/__tests__/workers/later-worker.spec.ts b/backend/__tests__/workers/later-worker.spec.ts new file mode 100644 index 000000000000..a63b0457df17 --- /dev/null +++ b/backend/__tests__/workers/later-worker.spec.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import * as LaterWorker from "../../src/workers/later-worker"; +const calculateXpReward = LaterWorker.__testing.calculateXpReward; + +describe("later-worker", () => { + describe("calculateXpReward", () => { + it("should return the correct XP reward for a given rank", () => { + //GIVEN + const xpRewardBrackets = [ + { minRank: 1, maxRank: 1, minReward: 100, maxReward: 100 }, + { minRank: 2, maxRank: 10, minReward: 50, maxReward: 90 }, + ]; + + //WHEN / THEN + expect(calculateXpReward(xpRewardBrackets, 5)).toBe(75); + expect(calculateXpReward(xpRewardBrackets, 11)).toBeUndefined(); + }); + + it("should return the highest XP reward if brackets overlap", () => { + //GIVEN + const xpRewardBrackets = [ + { minRank: 1, maxRank: 5, minReward: 900, maxReward: 1000 }, + { minRank: 2, maxRank: 20, minReward: 50, maxReward: 90 }, + ]; + + //WHEN + const reward = calculateXpReward(xpRewardBrackets, 5); + + //THEN + expect(reward).toBe(900); + }); + }); +}); diff --git a/backend/package.json b/backend/package.json index 6b5e582ca935..552476eefaa7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,13 +38,13 @@ "cron": "2.3.0", "date-fns": "3.6.0", "dotenv": "16.4.5", + "es-toolkit": "1.39.10", "etag": "1.8.1", "express": "5.1.0", "express-rate-limit": "7.5.1", "firebase-admin": "12.0.0", "helmet": "4.6.0", "ioredis": "4.28.5", - "lodash": "4.17.21", "lru-cache": "7.10.1", "mjml": "4.15.0", "mongodb": "6.3.0", @@ -72,7 +72,6 @@ "@types/cron": "1.7.3", "@types/express": "5.0.3", "@types/ioredis": "4.28.10", - "@types/lodash": "4.14.178", "@types/mjml": "4.7.4", "@types/mustache": "4.2.2", "@types/node": "20.14.11", diff --git a/backend/src/api/controllers/ape-key.ts b/backend/src/api/controllers/ape-key.ts index 828d7c3a0455..38993d8f8d34 100644 --- a/backend/src/api/controllers/ape-key.ts +++ b/backend/src/api/controllers/ape-key.ts @@ -1,4 +1,4 @@ -import _ from "lodash"; +import { keyBy, mapValues, omit } from "es-toolkit"; import { randomBytes } from "crypto"; import { hash } from "bcrypt"; import * as ApeKeysDAL from "../../dal/ape-keys"; @@ -18,7 +18,7 @@ import { ApeKey } from "@monkeytype/schemas/ape-keys"; import { MonkeyRequest } from "../types"; function cleanApeKey(apeKey: ApeKeysDAL.DBApeKey): ApeKey { - return _.omit(apeKey, "hash", "_id", "uid", "useCount"); + return omit(apeKey, ["hash", "_id", "uid", "useCount"]) as ApeKey; } export async function getApeKeys( @@ -27,7 +27,10 @@ export async function getApeKeys( const { uid } = req.ctx.decodedToken; const apeKeys = await ApeKeysDAL.getApeKeys(uid); - const cleanedKeys = _(apeKeys).keyBy("_id").mapValues(cleanApeKey).value(); + const cleanedKeys = mapValues( + keyBy(apeKeys, (it) => it._id.toHexString()), + cleanApeKey + ); return new MonkeyResponse("ApeKeys retrieved", cleanedKeys); } diff --git a/backend/src/api/controllers/leaderboard.ts b/backend/src/api/controllers/leaderboard.ts index 689a2341e6c4..76c02b722e56 100644 --- a/backend/src/api/controllers/leaderboard.ts +++ b/backend/src/api/controllers/leaderboard.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import { MonkeyResponse } from "../../utils/monkey-response"; import * as LeaderboardsDAL from "../../dal/leaderboards"; import MonkeyError from "../../utils/error"; @@ -26,6 +25,7 @@ import { MILLISECONDS_IN_DAY, } from "@monkeytype/util/date-and-time"; import { MonkeyRequest } from "../types"; +import { omit } from "es-toolkit"; export async function getLeaderboard( req: MonkeyRequest @@ -57,7 +57,7 @@ export async function getLeaderboard( } const count = await LeaderboardsDAL.getCount(mode, mode2, language); - const normalizedLeaderboard = leaderboard.map((it) => _.omit(it, ["_id"])); + const normalizedLeaderboard = leaderboard.map((it) => omit(it, ["_id"])); return new MonkeyResponse("Leaderboard retrieved", { count, diff --git a/backend/src/api/controllers/quote.ts b/backend/src/api/controllers/quote.ts index 6862c811df1a..66d190dca27c 100644 --- a/backend/src/api/controllers/quote.ts +++ b/backend/src/api/controllers/quote.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import { v4 as uuidv4 } from "uuid"; import { getPartialUser, updateQuoteRatings } from "../../dal/user"; import * as ReportDAL from "../../dal/report"; @@ -24,6 +23,7 @@ import { import { replaceObjectId, replaceObjectIds } from "../../utils/misc"; import { MonkeyRequest } from "../types"; import { Language } from "@monkeytype/schemas/languages"; +import { setWith } from "es-toolkit/compat"; async function verifyCaptcha(captcha: string): Promise { if (!(await verify(captcha))) { @@ -126,7 +126,7 @@ export async function submitRating( shouldUpdateRating ); - _.setWith(userQuoteRatings, `[${language}][${quoteId}]`, rating, Object); + setWith(userQuoteRatings, `[${language}][${quoteId}]`, rating, Object); await updateQuoteRatings(uid, userQuoteRatings); diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index e46760dd80c4..7dd092dcbfd7 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -26,7 +26,7 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards"; import AutoRoleList from "../../constants/auto-roles"; import * as UserDAL from "../../dal/user"; import { buildMonkeyMail } from "../../utils/monkey-mail"; -import _, { omit } from "lodash"; +import { omit, sumBy, uniq } from "es-toolkit"; import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard"; import { UAParser } from "ua-parser-js"; import { canFunboxGetPb } from "../../utils/pb"; @@ -224,7 +224,7 @@ export async function addResult( const resulthash = completedEvent.hash; if (req.ctx.configuration.results.objectHashCheckEnabled) { - const objectToHash = omit(completedEvent, "hash"); + const objectToHash = omit(completedEvent, ["hash"]); const serverhash = objectHash(objectToHash); if (serverhash !== resulthash) { void addLog( @@ -243,7 +243,7 @@ export async function addResult( Logger.warning("Object hash check is disabled, skipping hash check"); } - if (completedEvent.funbox.length !== _.uniq(completedEvent.funbox).length) { + if (completedEvent.funbox.length !== uniq(completedEvent.funbox).length) { throw new MonkeyError(400, "Duplicate funboxes"); } @@ -758,7 +758,7 @@ async function calculateXp( } if (funboxBonusConfiguration > 0 && resultFunboxes.length !== 0) { - const funboxModifier = _.sumBy(resultFunboxes, (funboxName) => { + const funboxModifier = sumBy(resultFunboxes, (funboxName) => { const funbox = getFunbox(funboxName); const difficultyLevel = funbox?.difficultyLevel ?? 0; return Math.max(difficultyLevel * funboxBonusConfiguration, 0); diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 994d7ad72bec..bb7b085cb05c 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import * as UserDAL from "../../dal/user"; import MonkeyError, { getErrorMessage, @@ -89,6 +88,8 @@ import { import { MILLISECONDS_IN_DAY } from "@monkeytype/util/date-and-time"; import { MonkeyRequest } from "../types"; import { tryCatch } from "@monkeytype/util/trycatch"; +import { mapValues, omit, pick } from "es-toolkit"; +import { filter } from "es-toolkit/compat"; async function verifyCaptcha(captcha: string): Promise { const { data: verified, error } = await tryCatch(verify(captcha)); @@ -511,7 +512,7 @@ type RelevantUserInfo = Omit< >; function getRelevantUserInfo(user: UserDAL.DBUser): RelevantUserInfo { - return _.omit(user, [ + return omit(user, [ "bananas", "lbPersonalBests", "inbox", @@ -578,7 +579,7 @@ export async function getUser(req: MonkeyRequest): Promise { let inboxUnreadSize = 0; if (req.ctx.configuration.users.inbox.enabled) { - inboxUnreadSize = _.filter(userInfo.inbox, { read: false }).length; + inboxUnreadSize = filter(userInfo.inbox, { read: false }).length; } if (!userInfo.name) { @@ -929,8 +930,8 @@ export async function getProfile( lbOptOut, } = user; - const validTimePbs = _.pick(personalBests?.time, "15", "30", "60", "120"); - const validWordsPbs = _.pick(personalBests?.words, "10", "25", "50", "100"); + const validTimePbs = pick(personalBests?.time, ["15", "30", "60", "120"]); + const validWordsPbs = pick(personalBests?.words, ["10", "25", "50", "100"]); const typingStats = { completedTests, @@ -1012,8 +1013,8 @@ export async function updateProfile( const profileDetailsUpdates: Partial = { bio: sanitizeString(bio), keyboard: sanitizeString(keyboard), - socialProfiles: _.mapValues( - socialProfiles, + socialProfiles: mapValues( + socialProfiles ?? {}, sanitizeString ) as UserProfileDetails["socialProfiles"], showActivityOnPublicProfile, diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index d416c9f3b5ea..62bd305bb4cb 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import { contract } from "@monkeytype/contracts/index"; import psas from "./psas"; import publicStats from "./public"; @@ -23,7 +22,6 @@ import { IRouter, NextFunction, Response, - Router, static as expressStatic, } from "express"; import { isDevEnvironment } from "../../utils/misc"; @@ -188,8 +186,8 @@ function applyApiRoutes(app: Application): void { ); }); - _.each(API_ROUTE_MAP, (router: Router, route) => { + for (const [route, router] of Object.entries(API_ROUTE_MAP)) { const apiRoute = `${BASE_ROUTE}${route}`; app.use(apiRoute, router); - }); + } } diff --git a/backend/src/constants/monkey-status-codes.ts b/backend/src/constants/monkey-status-codes.ts index b8ae0952c077..4d24e8b4d6fb 100644 --- a/backend/src/constants/monkey-status-codes.ts +++ b/backend/src/constants/monkey-status-codes.ts @@ -1,5 +1,3 @@ -import _ from "lodash"; - type Status = { code: number; message: string; @@ -71,8 +69,8 @@ const statuses: Statuses = { }, }; -const CUSTOM_STATUS_CODES = new Set( - _.map(statuses, (status: Status) => status.code) +const CUSTOM_STATUS_CODES = new Set( + Object.values(statuses).map((status) => status.code) ); export function isCustomCode(code: number): boolean { diff --git a/backend/src/dal/ape-keys.ts b/backend/src/dal/ape-keys.ts index 78a1153a17c4..a033143edafa 100644 --- a/backend/src/dal/ape-keys.ts +++ b/backend/src/dal/ape-keys.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import * as db from "../init/db"; import { type Filter, @@ -9,6 +8,7 @@ import { } from "mongodb"; import MonkeyError from "../utils/error"; import { ApeKey } from "@monkeytype/schemas/ape-keys"; +import { isNil, pickBy } from "es-toolkit"; export type DBApeKey = ApeKey & { _id: ObjectId; @@ -52,8 +52,8 @@ async function updateApeKey( const updateResult = await getApeKeysCollection().updateOne( getApeKeyFilter(uid, keyId), { - $inc: { useCount: _.has(updates, "lastUsedOn") ? 1 : 0 }, - $set: _.pickBy(updates, (value) => !_.isNil(value)), + $inc: { useCount: "lastUsedOn" in updates ? 1 : 0 }, + $set: pickBy(updates, (value) => !isNil(value)), } ); diff --git a/backend/src/dal/config.ts b/backend/src/dal/config.ts index e94ef0216750..bffad90b2df0 100644 --- a/backend/src/dal/config.ts +++ b/backend/src/dal/config.ts @@ -1,29 +1,29 @@ import { Collection, ObjectId, UpdateResult } from "mongodb"; import * as db from "../init/db"; -import _ from "lodash"; import { Config, PartialConfig } from "@monkeytype/schemas/configs"; +import { mapKeys } from "es-toolkit"; + +const configLegacyProperties: Record = { + "config.swapEscAndTab": "", + "config.quickTab": "", + "config.chartStyle": "", + "config.chartAverage10": "", + "config.chartAverage100": "", + "config.alwaysShowCPM": "", + "config.resultFilters": "", + "config.chartAccuracy": "", + "config.liveSpeed": "", + "config.extraTestColor": "", + "config.savedLayout": "", + "config.showTimerBar": "", + "config.showDiscordDot": "", + "config.maxConfidence": "", + "config.capsLockBackspace": "", + "config.showAvg": "", + "config.enableAds": "", +}; -const configLegacyProperties = [ - "swapEscAndTab", - "quickTab", - "chartStyle", - "chartAverage10", - "chartAverage100", - "alwaysShowCPM", - "resultFilters", - "chartAccuracy", - "liveSpeed", - "extraTestColor", - "savedLayout", - "showTimerBar", - "showDiscordDot", - "maxConfidence", - "capsLockBackspace", - "showAvg", - "enableAds", -]; - -type DBConfig = { +export type DBConfig = { _id: ObjectId; uid: string; config: PartialConfig; @@ -37,15 +37,11 @@ export async function saveConfig( uid: string, config: Partial ): Promise { - const configChanges = _.mapKeys(config, (_value, key) => `config.${key}`); - - const unset = _.fromPairs( - _.map(configLegacyProperties, (key) => [`config.${key}`, ""]) - ) as Record; + const configChanges = mapKeys(config, (_value, key) => `config.${key}`); return await getConfigCollection().updateOne( { uid }, - { $set: configChanges, $unset: unset }, + { $set: configChanges, $unset: configLegacyProperties }, { upsert: true } ); } @@ -58,3 +54,7 @@ export async function getConfig(uid: string): Promise { export async function deleteConfig(uid: string): Promise { await getConfigCollection().deleteOne({ uid }); } + +export const __testing = { + getConfigCollection, +}; diff --git a/backend/src/dal/leaderboards.ts b/backend/src/dal/leaderboards.ts index 42594eb1baf1..d13e8faf897a 100644 --- a/backend/src/dal/leaderboards.ts +++ b/backend/src/dal/leaderboards.ts @@ -11,7 +11,7 @@ import { import { addLog } from "./logs"; import { Collection, ObjectId } from "mongodb"; import { LeaderboardEntry } from "@monkeytype/schemas/leaderboards"; -import { omit } from "lodash"; +import { omit } from "es-toolkit"; import { DBUser, getUsersCollection } from "./user"; import MonkeyError from "../utils/error"; @@ -52,7 +52,7 @@ export async function get( .toArray(); if (!premiumFeaturesEnabled) { - return preset.map((it) => omit(it, "isPremium")); + return preset.map((it) => omit(it, ["isPremium"])); } return preset; diff --git a/backend/src/dal/preset.ts b/backend/src/dal/preset.ts index aae2d3c42dfb..69f568f547c2 100644 --- a/backend/src/dal/preset.ts +++ b/backend/src/dal/preset.ts @@ -2,7 +2,7 @@ import MonkeyError from "../utils/error"; import * as db from "../init/db"; import { ObjectId, type Filter, Collection, type WithId } from "mongodb"; import { EditPresetRequest, Preset } from "@monkeytype/schemas/presets"; -import { omit } from "lodash"; +import { omit } from "es-toolkit"; import { WithObjectId } from "../utils/misc"; const MAX_PRESETS = 10; @@ -62,7 +62,7 @@ export async function editPreset( uid: string, preset: EditPresetRequest ): Promise { - const update: Partial> = omit(preset, "_id"); + const update: Partial> = omit(preset, ["_id"]); if ( preset.config === undefined || preset.config === null || diff --git a/backend/src/dal/result.ts b/backend/src/dal/result.ts index 4c6aa6b07f83..f86e5c1cecf3 100644 --- a/backend/src/dal/result.ts +++ b/backend/src/dal/result.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import { Collection, type DeleteResult, @@ -10,6 +9,8 @@ import * as db from "../init/db"; import { getUser, getTags } from "./user"; import { DBResult, replaceLegacyValues } from "../utils/result"; import { tryCatch } from "@monkeytype/util/trycatch"; +import { isNil } from "es-toolkit"; +import { isNaN } from "es-toolkit/compat"; export const getResultCollection = (): Collection => db.collection("results"); @@ -115,8 +116,8 @@ export async function getResults( .find( { uid, - ...(!_.isNil(onOrAfterTimestamp) && - !_.isNaN(onOrAfterTimestamp) && { + ...(!isNil(onOrAfterTimestamp) && + !isNaN(onOrAfterTimestamp) && { timestamp: { $gte: onOrAfterTimestamp }, }), }, diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 6b534b6f4be3..450657c48bb1 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import { canFunboxGetPb, checkAndUpdatePb, LbPersonalBests } from "../utils/pb"; import * as db from "../init/db"; import MonkeyError from "../utils/error"; @@ -33,6 +32,8 @@ import { Result as ResultType } from "@monkeytype/schemas/results"; import { Configuration } from "@monkeytype/schemas/configuration"; import { isToday, isYesterday } from "@monkeytype/util/date-and-time"; import GeorgeQueue from "../queues/george-queue"; +import { isEmpty, identity } from "es-toolkit/compat"; +import { isPlainObject, omitBy, pickBy } from "es-toolkit"; export type DBUserTag = WithObjectId; @@ -54,11 +55,13 @@ export type DBUser = Omit< inbox?: MonkeyMail[]; ips?: string[]; canReport?: boolean; + nameHistory?: string[]; lastNameChange?: number; canManageApeKeys?: boolean; bananas?: number; testActivity?: CountByYearAndDay; suspicious?: boolean; + note?: string; }; const SECONDS_PER_HOUR = 3600; @@ -599,9 +602,9 @@ export async function linkDiscord( discordId: string, discordAvatar?: string ): Promise { - const updates: Partial = _.pickBy( + const updates: Partial = pickBy( { discordId, discordAvatar }, - _.identity + identity ); await updateUser({ uid }, { $set: updates }, { stack: "link discord" }); } @@ -903,10 +906,9 @@ export async function updateProfile( profileDetailUpdates: Partial, inventory?: UserInventory ): Promise { - const profileUpdates = _.omitBy( + const profileUpdates = omitBy( flattenObjectDeep(profileDetailUpdates, "profileDetails"), - (value) => - value === undefined || (_.isPlainObject(value) && _.isEmpty(value)) + (value) => value === undefined || (isPlainObject(value) && isEmpty(value)) ); const updates = { diff --git a/backend/src/init/configuration.ts b/backend/src/init/configuration.ts index 6fc5e218c861..08f41cddf74d 100644 --- a/backend/src/init/configuration.ts +++ b/backend/src/init/configuration.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import * as db from "./db"; import { ObjectId } from "mongodb"; import Logger from "../utils/logger"; @@ -15,6 +14,8 @@ import { join } from "path"; import { existsSync, readFileSync } from "fs"; import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; import { z } from "zod"; +import { intersection, isPlainObject, omit } from "es-toolkit"; +import { keys } from "es-toolkit/compat"; const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes const SERVER_CONFIG_FILE_PATH = join( @@ -26,22 +27,19 @@ function mergeConfigurations( baseConfiguration: Configuration, liveConfiguration: PartialConfiguration ): void { - if ( - !_.isPlainObject(baseConfiguration) || - !_.isPlainObject(liveConfiguration) - ) { + if (!isPlainObject(baseConfiguration) || !isPlainObject(liveConfiguration)) { return; } function merge(base: object, source: object): void { - const commonKeys = _.intersection(_.keys(base), _.keys(source)); + const commonKeys = intersection(keys(base), keys(source)); commonKeys.forEach((key) => { const baseValue = base[key] as object; const sourceValue = source[key] as object; - const isBaseValueObject = _.isPlainObject(baseValue); - const isSourceValueObject = _.isPlainObject(sourceValue); + const isBaseValueObject = isPlainObject(baseValue); + const isSourceValueObject = isPlainObject(sourceValue); if (isBaseValueObject && isSourceValueObject) { merge(baseValue, sourceValue); @@ -81,12 +79,11 @@ export async function getLiveConfiguration(): Promise { const liveConfiguration = await configurationCollection.findOne(); if (liveConfiguration) { - const baseConfiguration = _.cloneDeep(BASE_CONFIGURATION); + const baseConfiguration = structuredClone(BASE_CONFIGURATION); - const liveConfigurationWithoutId = _.omit( - liveConfiguration, - "_id" - ) as Configuration; + const liveConfigurationWithoutId = omit(liveConfiguration, [ + "_id", + ]) as Configuration; mergeConfigurations(baseConfiguration, liveConfigurationWithoutId); await pushConfiguration(baseConfiguration); @@ -129,7 +126,7 @@ export async function patchConfiguration( configurationUpdates: PartialConfiguration ): Promise { try { - const currentConfiguration = _.cloneDeep(configuration); + const currentConfiguration = structuredClone(configuration); mergeConfigurations(currentConfiguration, configurationUpdates); await db @@ -166,3 +163,7 @@ export async function updateFromConfigurationFile(): Promise { await patchConfiguration(data.configuration); } } + +export const __testing = { + mergeConfigurations, +}; diff --git a/backend/src/init/redis.ts b/backend/src/init/redis.ts index 8b816d3aa904..6d08aedd309c 100644 --- a/backend/src/init/redis.ts +++ b/backend/src/init/redis.ts @@ -1,10 +1,10 @@ import fs from "fs"; -import _ from "lodash"; import { join } from "path"; import IORedis, { Redis } from "ioredis"; import Logger from "../utils/logger"; import { isDevEnvironment } from "../utils/misc"; import { getErrorMessage } from "../utils/error"; +import { camelCase } from "es-toolkit"; // Define Redis connection with custom methods for type safety export type RedisConnectionWithCustomMethods = Redis & { @@ -53,7 +53,7 @@ function loadScripts(client: IORedis.Redis): void { scriptFiles.forEach((scriptFile) => { const scriptPath = join(REDIS_SCRIPTS_DIRECTORY_PATH, scriptFile); const scriptSource = fs.readFileSync(scriptPath, "utf-8"); - const scriptName = _.camelCase(scriptFile.split(".")[0]); + const scriptName = camelCase(scriptFile.split(".")[0] as string); client.defineCommand(scriptName, { lua: scriptSource, diff --git a/backend/src/middlewares/permission.ts b/backend/src/middlewares/permission.ts index 4fdcdaddc42b..59787afcd56d 100644 --- a/backend/src/middlewares/permission.ts +++ b/backend/src/middlewares/permission.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import MonkeyError from "../utils/error"; import type { Response, NextFunction } from "express"; import { DBUser, getPartialUser } from "../dal/user"; diff --git a/backend/src/middlewares/rate-limit.ts b/backend/src/middlewares/rate-limit.ts index 14340ee20780..77e04905bd9a 100644 --- a/backend/src/middlewares/rate-limit.ts +++ b/backend/src/middlewares/rate-limit.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import MonkeyError from "../utils/error"; import type { Response, NextFunction, Request } from "express"; import { RateLimiterMemory } from "rate-limiter-flexible"; diff --git a/backend/src/middlewares/utility.ts b/backend/src/middlewares/utility.ts index 8964f9de5d32..26c6577a2eb0 100644 --- a/backend/src/middlewares/utility.ts +++ b/backend/src/middlewares/utility.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import type { Request, Response, NextFunction, RequestHandler } from "express"; import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus"; import { isDevEnvironment } from "../utils/misc"; diff --git a/backend/src/services/weekly-xp-leaderboard.ts b/backend/src/services/weekly-xp-leaderboard.ts index 3d2bcfaba517..e8db94fef1b6 100644 --- a/backend/src/services/weekly-xp-leaderboard.ts +++ b/backend/src/services/weekly-xp-leaderboard.ts @@ -9,7 +9,7 @@ import { } from "@monkeytype/schemas/leaderboards"; import { getCurrentWeekTimestamp } from "@monkeytype/util/date-and-time"; import MonkeyError from "../utils/error"; -import { omit } from "lodash"; +import { omit } from "es-toolkit"; import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json"; import { tryCatchSync } from "@monkeytype/util/trycatch"; @@ -190,7 +190,7 @@ export class WeeklyXpLeaderboard { ); if (!premiumFeaturesEnabled) { - return resultsWithRanks.map((it) => omit(it, "isPremium")); + return resultsWithRanks.map((it) => omit(it, ["isPremium"])); } return resultsWithRanks; diff --git a/backend/src/utils/daily-leaderboards.ts b/backend/src/utils/daily-leaderboards.ts index c0b5aa57ef2e..d64e788599cb 100644 --- a/backend/src/utils/daily-leaderboards.ts +++ b/backend/src/utils/daily-leaderboards.ts @@ -1,4 +1,4 @@ -import _, { omit } from "lodash"; +import { omit } from "es-toolkit"; import * as RedisClient from "../init/redis"; import LaterQueue from "../queues/later-queue"; import { matchesAPattern, kogascore } from "./misc"; @@ -167,7 +167,7 @@ export class DailyLeaderboard { ); if (!premiumFeaturesEnabled) { - return resultsWithRanks.map((it) => omit(it, "isPremium")); + return resultsWithRanks.map((it) => omit(it, ["isPremium"])); } return resultsWithRanks; diff --git a/backend/src/utils/misc.ts b/backend/src/utils/misc.ts index 44101d1c6a10..7569d2f8e289 100644 --- a/backend/src/utils/misc.ts +++ b/backend/src/utils/misc.ts @@ -1,6 +1,6 @@ import { MILLISECONDS_IN_DAY } from "@monkeytype/util/date-and-time"; import { roundTo2 } from "@monkeytype/util/numbers"; -import _, { omit } from "lodash"; +import { isPlainObject, omit } from "es-toolkit"; import uaparser from "ua-parser-js"; import { MonkeyRequest } from "../api/types"; import { ObjectId } from "mongodb"; @@ -97,7 +97,7 @@ export function flattenObjectDeep( const newPrefix = prefix.length > 0 ? `${prefix}.${key}` : key; - if (_.isPlainObject(value)) { + if (isPlainObject(value)) { const flattened = flattenObjectDeep(value as Record); const flattenedKeys = Object.keys(flattened); @@ -221,7 +221,7 @@ export function replaceObjectId( } const result = { _id: data._id.toString(), - ...omit(data, "_id"), + ...omit(data, ["_id"]), } as T & { _id: string }; return result; } diff --git a/backend/src/utils/pb.ts b/backend/src/utils/pb.ts index 80897febb6cb..635411d6f229 100644 --- a/backend/src/utils/pb.ts +++ b/backend/src/utils/pb.ts @@ -1,7 +1,8 @@ -import _ from "lodash"; import { Mode, PersonalBest, PersonalBests } from "@monkeytype/schemas/shared"; import { Result as ResultType } from "@monkeytype/schemas/results"; import { getFunbox } from "@monkeytype/funbox"; +import { isNil } from "es-toolkit"; +import { isEmpty } from "es-toolkit/compat"; export type LbPersonalBests = { time: Record>; @@ -46,7 +47,7 @@ export function checkAndUpdatePb( (userPb[mode][mode2] as PersonalBest[]).push(buildPersonalBest(result)); } - if (!_.isNil(lbPersonalBests)) { + if (!isNil(lbPersonalBests)) { const newLbPb = updateLeaderboardPersonalBests( userPb, lbPersonalBests, @@ -186,9 +187,9 @@ export function updateLeaderboardPersonalBests( } } ); - _.each(bestForEveryLanguage, (pb: PersonalBest, language: string) => { + Object.entries(bestForEveryLanguage).forEach(([language, pb]) => { const languageDoesNotExist = lbPb[mode][mode2]?.[language] === undefined; - const languageIsEmpty = _.isEmpty(lbPb[mode][mode2]?.[language]); + const languageIsEmpty = isEmpty(lbPb[mode][mode2]?.[language]); if ( (languageDoesNotExist || diff --git a/backend/src/utils/validation.ts b/backend/src/utils/validation.ts index 66bc2b4a52aa..eb5bd94e434a 100644 --- a/backend/src/utils/validation.ts +++ b/backend/src/utils/validation.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import { CompletedEvent } from "@monkeytype/schemas/results"; export function isTestTooShort(result: CompletedEvent): boolean { diff --git a/backend/src/workers/email-worker.ts b/backend/src/workers/email-worker.ts index a4f7362d75bf..59a8daccccf2 100644 --- a/backend/src/workers/email-worker.ts +++ b/backend/src/workers/email-worker.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import IORedis from "ioredis"; import { Worker, Job, type ConnectionOptions } from "bullmq"; import Logger from "../utils/logger"; diff --git a/backend/src/workers/later-worker.ts b/backend/src/workers/later-worker.ts index 2ca2ea8f31dc..ff211e171a45 100644 --- a/backend/src/workers/later-worker.ts +++ b/backend/src/workers/later-worker.ts @@ -1,4 +1,3 @@ -import _ from "lodash"; import IORedis from "ioredis"; import { Worker, Job, type ConnectionOptions } from "bullmq"; import Logger from "../utils/logger"; @@ -17,6 +16,7 @@ import { recordTimeToCompleteJob } from "../utils/prometheus"; import { WeeklyXpLeaderboard } from "../services/weekly-xp-leaderboard"; import { MonkeyMail } from "@monkeytype/schemas/users"; import { isSafeNumber, mapRange } from "@monkeytype/util/numbers"; +import { RewardBracket } from "@monkeytype/schemas/configuration"; async function handleDailyLeaderboardResults( ctx: LaterTaskContexts["daily-leaderboard-results"] @@ -61,18 +61,7 @@ async function handleDailyLeaderboardResults( const placementString = getOrdinalNumberString(rank); - const xpReward = _(xpRewardBrackets) - .filter((bracket) => rank >= bracket.minRank && rank <= bracket.maxRank) - .map((bracket) => - mapRange( - rank, - bracket.minRank, - bracket.maxRank, - bracket.maxReward, - bracket.minReward - ) - ) - .max(); + const xpReward = calculateXpReward(xpRewardBrackets, rank); if (!isSafeNumber(xpReward)) return; @@ -151,18 +140,7 @@ async function handleWeeklyXpLeaderboardResults( const xp = Math.round(totalXp); const placementString = getOrdinalNumberString(rank); - const xpReward = _(xpRewardBrackets) - .filter((bracket) => rank >= bracket.minRank && rank <= bracket.maxRank) - .map((bracket) => - mapRange( - rank, - bracket.minRank, - bracket.maxRank, - bracket.maxReward, - bracket.minReward - ) - ) - .max(); + const xpReward = calculateXpReward(xpRewardBrackets, rank); if (!isSafeNumber(xpReward)) return; @@ -208,6 +186,24 @@ async function jobHandler(job: Job>): Promise { Logger.success(`Job: ${taskName} - completed in ${elapsed}ms`); } +function calculateXpReward( + xpRewardBrackets: RewardBracket[], + rank: number +): number | undefined { + const rewards = xpRewardBrackets + .filter((bracket) => rank >= bracket.minRank && rank <= bracket.maxRank) + .map((bracket) => + mapRange( + rank, + bracket.minRank, + bracket.maxRank, + bracket.maxReward, + bracket.minReward + ) + ); + return rewards.length ? Math.max(...rewards) : undefined; +} + export default (redisConnection?: IORedis.Redis): Worker => { const worker = new Worker(LaterQueue.queueName, jobHandler, { autorun: false, @@ -220,3 +216,6 @@ export default (redisConnection?: IORedis.Redis): Worker => { }); return worker; }; +export const __testing = { + calculateXpReward, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8a113665729..862d9b069f06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: link:packages/release '@vitest/coverage-v8': specifier: 3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0)) + version: 3.2.4(vitest@3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0)) conventional-changelog: specifier: 6.0.0 version: 6.0.0(conventional-commits-filter@5.0.0) @@ -49,7 +49,7 @@ importers: version: 2.5.6 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + version: 3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) backend: dependencies: @@ -95,6 +95,9 @@ importers: dotenv: specifier: 16.4.5 version: 16.4.5 + es-toolkit: + specifier: 1.39.10 + version: 1.39.10 etag: specifier: 1.8.1 version: 1.8.1 @@ -113,9 +116,6 @@ importers: ioredis: specifier: 4.28.5 version: 4.28.5 - lodash: - specifier: 4.17.21 - version: 4.17.21 lru-cache: specifier: 7.10.1 version: 7.10.1 @@ -192,9 +192,6 @@ importers: '@types/ioredis': specifier: 4.28.10 version: 4.28.10 - '@types/lodash': - specifier: 4.14.178 - version: 4.14.178 '@types/mjml': specifier: 4.7.4 version: 4.7.4 @@ -230,7 +227,7 @@ importers: version: 10.0.0 '@vitest/coverage-v8': specifier: 3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0)) + version: 3.2.4(vitest@3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0)) concurrently: specifier: 8.2.2 version: 8.2.2 @@ -263,7 +260,7 @@ importers: version: 5.5.4 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) frontend: dependencies: @@ -535,7 +532,7 @@ importers: version: 5.5.4 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) packages/eslint-config: devDependencies: @@ -599,7 +596,7 @@ importers: version: 5.5.4 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) packages/oxlint-config: {} @@ -660,7 +657,7 @@ importers: version: 5.5.4 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) packages/tsup-config: dependencies: @@ -711,7 +708,7 @@ importers: version: 5.5.4 vitest: specifier: 3.2.4 - version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + version: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) zod: specifier: 3.23.8 version: 3.23.8 @@ -3023,9 +3020,6 @@ packages: '@types/jsonwebtoken@9.0.6': resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} - '@types/lodash@4.14.178': - resolution: {integrity: sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==} - '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} @@ -4807,6 +4801,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.39.10: + resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} @@ -8636,7 +8633,7 @@ packages: superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superstatic@9.0.3: resolution: {integrity: sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==} @@ -8721,11 +8718,6 @@ packages: engines: {node: '>=10'} hasBin: true - terser@5.43.1: - resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} - engines: {node: '>=10'} - hasBin: true - terser@5.44.0: resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} engines: {node: '>=10'} @@ -12463,8 +12455,6 @@ snapshots: dependencies: '@types/node': 20.14.11 - '@types/lodash@4.14.178': {} - '@types/long@4.0.2': {} '@types/methods@1.1.4': {} @@ -12735,25 +12725,6 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.3 - debug: 4.4.1 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.9.0 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) - transitivePeerDependencies: - - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0))': dependencies: '@ampproject/remapping': 2.3.0 @@ -12773,7 +12744,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -12788,7 +12759,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + vitest: 3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) transitivePeerDependencies: - supports-color @@ -12800,14 +12771,6 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) - '@vitest/mocker@3.2.4(vite@6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0))': dependencies: '@vitest/spy': 3.2.4 @@ -12816,13 +12779,13 @@ snapshots: optionalDependencies: vite: 6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) - '@vitest/mocker@3.2.4(vite@6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0))': + '@vitest/mocker@3.2.4(vite@6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -14592,6 +14555,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.39.10: {} + es6-promise@3.3.1: {} esbuild@0.21.5: @@ -19460,14 +19425,6 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - terser@5.43.1: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 - commander: 2.20.3 - source-map-support: 0.5.21 - optional: true - terser@5.44.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -20031,27 +19988,6 @@ snapshots: dependencies: vite: 6.3.6(@types/node@20.14.11)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) - vite-node@3.2.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): - dependencies: - cac: 6.7.14 - debug: 4.4.1 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 6.3.6(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite-node@3.2.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: cac: 6.7.14 @@ -20073,13 +20009,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): + vite-node@3.2.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.6(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + vite: 6.3.6(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) transitivePeerDependencies: - '@types/node' - jiti @@ -20160,22 +20096,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite@6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): - dependencies: - esbuild: 0.25.0 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.3 - rollup: 4.40.0 - tinyglobby: 0.2.13 - optionalDependencies: - '@types/node': 20.14.11 - fsevents: 2.3.3 - sass: 1.70.0 - terser: 5.43.1 - tsx: 4.16.2 - yaml: 2.5.0 - vite@6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: esbuild: 0.25.0 @@ -20192,7 +20112,7 @@ snapshots: tsx: 4.16.2 yaml: 2.5.0 - vite@6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): + vite@6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: esbuild: 0.25.0 fdir: 6.4.4(picomatch@4.0.2) @@ -20204,23 +20124,7 @@ snapshots: '@types/node': 20.5.1 fsevents: 2.3.3 sass: 1.70.0 - terser: 5.43.1 - tsx: 4.16.2 - yaml: 2.5.0 - - vite@6.3.6(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): - dependencies: - esbuild: 0.25.0 - fdir: 6.4.4(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.3 - rollup: 4.40.0 - tinyglobby: 0.2.14 - optionalDependencies: - '@types/node': 20.14.11 - fsevents: 2.3.3 - sass: 1.70.0 - terser: 5.43.1 + terser: 5.44.0 tsx: 4.16.2 yaml: 2.5.0 @@ -20240,7 +20144,7 @@ snapshots: tsx: 4.16.2 yaml: 2.5.0 - vite@6.3.6(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): + vite@6.3.6(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: esbuild: 0.25.0 fdir: 6.4.4(picomatch@4.0.3) @@ -20252,52 +20156,10 @@ snapshots: '@types/node': 20.5.1 fsevents: 2.3.3 sass: 1.70.0 - terser: 5.43.1 + terser: 5.44.0 tsx: 4.16.2 yaml: 2.5.0 - vitest@3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): - dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.1 - debug: 4.4.1 - expect-type: 1.2.2 - magic-string: 0.30.17 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.14 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 6.3.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) - vite-node: 3.2.4(@types/node@20.14.11)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.14.11 - happy-dom: 15.10.2 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.2.4(@types/node@20.14.11)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: '@types/chai': 5.2.2 @@ -20340,11 +20202,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0): + vitest@3.2.4(@types/node@20.5.1)(happy-dom@15.10.2)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0)) + '@vitest/mocker': 3.2.4(vite@6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -20362,8 +20224,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) - vite-node: 3.2.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.43.1)(tsx@4.16.2)(yaml@2.5.0) + vite: 6.3.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) + vite-node: 3.2.4(@types/node@20.5.1)(sass@1.70.0)(terser@5.44.0)(tsx@4.16.2)(yaml@2.5.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.5.1