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 => { + const queryParams: Record = {}; + if (payload?.offset !== null && payload?.offset !== undefined) { + queryParams.o = payload.offset; + } + if (payload?.count !== null && payload?.count !== undefined) { + queryParams.c = payload.count; + } + + const url = buildRequestUrl( + apiBaseUrl, + "/API_GetUsersIFollow.php", + authorization, + queryParams + ); + + const rawResponse = await call({ url }); + + return serializeProperties(rawResponse, { + shouldCastToNumbers: ["Points", "PointsSoftcore"], + shouldMapToBooleans: ["AmIFollowing"], + }); +}; diff --git a/src/user/index.ts b/src/user/index.ts index e3e317c..1302cca 100644 --- a/src/user/index.ts +++ b/src/user/index.ts @@ -11,6 +11,7 @@ export * from "./getUserProfile"; export * from "./getUserProgress"; export * from "./getUserRecentAchievements"; export * from "./getUserRecentlyPlayedGames"; +export * from "./getUsersIFollow"; export * from "./getUserSummary"; export * from "./getUserWantToPlayList"; export * from "./models"; diff --git a/src/user/models/get-users-i-follow-response.model.ts b/src/user/models/get-users-i-follow-response.model.ts new file mode 100644 index 0000000..9330de9 --- /dev/null +++ b/src/user/models/get-users-i-follow-response.model.ts @@ -0,0 +1,11 @@ +export interface GetUsersIFollowResponse { + Count: number; + Total: number; + Results: Array<{ + User: string; + ULID: string; + Points: number; + PointsSoftcore: number; + IsFollowingMe: boolean; + }>; +} diff --git a/src/user/models/index.ts b/src/user/models/index.ts index 91c2d99..9ed5ea2 100644 --- a/src/user/models/index.ts +++ b/src/user/models/index.ts @@ -14,6 +14,7 @@ export * from "./get-user-recent-achievements-response.model"; export * from "./get-user-recently-played-games-response.model"; export * from "./get-user-summary-response.model"; export * from "./get-user-want-to-play-list-response.model"; +export * from "./get-users-i-follow-response.model"; export * from "./user-awards.model"; export * from "./user-claims.model"; export * from "./user-claims-response.model"; @@ -28,3 +29,4 @@ export * from "./user-recent-achievement.model"; export * from "./user-recently-played-games.model"; export * from "./user-summary.model"; export * from "./user-want-to-play-list.model"; +export * from "./users-i-follow.model"; diff --git a/src/user/models/users-i-follow.model.ts b/src/user/models/users-i-follow.model.ts new file mode 100644 index 0000000..5c9d872 --- /dev/null +++ b/src/user/models/users-i-follow.model.ts @@ -0,0 +1,11 @@ +export interface UsersIFollow { + count: number; + total: number; + results: Array<{ + user: string; + ulid: string; + points: number; + pointsSoftcore: number; + isFollowingMe: boolean; + }>; +}