Skip to content

Commit 5180249

Browse files
chore(api): add ExamEnvironmentAuthorizationToken -> user relation (freeCodeCamp#56627)
Co-authored-by: Oliver Eyton-Williams <[email protected]>
1 parent 90e122b commit 5180249

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed

api/jest.utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ export const defaultUserEmail = '[email protected]';
207207
export const defaultUsername = 'fcc-test-user';
208208

209209
export const resetDefaultUser = async (): Promise<void> => {
210+
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany(
211+
{
212+
where: { userId: defaultUserId }
213+
}
214+
);
210215
await fastifyTestInstance.prisma.user.deleteMany({
211216
where: { email: defaultUserEmail }
212217
});

api/prisma/schema.prisma

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ model user {
143143
isClassroomAccount Boolean? // Undefined
144144
145145
// Relations
146-
examAttempts EnvExamAttempt[]
146+
examAttempts EnvExamAttempt[]
147+
examEnvironmentAuthorizationToken ExamEnvironmentAuthorizationToken?
147148
}
148149

149150
// -----------------------------------
@@ -377,15 +378,13 @@ model UserToken {
377378
@@index([userId], map: "userId_1")
378379
}
379380

380-
/// TODO: Token has to outlive the exam attempt
381-
/// Validation has to be taken as the attempt is requested
382-
/// to ensure it lives long enough.
383381
model ExamEnvironmentAuthorizationToken {
384382
id String @id @map("_id")
385383
createdDate DateTime @db.Date
386-
userId String @db.ObjectId
384+
userId String @unique @db.ObjectId
387385
388-
@@index([userId], map: "userId_1")
386+
// Relations
387+
user user @relation(fields: [userId], references: [id])
389388
}
390389

391390
model sessions {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async function tokenVerifyHandler(
107107
const examEnvironmentAuthorizationToken =
108108
payload.examEnvironmentAuthorizationToken;
109109

110-
const token = await this.prisma.examEnvironmentAuthorizationToken.findFirst({
110+
const token = await this.prisma.examEnvironmentAuthorizationToken.findUnique({
111111
where: {
112112
id: examEnvironmentAuthorizationToken
113113
}

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

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

2122
const mockedFetch = jest.fn();
@@ -564,6 +565,11 @@ describe('userRoutes', () => {
564565
await fastifyTestInstance.prisma.userToken.deleteMany({
565566
where: { id: userTokenId }
566567
});
568+
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany(
569+
{
570+
where: { userId: defaultUserId }
571+
}
572+
);
567573
});
568574

569575
test('GET rejects with 500 status code if the username is missing', async () => {
@@ -1130,6 +1136,72 @@ Thanks and regards,
11301136
});
11311137
});
11321138
});
1139+
1140+
describe('/user/exam-environment/token', () => {
1141+
afterEach(async () => {
1142+
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany(
1143+
{
1144+
where: { userId: defaultUserId }
1145+
}
1146+
);
1147+
});
1148+
1149+
test('POST generates a new token if one does not exist', async () => {
1150+
const response = await superPost('/user/exam-environment/token');
1151+
const { examEnvironmentAuthorizationToken } = response.body.data;
1152+
1153+
const decodedToken = jwt.decode(examEnvironmentAuthorizationToken);
1154+
1155+
expect(decodedToken).toStrictEqual({
1156+
examEnvironmentAuthorizationToken:
1157+
expect.stringMatching(/^[a-zA-Z0-9]{64}$/),
1158+
iat: expect.any(Number)
1159+
});
1160+
1161+
expect(() =>
1162+
jwt.verify(examEnvironmentAuthorizationToken, 'wrong-secret')
1163+
).toThrow();
1164+
expect(() =>
1165+
jwt.verify(examEnvironmentAuthorizationToken, JWT_SECRET)
1166+
).not.toThrow();
1167+
1168+
expect(response.status).toBe(200);
1169+
});
1170+
1171+
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()
1179+
}
1180+
}
1181+
);
1182+
1183+
const response = await superPost('/user/exam-environment/token');
1184+
1185+
const { examEnvironmentAuthorizationToken } = response.body.data;
1186+
1187+
const decodedToken = jwt.decode(examEnvironmentAuthorizationToken);
1188+
1189+
expect(decodedToken).not.toHaveProperty(
1190+
'examEnvironmentAuthorizationToken',
1191+
id
1192+
);
1193+
1194+
expect(response.status).toBe(200);
1195+
1196+
const tokens =
1197+
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.findMany(
1198+
{
1199+
where: { userId: defaultUserId }
1200+
}
1201+
);
1202+
expect(tokens).toHaveLength(1);
1203+
});
1204+
});
11331205
});
11341206

11351207
describe('Unauthenticated user', () => {

0 commit comments

Comments
 (0)