diff --git a/README.md b/README.md index 791186e..1c46c1b 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Click the function names to open their complete docs on the docs site. - [`getUserSummary()`](https://api-docs.retroachievements.org/v1/get-user-summary.html) - Get a user's profile metadata. - [`getUserCompletedGames()`](https://api-docs.retroachievements.org/v1/get-user-completed-games.html) - Deprecated function. Get hardcore and softcore completion metadata about games a user has played. - [`getUserWantToPlayList()`](https://api-docs.retroachievements.org/v1/get-user-want-to-play-list.html) - Get a user's "Want to Play Games" list. +- [`getUsersIFollow()`](https://api-docs.retroachievements.org/v1/get-users-i-follow.html) - Get the caller's "Following" users list. ### Game diff --git a/src/user/getUsersIFollow.test.ts b/src/user/getUsersIFollow.test.ts new file mode 100644 index 0000000..056b8c3 --- /dev/null +++ b/src/user/getUsersIFollow.test.ts @@ -0,0 +1,124 @@ +import { http, HttpResponse } from "msw"; +import { setupServer } from "msw/node"; + +import { apiBaseUrl } from "../utils/internal"; +import { buildAuthorization } from "../utils/public"; +import { getUsersIFollow } from "./getUsersIFollow"; +import type { GetUsersIFollowResponse, UsersIFollow } from "./models"; + +const server = setupServer(); + +describe("Funcion: getUsersIFollow", () => { + // MSW Setup + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + it("is defined #sanity", () => { + // ASSERT + expect(getUsersIFollow).toBeDefined(); + }); + + it("using defaults, retrieves the list of users that the caller follows", async () => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + const mockResponse = mockGetUsersIFollowResponse; + + server.use( + http.get(`${apiBaseUrl}/API_GetUsersIFollow.php`, (info) => { + const url = new URL(info.request.url); + expect(url.searchParams.has("c")).toBeFalsy(); + expect(url.searchParams.has("o")).toBeFalsy(); + return HttpResponse.json(mockResponse); + }) + ); + + // ACT + const response = await getUsersIFollow(authorization); + expect(response).toEqual(mockUsersIFollowValue); + }); + + it.each([{ offset: 1, count: 1 }, { offset: 5 }, { count: 20 }])( + "calls the 'Users I Follow' endpoint with a given offset ($offset) and/or count ($count)", + async (mockPayload) => { + // ARRANGE + const authorization = buildAuthorization({ + username: "mockUserName", + webApiKey: "mockWebApiKey", + }); + + server.use( + http.get(`${apiBaseUrl}/API_GetUsersIFollow.php`, (info) => { + const url = new URL(info.request.url); + const c = url.searchParams.get("c"); + const o = url.searchParams.get("o"); + expect(String(c)).toEqual(String(mockPayload.count ?? null)); + expect(String(o)).toEqual(String(mockPayload.offset ?? null)); + return HttpResponse.json(mockGetUsersIFollowResponse); + }) + ); + + // ACT + await getUsersIFollow(authorization, mockPayload); + } + ); + + 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_GetUsersIFollow.php`, () => + HttpResponse.json(mockResponse, { status, statusText }) + ) + ); + + // ASSERT + await expect( + getUsersIFollow(authorization, { count: 0 }) + ).rejects.toThrow(); + } + ); +}); + +const mockGetUsersIFollowResponse: GetUsersIFollowResponse = { + Count: 1, + Total: 1, + Results: [ + { + User: "Example", + ULID: "0123456789ABCDEFGHIJKLMNO", + Points: 9001, + PointsSoftcore: 101, + IsFollowingMe: false, + }, + ], +}; + +const mockUsersIFollowValue: UsersIFollow = { + count: 1, + total: 1, + results: [ + { + user: "Example", + ulid: "0123456789ABCDEFGHIJKLMNO", + points: 9001, + pointsSoftcore: 101, + isFollowingMe: false, + }, + ], +}; diff --git a/src/user/getUsersIFollow.ts b/src/user/getUsersIFollow.ts new file mode 100644 index 0000000..288a06c --- /dev/null +++ b/src/user/getUsersIFollow.ts @@ -0,0 +1,75 @@ +import { + apiBaseUrl, + buildRequestUrl, + call, + serializeProperties, +} from "../utils/internal"; +import type { AuthObject } from "../utils/public"; +import type { GetUsersIFollowResponse, UsersIFollow } from "./models"; + +/** + * A call to this function will retrieve the list of users that the + * caller is following. + * + * @param authorization An object containing your username and webApiKey. + * This can be constructed with `buildAuthorization()`. + * + * @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 usersIFollow = await getUsersIFollow(authorization); + * ``` + * + * @returns An object containing a list of users that the caller is + * following. + * ```json + * { + * "count": 1, + * "total": 1, + * "results": [ + * { + * "user": "Example", + * "ulid": "0123456789ABCDEFGHIJKLMNO", + * "points": 9001, + * "pointsSoftcore": 101, + * "isFollowingMe": false + * } + * ] + * } + * ``` + * + * @throws If the API was given invalid parameters (422) or if the + * API is currently down (503). + */ +export const getUsersIFollow = async ( + authorization: AuthObject, + payload?: { offset?: number; count?: number } +): Promise