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
101 changes: 101 additions & 0 deletions packages/webapp/__tests__/UsersLeaderboardStaticProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,107 @@ describe('leaderboard static props', () => {
});
});

it('should tolerate partial leaderboard data when a single resolver fails', async () => {
const highestReputation: UserLeaderboard[] = [
{
score: 500,
user: {
id: 'user-1',
username: 'user-1',
name: 'User One',
image: '',
permalink: '/users/user-1',
createdAt: new Date().toISOString(),
providers: [],
balance: { amount: 0 },
},
},
];

const partialDataError = {
response: {
data: {
...baseLeaderboardResponse,
highestReputation,
mostAchievementPoints: null,
},
errors: [{ message: 'Unexpected error' }],
},
};

mockRequest.mockImplementation((query: string) => {
if (query === LEADERBOARD_QUERY) {
return Promise.reject(partialDataError);
}

if (query === POPULAR_HOT_TAKES_QUERY) {
return Promise.resolve({ popularHotTakes: [] });
}

if (query === HIGHEST_LEVEL_QUERY) {
return Promise.resolve({ highestLevel: [] });
}

return Promise.reject(new Error('Unexpected query'));
});

const result = await getUsersStaticProps();

expect(result).toMatchObject({
props: {
highestReputation,
mostAchievementPoints: [],
},
revalidate: 3600,
});
});

it('should fail the build when the leaderboard response has no data at all', async () => {
const totalFailure = {
response: {
errors: [{ message: 'Internal server error' }],
},
};

mockRequest.mockImplementation((query: string) => {
if (query === LEADERBOARD_QUERY) {
return Promise.reject(totalFailure);
}

return Promise.reject(new Error('Unexpected query'));
});

await expect(getUsersStaticProps()).rejects.toBe(totalFailure);
});

it('should fail the build when every leaderboard field errored', async () => {
const allFieldsFailed = {
response: {
data: {
highestReputation: null,
longestStreak: null,
highestPostViews: null,
mostUpvoted: null,
mostReferrals: null,
mostReadingDays: null,
mostAchievementPoints: null,
mostVerifiedUsers: null,
},
errors: [{ message: 'Internal server error' }],
},
};

mockRequest.mockImplementation((query: string) => {
if (query === LEADERBOARD_QUERY) {
return Promise.reject(allFieldsFailed);
}

return Promise.reject(new Error('Unexpected query'));
});

await expect(getUsersStaticProps()).rejects.toBe(allFieldsFailed);
});

it('should return notFound for highest-level detail when schema is missing', async () => {
mockRequest.mockRejectedValue(
createMissingSchemaError(
Expand Down
58 changes: 47 additions & 11 deletions packages/webapp/pages/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,44 @@ interface PageProps {
popularHotTakes: PopularHotTakes[];
}

type LeaderboardQueryData = Omit<
PageProps,
'highestLevel' | 'isHighestLevelSupported' | 'popularHotTakes'
>;

const leaderboardListFields: (keyof LeaderboardQueryData)[] = [
'highestReputation',
'longestStreak',
'highestPostViews',
'mostUpvoted',
'mostReferrals',
'mostReadingDays',
'mostAchievementPoints',
'mostVerifiedUsers',
];

// A single flaky resolver makes graphql-request throw even though the response
// carries data for every other leaderboard; with hourly ISR an empty section is
// better than failing the whole static export.
const fetchLeaderboards = async (): Promise<Partial<LeaderboardQueryData>> => {
try {
return await gqlClient.request<LeaderboardQueryData>(LEADERBOARD_QUERY);
} catch (err) {
const partialData = (
err as { response?: { data?: Partial<LeaderboardQueryData> } }
)?.response?.data;
const hasUsableData =
partialData &&
leaderboardListFields.some((field) => Array.isArray(partialData[field]));

if (!hasUsableData) {
throw err;
}

return partialData;
}
};

const isHighestLevelSchemaMissing = (error: GraphQLError): boolean => {
return (
error?.response?.errors?.some(
Expand Down Expand Up @@ -210,9 +248,7 @@ export async function getStaticProps(): Promise<
GetStaticPropsResult<PageProps>
> {
try {
const res = await gqlClient.request<
Omit<PageProps, 'highestLevel' | 'popularHotTakes'>
>(LEADERBOARD_QUERY);
const res = await fetchLeaderboards();

let highestLevel: UserLeaderboard[] = [];
let isHighestLevelSupported = false;
Expand Down Expand Up @@ -247,16 +283,16 @@ export async function getStaticProps(): Promise<

return {
props: {
highestReputation: res.highestReputation,
longestStreak: res.longestStreak,
highestPostViews: res.highestPostViews,
mostUpvoted: res.mostUpvoted,
mostReferrals: res.mostReferrals,
mostReadingDays: res.mostReadingDays,
mostAchievementPoints: res.mostAchievementPoints,
highestReputation: res.highestReputation ?? [],
longestStreak: res.longestStreak ?? [],
highestPostViews: res.highestPostViews ?? [],
mostUpvoted: res.mostUpvoted ?? [],
mostReferrals: res.mostReferrals ?? [],
mostReadingDays: res.mostReadingDays ?? [],
mostAchievementPoints: res.mostAchievementPoints ?? [],
highestLevel,
isHighestLevelSupported,
mostVerifiedUsers: res.mostVerifiedUsers,
mostVerifiedUsers: res.mostVerifiedUsers ?? [],
popularHotTakes,
},
revalidate: 3600,
Expand Down
Loading