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.
- [`getUsersFollowingMe()`](https://api-docs.retroachievements.org/v1/get-users-following-me.html) - Get the caller's "Followers" users 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/getUsersFollowingMe.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 { getUsersFollowingMe } from "./getUsersFollowingMe";
import type { GetUsersFollowingMeResponse, UsersFollowingMe } from "./models";

const server = setupServer();

describe("Function: getUsersFollowingMe", () => {
// MSW Setup
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

it("is defined #sanity", () => {
// ASSERT
expect(getUsersFollowingMe).toBeDefined();
});

it("using defaults, retrieves the list of users that are following the caller", async () => {
// ARRANGE
const authorization = buildAuthorization({
username: "mockUserName",
webApiKey: "mockWebApiKey",
});

const mockResponse = mockGetUsersFollowingMeResponse;

server.use(
http.get(`${apiBaseUrl}/API_GetUsersFollowingMe.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 getUsersFollowingMe(authorization);
expect(response).toEqual(mockUsersFollowingMeValue);
});

it.each([{ offset: 1, count: 1 }, { offset: 5 }, { count: 20 }])(
"calls the 'Users Following Me' 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_GetUsersFollowingMe.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(mockGetUsersFollowingMeResponse);
})
);

// ACT
await getUsersFollowingMe(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_GetUsersFollowingMe.php`, () =>
HttpResponse.json(mockResponse, { status, statusText })
)
);

// ASSERT
await expect(
getUsersFollowingMe(authorization, { count: 0 })
).rejects.toThrow();
}
);
});

const mockGetUsersFollowingMeResponse: GetUsersFollowingMeResponse = {
Count: 1,
Total: 1,
Results: [
{
User: "Example",
ULID: "0123456789ABCDEFGHIJKLMNO",
Points: 9001,
PointsSoftcore: 101,
AmIFollowing: true,
},
],
};

const mockUsersFollowingMeValue: UsersFollowingMe = {
count: 1,
total: 1,
results: [
{
user: "Example",
ulid: "0123456789ABCDEFGHIJKLMNO",
points: 9001,
pointsSoftcore: 101,
amIFollowing: true,
},
],
};
75 changes: 75 additions & 0 deletions src/user/getUsersFollowingMe.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 { GetUsersFollowingMeResponse, UsersFollowingMe } from "./models";

/**
* A call to this function will retrieve the list of users that are
* following the caller.
*
* @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 usersFollowingMe = await getUsersFollowingMe(authorization);
* ```
*
* @returns An object containing a list of users that are following
* the caller.
* ```json
* {
* "count": 1,
* "total": 1,
* "results": [
* {
* "user": "Example",
* "ulid": "0123456789ABCDEFGHIJKLMNO",
* "points": 9001,
* "pointsSoftcore": 101,
* "amIFollowing": true
* }
* ]
* }
* ```
*
* @throws If the API was given invalid parameters (422) or if the
* API is currently down (503).
*/
export const getUsersFollowingMe = async (
authorization: AuthObject,
payload?: { offset?: number; count?: number }
): Promise<UsersFollowingMe> => {
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_GetUsersFollowingMe.php",
authorization,
queryParams
);

const rawResponse = await call<GetUsersFollowingMeResponse>({ 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 "./getUsersFollowingMe";
export * from "./getUsersIFollow";
export * from "./getUserSummary";
export * from "./getUserWantToPlayList";
Expand Down
11 changes: 11 additions & 0 deletions src/user/models/get-users-following-me-response.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface GetUsersFollowingMeResponse {
Count: number;
Total: number;
Results: Array<{
User: string;
ULID: string;
Points: number;
PointsSoftcore: number;
AmIFollowing: 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-following-me-response.model";
export * from "./get-users-i-follow-response.model";
export * from "./user-awards.model";
export * from "./user-claims.model";
Expand All @@ -29,4 +30,5 @@ 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-following-me.model";
export * from "./users-i-follow.model";
11 changes: 11 additions & 0 deletions src/user/models/users-following-me.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface UsersFollowingMe {
count: number;
total: number;
results: Array<{
user: string;
ulid: string;
points: number;
pointsSoftcore: number;
amIFollowing: boolean;
}>;
}