diff --git a/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts b/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts index 17847fe0b345..41a2b63f1930 100644 --- a/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts +++ b/backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts @@ -12,6 +12,7 @@ import { LbPersonalBests } from "../../../src/utils/pb"; import { pb } from "../../__testData__/users"; import { createConnection } from "../../__testData__/connections"; import { omit } from "../../../src/utils/misc"; +import { LeaderboardEntry } from "@monkeytype/schemas/leaderboards"; describe("LeaderboardsDal", () => { afterEach(async () => { @@ -122,7 +123,7 @@ describe("LeaderboardsDal", () => { it("should remove consistency from results if null", async () => { //GIVEN const stats = pb(100, 90, 2); - //@ts-ignore + //@ts-expect-error ok for testing stats.consistency = undefined; await createUser(lbBests(stats)); @@ -468,11 +469,10 @@ describe("LeaderboardsDal", () => { function expectedLbEntry( time: string, { rank, user, badgeId, isPremium, friendsRank }: ExpectedLbEntry, -) { - // @ts-expect-error - const lbBest: PersonalBest = - // @ts-expect-error - user.lbPersonalBests?.time[Number.parseInt(time)].english; +): LeaderboardEntry { + const lbBest: PersonalBest = user.lbPersonalBests?.time[ + Number.parseInt(time) + ]?.["english"] as PersonalBest; return { rank, @@ -523,7 +523,7 @@ function lbBests(pb15?: PersonalBest, pb60?: PersonalBest): LbPersonalBests { return result; } -function premium(expirationDeltaSeconds: number) { +function premium(expirationDeltaSeconds: number): Partial { return { premium: { startTimestamp: 0, diff --git a/backend/__tests__/__integration__/dal/preset.spec.ts b/backend/__tests__/__integration__/dal/preset.spec.ts index 76d9451e65b8..5c3dcf54bbae 100644 --- a/backend/__tests__/__integration__/dal/preset.spec.ts +++ b/backend/__tests__/__integration__/dal/preset.spec.ts @@ -67,7 +67,7 @@ describe("PresetDal", () => { } //WHEN / THEN - await expect(() => + await expect(async () => PresetDal.addPreset(uid, { name: "max", config: {} }), ).rejects.toThrow("Too many presets"); }); @@ -356,7 +356,7 @@ describe("PresetDal", () => { describe("removePreset", () => { it("should fail if preset is unknown", async () => { const uid = new ObjectId().toHexString(); - await expect(() => + await expect(async () => PresetDal.removePreset(uid, new ObjectId().toHexString()), ).rejects.toThrow("Preset not found"); }); @@ -419,7 +419,7 @@ describe("PresetDal", () => { ).presetId; //WHEN - await expect(() => + await expect(async () => PresetDal.removePreset(decoyUid, first), ).rejects.toThrow("Preset not found"); diff --git a/backend/__tests__/__integration__/dal/user.spec.ts b/backend/__tests__/__integration__/dal/user.spec.ts index 7330dfe8f532..5f44cffe1d41 100644 --- a/backend/__tests__/__integration__/dal/user.spec.ts +++ b/backend/__tests__/__integration__/dal/user.spec.ts @@ -1079,9 +1079,9 @@ describe("UserDal", () => { await UserDAL.incrementTestActivity(user, 1712102400000); //then - const read = (await UserDAL.getUser(user.uid, "")).testActivity || {}; + const read = (await UserDAL.getUser(user.uid, "")).testActivity ?? {}; expect(read).toHaveProperty("2024"); - const year2024 = read["2024"] as any; + const year2024 = read["2024"] as number[]; expect(year2024).toHaveLength(94); //fill previous days with null expect(year2024.slice(0, 93)).toEqual(new Array(93).fill(null)); @@ -1097,9 +1097,9 @@ describe("UserDal", () => { await UserDAL.incrementTestActivity(user, 1712102400000); //then - const read = (await UserDAL.getUser(user.uid, "")).testActivity || {}; + const read = (await UserDAL.getUser(user.uid, "")).testActivity ?? {}; expect(read).toHaveProperty("2024"); - const year2024 = read["2024"] as any; + const year2024 = read["2024"] as number[]; expect(year2024).toHaveLength(94); expect(year2024[0]).toBeNull(); @@ -1117,7 +1117,7 @@ describe("UserDal", () => { await UserDAL.incrementTestActivity(user, 1712102400000); //then - const read = (await UserDAL.getUser(user.uid, "")).testActivity || {}; + const read = (await UserDAL.getUser(user.uid, "")).testActivity ?? {}; const year2024 = read["2024"] as any; expect(year2024[93]).toEqual(2); }); @@ -1581,7 +1581,7 @@ describe("UserDal", () => { const count = 100; const calls = new Array(count) .fill(0) - .map(() => + .map(async () => UserDAL.updateInbox( user.uid, [rewardOne.id, rewardTwo.id, rewardThree.id], @@ -2084,10 +2084,9 @@ describe("UserDal", () => { it("should clear streak hour offset", async () => { // given const { uid } = await UserTestData.createUser({ - //@ts-expect-error streak: { hourOffset: 1, - }, + } as any, }); // when diff --git a/backend/__tests__/__integration__/redis.ts b/backend/__tests__/__integration__/redis.ts index 3145d966863a..0042813e7b0e 100644 --- a/backend/__tests__/__integration__/redis.ts +++ b/backend/__tests__/__integration__/redis.ts @@ -7,5 +7,5 @@ export async function cleanupKeys(prefix: string): Promise { // oxlint-disable-next-line no-non-null-assertion const connection = getConnection()!; const keys = await connection.keys(`${prefix}*`); - await Promise.all(keys?.map((it) => connection.del(it))); + await Promise.all(keys?.map(async (it) => connection.del(it))); } diff --git a/backend/__tests__/__integration__/setup-integration-tests.ts b/backend/__tests__/__integration__/setup-integration-tests.ts index b71258cfeb3d..48b3677f2b86 100644 --- a/backend/__tests__/__integration__/setup-integration-tests.ts +++ b/backend/__tests__/__integration__/setup-integration-tests.ts @@ -5,8 +5,8 @@ import { getConnection } from "../../src/init/redis"; process.env["MODE"] = "dev"; -let db: Db; -let client: MongoClient; +let db: Db | undefined; +let client: MongoClient | undefined; beforeAll(async () => { client = new MongoClient(process.env["TEST_DB_URL"] as string); @@ -15,9 +15,9 @@ beforeAll(async () => { vi.mock("../../src/init/db", () => ({ __esModule: true, - getDb: (): Db => db, + getDb: (): Db => db as Db, collection: (name: string): Collection> => - db.collection>(name), + (db as Db).collection>(name), close: () => { // }, @@ -35,9 +35,8 @@ afterEach(async () => { afterAll(async () => { await client?.close(); - // @ts-ignore + db = undefined; - //@ts-ignore client = undefined; await getConnection()?.quit(); diff --git a/backend/__tests__/__integration__/utils/daily-leaderboards.spec.ts b/backend/__tests__/__integration__/utils/daily-leaderboards.spec.ts index ece13c8d305a..e60d9ef7e116 100644 --- a/backend/__tests__/__integration__/utils/daily-leaderboards.spec.ts +++ b/backend/__tests__/__integration__/utils/daily-leaderboards.spec.ts @@ -138,7 +138,7 @@ describe("Daily Leaderboards", () => { await Promise.all( new Array(maxResults - 1) .fill(0) - .map(() => givenResult({ wpm: 20 + Math.random() * 100 })), + .map(async () => givenResult({ wpm: 20 + Math.random() * 100 })), ); expect( await lb.getResults(0, 5, dailyLeaderboardsConfig, true), diff --git a/backend/__tests__/__testData__/auth.ts b/backend/__tests__/__testData__/auth.ts index ec5a446291e2..ba65badaaeaf 100644 --- a/backend/__tests__/__testData__/auth.ts +++ b/backend/__tests__/__testData__/auth.ts @@ -40,7 +40,28 @@ export async function mockAuthenticateWithApeKey( return base64UrlEncode(`${apeKeyId}.${apiKey}`); } -export function mockBearerAuthentication(uid: string) { +export type BearerAuthenticationMock = { + /** + * Reset the mock and return a default token. Call this method in the `beforeEach` of all tests. + */ + beforeEach: () => void; + /** + * Reset the mock results in the authentication to fail. + */ + noAuth: () => void; + /** + * verify the authentication has been called + */ + expectToHaveBeenCalled: () => void; + /** + * modify the token returned by the mock. This can be used to e.g. return a stale token. + * @param customize + */ + modifyToken: (customize: Partial) => void; +}; +export function mockBearerAuthentication( + uid: string, +): BearerAuthenticationMock { const mockDecodedToken = { uid, email: "newuser@mail.com", @@ -49,29 +70,19 @@ export function mockBearerAuthentication(uid: string) { const verifyIdTokenMock = vi.spyOn(AuthUtils, "verifyIdToken"); return { - /** - * Reset the mock and return a default token. Call this method in the `beforeEach` of all tests. - */ beforeEach: (): void => { verifyIdTokenMock.mockClear(); verifyIdTokenMock.mockResolvedValue(mockDecodedToken); }, - /** - * Reset the mock results in the authentication to fail. - */ + noAuth: (): void => { verifyIdTokenMock.mockClear(); }, - /** - * verify the authentication has been called - */ + expectToHaveBeenCalled: (): void => { expect(verifyIdTokenMock).toHaveBeenCalled(); }, - /** - * modify the token returned by the mock. This can be used to e.g. return a stale token. - * @param customize - */ + modifyToken: (customize: Partial): void => { verifyIdTokenMock.mockClear(); verifyIdTokenMock.mockResolvedValue({ diff --git a/backend/__tests__/__testData__/controller-test.ts b/backend/__tests__/__testData__/controller-test.ts index abcc70e439d2..82af35ec8964 100644 --- a/backend/__tests__/__testData__/controller-test.ts +++ b/backend/__tests__/__testData__/controller-test.ts @@ -1,10 +1,15 @@ import request from "supertest"; import app from "../../src/app"; import { ObjectId } from "mongodb"; -import { mockBearerAuthentication } from "./auth"; +import { BearerAuthenticationMock, mockBearerAuthentication } from "./auth"; import { beforeEach } from "vitest"; +import TestAgent from "supertest/lib/agent"; -export function setup() { +export function setup(): { + mockApp: TestAgent; + uid: string; + mockAuth: BearerAuthenticationMock; +} { const mockApp = request(app); const uid = new ObjectId().toHexString(); const mockAuth = mockBearerAuthentication(uid); diff --git a/backend/__tests__/api/controllers/preset.spec.ts b/backend/__tests__/api/controllers/preset.spec.ts index 7b6dad709df0..b0ce8cbef536 100644 --- a/backend/__tests__/api/controllers/preset.spec.ts +++ b/backend/__tests__/api/controllers/preset.spec.ts @@ -33,8 +33,7 @@ describe("PresetController", () => { showAverage: "off", }, }; - //@ts-expect-error - getPresetsMock.mockResolvedValue([presetOne, presetTwo]); + getPresetsMock.mockResolvedValue([presetOne, presetTwo] as any); //WHEN const { body } = await mockApp diff --git a/backend/__tests__/api/controllers/quotes.spec.ts b/backend/__tests__/api/controllers/quotes.spec.ts index 4275d2299672..188f47b44315 100644 --- a/backend/__tests__/api/controllers/quotes.spec.ts +++ b/backend/__tests__/api/controllers/quotes.spec.ts @@ -18,8 +18,8 @@ describe("QuotesController", () => { const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser"); const logsAddLogMock = vi.spyOn(LogsDal, "addLog"); - beforeEach(() => { - enableQuotes(true); + beforeEach(async () => { + await enableQuotes(true); const user = { quoteMod: true, name: "Bob" } as any; getPartialUserMock.mockClear().mockResolvedValue(user); @@ -128,7 +128,6 @@ describe("QuotesController", () => { describe("isSubmissionsEnabled", () => { it("should return for quotes enabled without authentication", async () => { //GIVEN - enableQuotes(true); //WHEN const { body } = await mockApp @@ -202,7 +201,7 @@ describe("QuotesController", () => { }); it("should fail if feature is disabled", async () => { //GIVEN - enableQuotes(false); + await enableQuotes(false); //WHEN const { body } = await mockApp @@ -755,8 +754,8 @@ describe("QuotesController", () => { const verifyCaptchaMock = vi.spyOn(Captcha, "verify"); const createReportMock = vi.spyOn(ReportDal, "createReport"); - beforeEach(() => { - enableQuoteReporting(true); + beforeEach(async () => { + await enableQuoteReporting(true); verifyCaptchaMock.mockClear().mockResolvedValue(true); createReportMock.mockClear().mockResolvedValue(); @@ -843,7 +842,7 @@ describe("QuotesController", () => { }); it("should fail if feature is disabled", async () => { //GIVEN - enableQuoteReporting(false); + await enableQuoteReporting(false); //WHEN const { body } = await mockApp diff --git a/backend/__tests__/api/controllers/result.spec.ts b/backend/__tests__/api/controllers/result.spec.ts index a0b3fbe38c48..516f66549f3b 100644 --- a/backend/__tests__/api/controllers/result.spec.ts +++ b/backend/__tests__/api/controllers/result.spec.ts @@ -205,7 +205,7 @@ describe("result controller test", () => { it("should get results within regular limits for premium users even if premium is globally disabled", async () => { //GIVEN vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true); - enablePremiumFeatures(false); + await enablePremiumFeatures(false); //WHEN await mockApp @@ -225,7 +225,7 @@ describe("result controller test", () => { it("should fail exceeding max limit for premium user if premium is globally disabled", async () => { //GIVEN vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true); - enablePremiumFeatures(false); + await enablePremiumFeatures(false); //WHEN const { body } = await mockApp @@ -241,7 +241,7 @@ describe("result controller test", () => { it("should get results with regular limit as default for premium users if premium is globally disabled", async () => { //GIVEN vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true); - enablePremiumFeatures(false); + await enablePremiumFeatures(false); //WHEN await mockApp diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index 7959dc49eb13..867f050cbfa2 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -679,7 +679,7 @@ describe("user controller test", () => { discordId: "discordId", banned: true, } as Partial as UserDal.DBUser; - await getUserMock.mockResolvedValue(user); + getUserMock.mockResolvedValue(user); //WHEN await mockApp @@ -1544,8 +1544,8 @@ describe("user controller test", () => { describe("get oauth link", () => { const getOauthLinkMock = vi.spyOn(DiscordUtils, "getOauthLink"); const url = "http://example.com:1234?test"; - beforeEach(() => { - enableDiscordIntegration(true); + beforeEach(async () => { + await enableDiscordIntegration(true); getOauthLinkMock.mockClear().mockResolvedValue(url); }); @@ -1565,7 +1565,7 @@ describe("user controller test", () => { }); it("should fail if feature is not enabled", async () => { //GIVEN - enableDiscordIntegration(false); + await enableDiscordIntegration(false); //WHEN const { body } = await mockApp @@ -2066,7 +2066,7 @@ describe("user controller test", () => { }); it("should fail if feature is disabled", async () => { //GIVEN - enableResultFilterPresets(false); + await enableResultFilterPresets(false); //WHEN const { body } = await mockApp .post("/users/resultFilterPresets") @@ -2086,8 +2086,8 @@ describe("user controller test", () => { "removeResultFilterPreset", ); - beforeEach(() => { - enableResultFilterPresets(true); + beforeEach(async () => { + await enableResultFilterPresets(true); removeResultFilterPresetMock.mockClear().mockResolvedValue(); }); @@ -2107,7 +2107,7 @@ describe("user controller test", () => { }); it("should fail if feature is disabled", async () => { //GIVEN - enableResultFilterPresets(false); + await enableResultFilterPresets(false); //WHEN const { body } = await mockApp @@ -3900,14 +3900,14 @@ describe("user controller test", () => { describe("get friends", () => { const getFriendsMock = vi.spyOn(UserDal, "getFriends"); - beforeEach(() => { - enableConnectionsEndpoints(true); + beforeEach(async () => { + await enableConnectionsEndpoints(true); getFriendsMock.mockClear(); }); it("gets with premium enabled", async () => { //GIVEN - enablePremiumFeatures(true); + await enablePremiumFeatures(true); const friend: UserDal.DBFriend = { name: "Bob", isPremium: true, @@ -3926,7 +3926,7 @@ describe("user controller test", () => { it("gets with premium disabled", async () => { //GIVEN - enablePremiumFeatures(false); + await enablePremiumFeatures(false); const friend: UserDal.DBFriend = { name: "Bob", isPremium: true, diff --git a/backend/__tests__/middlewares/auth.spec.ts b/backend/__tests__/middlewares/auth.spec.ts index d86e680dccef..902e3196691c 100644 --- a/backend/__tests__/middlewares/auth.spec.ts +++ b/backend/__tests__/middlewares/auth.spec.ts @@ -17,6 +17,7 @@ import { import * as Prometheus from "../../src/utils/prometheus"; import { TsRestRequestWithContext } from "../../src/api/types"; import { enableMonkeyErrorExpects } from "../__testData__/monkey-error"; +import { Context } from "../../src/middlewares/context"; enableMonkeyErrorExpects(); const mockDecodedToken: DecodedIdToken = { @@ -72,7 +73,7 @@ describe("middlewares/auth", () => { json: vi.fn(), }; nextFunction = vi.fn((error) => { - if (error) { + if (error !== undefined) { throw error; } return "Next function called"; @@ -104,7 +105,7 @@ describe("middlewares/auth", () => { ); //WHEN - await expect(() => + await expect(async () => authenticate({}, { requireFreshToken: true }), ).rejects.toMatchMonkeyError(expectedError); @@ -149,7 +150,7 @@ describe("middlewares/auth", () => { }); it("should fail with apeKey if apeKey is not supported", async () => { //WHEN - await expect(() => + await expect(async () => authenticate( { headers: { authorization: "ApeKey aWQua2V5" } }, { acceptApeKeys: false }, @@ -161,11 +162,10 @@ describe("middlewares/auth", () => { it("should fail with apeKey if apeKeys are disabled", async () => { //GIVEN - //@ts-expect-error - mockRequest.ctx.configuration.apeKeys.acceptKeys = false; + (mockRequest.ctx as Context).configuration.apeKeys.acceptKeys = false; //WHEN - await expect(() => + await expect(async () => authenticate( { headers: { authorization: "ApeKey aWQua2V5" } }, { acceptApeKeys: false }, @@ -246,14 +246,14 @@ describe("middlewares/auth", () => { isDevModeMock.mockReturnValue(false); //WHEN / THEN - await expect(() => + await expect(async () => authenticate({ headers: { authorization: "Uid 123" } }), ).rejects.toMatchMonkeyError( new MonkeyError(401, "Bearer type uid is not supported"), ); }); it("should fail without authentication", async () => { - await expect(() => authenticate({ headers: {} })).rejects.toThrow( + await expect(async () => authenticate({ headers: {} })).rejects.toThrow( "Unauthorized\nStack: endpoint: /api/v1 no authorization header found", ); @@ -267,7 +267,7 @@ describe("middlewares/auth", () => { ); }); it("should fail with empty authentication", async () => { - await expect(() => + await expect(async () => authenticate({ headers: { authorization: "" } }), ).rejects.toThrow( "Unauthorized\nStack: endpoint: /api/v1 no authorization header found", @@ -283,7 +283,7 @@ describe("middlewares/auth", () => { ); }); it("should fail with missing authentication token", async () => { - await expect(() => + await expect(async () => authenticate({ headers: { authorization: "Bearer" } }), ).rejects.toThrow( "Missing authentication token\nStack: authenticateWithAuthHeader", @@ -299,7 +299,7 @@ describe("middlewares/auth", () => { ); }); it("should fail with unknown authentication scheme", async () => { - await expect(() => + await expect(async () => authenticate({ headers: { authorization: "unknown format" } }), ).rejects.toThrow( 'Unknown authentication scheme\nStack: The authentication scheme "unknown" is not implemented', @@ -378,9 +378,7 @@ describe("middlewares/auth", () => { }); it("should allow with apeKey if apeKeys are disabled on dev public endpoint", async () => { //GIVEN - - //@ts-expect-error - mockRequest.ctx.configuration.apeKeys.acceptKeys = false; + (mockRequest.ctx as Context).configuration.apeKeys.acceptKeys = false; //WHEN const result = await authenticate( @@ -415,7 +413,7 @@ describe("middlewares/auth", () => { isDevModeMock.mockReturnValue(false); //THEN - await expect(() => + await expect(async () => authenticate({ headers: {} }, { isPublicOnDev: true }), ).rejects.toThrow("Unauthorized"); }); @@ -468,7 +466,7 @@ describe("middlewares/auth", () => { vi.stubEnv("GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET"); timingSafeEqualMock.mockReturnValue(false); - await expect(() => + await expect(async () => authenticate( { headers: { "x-hub-signature-256": "the-signature" }, @@ -489,7 +487,7 @@ describe("middlewares/auth", () => { }); it("should fail without header when endpoint is using githubwebhook", async () => { vi.stubEnv("GITHUB_WEBHOOK_SECRET", "GITHUB_WEBHOOK_SECRET"); - await expect(() => + await expect(async () => authenticate( { headers: {}, @@ -510,7 +508,7 @@ describe("middlewares/auth", () => { }); it("should fail with missing GITHUB_WEBHOOK_SECRET when endpoint is using githubwebhook", async () => { vi.stubEnv("GITHUB_WEBHOOK_SECRET", ""); - await expect(() => + await expect(async () => authenticate( { headers: { "x-hub-signature-256": "the-signature" }, @@ -534,7 +532,7 @@ describe("middlewares/auth", () => { timingSafeEqualMock.mockImplementation(() => { throw new Error("could not validate"); }); - await expect(() => + await expect(async () => authenticate( { headers: { "x-hub-signature-256": "the-signature" }, diff --git a/backend/__tests__/middlewares/configuration.spec.ts b/backend/__tests__/middlewares/configuration.spec.ts index 0c8e7f01f361..604fa895ccb2 100644 --- a/backend/__tests__/middlewares/configuration.spec.ts +++ b/backend/__tests__/middlewares/configuration.spec.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { RequireConfiguration } from "@monkeytype/contracts/require-configuration/index"; -import { verifyRequiredConfiguration } from "../../src/middlewares/configuration"; import { Configuration } from "@monkeytype/schemas/configuration"; import { Response } from "express"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TsRestRequestWithContext } from "../../src/api/types"; +import { verifyRequiredConfiguration } from "../../src/middlewares/configuration"; import MonkeyError from "../../src/utils/error"; -import { TsRestRequest } from "../../src/api/types"; import { enableMonkeyErrorExpects } from "../__testData__/monkey-error"; enableMonkeyErrorExpects(); @@ -21,27 +21,27 @@ describe("configuration middleware", () => { expect(next).toHaveBeenCalledOnce(); }); - it("should pass without requireConfiguration", async () => { + it("should pass without requireConfiguration", () => { //GIVEN const req = { tsRestRoute: { metadata: {} } } as any; //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith(); }); - it("should pass for enabled configuration", async () => { + it("should pass for enabled configuration", () => { //GIVEN const req = givenRequest({ path: "maintenance" }, { maintenance: true }); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith(); }); - it("should pass for enabled configuration with complex path", async () => { + it("should pass for enabled configuration with complex path", () => { //GIVEN const req = givenRequest( { path: "users.xp.streak.enabled" }, @@ -49,17 +49,17 @@ describe("configuration middleware", () => { ); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith(); }); - it("should fail for disabled configuration", async () => { + it("should fail for disabled configuration", () => { //GIVEN const req = givenRequest({ path: "maintenance" }, { maintenance: false }); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith( @@ -68,7 +68,7 @@ describe("configuration middleware", () => { ), ); }); - it("should fail for disabled configuration and custom message", async () => { + it("should fail for disabled configuration and custom message", () => { //GIVEN const req = givenRequest( { path: "maintenance", invalidMessage: "Feature not enabled." }, @@ -76,19 +76,19 @@ describe("configuration middleware", () => { ); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith( expect.toMatchMonkeyError(new MonkeyError(503, "Feature not enabled.")), ); }); - it("should fail for invalid path", async () => { + it("should fail for invalid path", () => { //GIVEN const req = givenRequest({ path: "invalid.path" as any }, {}); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith( @@ -97,7 +97,7 @@ describe("configuration middleware", () => { ), ); }); - it("should fail for undefined value", async () => { + it("should fail for undefined value", () => { //GIVEN const req = givenRequest( { path: "admin.endpointsEnabled" }, @@ -105,7 +105,7 @@ describe("configuration middleware", () => { ); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith( @@ -117,7 +117,7 @@ describe("configuration middleware", () => { ), ); }); - it("should fail for null value", async () => { + it("should fail for null value", () => { //GIVEN const req = givenRequest( { path: "admin.endpointsEnabled" }, @@ -125,7 +125,7 @@ describe("configuration middleware", () => { ); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith( @@ -137,7 +137,7 @@ describe("configuration middleware", () => { ), ); }); - it("should fail for non booean value", async () => { + it("should fail for non booean value", () => { //GIVEN const req = givenRequest( { path: "admin.endpointsEnabled" }, @@ -145,7 +145,7 @@ describe("configuration middleware", () => { ); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith( @@ -157,7 +157,7 @@ describe("configuration middleware", () => { ), ); }); - it("should pass for multiple configurations", async () => { + it("should pass for multiple configurations", () => { //GIVEN const req = givenRequest( [{ path: "maintenance" }, { path: "admin.endpointsEnabled" }], @@ -165,12 +165,12 @@ describe("configuration middleware", () => { ); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith(); }); - it("should fail for multiple configurations", async () => { + it("should fail for multiple configurations", () => { //GIVEN const req = givenRequest( [ @@ -181,7 +181,7 @@ describe("configuration middleware", () => { ); //WHEN - await handler(req, res, next); + handler(req, res, next); //THEN expect(next).toHaveBeenCalledWith( @@ -193,9 +193,9 @@ describe("configuration middleware", () => { function givenRequest( requireConfiguration: RequireConfiguration | RequireConfiguration[], configuration: Partial, -): TsRestRequest { +): TsRestRequestWithContext { return { tsRestRoute: { metadata: { requireConfiguration } }, - ctx: { configuration }, - } as any; + ctx: { configuration: configuration }, + } as TsRestRequestWithContext; } diff --git a/backend/__tests__/middlewares/permission.spec.ts b/backend/__tests__/middlewares/permission.spec.ts index 78a17ce2432e..a10174bfda99 100644 --- a/backend/__tests__/middlewares/permission.spec.ts +++ b/backend/__tests__/middlewares/permission.spec.ts @@ -1,13 +1,13 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { Response } from "express"; -import { verifyPermissions } from "../../src/middlewares/permission"; import { EndpointMetadata } from "@monkeytype/contracts/util/api"; -import * as Misc from "../../src/utils/misc"; +import { Response } from "express"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TsRestRequestWithContext } from "../../src/api/types"; import * as AdminUids from "../../src/dal/admin-uids"; import * as UserDal from "../../src/dal/user"; -import MonkeyError from "../../src/utils/error"; import { DecodedToken } from "../../src/middlewares/auth"; -import { TsRestRequest } from "../../src/api/types"; +import { verifyPermissions } from "../../src/middlewares/permission"; +import MonkeyError from "../../src/utils/error"; +import * as Misc from "../../src/utils/misc"; import { enableMonkeyErrorExpects } from "../__testData__/monkey-error"; enableMonkeyErrorExpects(); @@ -333,6 +333,9 @@ describe("permission middleware", () => { function givenRequest( metadata: EndpointMetadata, decodedToken?: Partial, -): TsRestRequest { - return { tsRestRoute: { metadata }, ctx: { decodedToken } } as any; +): TsRestRequestWithContext { + return { + tsRestRoute: { metadata }, + ctx: { decodedToken }, + } as TsRestRequestWithContext; } diff --git a/backend/__tests__/setup-common-mocks.ts b/backend/__tests__/setup-common-mocks.ts index 9ae3a1d64a12..4ef4a35ee9b9 100644 --- a/backend/__tests__/setup-common-mocks.ts +++ b/backend/__tests__/setup-common-mocks.ts @@ -1,6 +1,6 @@ import { vi } from "vitest"; -export function setupCommonMocks() { +export function setupCommonMocks(): void { vi.mock("../src/utils/logger", () => ({ __esModule: true, default: { diff --git a/backend/__tests__/vitest.d.ts b/backend/__tests__/vitest.d.ts index 56ef7efd2e5c..0e985caa340b 100644 --- a/backend/__tests__/vitest.d.ts +++ b/backend/__tests__/vitest.d.ts @@ -1,3 +1,4 @@ +// oxlint-disable typescript/consistent-type-definitions import type { Assertion, AsymmetricMatchersContaining } from "vitest"; import type { Test as SuperTest } from "supertest"; import MonkeyError from "../src/utils/error"; @@ -9,7 +10,9 @@ type ExpectedRateLimit = { windowMs: number; }; interface RestRequestMatcher { - toBeRateLimited: (expected: ExpectedRateLimit) => RestRequestMatcher; + toBeRateLimited: ( + expected: ExpectedRateLimit, + ) => Promise>; } interface ThrowMatcher { toMatchMonkeyError: (expected: { diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 3b1530aff37a..df2e8570c4a4 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -12,14 +12,13 @@ import { } from "../utils/prometheus"; import crypto from "crypto"; import { performance } from "perf_hooks"; -import { TsRestRequestHandler } from "@ts-rest/express"; import { AppRoute, AppRouter } from "@ts-rest/core"; import { EndpointMetadata, RequestAuthenticationOptions, } from "@monkeytype/contracts/util/api"; import { Configuration } from "@monkeytype/schemas/configuration"; -import { getMetadata } from "./utility"; +import { AsyncTsRestRequestHandler, getMetadata } from "./utility"; import { TsRestRequestWithContext } from "../api/types"; export type DecodedToken = { @@ -43,7 +42,7 @@ const DEFAULT_OPTIONS: RequestAuthenticationOptions = { */ export function authenticateTsRestRequest< T extends AppRouter | AppRoute, ->(): TsRestRequestHandler { +>(): AsyncTsRestRequestHandler { return async ( req: TsRestRequestWithContext, _res: Response, diff --git a/backend/src/middlewares/permission.ts b/backend/src/middlewares/permission.ts index ec1fad970090..9eb8f7f17dd7 100644 --- a/backend/src/middlewares/permission.ts +++ b/backend/src/middlewares/permission.ts @@ -2,14 +2,13 @@ import MonkeyError from "../utils/error"; import type { Response, NextFunction } from "express"; import { DBUser, getPartialUser } from "../dal/user"; import { isAdmin } from "../dal/admin-uids"; -import { TsRestRequestHandler } from "@ts-rest/express"; import { EndpointMetadata, RequestAuthenticationOptions, PermissionId, } from "@monkeytype/contracts/util/api"; import { isDevEnvironment } from "../utils/misc"; -import { getMetadata } from "./utility"; +import { AsyncTsRestRequestHandler, getMetadata } from "./utility"; import { TsRestRequestWithContext } from "../api/types"; import { DecodedToken } from "./auth"; import { AppRoute, AppRouter } from "@ts-rest/core"; @@ -73,7 +72,7 @@ const permissionChecks: Record = { export function verifyPermissions< T extends AppRouter | AppRoute, ->(): TsRestRequestHandler { +>(): AsyncTsRestRequestHandler { return async ( req: TsRestRequestWithContext, _res: Response, diff --git a/backend/src/middlewares/utility.ts b/backend/src/middlewares/utility.ts index ceb5b4b6c674..3e20e407193e 100644 --- a/backend/src/middlewares/utility.ts +++ b/backend/src/middlewares/utility.ts @@ -1,10 +1,15 @@ -import type { Request, Response, NextFunction, RequestHandler } from "express"; -import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus"; -import { isDevEnvironment } from "../utils/misc"; -import MonkeyError from "../utils/error"; import { EndpointMetadata } from "@monkeytype/contracts/util/api"; +import { AppRoute, AppRouter } from "@ts-rest/core"; +import { TsRestRequestHandler } from "@ts-rest/express"; +import type { NextFunction, Request, RequestHandler, Response } from "express"; import { TsRestRequestWithContext } from "../api/types"; +import MonkeyError from "../utils/error"; +import { isDevEnvironment } from "../utils/misc"; +import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus"; +export type AsyncTsRestRequestHandler = ( + ...args: Parameters> +) => Promise; /** * record the client version from the `x-client-version` or ` client-version` header to prometheus */ diff --git a/frontend/__tests__/__harness__/mock-dom.ts b/frontend/__tests__/__harness__/mock-dom.ts index e730ea4ed1ff..13ff89310041 100644 --- a/frontend/__tests__/__harness__/mock-dom.ts +++ b/frontend/__tests__/__harness__/mock-dom.ts @@ -55,10 +55,10 @@ vi.mock("../../src/ts/utils/dom", async (importOriginal) => { }; }; - const actual = await importOriginal(); + const actual = (await importOriginal()) as any; + // oxlint-disable-next-line typescript/no-unsafe-return return { - //@ts-expect-error - mocking private method ...actual, qsr: vi.fn().mockImplementation(() => createMockElement()), qs: vi.fn().mockImplementation(() => createMockElement()), @@ -67,7 +67,7 @@ vi.mock("../../src/ts/utils/dom", async (importOriginal) => { }); // Mock document.querySelector to return a div -// oxlint-disable-next-line no-deprecated -global.document.querySelector = vi +// oxlint-disable-next-line typescript/no-deprecated +globalThis.document.querySelector = vi .fn() .mockReturnValue(document.createElement("div")); diff --git a/frontend/__tests__/commandline/util.spec.ts b/frontend/__tests__/commandline/util.spec.ts index 5f209a1bdefa..fe3d9cde0808 100644 --- a/frontend/__tests__/commandline/util.spec.ts +++ b/frontend/__tests__/commandline/util.spec.ts @@ -7,6 +7,7 @@ import type { CommandlineConfigMetadata } from "../../src/ts/commandline/command import type { ConfigKey } from "@monkeytype/schemas/configs"; import type { ConfigMetadata } from "../../src/ts/config/metadata"; import { z, ZodSchema } from "zod"; +import { Command } from "../../src/ts/commandline/types"; const buildCommandForConfigKey = Util.__testing._buildCommandForConfigKey; @@ -113,7 +114,9 @@ describe("CommandlineUtils", () => { it("sets available", () => { //GIVEN const schema = z.boolean(); - const isAvailable = (val: any) => (val ? () => true : undefined); + const isAvailable = (val: any): (() => true) | undefined => + // oxlint-disable-next-line typescript/strict-boolean-expressions + val ? () => true : undefined; //WHEN const cmd = buildCommand({ @@ -135,7 +138,7 @@ describe("CommandlineUtils", () => { describe("type subgroupWithInput", () => { it("uses commandValues for number schema", () => { //GIVEN - const afterExec = () => "test"; + const afterExec = (): string => "test"; const schema = z.number().int(); //WHEN @@ -189,7 +192,7 @@ describe("CommandlineUtils", () => { describe("type input", () => { it("has basic properties", () => { //GIVEN - const afterExec = () => "test"; + const afterExec = (): string => "test"; const schema = z.string(); //WHEN const cmd = buildCommand({ @@ -363,7 +366,7 @@ describe("CommandlineUtils", () => { it("uses validation with isValid", () => { //GIVEN const schema = z.enum(["on", "off"]); - const isValid = (_val: any): Promise => + const isValid = async (_val: any): Promise => Promise.resolve("error"); //WHEN @@ -413,7 +416,7 @@ function buildCommand({ configMeta?: Partial>; schema?: ZodSchema; key?: K; -}) { +}): Command { return buildCommandForConfigKey( key ?? ("" as any), configMeta ?? ({} as any), diff --git a/frontend/__tests__/components/common/anime/Anime.spec.tsx b/frontend/__tests__/components/common/anime/Anime.spec.tsx index f95442b884d2..a9218376eef9 100644 --- a/frontend/__tests__/components/common/anime/Anime.spec.tsx +++ b/frontend/__tests__/components/common/anime/Anime.spec.tsx @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { mockAnimate } = vi.hoisted(() => ({ mockAnimate: vi.fn().mockReturnValue({ pause: vi.fn(), - then: vi.fn((_cb: unknown) => Promise.resolve()), + then: vi.fn(async (_cb: unknown) => Promise.resolve()), }), })); diff --git a/frontend/__tests__/components/common/anime/AnimeShow.spec.tsx b/frontend/__tests__/components/common/anime/AnimeShow.spec.tsx index ef33c93522b8..33a12d21a689 100644 --- a/frontend/__tests__/components/common/anime/AnimeShow.spec.tsx +++ b/frontend/__tests__/components/common/anime/AnimeShow.spec.tsx @@ -7,7 +7,7 @@ const { mockAnimate } = vi.hoisted(() => ({ const callbacks: Array<() => void> = []; const animation = { pause: vi.fn(), - then: vi.fn((cb: () => void) => { + then: vi.fn(async (cb: () => void) => { callbacks.push(cb); // Invoke immediately so exit animations complete synchronously in tests cb(); diff --git a/frontend/__tests__/components/common/anime/AnimeSwitch.spec.tsx b/frontend/__tests__/components/common/anime/AnimeSwitch.spec.tsx index 0c0eb51c4b6e..bbc93e7b1f57 100644 --- a/frontend/__tests__/components/common/anime/AnimeSwitch.spec.tsx +++ b/frontend/__tests__/components/common/anime/AnimeSwitch.spec.tsx @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { mockAnimate } = vi.hoisted(() => ({ mockAnimate: vi.fn().mockImplementation(() => ({ pause: vi.fn(), - then: vi.fn((cb: () => void) => { + then: vi.fn(async (cb: () => void) => { cb(); return Promise.resolve(); }), diff --git a/frontend/__tests__/components/core/Theme.spec.tsx b/frontend/__tests__/components/core/Theme.spec.tsx index 35a2f5ec604e..1ba1c9f3f9e8 100644 --- a/frontend/__tests__/components/core/Theme.spec.tsx +++ b/frontend/__tests__/components/core/Theme.spec.tsx @@ -83,12 +83,14 @@ describe("Theme component", () => { }); it("removes CSS when theme has no CSS", async () => { + // oxlint-disable-next-line typescript/no-unsafe-return themeSignalMock.mockImplementation(() => ({ name: "light" }) as any); const { css } = renderComponent(); expect(css).not.toBeInTheDocument(); }); it("removes CSS when theme is custom", async () => { + // oxlint-disable-next-line typescript/no-unsafe-return themeSignalMock.mockImplementation(() => ({ name: "custom" }) as any); const { css } = renderComponent(); expect(css).not.toBeInTheDocument(); diff --git a/frontend/__tests__/components/ui/form/Checkbox.spec.tsx b/frontend/__tests__/components/ui/form/Checkbox.spec.tsx index 71936a160875..75887dbd0f50 100644 --- a/frontend/__tests__/components/ui/form/Checkbox.spec.tsx +++ b/frontend/__tests__/components/ui/form/Checkbox.spec.tsx @@ -1,15 +1,16 @@ import { render, screen, fireEvent } from "@solidjs/testing-library"; +import { AnyFieldApi } from "@tanstack/solid-form"; import { describe, it, expect, vi } from "vitest"; import { Checkbox } from "../../../../src/ts/components/ui/form/Checkbox"; -function makeField(name: string, checked = false) { +function makeField(name: string, checked = false): AnyFieldApi { return { name, state: { value: checked }, handleBlur: vi.fn(), handleChange: vi.fn(), - } as any; + } as unknown as AnyFieldApi; } describe("Checkbox", () => { @@ -50,7 +51,7 @@ describe("Checkbox", () => { render(() => field} />); const input = screen.getByRole("checkbox", { hidden: true }); - await fireEvent.change(input, { target: { checked: true } }); + fireEvent.change(input, { target: { checked: true } }); expect(field.handleChange).toHaveBeenCalledWith(true); }); @@ -59,7 +60,7 @@ describe("Checkbox", () => { render(() => field} />); const input = screen.getByRole("checkbox", { hidden: true }); - await fireEvent.blur(input); + fireEvent.blur(input); expect(field.handleBlur).toHaveBeenCalled(); }); diff --git a/frontend/__tests__/components/ui/form/FieldIndicator.spec.tsx b/frontend/__tests__/components/ui/form/FieldIndicator.spec.tsx index 5a96d645125c..654156457cee 100644 --- a/frontend/__tests__/components/ui/form/FieldIndicator.spec.tsx +++ b/frontend/__tests__/components/ui/form/FieldIndicator.spec.tsx @@ -1,4 +1,5 @@ import { render } from "@solidjs/testing-library"; +import { AnyFieldApi } from "@tanstack/solid-form"; import { describe, it, expect } from "vitest"; import { FieldIndicator } from "../../../../src/ts/components/ui/form/FieldIndicator"; @@ -11,7 +12,7 @@ function makeField(overrides: { errors?: string[]; hasWarning?: boolean; warnings?: string[]; -}) { +}): AnyFieldApi { return { state: { meta: { @@ -26,7 +27,7 @@ function makeField(overrides: { hasWarning: overrides.hasWarning ?? false, warnings: overrides.warnings ?? [], }), - } as any; + } as unknown as AnyFieldApi; } describe("FieldIndicator", () => { diff --git a/frontend/__tests__/components/ui/form/InputField.spec.tsx b/frontend/__tests__/components/ui/form/InputField.spec.tsx index 2e27b11bb5bc..8b079abdcf3e 100644 --- a/frontend/__tests__/components/ui/form/InputField.spec.tsx +++ b/frontend/__tests__/components/ui/form/InputField.spec.tsx @@ -1,9 +1,10 @@ import { render, screen, fireEvent } from "@solidjs/testing-library"; +import { AnyFieldApi } from "@tanstack/solid-form"; import { describe, it, expect, vi } from "vitest"; import { InputField } from "../../../../src/ts/components/ui/form/InputField"; -function makeField(name: string, value = "") { +function makeField(name: string, value = ""): AnyFieldApi { return { name, state: { @@ -20,7 +21,7 @@ function makeField(name: string, value = "") { handleBlur: vi.fn(), handleChange: vi.fn(), getMeta: () => ({ hasWarning: false, warnings: [] }), - } as any; + } as unknown as AnyFieldApi; } describe("InputField", () => { @@ -54,7 +55,7 @@ describe("InputField", () => { const field = makeField("name"); render(() => field} />); - await fireEvent.input(screen.getByRole("textbox"), { + fireEvent.input(screen.getByRole("textbox"), { target: { value: "test" }, }); expect(field.handleChange).toHaveBeenCalledWith("test"); @@ -64,7 +65,7 @@ describe("InputField", () => { const field = makeField("name"); render(() => field} />); - await fireEvent.blur(screen.getByRole("textbox")); + fireEvent.blur(screen.getByRole("textbox")); expect(field.handleBlur).toHaveBeenCalled(); }); @@ -73,7 +74,7 @@ describe("InputField", () => { const onFocus = vi.fn(); render(() => field} onFocus={onFocus} />); - await fireEvent.focus(screen.getByRole("textbox")); + fireEvent.focus(screen.getByRole("textbox")); expect(onFocus).toHaveBeenCalled(); }); @@ -86,7 +87,7 @@ describe("InputField", () => { it("shows FieldIndicator", () => { const field = makeField("name"); - field.options = { validators: { onChange: () => undefined } }; + field.options = { validators: { onChange: () => undefined } } as any; field.state.meta.isValidating = true; const { container } = render(() => field} />); diff --git a/frontend/__tests__/components/ui/form/SubmitButton.spec.tsx b/frontend/__tests__/components/ui/form/SubmitButton.spec.tsx index 3ba0c243b5cf..7a6ad26dd330 100644 --- a/frontend/__tests__/components/ui/form/SubmitButton.spec.tsx +++ b/frontend/__tests__/components/ui/form/SubmitButton.spec.tsx @@ -5,11 +5,12 @@ import { describe, it, expect } from "vitest"; import { FormStateSlice, SubmitButton, + SubscribableForm, } from "../../../../src/ts/components/ui/form/SubmitButton"; type FormState = FormStateSlice; -function makeForm(state: Partial = {}) { +function makeForm(state: Partial = {}): SubscribableForm { const fullState: FormState = { canSubmit: true, isSubmitting: false, diff --git a/frontend/__tests__/components/ui/form/utils.spec.ts b/frontend/__tests__/components/ui/form/utils.spec.ts index 1b1447ca31ef..0b25a9182bc9 100644 --- a/frontend/__tests__/components/ui/form/utils.spec.ts +++ b/frontend/__tests__/components/ui/form/utils.spec.ts @@ -8,6 +8,7 @@ import { fieldMandatory, type ValidationResult, } from "../../../../src/ts/components/ui/form/utils"; +import { AnyFieldApi } from "@tanstack/solid-form"; describe("fromSchema", () => { const schema = z.string().min(3, "too short").max(10, "too long"); @@ -34,9 +35,9 @@ describe("fromSchema", () => { describe("handleResult", () => { const mockSetMeta = vi.fn(); - function makeField() { + function makeField(): AnyFieldApi { mockSetMeta.mockClear(); - return { setMeta: mockSetMeta } as any; + return { setMeta: mockSetMeta } as unknown as AnyFieldApi; } it("returns undefined for undefined results", () => { @@ -68,7 +69,9 @@ describe("handleResult", () => { expect(result).toBeUndefined(); expect(mockSetMeta).toHaveBeenCalledOnce(); + // oxlint-disable-next-line typescript/no-non-null-assertion const updater = mockSetMeta.mock.calls[0]![0]; + // oxlint-disable-next-line typescript/no-unsafe-call const newMeta = updater({ existing: true }); expect(newMeta).toEqual({ existing: true, diff --git a/frontend/__tests__/components/ui/table/DataTable.spec.tsx b/frontend/__tests__/components/ui/table/DataTable.spec.tsx index 7e42fafb22c0..53dee63ece05 100644 --- a/frontend/__tests__/components/ui/table/DataTable.spec.tsx +++ b/frontend/__tests__/components/ui/table/DataTable.spec.tsx @@ -33,6 +33,7 @@ const columns = [ id: "name", accessorKey: "name", header: "Name", + // oxlint-disable-next-line typescript/no-unsafe-return typescript/no-unsafe-call cell: (info: any) => info.getValue(), meta: { maxBreakpoint: "sm" }, }, @@ -40,6 +41,7 @@ const columns = [ id: "age", accessorKey: "age", header: "Age", + // oxlint-disable-next-line typescript/no-unsafe-return typescript/no-unsafe-call cell: (info: any) => info.getValue(), meta: { breakpoint: "sm" }, }, @@ -95,7 +97,7 @@ describe("DataTable", () => { expect(ageHeaderCell?.querySelector("i")).toHaveClass("fa-fw"); // Descending - await fireEvent.click(ageHeaderButton); + fireEvent.click(ageHeaderButton); expect(ageHeaderCell).toHaveAttribute("aria-sort", "descending"); expect(ageHeaderCell?.querySelector("i")).toHaveClass( "fa-sort-down", @@ -114,7 +116,7 @@ describe("DataTable", () => { expect(rows[2]).toHaveTextContent("Bob"); // age 20 // Ascending - await fireEvent.click(ageHeaderButton); + fireEvent.click(ageHeaderButton); expect(ageHeaderCell).toHaveAttribute("aria-sort", "ascending"); expect(ageHeaderCell?.querySelector("i")).toHaveClass( "fa-sort-up", @@ -133,7 +135,7 @@ describe("DataTable", () => { expect(rows[2]).toHaveTextContent("Alice"); //back to initial - await fireEvent.click(ageHeaderButton); + fireEvent.click(ageHeaderButton); expect(ageHeaderCell).toHaveAttribute("aria-sort", "none"); expect(localStorage()).toEqual([]); }); diff --git a/frontend/__tests__/elements/test-activity-calendar.spec.ts b/frontend/__tests__/elements/test-activity-calendar.spec.ts index f68e51566f1b..9e2e630a8fc0 100644 --- a/frontend/__tests__/elements/test-activity-calendar.spec.ts +++ b/frontend/__tests__/elements/test-activity-calendar.spec.ts @@ -1238,11 +1238,11 @@ expect.extend({ }; }, toHaveTests(received: TestActivityDay, expected: number): MatcherResult { - const expectedLabel = `${expected} ${expected == 1 ? "test" : "tests"}`; + const expectedLabel = `${expected} ${expected === 1 ? "test" : "tests"}`; const actual = received.label?.substring(0, received.label.indexOf(" on")); return { - pass: actual == expectedLabel, + pass: actual === expectedLabel, message: () => `Tests ${actual} is not ${expectedLabel}`, actual: actual, expected: expectedLabel, diff --git a/frontend/__tests__/input/helpers/fail-or-finish.spec.ts b/frontend/__tests__/input/helpers/fail-or-finish.spec.ts index 2893e07d0dcb..0d9faaaaf21f 100644 --- a/frontend/__tests__/input/helpers/fail-or-finish.spec.ts +++ b/frontend/__tests__/input/helpers/fail-or-finish.spec.ts @@ -36,7 +36,9 @@ describe("checkIfFailedDueToMinBurst", () => { mode: "time", minBurstCustomSpeed: 100, }); + // oxlint-disable-next-line typescript/no-unsafe-call (Misc.whorf as any).mockReturnValue(0); + // oxlint-disable-next-line typescript/no-unsafe-call (TestLogic.areAllTestWordsGenerated as any).mockReturnValue(true); }); @@ -86,6 +88,7 @@ describe("checkIfFailedDueToMinBurst", () => { ])("$desc", ({ config, lastBurst, whorfRet, expected }) => { replaceConfig(config as any); if (whorfRet !== undefined) { + // oxlint-disable-next-line typescript/no-unsafe-call (Misc.whorf as any).mockReturnValue(whorfRet); } @@ -265,7 +268,9 @@ describe("checkIfFinished", () => { quickEnd: false, stopOnError: "off", }); + // oxlint-disable-next-line typescript/no-unsafe-call (Strings.isSpace as any).mockReturnValue(false); + // oxlint-disable-next-line typescript/no-unsafe-call (TestLogic.areAllTestWordsGenerated as any).mockReturnValue(true); }); diff --git a/frontend/__tests__/input/helpers/validation.spec.ts b/frontend/__tests__/input/helpers/validation.spec.ts index 94b3b2416230..59c7bf5a145f 100644 --- a/frontend/__tests__/input/helpers/validation.spec.ts +++ b/frontend/__tests__/input/helpers/validation.spec.ts @@ -35,9 +35,11 @@ describe("isCharCorrect", () => { difficulty: "normal", strictSpace: false, }); + // oxlint-disable-next-line typescript/no-unsafe-call (FunboxList.findSingleActiveFunboxWithFunction as any).mockReturnValue( null, ); + // oxlint-disable-next-line typescript/no-unsafe-call (Strings.areCharactersVisuallyEqual as any).mockReturnValue(false); }); diff --git a/frontend/__tests__/stores/notifications.spec.ts b/frontend/__tests__/stores/notifications.spec.ts index aaf9f8176cef..0f05b6f5276b 100644 --- a/frontend/__tests__/stores/notifications.spec.ts +++ b/frontend/__tests__/stores/notifications.spec.ts @@ -31,8 +31,8 @@ describe("notifications store", () => { const notifications = getNotifications(); expect(notifications).toHaveLength(1); - expect(notifications[0]!.message).toBe("test message"); - expect(notifications[0]!.level).toBe("notice"); + expect(notifications[0]?.message).toBe("test message"); + expect(notifications[0]?.level).toBe("notice"); }); it("prepends new notifications", () => { @@ -41,27 +41,27 @@ describe("notifications store", () => { const notifications = getNotifications(); expect(notifications).toHaveLength(2); - expect(notifications[0]!.message).toBe("second"); - expect(notifications[1]!.message).toBe("first"); + expect(notifications[0]?.message).toBe("second"); + expect(notifications[1]?.message).toBe("first"); }); it("defaults error duration to 0 (sticky)", () => { addNotificationWithLevel("error msg", "error"); - expect(getNotifications()[0]!.durationMs).toBe(0); + expect(getNotifications()[0]?.durationMs).toBe(0); }); it("defaults non-error duration to 3000", () => { addNotificationWithLevel("notice msg", "notice"); - expect(getNotifications()[0]!.durationMs).toBe(3000); + expect(getNotifications()[0]?.durationMs).toBe(3000); addNotificationWithLevel("success msg", "success"); - expect(getNotifications()[0]!.durationMs).toBe(3000); + expect(getNotifications()[0]?.durationMs).toBe(3000); }); it("respects custom durationMs", () => { addNotificationWithLevel("msg", "notice", { durationMs: 5000 }); - expect(getNotifications()[0]!.durationMs).toBe(5000); + expect(getNotifications()[0]?.durationMs).toBe(5000); }); it("appends response body message", () => { @@ -71,7 +71,7 @@ describe("notifications store", () => { } as AddNotificationOptions["response"]; addNotificationWithLevel("Request failed", "error", { response }); - expect(getNotifications()[0]!.message).toBe( + expect(getNotifications()[0]?.message).toBe( "Request failed: Bad request", ); }); @@ -80,19 +80,19 @@ describe("notifications store", () => { addNotificationWithLevel("Something broke", "error", { error: new Error("underlying cause"), }); - expect(getNotifications()[0]!.message).toContain("underlying cause"); + expect(getNotifications()[0]?.message).toContain("underlying cause"); }); it("sets useInnerHtml when specified", () => { addNotificationWithLevel("html bold", "notice", { useInnerHtml: true, }); - expect(getNotifications()[0]!.useInnerHtml).toBe(true); + expect(getNotifications()[0]?.useInnerHtml).toBe(true); }); it("defaults useInnerHtml to false", () => { addNotificationWithLevel("plain", "notice"); - expect(getNotifications()[0]!.useInnerHtml).toBe(false); + expect(getNotifications()[0]?.useInnerHtml).toBe(false); }); it("sets customTitle and customIcon", () => { @@ -100,9 +100,9 @@ describe("notifications store", () => { customTitle: "Custom", customIcon: "gift", }); - const n = getNotifications()[0]!; - expect(n.customTitle).toBe("Custom"); - expect(n.customIcon).toBe("gift"); + const n = getNotifications()[0]; + expect(n?.customTitle).toBe("Custom"); + expect(n?.customIcon).toBe("gift"); }); }); @@ -120,7 +120,7 @@ describe("notifications store", () => { it("does not auto-remove sticky notifications (durationMs 0)", () => { addNotificationWithLevel("sticky", "error"); - expect(getNotifications()[0]!.durationMs).toBe(0); + expect(getNotifications()[0]?.durationMs).toBe(0); vi.advanceTimersByTime(60000); expect(getNotifications()).toHaveLength(1); @@ -140,21 +140,24 @@ describe("notifications store", () => { describe("removeNotification", () => { it("removes a specific notification by id", () => { - addNotificationWithLevel("first", "notice", { durationMs: 0 }); - addNotificationWithLevel("second", "notice", { durationMs: 0 }); + const id = addNotificationWithLevel("first", "notice", { durationMs: 0 }); + addNotificationWithLevel("second", "notice", { + durationMs: 0, + }); - const id = getNotifications()[1]!.id; removeNotification(id); expect(getNotifications()).toHaveLength(1); - expect(getNotifications()[0]!.message).toBe("second"); + expect(getNotifications()[0]?.message).toBe("second"); }); it("calls onDismiss with 'click' by default", () => { const onDismiss = vi.fn(); - addNotificationWithLevel("msg", "notice", { durationMs: 0, onDismiss }); + const id = addNotificationWithLevel("msg", "notice", { + durationMs: 0, + onDismiss, + }); - const id = getNotifications()[0]!.id; removeNotification(id); expect(onDismiss).toHaveBeenCalledWith("click"); @@ -162,12 +165,11 @@ describe("notifications store", () => { it("cancels auto-remove timer on manual removal", () => { const onDismiss = vi.fn(); - addNotificationWithLevel("msg", "notice", { + const id = addNotificationWithLevel("msg", "notice", { durationMs: 5000, onDismiss, }); - const id = getNotifications()[0]!.id; removeNotification(id); vi.advanceTimersByTime(10000); @@ -253,17 +255,17 @@ describe("notifications store", () => { describe("convenience functions", () => { it("showNoticeNotification adds notice level", () => { showNoticeNotification("notice msg"); - expect(getNotifications()[0]!.level).toBe("notice"); + expect(getNotifications()[0]?.level).toBe("notice"); }); it("showSuccessNotification adds success level", () => { showSuccessNotification("success msg"); - expect(getNotifications()[0]!.level).toBe("success"); + expect(getNotifications()[0]?.level).toBe("success"); }); it("showErrorNotification adds error level", () => { showErrorNotification("error msg"); - expect(getNotifications()[0]!.level).toBe("error"); + expect(getNotifications()[0]?.level).toBe("error"); }); }); @@ -273,9 +275,9 @@ describe("notifications store", () => { const history = getNotificationHistory(); expect(history).toHaveLength(1); - expect(history[0]!.message).toBe("msg"); - expect(history[0]!.level).toBe("success"); - expect(history[0]!.title).toBe("Success"); + expect(history[0]?.message).toBe("msg"); + expect(history[0]?.level).toBe("success"); + expect(history[0]?.title).toBe("Success"); }); it("uses correct default titles", () => { @@ -284,21 +286,21 @@ describe("notifications store", () => { addNotificationWithLevel("c", "notice"); const history = getNotificationHistory(); - expect(history[0]!.title).toBe("Success"); - expect(history[1]!.title).toBe("Error"); - expect(history[2]!.title).toBe("Notice"); + expect(history[0]?.title).toBe("Success"); + expect(history[1]?.title).toBe("Error"); + expect(history[2]?.title).toBe("Notice"); }); it("uses customTitle in history when provided", () => { addNotificationWithLevel("msg", "notice", { customTitle: "Reward" }); - expect(getNotificationHistory()[0]!.title).toBe("Reward"); + expect(getNotificationHistory()[0]?.title).toBe("Reward"); }); it("stores details in history", () => { addNotificationWithLevel("msg", "notice", { details: { foo: "bar" }, }); - expect(getNotificationHistory()[0]!.details).toEqual({ foo: "bar" }); + expect(getNotificationHistory()[0]?.details).toEqual({ foo: "bar" }); }); it("caps history at 25 entries", () => { @@ -308,8 +310,8 @@ describe("notifications store", () => { const history = getNotificationHistory(); expect(history).toHaveLength(25); - expect(history[0]!.message).toBe("msg 5"); - expect(history[24]!.message).toBe("msg 29"); + expect(history[0]?.message).toBe("msg 5"); + expect(history[24]?.message).toBe("msg 29"); }); it("is not affected by clearAllNotifications", () => { diff --git a/frontend/__tests__/test/events/data.spec.ts b/frontend/__tests__/test/events/data.spec.ts index 99c6f336d7b6..48fe43d48423 100644 --- a/frontend/__tests__/test/events/data.spec.ts +++ b/frontend/__tests__/test/events/data.spec.ts @@ -78,9 +78,9 @@ describe("data.ts", () => { const events = getAllTestEvents(); expect(events).toHaveLength(3); - expect(events[0]!.type).toBe("timer"); - expect(events[1]!.type).toBe("keydown"); - expect(events[2]!.type).toBe("input"); + expect(events[0]?.type).toBe("timer"); + expect(events[1]?.type).toBe("keydown"); + expect(events[2]?.type).toBe("input"); }); it("input events with the same ms as timer end are kept", () => { @@ -117,9 +117,9 @@ describe("data.ts", () => { logTestEvent("keydown", 1020, keyDown()); const events = getAllTestEvents(); expect(events).toHaveLength(3); - expect(events[0]!.type).toBe("keydown"); - expect(events[1]!.type).toBe("keyup"); - expect(events[2]!.type).toBe("keydown"); + expect(events[0]?.type).toBe("keydown"); + expect(events[1]?.type).toBe("keyup"); + expect(events[2]?.type).toBe("keydown"); }); it("allows keydown after keyup", () => { @@ -157,8 +157,8 @@ describe("data.ts", () => { const events = getAllTestEvents(); expect(events).toHaveLength(2); - expect(events[0]!.type).toBe("keydown"); - expect(events[1]!.type).toBe("keyup"); + expect(events[0]?.type).toBe("keydown"); + expect(events[1]?.type).toBe("keyup"); }); it("stores indexed code on keydown events", () => { @@ -166,8 +166,8 @@ describe("data.ts", () => { logTestEvent("keydown", 1020, keyDown("NoCode")); const events = getAllTestEvents() as KeydownEvent[]; - expect(events[0]!.data.code).toBe("NoCode0"); - expect(events[1]!.data.code).toBe("NoCode1"); + expect(events[0]?.data.code).toBe("NoCode0"); + expect(events[1]?.data.code).toBe("NoCode1"); }); it("stores matching indexed code on keyup events", () => { @@ -280,7 +280,7 @@ describe("data.ts", () => { const perWord = getInputEventsPerWord(50); expect(perWord.get(0)).toHaveLength(1); - expect(perWord.get(0)![0]!.data.charIndex).toBe(1); + expect(perWord.get(0)?.[0]?.data.charIndex).toBe(1); }); }); @@ -439,11 +439,7 @@ describe("data.ts", () => { const inputs = events.filter((e) => e.type === "input"); expect(inputs).toHaveLength(0); expect( - events.filter( - (e) => - e.type === "keydown" && - (e.data as KeydownEventData).code === "KeyD", - ), + events.filter((e) => e.type === "keydown" && e.data.code === "KeyD"), ).toHaveLength(0); }); }); diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts index b8b5273b2fe4..c06d81082843 100644 --- a/frontend/__tests__/test/events/stats.spec.ts +++ b/frontend/__tests__/test/events/stats.spec.ts @@ -1007,7 +1007,7 @@ describe("stats.ts", () => { ); // avg duration = (80+120)/2 = 100, so keyup at 1400+100 = 1500, testMs = 1500 - 1000 = 500 expect(keyup).toBeDefined(); - expect(keyup!.testMs).toBe(500); + expect(keyup?.testMs).toBe(500); }); it("uses default 80ms when no completed key durations exist", () => { @@ -1021,7 +1021,7 @@ describe("stats.ts", () => { (e) => e.type === "keyup" && e.data.code === "KeyA", ); expect(keyup).toBeDefined(); - expect(keyup!.testMs).toBe(280); + expect(keyup?.testMs).toBe(280); }); it("does nothing when no keys are pressed", () => { diff --git a/frontend/__tests__/utils/date-and-time.spec.ts b/frontend/__tests__/utils/date-and-time.spec.ts index 79b87054dc80..ee125eaac792 100644 --- a/frontend/__tests__/utils/date-and-time.spec.ts +++ b/frontend/__tests__/utils/date-and-time.spec.ts @@ -35,7 +35,7 @@ describe("date-and-time", () => { //GIVEN languageMock.mockReturnValue(locale); localeMock.mockImplementation(function (this: any) { - return { weekInfo: { firstDay: firstDayOfWeek } } as any; + return { weekInfo: { firstDay: firstDayOfWeek } }; }); //WHEN/THEN @@ -47,7 +47,7 @@ describe("date-and-time", () => { it("with getWeekInfo on monday", () => { languageMock.mockReturnValue("en-US"); localeMock.mockImplementationOnce(function (this: any) { - return { getWeekInfo: () => ({ firstDay: 1 }) } as any; + return { getWeekInfo: () => ({ firstDay: 1 }) }; }); //WHEN/THEN @@ -56,7 +56,7 @@ describe("date-and-time", () => { it("with getWeekInfo on sunday", () => { languageMock.mockReturnValue("en-US"); localeMock.mockImplementationOnce(function (this: any) { - return { getWeekInfo: () => ({ firstDay: 7 }) } as any; + return { getWeekInfo: () => ({ firstDay: 7 }) }; }); //WHEN/THEN @@ -67,7 +67,7 @@ describe("date-and-time", () => { describe("without weekInfo (firefox)", () => { beforeEach(() => { localeMock.mockImplementation(function (this: any) { - return {} as any; + return {}; }); }); diff --git a/frontend/__tests__/utils/dom.jsdom-spec.ts b/frontend/__tests__/utils/dom.jsdom-spec.ts index 2f2797912172..6e41d9ecd667 100644 --- a/frontend/__tests__/utils/dom.jsdom-spec.ts +++ b/frontend/__tests__/utils/dom.jsdom-spec.ts @@ -23,6 +23,7 @@ describe("dom", () => { ): void { const parent = options?.parent ?? qsr("#parent"); parent?.onChild(event, selector, (e) => + // oxlint-disable-next-line typescript/no-unsafe-return handler({ target: e.target, childTarget: e.childTarget, diff --git a/frontend/__tests__/utils/local-storage-with-schema.spec.ts b/frontend/__tests__/utils/local-storage-with-schema.spec.ts index abe21555d87a..4782e723c34f 100644 --- a/frontend/__tests__/utils/local-storage-with-schema.spec.ts +++ b/frontend/__tests__/utils/local-storage-with-schema.spec.ts @@ -183,6 +183,7 @@ describe("local-storage-with-schema.ts", () => { fontSize: 1, }; + // oxlint-disable-next-line typescript/no-unsafe-return const migrateFnMock = vi.fn(() => migrated as any); const ls = new LocalStorageWithSchema({ @@ -271,6 +272,7 @@ describe("local-storage-with-schema.ts", () => { fontSize: 1, }; + // oxlint-disable-next-line typescript/no-unsafe-return const migrateFnMock = vi.fn(() => invalidMigrated as any); const ls = new LocalStorageWithSchema({ diff --git a/frontend/__tests__/utils/numbers.spec.ts b/frontend/__tests__/utils/numbers.spec.ts index 11cc400ca594..1741300bafa2 100644 --- a/frontend/__tests__/utils/numbers.spec.ts +++ b/frontend/__tests__/utils/numbers.spec.ts @@ -55,11 +55,11 @@ describe("numbers", () => { }); it("should return undefined when given null", () => { - expect(Numbers.parseIntOptional(null)).toBeUndefined(); + expect(Numbers.parseIntOptional(null as any)).toBeUndefined(); }); it("should return undefined when given undefined", () => { - expect(Numbers.parseIntOptional(undefined)).toBeUndefined(); + expect(Numbers.parseIntOptional(undefined as any)).toBeUndefined(); }); it("should handle non-numeric strings", () => { diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 293cd57c01bc..3d25abd070e2 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -345,20 +345,14 @@ describe("string utils", () => { }); describe("caching", () => { - let mapGetSpy: ReturnType; - let mapSetSpy: ReturnType; - let mapClearSpy: ReturnType; - - beforeEach(() => { - mapGetSpy = vi.spyOn(Map.prototype, "get"); - mapSetSpy = vi.spyOn(Map.prototype, "set"); - mapClearSpy = vi.spyOn(Map.prototype, "clear"); - }); + const mapGetSpy = vi.spyOn(Map.prototype, "get"); + const mapSetSpy = vi.spyOn(Map.prototype, "set"); + const mapClearSpy = vi.spyOn(Map.prototype, "clear"); afterEach(() => { - mapGetSpy.mockRestore(); - mapSetSpy.mockRestore(); - mapClearSpy.mockRestore(); + mapGetSpy.mockReset(); + mapSetSpy.mockReset(); + mapClearSpy.mockReset(); }); it("should use cache for repeated calls", () => { @@ -816,6 +810,22 @@ describe("string utils", () => { missed: 0, }, }, + { + description: "incorrect, last word, early space", + input: { + inputWord: "he ", + targetWord: "hello", + lastWord: true, + shouldLastPartialWordCount: false, + }, + expected: { + allCorrect: 2, + correctWord: 0, + incorrect: 0, + extra: 0, + missed: 3, + }, + }, { description: "incorrect, last word, noquick end", input: { @@ -828,7 +838,7 @@ describe("string utils", () => { allCorrect: 4, correctWord: 0, incorrect: 1, - extra: 1, + extra: 0, missed: 0, }, }, diff --git a/frontend/__tests__/utils/zod.spec.ts b/frontend/__tests__/utils/zod.spec.ts new file mode 100644 index 000000000000..c6bbfe12bdb1 --- /dev/null +++ b/frontend/__tests__/utils/zod.spec.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from "vitest"; +import { z, ZodString, ZodNumber } from "zod"; +import { unwrapSchema } from "../../src/ts/utils/zod"; + +describe("unwrapSchema", () => { + it("unwraps optional", () => { + const schema = z.string().optional(); + const unwrapped = unwrapSchema(schema); + + expect(unwrapped).toBeInstanceOf(ZodString); + }); + + it("unwraps default", () => { + const schema = z.string().default("hello"); + const unwrapped = unwrapSchema(schema); + + expect(unwrapped).toBeInstanceOf(ZodString); + }); + + it("unwraps nullable", () => { + const schema = z.number().nullable(); + const unwrapped = unwrapSchema(schema); + + expect(unwrapped).toBeInstanceOf(ZodNumber); + }); + + it("unwraps branded", () => { + const schema = z.string().brand("UserId"); + const unwrapped = unwrapSchema(schema); + + expect(unwrapped).toBeInstanceOf(ZodString); + }); + + it("unwraps effects", () => { + const schema = z.string().transform((v) => v.toUpperCase()); + const unwrapped = unwrapSchema(schema); + + expect(unwrapped).toBeInstanceOf(ZodString); + }); + + it("unwraps multiple nested wrappers", () => { + const schema = z + .string() + .brand("X") + .optional() + .nullable() + .default("test") + .transform((v) => v); + + const unwrapped = unwrapSchema(schema); + + expect(unwrapped).toBeInstanceOf(ZodString); + }); + + it("returns the same schema if no wrappers exist", () => { + const schema = z.string(); + const unwrapped = unwrapSchema(schema); + + expect(unwrapped).toBe(schema); + }); +}); diff --git a/frontend/__tests__/vitest.d.ts b/frontend/__tests__/vitest.d.ts index 2bff202469b9..89d2aae79668 100644 --- a/frontend/__tests__/vitest.d.ts +++ b/frontend/__tests__/vitest.d.ts @@ -1,3 +1,4 @@ +// oxlint-disable typescript/consistent-type-definitions import type { Assertion, AsymmetricMatchersContaining } from "vitest"; import { TestActivityDay } from "../src/ts/elements/test-activity-calendar"; @@ -12,7 +13,9 @@ interface ActivityDayMatchers { import "@testing-library/jest-dom"; declare module "vitest" { + // oxlint-disable-next-line typescript/no-empty-object-type interface Assertion extends ActivityDayMatchers {} + // oxlint-disable-next-line typescript/no-empty-object-type interface AsymmetricMatchersContaining extends ActivityDayMatchers {} } diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html index 216026bc2132..736ca9f527b9 100644 --- a/frontend/src/html/popups.html +++ b/frontend/src/html/popups.html @@ -45,10 +45,6 @@ - -