mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
breaking(api): refactor exam environment endpoints (#56806)
This commit is contained in:
@@ -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])
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -419,9 +417,7 @@ describe('/exam-environment/', () => {
|
||||
expect(res).toMatchObject({
|
||||
status: 200,
|
||||
body: {
|
||||
data: {
|
||||
examAttempt: latestAttempt
|
||||
}
|
||||
examAttempt: latestAttempt
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -555,10 +551,8 @@ describe('/exam-environment/', () => {
|
||||
expect(res).toMatchObject({
|
||||
status: 200,
|
||||
body: {
|
||||
data: {
|
||||
examAttempt,
|
||||
exam: userExam
|
||||
}
|
||||
examAttempt,
|
||||
exam: userExam
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -85,9 +85,9 @@ 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;
|
||||
@@ -95,8 +95,8 @@ async function tokenVerifyHandler(
|
||||
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))
|
||||
);
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
import { STANDARD_ERROR } from '../utils/errors';
|
||||
|
||||
export const examEnvironmentTokenVerify = {
|
||||
headers: Type.Object({
|
||||
'exam-environment-authorization-token': Type.String()
|
||||
}),
|
||||
response: {
|
||||
200: Type.Union([
|
||||
Type.Object({
|
||||
data: Type.Union([
|
||||
Type.String(),
|
||||
Type.Object({
|
||||
createdDate: Type.String({ format: 'date-time' })
|
||||
})
|
||||
])
|
||||
}),
|
||||
STANDARD_ERROR
|
||||
])
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -410,10 +411,9 @@ async function examEnvironmentTokenHandler(
|
||||
JWT_SECRET
|
||||
);
|
||||
|
||||
void reply.code(201);
|
||||
void reply.send({
|
||||
data: {
|
||||
examEnvironmentAuthorizationToken
|
||||
}
|
||||
examEnvironmentAuthorizationToken
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user