Skip to content

Commit 54e56df

Browse files
Merge pull request #725 from freeCodeCamp/main
Create a new pull request by comparing changes across two branches
2 parents 97549c1 + 7b885c0 commit 54e56df

File tree

975 files changed

+136339
-9873
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

975 files changed

+136339
-9873
lines changed

api/src/exam-environment/routes/exam-environment.test.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -573,19 +573,17 @@ describe('/exam-environment/', () => {
573573
expect(res.status).toBe(200);
574574

575575
expect(res.body).toStrictEqual({
576-
data: {
577-
exams: [
578-
{
579-
canTake: true,
580-
config: {
581-
name: mock.exam.config.name,
582-
note: mock.exam.config.note,
583-
totalTimeInMS: mock.exam.config.totalTimeInMS
584-
},
585-
id: mock.examId
586-
}
587-
]
588-
}
576+
exams: [
577+
{
578+
canTake: true,
579+
config: {
580+
name: mock.exam.config.name,
581+
note: mock.exam.config.note,
582+
totalTimeInMS: mock.exam.config.totalTimeInMS
583+
},
584+
id: mock.examId
585+
}
586+
]
589587
});
590588
});
591589
});

api/src/exam-environment/routes/exam-environment.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,6 @@ async function getExams(
599599
});
600600

601601
return reply.send({
602-
data: {
603-
exams: availableExams
604-
}
602+
exams: availableExams
605603
});
606604
}

api/src/exam-environment/schemas/exams.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,17 @@ export const examEnvironmentExams = {
77
response: {
88
200: Type.Union([
99
Type.Object({
10-
data: Type.Object({
11-
exams: Type.Array(
12-
Type.Object({
13-
id: Type.String(),
14-
config: Type.Object({
15-
name: Type.String(),
16-
note: Type.String(),
17-
totalTimeInMS: Type.Number()
18-
}),
19-
canTake: Type.Boolean()
20-
})
21-
)
22-
})
10+
exams: Type.Array(
11+
Type.Object({
12+
id: Type.String(),
13+
config: Type.Object({
14+
name: Type.String(),
15+
note: Type.String(),
16+
totalTimeInMS: Type.Number()
17+
}),
18+
canTake: Type.Boolean()
19+
})
20+
)
2321
}),
2422
STANDARD_ERROR
2523
])
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { nanoidCharSet } from '../../utils/create-user';
2+
3+
const uuidRe = /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/;
4+
const fccUuidRe = /^fcc-[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/;
5+
const unsubscribeIdRe = new RegExp(`^[${nanoidCharSet}]{21}$`);
6+
const mongodbIdRe = /^[a-f0-9]{24}$/;
7+
8+
9+
// eslint-disable-next-line jsdoc/require-jsdoc
10+
export const newUser = (email: string) => ({
11+
about: '',
12+
acceptedPrivacyTerms: false,
13+
completedChallenges: [],
14+
completedExams: [],
15+
currentChallengeId: '',
16+
donationEmails: [],
17+
email,
18+
emailAuthLinkTTL: null,
19+
emailVerified: true,
20+
emailVerifyTTL: null,
21+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
22+
externalId: expect.stringMatching(uuidRe),
23+
githubProfile: null,
24+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
25+
id: expect.stringMatching(mongodbIdRe),
26+
is2018DataVisCert: false,
27+
is2018FullStackCert: false,
28+
isApisMicroservicesCert: false,
29+
isBackEndCert: false,
30+
isBanned: false,
31+
isCheater: false,
32+
isClassroomAccount: null,
33+
isDataAnalysisPyCertV7: false,
34+
isDataVisCert: false,
35+
isDonating: false,
36+
isFoundationalCSharpCertV8: false,
37+
isFrontEndCert: false,
38+
isFrontEndLibsCert: false,
39+
isFullStackCert: false,
40+
isHonest: false,
41+
isInfosecCertV7: false,
42+
isInfosecQaCert: false,
43+
isJsAlgoDataStructCert: false,
44+
isJsAlgoDataStructCertV8: false,
45+
isMachineLearningPyCertV7: false,
46+
isQaCertV7: false,
47+
isRelationalDatabaseCertV8: false,
48+
isCollegeAlgebraPyCertV8: false,
49+
isRespWebDesignCert: false,
50+
isSciCompPyCertV7: false,
51+
isUpcomingPythonCertV8: null,
52+
keyboardShortcuts: false,
53+
linkedin: null,
54+
location: '',
55+
name: '',
56+
needsModeration: false,
57+
newEmail: null,
58+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
59+
unsubscribeId: expect.stringMatching(unsubscribeIdRe),
60+
partiallyCompletedChallenges: [],
61+
password: null,
62+
picture: '',
63+
portfolio: [],
64+
profileUI: {
65+
isLocked: false,
66+
showAbout: false,
67+
showCerts: false,
68+
showDonation: false,
69+
showHeatMap: false,
70+
showLocation: false,
71+
showName: false,
72+
showPoints: false,
73+
showPortfolio: false,
74+
showTimeLine: false
75+
},
76+
progressTimestamps: [expect.any(Number)],
77+
rand: null, // TODO(Post-MVP): delete from schema (it's not used or required).
78+
savedChallenges: [],
79+
sendQuincyEmail: false,
80+
theme: 'default',
81+
timezone: null,
82+
twitter: null,
83+
updateCount: 0, // see extendClient in prisma.ts
84+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
85+
username: expect.stringMatching(fccUuidRe),
86+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
87+
usernameDisplay: expect.stringMatching(fccUuidRe),
88+
verificationToken: null,
89+
website: null,
90+
yearsTopContributor: []
91+
}
92+
)

api/src/plugins/auth-dev.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
2+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
3+
import Fastify, { FastifyInstance } from 'fastify';
4+
5+
import { defaultUserEmail } from '../../jest.utils';
6+
import { HOME_LOCATION } from '../utils/env';
7+
import { devAuth } from '../plugins/auth-dev';
8+
import prismaPlugin from '../db/prisma';
9+
import auth from './auth';
10+
import cookies from './cookies';
11+
12+
import { newUser } from './__fixtures__/user';
13+
14+
describe('dev login', () => {
15+
let fastify: FastifyInstance;
16+
17+
beforeAll(async () => {
18+
fastify = Fastify();
19+
20+
await fastify.register(cookies);
21+
await fastify.register(auth);
22+
await fastify.register(devAuth);
23+
await fastify.register(prismaPlugin);
24+
});
25+
26+
beforeEach(async () => {
27+
await fastify.prisma.user.deleteMany({
28+
where: { email: defaultUserEmail }
29+
});
30+
});
31+
32+
afterAll(async () => {
33+
await fastify.prisma.user.deleteMany({
34+
where: { email: defaultUserEmail }
35+
});
36+
await fastify.close();
37+
});
38+
39+
describe('GET /signin', () => {
40+
it('should create an account if one does not exist', async () => {
41+
const before = await fastify.prisma.user.count({});
42+
await fastify.inject({
43+
method: 'GET',
44+
url: '/signin'
45+
});
46+
47+
const after = await fastify.prisma.user.count({});
48+
49+
expect(before).toBe(0);
50+
expect(after).toBe(before + 1);
51+
});
52+
53+
it('should populate the user with the correct data', async () => {
54+
await fastify.inject({
55+
method: 'GET',
56+
url: '/signin'
57+
});
58+
59+
const user = await fastify.prisma.user.findFirstOrThrow({
60+
where: { email: defaultUserEmail }
61+
});
62+
63+
expect(user).toEqual(newUser(defaultUserEmail));
64+
expect(user.username).toBe(user.usernameDisplay);
65+
});
66+
67+
it('should set the jwt_access_token cookie', async () => {
68+
const res = await fastify.inject({
69+
method: 'GET',
70+
url: '/signin'
71+
});
72+
73+
expect(res.statusCode).toBe(302);
74+
75+
expect(res.cookies).toEqual(
76+
expect.arrayContaining([
77+
expect.objectContaining({ name: 'jwt_access_token' })
78+
])
79+
);
80+
});
81+
82+
it.todo('should create a session');
83+
84+
it('should redirect to the Referer (if it is a valid origin)', async () => {
85+
const res = await fastify.inject({
86+
method: 'GET',
87+
url: '/signin',
88+
headers: {
89+
referer: 'https://www.freecodecamp.org/some-path/or/other'
90+
}
91+
});
92+
93+
expect(res.statusCode).toBe(302);
94+
expect(res.headers.location).toBe(
95+
'https://www.freecodecamp.org/some-path/or/other'
96+
);
97+
});
98+
99+
it('should redirect to /valid-language/learn when signing in from /valid-language', async () => {
100+
const res = await fastify.inject({
101+
method: 'GET',
102+
url: '/signin',
103+
headers: {
104+
referer: 'https://www.freecodecamp.org/espanol'
105+
}
106+
});
107+
108+
expect(res.statusCode).toBe(302);
109+
expect(res.headers.location).toBe(
110+
'https://www.freecodecamp.org/espanol/learn'
111+
);
112+
});
113+
114+
it('should handle referers with trailing slahes', async () => {
115+
const res = await fastify.inject({
116+
method: 'GET',
117+
url: '/signin',
118+
headers: {
119+
referer: 'https://www.freecodecamp.org/espanol/'
120+
}
121+
});
122+
123+
expect(res.statusCode).toBe(302);
124+
expect(res.headers.location).toBe(
125+
'https://www.freecodecamp.org/espanol/learn'
126+
);
127+
});
128+
129+
it('should redirect to /learn by default', async () => {
130+
const res = await fastify.inject({
131+
method: 'GET',
132+
url: '/signin'
133+
});
134+
135+
expect(res.statusCode).toBe(302);
136+
expect(res.headers.location).toBe(`${HOME_LOCATION}/learn`);
137+
});
138+
});
139+
});

api/src/plugins/auth-dev.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
2+
import type { FastifyReply, FastifyRequest } from 'fastify';
3+
import {
4+
getRedirectParams,
5+
getPrefixedLandingPath,
6+
haveSamePath
7+
} from '../utils/redirection';
8+
import { findOrCreateUser } from '../routes/helpers/auth-helpers';
9+
import { createAccessToken } from '../utils/tokens';
10+
11+
const trimTrailingSlash = (str: string) =>
12+
str.endsWith('/') ? str.slice(0, -1) : str;
13+
14+
async function handleRedirects(req: FastifyRequest, reply: FastifyReply) {
15+
const params = getRedirectParams(req);
16+
const { origin, pathPrefix } = params;
17+
const returnTo = trimTrailingSlash(params.returnTo);
18+
const landingUrl = getPrefixedLandingPath(origin, pathPrefix);
19+
20+
return await reply.redirect(
21+
haveSamePath(landingUrl, returnTo) ? `${returnTo}/learn` : returnTo
22+
);
23+
}
24+
25+
/**
26+
* Fastify plugin for dev authentication.
27+
*
28+
* @param fastify - The Fastify instance.
29+
* @param _options - The plugin options.
30+
* @param done - The callback function.
31+
*/
32+
export const devAuth: FastifyPluginCallbackTypebox = (
33+
fastify,
34+
_options,
35+
done
36+
) => {
37+
fastify.get('/signin', async (req, reply) => {
38+
const email = '[email protected]';
39+
40+
const { id } = await findOrCreateUser(fastify, email);
41+
42+
reply.setAccessTokenCookie(createAccessToken(id));
43+
44+
await handleRedirects(req, reply);
45+
});
46+
47+
done();
48+
};

0 commit comments

Comments
 (0)