Skip to content

Commit bb16ab9

Browse files
breaking(api): refactor exam environment endpoints (freeCodeCamp#56806)
1 parent a580118 commit bb16ab9

File tree

10 files changed

+74
-93
lines changed

10 files changed

+74
-93
lines changed

api/prisma/schema.prisma

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,11 @@ model UserToken {
379379
}
380380

381381
model ExamEnvironmentAuthorizationToken {
382-
id String @id @map("_id")
383-
createdDate DateTime @db.Date
384-
userId String @unique @db.ObjectId
382+
/// An ObjectId is used to provide access to the created timestamp
383+
id String @id @default(auto()) @map("_id") @db.ObjectId
384+
/// Used to set an `expireAt` index to delete documents
385+
expireAt DateTime @db.Date
386+
userId String @unique @db.ObjectId
385387
386388
// Relations
387389
user user @relation(fields: [userId], references: [id])

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

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ describe('/exam-environment/', () => {
3535
await mock.seedEnvExam();
3636
// Add exam environment authorization token
3737
const res = await superPost('/user/exam-environment/token');
38-
expect(res.status).toBe(200);
38+
expect(res.status).toBe(201);
3939
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
4040
examEnvironmentAuthorizationToken =
4141
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
42-
res.body.data.examEnvironmentAuthorizationToken;
42+
res.body.examEnvironmentAuthorizationToken;
4343
});
4444

4545
describe('POST /exam-environment/exam/attempt', () => {
@@ -389,11 +389,9 @@ describe('/exam-environment/', () => {
389389
expect(res).toMatchObject({
390390
status: 200,
391391
body: {
392-
data: {
393-
examAttempt: {
394-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
395-
id: expect.not.stringMatching(mock.examAttempt.id)
396-
}
392+
examAttempt: {
393+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
394+
id: expect.not.stringMatching(mock.examAttempt.id)
397395
}
398396
}
399397
});
@@ -419,9 +417,7 @@ describe('/exam-environment/', () => {
419417
expect(res).toMatchObject({
420418
status: 200,
421419
body: {
422-
data: {
423-
examAttempt: latestAttempt
424-
}
420+
examAttempt: latestAttempt
425421
}
426422
});
427423
});
@@ -555,10 +551,8 @@ describe('/exam-environment/', () => {
555551
expect(res).toMatchObject({
556552
status: 200,
557553
body: {
558-
data: {
559-
examAttempt,
560-
exam: userExam
561-
}
554+
examAttempt,
555+
exam: userExam
562556
}
563557
});
564558
});
@@ -644,15 +638,15 @@ describe('/exam-environment/', () => {
644638
});
645639
});
646640

647-
describe('POST /exam-environment/token/verify', () => {
641+
describe('GET /exam-environment/token-meta', () => {
648642
it('should allow a valid request', async () => {
649-
const res = await superPost('/exam-environment/token/verify').set(
643+
const res = await superGet('/exam-environment/token-meta').set(
650644
'exam-environment-authorization-token',
651645
'invalid-token'
652646
);
653647

654648
expect(res).toMatchObject({
655-
status: 200,
649+
status: 418,
656650
body: {
657651
code: 'FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN'
658652
}

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

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ export const examEnvironmentOpenRoutes: FastifyPluginCallbackTypebox = (
6464
_options,
6565
done
6666
) => {
67-
fastify.post(
68-
'/exam-environment/token/verify',
67+
fastify.get(
68+
'/exam-environment/token-meta',
6969
{
70-
schema: schemas.examEnvironmentTokenVerify
70+
schema: schemas.examEnvironmentTokenMeta
7171
},
72-
tokenVerifyHandler
72+
tokenMetaHandler
7373
);
7474
done();
7575
};
@@ -85,18 +85,18 @@ interface JwtPayload {
8585
*
8686
* **Note**: This has no guarantees of which user the token is for. Just that one exists in the database.
8787
*/
88-
async function tokenVerifyHandler(
88+
async function tokenMetaHandler(
8989
this: FastifyInstance,
90-
req: UpdateReqType<typeof schemas.examEnvironmentTokenVerify>,
90+
req: UpdateReqType<typeof schemas.examEnvironmentTokenMeta>,
9191
reply: FastifyReply
9292
) {
9393
const { 'exam-environment-authorization-token': encodedToken } = req.headers;
9494

9595
try {
9696
jwt.verify(encodedToken, JWT_SECRET);
9797
} catch (e) {
98-
// TODO: What to send back here? Request is valid, but token is not?
99-
void reply.code(200);
98+
// Server refuses to brew (verify) coffee (jwts) with a teapot (random strings)
99+
void reply.code(418);
100100
return reply.send(
101101
ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN(JSON.stringify(e))
102102
);
@@ -114,16 +114,17 @@ async function tokenVerifyHandler(
114114
});
115115

116116
if (!token) {
117-
void reply.code(200);
118-
return reply.send({
119-
data: 'Token does not appear to have been created.'
120-
});
117+
// Endpoint is valid, but resource does not exists
118+
void reply.code(404);
119+
return reply.send(
120+
ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN(
121+
'Token does not appear to exist'
122+
)
123+
);
121124
} else {
122125
void reply.code(200);
123126
return reply.send({
124-
data: {
125-
createdDate: token.createdDate
126-
}
127+
expireAt: token.expireAt
127128
});
128129
}
129130
}
@@ -257,10 +258,8 @@ async function postExamGeneratedExamHandler(
257258
const userExam = constructUserExam(generated.data, exam);
258259

259260
return reply.send({
260-
data: {
261-
exam: userExam,
262-
examAttempt: lastAttempt
263-
}
261+
exam: userExam,
262+
examAttempt: lastAttempt
264263
});
265264
}
266265
}
@@ -375,10 +374,8 @@ async function postExamGeneratedExamHandler(
375374

376375
void reply.code(200);
377376
return reply.send({
378-
data: {
379-
exam: userExam,
380-
examAttempt: attempt.data
381-
}
377+
exam: userExam,
378+
examAttempt: attempt.data
382379
});
383380
}
384381

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ export const examEnvironmentPostExamGeneratedExam = {
1010
}),
1111
response: {
1212
200: Type.Object({
13-
data: Type.Object({
14-
exam: Type.Record(Type.String(), Type.Unknown()),
15-
examAttempt: Type.Record(Type.String(), Type.Unknown())
16-
})
13+
exam: Type.Record(Type.String(), Type.Unknown()),
14+
examAttempt: Type.Record(Type.String(), Type.Unknown())
1715
}),
1816
403: STANDARD_ERROR,
1917
404: STANDARD_ERROR,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { examEnvironmentPostExamAttempt } from './exam-attempt';
22
export { examEnvironmentPostExamGeneratedExam } from './exam-generated-exam';
33
export { examEnvironmentPostScreenshot } from './screenshot';
4-
export { examEnvironmentTokenVerify } from './token-verify';
4+
export { examEnvironmentTokenMeta } from './token-meta';
55
export { examEnvironmentExams } from './exams';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Type } from '@fastify/type-provider-typebox';
2+
import { STANDARD_ERROR } from '../utils/errors';
3+
4+
export const examEnvironmentTokenMeta = {
5+
headers: Type.Object({
6+
'exam-environment-authorization-token': Type.String()
7+
}),
8+
response: {
9+
200: Type.Object({
10+
expireAt: Type.String({ format: 'date-time' })
11+
}),
12+
404: STANDARD_ERROR,
13+
418: STANDARD_ERROR
14+
}
15+
};

api/src/exam-environment/schemas/token-verify.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

api/src/routes/protected/user.test.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
createSuperRequest
1717
} from '../../../jest.utils';
1818
import { JWT_SECRET } from '../../utils/env';
19-
import { customNanoid } from '../../utils/ids';
2019
import { getMsTranscriptApiUrl } from './user';
2120

2221
const mockedFetch = jest.fn();
@@ -1148,13 +1147,13 @@ Thanks and regards,
11481147

11491148
test('POST generates a new token if one does not exist', async () => {
11501149
const response = await superPost('/user/exam-environment/token');
1151-
const { examEnvironmentAuthorizationToken } = response.body.data;
1150+
const { examEnvironmentAuthorizationToken } = response.body;
11521151

11531152
const decodedToken = jwt.decode(examEnvironmentAuthorizationToken);
11541153

11551154
expect(decodedToken).toStrictEqual({
11561155
examEnvironmentAuthorizationToken:
1157-
expect.stringMatching(/^[a-zA-Z0-9]{64}$/),
1156+
expect.stringMatching(/^[a-z0-9]{24}$/),
11581157
iat: expect.any(Number)
11591158
});
11601159

@@ -1165,33 +1164,32 @@ Thanks and regards,
11651164
jwt.verify(examEnvironmentAuthorizationToken, JWT_SECRET)
11661165
).not.toThrow();
11671166

1168-
expect(response.status).toBe(200);
1167+
expect(response.status).toBe(201);
11691168
});
11701169

11711170
test('POST only allows for one token per user id', async () => {
1172-
const id = customNanoid();
1173-
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.create(
1174-
{
1175-
data: {
1176-
userId: defaultUserId,
1177-
id,
1178-
createdDate: new Date()
1171+
const token =
1172+
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.create(
1173+
{
1174+
data: {
1175+
userId: defaultUserId,
1176+
expireAt: new Date()
1177+
}
11791178
}
1180-
}
1181-
);
1179+
);
11821180

11831181
const response = await superPost('/user/exam-environment/token');
11841182

1185-
const { examEnvironmentAuthorizationToken } = response.body.data;
1183+
const { examEnvironmentAuthorizationToken } = response.body;
11861184

11871185
const decodedToken = jwt.decode(examEnvironmentAuthorizationToken);
11881186

11891187
expect(decodedToken).not.toHaveProperty(
11901188
'examEnvironmentAuthorizationToken',
1191-
id
1189+
token.id
11921190
);
11931191

1194-
expect(response.status).toBe(200);
1192+
expect(response.status).toBe(201);
11951193

11961194
const tokens =
11971195
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.findMany(

api/src/routes/protected/user.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,11 @@ async function examEnvironmentTokenHandler(
397397
}
398398
});
399399

400+
const ONE_YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000;
401+
400402
const token = await this.prisma.examEnvironmentAuthorizationToken.create({
401403
data: {
402-
createdDate: new Date(),
403-
id: customNanoid(),
404+
expireAt: new Date(Date.now() + ONE_YEAR_IN_MS),
404405
userId
405406
}
406407
});
@@ -410,10 +411,9 @@ async function examEnvironmentTokenHandler(
410411
JWT_SECRET
411412
);
412413

414+
void reply.code(201);
413415
void reply.send({
414-
data: {
415-
examEnvironmentAuthorizationToken
416-
}
416+
examEnvironmentAuthorizationToken
417417
});
418418
}
419419

api/src/schemas/user/exam-environment-token.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { Type } from '@fastify/type-provider-typebox';
33
export const userExamEnvironmentToken = {
44
response: {
55
200: Type.Object({
6-
data: Type.Object({
7-
examEnvironmentAuthorizationToken: Type.String()
8-
})
6+
examEnvironmentAuthorizationToken: Type.String()
97
})
108
}
119
};

0 commit comments

Comments
 (0)