From 8a41ccee970edfbed34013cd3eb522c1d5ad346f Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 21 Feb 2025 16:52:20 +0100 Subject: [PATCH] fix: past leaderboard not fetching the users rank (@fehmer) (#6289) Show the users ranking for the last day on the daily and for the last week on the weekly leaderboard correctly. - Fix request query schema for the [daily rank](https://api.monkeytype.com/docs/internal#tag/leaderboards/operation/leaderboards.getDailyRank) having pagination - Fix request query schema for the [weekly rank](https://api.monkeytype.com/docs/internal#tag/leaderboards/operation/leaderboards.getWeeklyXpRank) missing the `weeksBefore` parameter - Fix frontend to include the `daysBefore` or `weeksBefore` parameter on `rank` calls --------- Co-authored-by: Miodec --- .../api/controllers/leaderboard.spec.ts | 78 +++++++++++++++++-- backend/src/api/controllers/leaderboard.ts | 9 ++- frontend/src/ts/pages/leaderboards.ts | 26 ++++--- packages/contracts/src/leaderboards.ts | 28 +++++-- 4 files changed, 113 insertions(+), 28 deletions(-) diff --git a/backend/__tests__/api/controllers/leaderboard.spec.ts b/backend/__tests__/api/controllers/leaderboard.spec.ts index 54a8ad7f5..46a612f87 100644 --- a/backend/__tests__/api/controllers/leaderboard.spec.ts +++ b/backend/__tests__/api/controllers/leaderboard.spec.ts @@ -1067,6 +1067,8 @@ describe("Loaderboard Controller", () => { beforeEach(async () => { getXpWeeklyLeaderboardMock.mockReset(); await weeklyLeaderboardEnabled(true); + vi.useFakeTimers(); + vi.setSystemTime(1722606812000); }); it("fails withouth authentication", async () => { @@ -1109,6 +1111,47 @@ describe("Loaderboard Controller", () => { expect(getRankMock).toHaveBeenCalledWith(uid, lbConf); }); + + it("should get for last week", async () => { + //GIVEN + const lbConf = (await configuration).leaderboards.weeklyXp; + + const resultData: XpLeaderboardEntry = { + totalXp: 100, + rank: 1, + timeTypedSeconds: 100, + uid: "user1", + name: "user1", + discordId: "discordId", + discordAvatar: "discordAvatar", + lastActivityTimestamp: 1000, + }; + const getRankMock = vi.fn(); + getRankMock.mockResolvedValue(resultData); + getXpWeeklyLeaderboardMock.mockReturnValue({ + getRank: getRankMock, + } as any); + + //WHEN + const { body } = await mockApp + .get("/leaderboards/xp/weekly/rank") + .query({ weeksBefore: 1 }) + .set("authorization", `Uid ${uid}`) + .expect(200); + + //THEN + expect(body).toEqual({ + message: "Weekly xp leaderboard rank retrieved", + data: resultData, + }); + + expect(getXpWeeklyLeaderboardMock).toHaveBeenCalledWith( + lbConf, + 1721606400000 + ); + + expect(getRankMock).toHaveBeenCalledWith(uid, lbConf); + }); it("fails if daily leaderboards are disabled", async () => { await weeklyLeaderboardEnabled(false); @@ -1122,6 +1165,36 @@ describe("Loaderboard Controller", () => { ); }); + it("fails for weeksBefore not one", async () => { + const { body } = await mockApp + .get("/leaderboards/xp/weekly/rank") + .set("authorization", `Uid ${uid}`) + .query({ + weeksBefore: 2, + }) + .expect(422); + + expect(body).toEqual({ + message: "Invalid query schema", + validationErrors: ['"weeksBefore" Invalid literal value, expected 1'], + }); + }); + + it("fails for unknown query", async () => { + const { body } = await mockApp + .get("/leaderboards/xp/weekly/rank") + .set("authorization", `Uid ${uid}`) + .query({ + extra: "value", + }) + .expect(422); + + expect(body).toEqual({ + message: "Invalid query schema", + validationErrors: ["Unrecognized key(s) in object: 'extra'"], + }); + }); + it("fails while leaderboard is missing", async () => { //GIVEN getXpWeeklyLeaderboardMock.mockReturnValue(null); @@ -1130,11 +1203,6 @@ describe("Loaderboard Controller", () => { const { body } = await mockApp .get("/leaderboards/xp/weekly/rank") .set("authorization", `Uid ${uid}`) - .query({ - language: "english", - mode: "time", - mode2: "60", - }) .expect(404); expect(body.message).toEqual("XP leaderboard for this week not found."); diff --git a/backend/src/api/controllers/leaderboard.ts b/backend/src/api/controllers/leaderboard.ts index 06d527372..dc1da7bbb 100644 --- a/backend/src/api/controllers/leaderboard.ts +++ b/backend/src/api/controllers/leaderboard.ts @@ -5,6 +5,7 @@ import MonkeyError from "../../utils/error"; import * as DailyLeaderboards from "../../utils/daily-leaderboards"; import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard"; import { + DailyLeaderboardQuery, GetDailyLeaderboardQuery, GetDailyLeaderboardRankQuery, GetDailyLeaderboardResponse, @@ -14,6 +15,7 @@ import { GetLeaderboardRankResponse, GetLeaderboardResponse as GetLeaderboardResponse, GetWeeklyXpLeaderboardQuery, + GetWeeklyXpLeaderboardRankQuery, GetWeeklyXpLeaderboardRankResponse, GetWeeklyXpLeaderboardResponse, } from "@monkeytype/contracts/leaderboards"; @@ -73,7 +75,7 @@ export async function getRankFromLeaderboard( } function getDailyLeaderboardWithError( - { language, mode, mode2, daysBefore }: GetDailyLeaderboardRankQuery, + { language, mode, mode2, daysBefore }: DailyLeaderboardQuery, config: Configuration["dailyLeaderboards"] ): DailyLeaderboards.DailyLeaderboard { const customTimestamp = @@ -187,12 +189,13 @@ export async function getWeeklyXpLeaderboardResults( } export async function getWeeklyXpLeaderboardRank( - req: MonkeyRequest + req: MonkeyRequest ): Promise { const { uid } = req.ctx.decodedToken; const weeklyXpLeaderboard = getWeeklyXpLeaderboardWithError( - req.ctx.configuration.leaderboards.weeklyXp + req.ctx.configuration.leaderboards.weeklyXp, + req.query.weeksBefore ); const rankEntry = await weeklyXpLeaderboard.getRank( uid, diff --git a/frontend/src/ts/pages/leaderboards.ts b/frontend/src/ts/pages/leaderboards.ts index 6f301ae47..3a446ed5f 100644 --- a/frontend/src/ts/pages/leaderboards.ts +++ b/frontend/src/ts/pages/leaderboards.ts @@ -246,6 +246,7 @@ async function requestData(update = false): Promise { requests.rank = Ape.leaderboards.getDailyRank({ query: { ...baseQuery, + daysBefore: state.yesterday ? 1 : undefined, }, }); } @@ -324,7 +325,11 @@ async function requestData(update = false): Promise { }); if (isAuthenticated() && state.userData === null) { - requests.rank = Ape.leaderboards.getWeeklyXpRank(); + requests.rank = Ape.leaderboards.getWeeklyXpRank({ + query: { + weeksBefore: state.lastWeek ? 1 : undefined, + }, + }); } const [dataResponse, rankResponse] = await Promise.all([ @@ -653,8 +658,14 @@ function fillUser(): void { } if (isAuthenticated() && state.type === "daily" && state.userData === null) { + let str = `Not qualified`; + + if (!state.yesterday) { + str += ` (min speed required: ${state.minWpm} wpm)`; + } + $(".page.pageLeaderboards .bigUser").html( - `
Not qualified (min speed required: ${state.minWpm} wpm)
` + `
${str}
` ); return; } @@ -670,15 +681,6 @@ function fillUser(): void { return; } - if ( - (state.type === "weekly" && state.lastWeek) || - (state.type === "daily" && state.yesterday) - ) { - $(".page.pageLeaderboards .bigUser").addClass("hidden"); - $(".page.pageLeaderboards .tableAndUser > .divider").removeClass("hidden"); - return; - } - if (state.type === "allTime" || state.type === "daily") { if (!state.userData || !state.count) { $(".page.pageLeaderboards .bigUser").addClass("hidden"); @@ -839,7 +841,7 @@ function fillUser(): void {
${formatted.time}
-
date
+
last activity
${format( userData.lastActivityTimestamp, "dd MMM yyyy HH:mm" diff --git a/packages/contracts/src/leaderboards.ts b/packages/contracts/src/leaderboards.ts index 7d7a000be..647448b77 100644 --- a/packages/contracts/src/leaderboards.ts +++ b/packages/contracts/src/leaderboards.ts @@ -62,11 +62,14 @@ export type GetLeaderboardRankResponse = z.infer< //-------------------------------------------------------------------------- -export const GetDailyLeaderboardQuerySchema = LanguageAndModeQuerySchema.merge( - PaginationQuerySchema -).extend({ +export const DailyLeaderboardQuerySchema = LanguageAndModeQuerySchema.extend({ daysBefore: z.literal(1).optional(), }); +export type DailyLeaderboardQuery = z.infer; + +export const GetDailyLeaderboardQuerySchema = DailyLeaderboardQuerySchema.merge( + PaginationQuerySchema +); export type GetDailyLeaderboardQuery = z.infer< typeof GetDailyLeaderboardQuerySchema >; @@ -82,10 +85,8 @@ export type GetDailyLeaderboardResponse = z.infer< //-------------------------------------------------------------------------- -export const GetDailyLeaderboardRankQuerySchema = - LanguageAndModeQuerySchema.merge(PaginationQuerySchema).extend({ - daysBefore: z.literal(1).optional(), - }); +export const GetDailyLeaderboardRankQuerySchema = DailyLeaderboardQuerySchema; + export type GetDailyLeaderboardRankQuery = z.infer< typeof GetDailyLeaderboardRankQuerySchema >; @@ -98,9 +99,13 @@ export type GetLeaderboardDailyRankResponse = z.infer< //-------------------------------------------------------------------------- -export const GetWeeklyXpLeaderboardQuerySchema = PaginationQuerySchema.extend({ +const WeeklyXpLeaderboardQuerySchema = z.object({ weeksBefore: z.literal(1).optional(), }); + +export const GetWeeklyXpLeaderboardQuerySchema = + WeeklyXpLeaderboardQuerySchema.merge(PaginationQuerySchema); + export type GetWeeklyXpLeaderboardQuery = z.infer< typeof GetWeeklyXpLeaderboardQuerySchema >; @@ -115,6 +120,12 @@ export type GetWeeklyXpLeaderboardResponse = z.infer< //-------------------------------------------------------------------------- +export const GetWeeklyXpLeaderboardRankQuerySchema = + WeeklyXpLeaderboardQuerySchema; +export type GetWeeklyXpLeaderboardRankQuery = z.infer< + typeof GetWeeklyXpLeaderboardRankQuerySchema +>; + export const GetWeeklyXpLeaderboardRankResponseSchema = responseWithNullableData(XpLeaderboardEntrySchema); export type GetWeeklyXpLeaderboardRankResponse = z.infer< @@ -210,6 +221,7 @@ export const leaderboardsContract = c.router( "Get the rank of the current user on the weekly xp leaderboard", method: "GET", path: "/xp/weekly/rank", + query: GetWeeklyXpLeaderboardRankQuerySchema.strict(), responses: { 200: GetWeeklyXpLeaderboardRankResponseSchema, },