Skip to content

Commit

Permalink
breaking(api): refactor exam environment endpoints (freeCodeCamp#56806)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShaunSHamilton authored Oct 30, 2024
1 parent a580118 commit bb16ab9
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 93 deletions.
8 changes: 5 additions & 3 deletions api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,11 @@ model UserToken {
}

model ExamEnvironmentAuthorizationToken {
id String @id @map("_id")
createdDate DateTime @db.Date
userId String @unique @db.ObjectId
/// An ObjectId is used to provide access to the created timestamp
id String @id @default(auto()) @map("_id") @db.ObjectId
/// Used to set an `expireAt` index to delete documents
expireAt DateTime @db.Date
userId String @unique @db.ObjectId
// Relations
user user @relation(fields: [userId], references: [id])
Expand Down
28 changes: 11 additions & 17 deletions api/src/exam-environment/routes/exam-environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ describe('/exam-environment/', () => {
await mock.seedEnvExam();
// Add exam environment authorization token
const res = await superPost('/user/exam-environment/token');
expect(res.status).toBe(200);
expect(res.status).toBe(201);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
examEnvironmentAuthorizationToken =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
res.body.data.examEnvironmentAuthorizationToken;
res.body.examEnvironmentAuthorizationToken;
});

describe('POST /exam-environment/exam/attempt', () => {
Expand Down Expand Up @@ -389,11 +389,9 @@ describe('/exam-environment/', () => {
expect(res).toMatchObject({
status: 200,
body: {
data: {
examAttempt: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
id: expect.not.stringMatching(mock.examAttempt.id)
}
examAttempt: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
id: expect.not.stringMatching(mock.examAttempt.id)
}
}
});
Expand All @@ -419,9 +417,7 @@ describe('/exam-environment/', () => {
expect(res).toMatchObject({
status: 200,
body: {
data: {
examAttempt: latestAttempt
}
examAttempt: latestAttempt
}
});
});
Expand Down Expand Up @@ -555,10 +551,8 @@ describe('/exam-environment/', () => {
expect(res).toMatchObject({
status: 200,
body: {
data: {
examAttempt,
exam: userExam
}
examAttempt,
exam: userExam
}
});
});
Expand Down Expand Up @@ -644,15 +638,15 @@ describe('/exam-environment/', () => {
});
});

describe('POST /exam-environment/token/verify', () => {
describe('GET /exam-environment/token-meta', () => {
it('should allow a valid request', async () => {
const res = await superPost('/exam-environment/token/verify').set(
const res = await superGet('/exam-environment/token-meta').set(
'exam-environment-authorization-token',
'invalid-token'
);

expect(res).toMatchObject({
status: 200,
status: 418,
body: {
code: 'FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN'
}
Expand Down
43 changes: 20 additions & 23 deletions api/src/exam-environment/routes/exam-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ export const examEnvironmentOpenRoutes: FastifyPluginCallbackTypebox = (
_options,
done
) => {
fastify.post(
'/exam-environment/token/verify',
fastify.get(
'/exam-environment/token-meta',
{
schema: schemas.examEnvironmentTokenVerify
schema: schemas.examEnvironmentTokenMeta
},
tokenVerifyHandler
tokenMetaHandler
);
done();
};
Expand All @@ -85,18 +85,18 @@ interface JwtPayload {
*
* **Note**: This has no guarantees of which user the token is for. Just that one exists in the database.
*/
async function tokenVerifyHandler(
async function tokenMetaHandler(
this: FastifyInstance,
req: UpdateReqType<typeof schemas.examEnvironmentTokenVerify>,
req: UpdateReqType<typeof schemas.examEnvironmentTokenMeta>,
reply: FastifyReply
) {
const { 'exam-environment-authorization-token': encodedToken } = req.headers;

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

if (!token) {
void reply.code(200);
return reply.send({
data: 'Token does not appear to have been created.'
});
// Endpoint is valid, but resource does not exists
void reply.code(404);
return reply.send(
ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_AUTHORIZATION_TOKEN(
'Token does not appear to exist'
)
);
} else {
void reply.code(200);
return reply.send({
data: {
createdDate: token.createdDate
}
expireAt: token.expireAt
});
}
}
Expand Down Expand Up @@ -257,10 +258,8 @@ async function postExamGeneratedExamHandler(
const userExam = constructUserExam(generated.data, exam);

return reply.send({
data: {
exam: userExam,
examAttempt: lastAttempt
}
exam: userExam,
examAttempt: lastAttempt
});
}
}
Expand Down Expand Up @@ -375,10 +374,8 @@ async function postExamGeneratedExamHandler(

void reply.code(200);
return reply.send({
data: {
exam: userExam,
examAttempt: attempt.data
}
exam: userExam,
examAttempt: attempt.data
});
}

Expand Down
6 changes: 2 additions & 4 deletions api/src/exam-environment/schemas/exam-generated-exam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ export const examEnvironmentPostExamGeneratedExam = {
}),
response: {
200: Type.Object({
data: Type.Object({
exam: Type.Record(Type.String(), Type.Unknown()),
examAttempt: Type.Record(Type.String(), Type.Unknown())
})
exam: Type.Record(Type.String(), Type.Unknown()),
examAttempt: Type.Record(Type.String(), Type.Unknown())
}),
403: STANDARD_ERROR,
404: STANDARD_ERROR,
Expand Down
2 changes: 1 addition & 1 deletion api/src/exam-environment/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { examEnvironmentPostExamAttempt } from './exam-attempt';
export { examEnvironmentPostExamGeneratedExam } from './exam-generated-exam';
export { examEnvironmentPostScreenshot } from './screenshot';
export { examEnvironmentTokenVerify } from './token-verify';
export { examEnvironmentTokenMeta } from './token-meta';
export { examEnvironmentExams } from './exams';
15 changes: 15 additions & 0 deletions api/src/exam-environment/schemas/token-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Type } from '@fastify/type-provider-typebox';
import { STANDARD_ERROR } from '../utils/errors';

export const examEnvironmentTokenMeta = {
headers: Type.Object({
'exam-environment-authorization-token': Type.String()
}),
response: {
200: Type.Object({
expireAt: Type.String({ format: 'date-time' })
}),
404: STANDARD_ERROR,
418: STANDARD_ERROR
}
};
21 changes: 0 additions & 21 deletions api/src/exam-environment/schemas/token-verify.ts

This file was deleted.

30 changes: 14 additions & 16 deletions api/src/routes/protected/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
createSuperRequest
} from '../../../jest.utils';
import { JWT_SECRET } from '../../utils/env';
import { customNanoid } from '../../utils/ids';
import { getMsTranscriptApiUrl } from './user';

const mockedFetch = jest.fn();
Expand Down Expand Up @@ -1148,13 +1147,13 @@ Thanks and regards,

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

const decodedToken = jwt.decode(examEnvironmentAuthorizationToken);

expect(decodedToken).toStrictEqual({
examEnvironmentAuthorizationToken:
expect.stringMatching(/^[a-zA-Z0-9]{64}$/),
expect.stringMatching(/^[a-z0-9]{24}$/),
iat: expect.any(Number)
});

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

expect(response.status).toBe(200);
expect(response.status).toBe(201);
});

test('POST only allows for one token per user id', async () => {
const id = customNanoid();
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.create(
{
data: {
userId: defaultUserId,
id,
createdDate: new Date()
const token =
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.create(
{
data: {
userId: defaultUserId,
expireAt: new Date()
}
}
}
);
);

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

const { examEnvironmentAuthorizationToken } = response.body.data;
const { examEnvironmentAuthorizationToken } = response.body;

const decodedToken = jwt.decode(examEnvironmentAuthorizationToken);

expect(decodedToken).not.toHaveProperty(
'examEnvironmentAuthorizationToken',
id
token.id
);

expect(response.status).toBe(200);
expect(response.status).toBe(201);

const tokens =
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.findMany(
Expand Down
10 changes: 5 additions & 5 deletions api/src/routes/protected/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,11 @@ async function examEnvironmentTokenHandler(
}
});

const ONE_YEAR_IN_MS = 365 * 24 * 60 * 60 * 1000;

const token = await this.prisma.examEnvironmentAuthorizationToken.create({
data: {
createdDate: new Date(),
id: customNanoid(),
expireAt: new Date(Date.now() + ONE_YEAR_IN_MS),
userId
}
});
Expand All @@ -410,10 +411,9 @@ async function examEnvironmentTokenHandler(
JWT_SECRET
);

void reply.code(201);
void reply.send({
data: {
examEnvironmentAuthorizationToken
}
examEnvironmentAuthorizationToken
});
}

Expand Down
4 changes: 1 addition & 3 deletions api/src/schemas/user/exam-environment-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { Type } from '@fastify/type-provider-typebox';
export const userExamEnvironmentToken = {
response: {
200: Type.Object({
data: Type.Object({
examEnvironmentAuthorizationToken: Type.String()
})
examEnvironmentAuthorizationToken: Type.String()
})
}
};

0 comments on commit bb16ab9

Please sign in to comment.