Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
124 changes: 124 additions & 0 deletions src/user/getUsersIFollow.test.ts
Original file line number Diff line number Diff line change
@@ -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 = `<html><body>${statusText}</body></html>`;

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,
},
],
};
75 changes: 75 additions & 0 deletions src/user/getUsersIFollow.ts
Original file line number Diff line number Diff line change
@@ -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<UsersIFollow> => {
const queryParams: Record<string, number> = {};
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<GetUsersIFollowResponse>({ url });

return serializeProperties(rawResponse, {
shouldCastToNumbers: ["Points", "PointsSoftcore"],
shouldMapToBooleans: ["AmIFollowing"],
});
};
1 change: 1 addition & 0 deletions src/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
11 changes: 11 additions & 0 deletions src/user/models/get-users-i-follow-response.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface GetUsersIFollowResponse {
Count: number;
Total: number;
Results: Array<{
User: string;
ULID: string;
Points: number;
PointsSoftcore: number;
IsFollowingMe: boolean;
}>;
}
2 changes: 2 additions & 0 deletions src/user/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
11 changes: 11 additions & 0 deletions src/user/models/users-i-follow.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface UsersIFollow {
count: number;
total: number;
results: Array<{
user: string;
ulid: string;
points: number;
pointsSoftcore: number;
isFollowingMe: boolean;
}>;
}