breaking(api): refactor exam environment endpoints (#56806)

This commit is contained in:
Shaun Hamilton
2024-10-30 07:15:31 +02:00
committed by GitHub
parent a580118e90
commit bb16ab9245
10 changed files with 74 additions and 93 deletions
+5 -3
View File
@@ -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 -1
View File
@@ -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
])
}
};
+14 -16
View File
@@ -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(
+5 -5
View File
@@ -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()
})
}
};