fix(api): ms validation new api (#53983)

This commit is contained in:
Oliver Eyton-Williams
2024-03-06 09:05:59 +01:00
committed by GitHub
parent 19be14b72f
commit eb066942d8
4 changed files with 76 additions and 35 deletions
+5 -5
View File
@@ -1032,7 +1032,7 @@ describe('challengeRoutes', () => {
// Create and Run Simple C# Console Applications's id:
const trophyChallengeId2 = '647f87dc07d29547b3bee1bf';
const nonTrophyChallengeId = 'bd7123c8c441eddfaeb5bdef';
const solutionUrl = `https://learn.microsoft.com/api/gamestatus/${msUserId}`;
const solutionUrl = `https://learn.microsoft.com/api/achievements/user/${msUserId}`;
const idIsMissingOrInvalid = {
type: 'error',
@@ -1149,7 +1149,7 @@ describe('challengeRoutes', () => {
mockVerifyTrophyWithMicrosoft.mockImplementationOnce(() =>
Promise.resolve({
type: 'success',
msGameStatusApiUrl: solutionUrl
msUserAchievementsApiUrl: solutionUrl
})
);
const msUsername = 'ANRandom';
@@ -1192,7 +1192,7 @@ describe('challengeRoutes', () => {
mockVerifyTrophyWithMicrosoft.mockImplementationOnce(() =>
Promise.resolve({
type: 'success',
msGameStatusApiUrl: solutionUrl
msUserAchievementsApiUrl: solutionUrl
})
);
const msUsername = 'ANRandom';
@@ -1205,7 +1205,7 @@ describe('challengeRoutes', () => {
mockVerifyTrophyWithMicrosoft.mockImplementationOnce(() =>
Promise.resolve({
type: 'success',
msGameStatusApiUrl: solutionUrl
msUserAchievementsApiUrl: solutionUrl
})
);
const resTwo = await superPost(
@@ -1217,7 +1217,7 @@ describe('challengeRoutes', () => {
mockVerifyTrophyWithMicrosoft.mockImplementationOnce(() =>
Promise.resolve({
type: 'success',
msGameStatusApiUrl: solutionUrl
msUserAchievementsApiUrl: solutionUrl
})
);
const resUpdate = await superPost(
+1 -1
View File
@@ -588,7 +588,7 @@ export const challengeRoutes: FastifyPluginCallbackTypebox = (
const newChallenge = {
id: challengeId,
completedDate,
solution: msTrophyStatus.msGameStatusApiUrl
solution: msTrophyStatus.msUserAchievementsApiUrl
};
await fastify.prisma.user.update({
where: { id: req.session.user.id },
@@ -79,7 +79,7 @@ describe('Challenge Helpers', () => {
const msUsername = 'ANRandom';
const msTrophyId = 'learn.wwl.get-started-c-sharp-part-3.trophy';
const verifyData = { msUsername, msTrophyId };
const gamestatusUrl = `https://learn.microsoft.com/api/gamestatus/${userId}`;
const achievementsUrl = `https://learn.microsoft.com/api/achievements/user/${userId}`;
afterEach(() => jest.clearAllMocks());
@@ -98,13 +98,13 @@ describe('Challenge Helpers', () => {
});
});
test("handles failure to reach Microsoft's gamestatus api", async () => {
test("handles failure to reach Microsoft's achievements api", async () => {
const fetchProfile = createFetchMock({ body: { userId } });
const fetchGameStatus = createFetchMock({ ok: false });
const fetchAchievements = createFetchMock({ ok: false });
jest
.spyOn(globalThis, 'fetch')
.mockImplementationOnce(fetchProfile)
.mockImplementationOnce(fetchGameStatus);
.mockImplementationOnce(fetchAchievements);
const verification = await verifyTrophyWithMicrosoft(verifyData);
@@ -116,11 +116,11 @@ describe('Challenge Helpers', () => {
test('handles the case where the user has no achievements', async () => {
const fetchProfile = createFetchMock({ body: { userId } });
const fetchGameStatus = createFetchMock({ body: { achievements: [] } });
const fetchAchievements = createFetchMock({ body: { achievements: [] } });
jest
.spyOn(globalThis, 'fetch')
.mockImplementationOnce(fetchProfile)
.mockImplementationOnce(fetchGameStatus);
.mockImplementationOnce(fetchAchievements);
const verification = await verifyTrophyWithMicrosoft(verifyData);
@@ -132,13 +132,13 @@ describe('Challenge Helpers', () => {
test("handles failure to find the trophy in the user's achievements", async () => {
const fetchProfile = createFetchMock({ body: { userId } });
const fetchGameStatus = createFetchMock({
body: { achievements: [{ awardUid: 'fake-id' }] }
const fetchAchievements = createFetchMock({
body: { achievements: [{ typeId: 'fake-id' }] }
});
jest
.spyOn(globalThis, 'fetch')
.mockImplementationOnce(fetchProfile)
.mockImplementationOnce(fetchGameStatus);
.mockImplementationOnce(fetchAchievements);
const verification = await verifyTrophyWithMicrosoft(verifyData);
@@ -151,21 +151,21 @@ describe('Challenge Helpers', () => {
});
});
test('returns msGameStatusApiUrl on success', async () => {
test('returns msUserAchievementsApiUrl on success', async () => {
const fetchProfile = createFetchMock({ body: { userId } });
const fetchGameStatus = createFetchMock({
body: { achievements: [{ awardUid: msTrophyId }] }
const fetchAchievements = createFetchMock({
body: { achievements: [{ typeId: msTrophyId }] }
});
jest
.spyOn(globalThis, 'fetch')
.mockImplementationOnce(fetchProfile)
.mockImplementationOnce(fetchGameStatus);
.mockImplementationOnce(fetchAchievements);
const verification = await verifyTrophyWithMicrosoft(verifyData);
expect(verification).toEqual({
type: 'success',
msGameStatusApiUrl: gamestatusUrl
msUserAchievementsApiUrl: achievementsUrl
});
});
});
+56 -15
View File
@@ -59,27 +59,64 @@ export const createProject = (
progressTimestamps: [...progressTimestamps, newChallenge.completedDate]
});
type MSProfileError = {
type: 'error';
message: 'flash.ms.profile.err';
variables: { msUsername: string };
};
type MSProfileSuccess = {
type: 'success';
userId: string;
};
async function getMSProfile(msUsername: string) {
const profileError = {
const error: MSProfileError = {
type: 'error',
message: 'flash.ms.profile.err',
variables: {
msUsername
}
} as const;
};
const msProfileApi = `https://learn.microsoft.com/api/profiles/${msUsername}`;
const msProfileApiRes = await fetch(msProfileApi);
if (!msProfileApiRes.ok) return profileError;
if (!msProfileApiRes.ok) return error;
const { userId } = (await msProfileApiRes.json()) as {
userId: string;
};
return userId ? ({ type: 'success', userId } as const) : profileError;
const success: MSProfileSuccess = {
type: 'success',
userId
};
return userId ? success : error;
}
type AchievementsError = {
type: 'error';
message: 'flash.ms.trophy.err-3';
};
type NoAchievementsError = {
type: 'error';
message: 'flash.ms.trophy.err-6';
};
type NoTrophyError = {
type: 'error';
message: 'flash.ms.trophy.err-4';
variables: { msUsername: string };
};
type Validated = {
type: 'success';
msUserAchievementsApiUrl: string;
};
/**
* Handles all communication with the Microsoft Learn APIs.
*
@@ -99,33 +136,37 @@ export async function verifyTrophyWithMicrosoft({
if (msProfile.type === 'error') return msProfile;
const msGameStatusApiUrl = `https://learn.microsoft.com/api/gamestatus/${msProfile.userId}`;
const msGameStatusApiRes = await fetch(msGameStatusApiUrl);
const msUserAchievementsApiUrl = `https://learn.microsoft.com/api/achievements/user/${msProfile.userId}`;
const msUserAchievementsApiRes = await fetch(msUserAchievementsApiUrl);
if (!msGameStatusApiRes.ok) {
if (!msUserAchievementsApiRes.ok) {
return {
type: 'error',
message: 'flash.ms.trophy.err-3'
} as const;
} as AchievementsError;
}
const { achievements } = (await msGameStatusApiRes.json()) as {
achievements?: { awardUid: string }[];
const { achievements } = (await msUserAchievementsApiRes.json()) as {
achievements?: { typeId: string }[];
};
if (!achievements?.length)
return {
type: 'error',
message: 'flash.ms.trophy.err-6'
} as const;
} as NoAchievementsError;
const earnedTrophy = achievements?.some(a => a.awardUid === msTrophyId);
// TODO: handle the case where there are achievements, but the `typeId` is not
// a property of the achievements. This suggests that Microsoft has changed
// their API and, to aid debugging, we should report a different error
// message.
const earnedTrophy = achievements?.some(a => a.typeId === msTrophyId);
if (earnedTrophy) {
return {
type: 'success',
msGameStatusApiUrl
} as const;
msUserAchievementsApiUrl
} as Validated;
} else {
return {
type: 'error',
@@ -133,6 +174,6 @@ export async function verifyTrophyWithMicrosoft({
variables: {
msUsername
}
} as const;
} as NoTrophyError;
}
}