mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(api): ms validation new api (#53983)
This commit is contained in:
committed by
GitHub
parent
19be14b72f
commit
eb066942d8
@@ -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(
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user