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 <jack@monkeytype.com>
This commit is contained in:
Christian Fehmer 2025-02-21 16:52:20 +01:00 committed by GitHub
parent 0b840d2b6b
commit 8a41ccee97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 28 deletions

View file

@ -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.");

View file

@ -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<GetWeeklyXpLeaderboardRankQuery>
): Promise<GetWeeklyXpLeaderboardRankResponse> {
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,

View file

@ -246,6 +246,7 @@ async function requestData(update = false): Promise<void> {
requests.rank = Ape.leaderboards.getDailyRank({
query: {
...baseQuery,
daysBefore: state.yesterday ? 1 : undefined,
},
});
}
@ -324,7 +325,11 @@ async function requestData(update = false): Promise<void> {
});
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(
`<div class="warning">Not qualified (min speed required: ${state.minWpm} wpm)</div>`
`<div class="warning">${str}</div>`
);
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 {
<div class="sub">${formatted.time}</div>
</div>
<div class="stat wide">
<div class="title">date</div>
<div class="title">last activity</div>
<div class="value">${format(
userData.lastActivityTimestamp,
"dd MMM yyyy HH:mm"

View file

@ -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<typeof DailyLeaderboardQuerySchema>;
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,
},