diff --git a/README.md b/README.md index fc9b44e..80237b5 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Click the function names to open their complete docs on the docs site. ### Leaderboard +- [`getGameLeaderboards()`](https://api-docs.retroachievements.org/v1/get-game-leaderboards.html) - Get a given game's list of leaderboards. - [`getLeaderboardEntries()`](https://api-docs.retroachievements.org/v1/get-leaderboard-entries.html) - Get a given leaderboard's entries. - [`getUserGameLeaderboards()`](https://api-docs.retroachievements.org/v1/get-user-game-leaderboards.html) - Get a user's list of leaderboards for a given game. diff --git a/src/leaderboard/getGameLeaderboards.test.ts b/src/leaderboard/getGameLeaderboards.test.ts new file mode 100644 index 0000000..5ac4fc1 --- /dev/null +++ b/src/leaderboard/getGameLeaderboards.test.ts @@ -0,0 +1,152 @@ +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; + +import { apiBaseUrl } from "../utils/internal"; +import { buildAuthorization } from "../utils/public"; +import { getGameLeaderboards } from "./getGameLeaderboards"; +import type { GameLeaderboards, GetGameLeaderboardsResponse } from "./models"; + +const server = setupServer(); + +describe("Function: getGameLeaderboards", () => { + // MSW Setup + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + it("is defined #sanity", () => { + // ASSERT + expect(getGameLeaderboards).toBeDefined(); + }); + + it("using defaults, retrieves the list of game leaderboards for the given game id", async () => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse = mockGetGameLeaderboardsResponse; + + server.use( + http.get(`${apiBaseUrl}/API_GetGameLeaderboards.php`, (info) => { + const url = new URL(info.request.url); + expect(url.searchParams.get("i")).toBe(mockGameId); + expect(url.searchParams.has("c")).toBeFalsy(); + expect(url.searchParams.has("o")).toBeFalsy(); + return HttpResponse.json(mockResponse); + }) + ); + + // ACT + const response = await getGameLeaderboards(authorization, { + gameId: mockGameId, + }); + expect(response).toEqual(mockGameLeaderboardsValue); + }); + + it.each([{ offset: 1, count: 1 }, { offset: 5 }, { count: 20 }])( + "calls the 'Game Leaderboards' endpoint with a given offset ($offset) and/or count ($count)", + async ({ offset: mockOffset, count: mockCount }) => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + server.use( + http.get(`${apiBaseUrl}/API_GetGameLeaderboards.php`, (info) => { + const url = new URL(info.request.url); + const c = url.searchParams.get("c"); + const o = url.searchParams.get("o"); + expect(url.searchParams.get("i")).toBe(mockGameId); + expect(String(c)).toEqual(String(mockCount ?? null)); + expect(String(o)).toEqual(String(mockOffset ?? null)); + return HttpResponse.json(mockGetGameLeaderboardsResponse); + }) + ); + + // ACT + await getGameLeaderboards(authorization, { + gameId: mockGameId, + offset: mockOffset, + count: mockCount, + }); + } + ); + + it.each([ + { status: 503, statusText: "The API is currently down" }, + { status: 422, statusText: "HTTP Error: Status 422 Unprocessable Entity" }, + ])( + "given the API returns a $status, throws an error", + async ({ status, statusText }) => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse = `
${statusText}`; + + server.use( + http.get(`${apiBaseUrl}/API_GetGameLeaderboards.php`, () => + HttpResponse.json(mockResponse, { status, statusText }) + ) + ); + + // ASSERT + await expect( + getGameLeaderboards(authorization, { gameId: mockGameId }) + ).rejects.toThrow(); + } + ); +}); + +const mockGameId = "14402"; + +const mockGetGameLeaderboardsResponse: GetGameLeaderboardsResponse = { + Count: 1, + Total: 1, + Results: [ + { + ID: 1234, + RankAsc: false, + Title: "South Island Conqueror", + Description: "Complete the game with the highest score possible.", + Format: "VALUE", + Author: "Example", + AuthorULID: "0123456789ABCDEFGHIJKLMNO", + State: "active", + TopEntry: { + User: "TopExample", + ULID: "ONMLKJIHGFEDCBA9876543210", + Score: 98_765, + FormattedScore: "98,765", + }, + }, + ], +}; + +const mockGameLeaderboardsValue: GameLeaderboards = { + count: 1, + total: 1, + results: [ + { + id: 1234, + rankAsc: false, + title: "South Island Conqueror", + description: "Complete the game with the highest score possible.", + format: "VALUE", + author: "Example", + authorUlid: "0123456789ABCDEFGHIJKLMNO", + state: "active", + topEntry: { + user: "TopExample", + ulid: "ONMLKJIHGFEDCBA9876543210", + score: 98_765, + formattedScore: "98,765", + }, + }, + ], +}; diff --git a/src/leaderboard/getGameLeaderboards.ts b/src/leaderboard/getGameLeaderboards.ts new file mode 100644 index 0000000..334211a --- /dev/null +++ b/src/leaderboard/getGameLeaderboards.ts @@ -0,0 +1,91 @@ +import type { ID } from "../utils/internal"; +import { + apiBaseUrl, + buildRequestUrl, + call, + serializeProperties, +} from "../utils/internal"; +import type { AuthObject } from "../utils/public"; +import type { GameLeaderboards, GetGameLeaderboardsResponse } from "./models"; + +/** + * A call to this function will retrieve a list of leaderboards for a + * given game ID. + * + * @param authorization An object containing your username and webApiKey. + * This can be constructed with `buildAuthorization()`. + * + * @param payload.gameId The ID of the game to retrieve leaderboards for. + * + * @param payload.offset The number of entries to skip. The API will default + * to 0 if the parameter is not specified. + * + * @param payload.count The number of entries to return. The API will + * default to 100 if the parameter is not specified. The max number + * of entries that can be returned is 500. + * + * @example + * ``` + * const gameLeaderboards = await getGameLeaderboards( + * authorization, + * { gameId: 14402 } + * ); + * ``` + * + * @returns An object containing a list of leaderboards that were created + * for the specified game. + * ```json + * { + * "count": 1, + * "total": 1, + * "results": [ + * { + * "id": 1234, + * "rankAsc": false, + * "title": "South Island Conqueror", + * "description": "Complete the game with the highest score possible.", + * "format": "VALUE", + * "author": "Example", + * "authorUlid": "0123456789ABCDEFGHIJKLMNO", + * "state": "active", + * "topEntry" : { + * "user": "TopExample", + * "ulid": "ONMLKJIHGFEDCBA9876543210", + * "score": 98765, + * "formattedScore": "98,765" + * } + * } + * ] + * } + * ``` + * + * @throws If the API was given invalid parameters (422) or if the + * API is currently down (503). + */ +export const getGameLeaderboards = async ( + authorization: AuthObject, + payload: { gameId: ID; offset?: number; count?: number } +): Promise