Skip to content

Commit d88691b

Browse files
breaking(api): remove screenshot api (freeCodeCamp#61300)
1 parent e807d58 commit d88691b

10 files changed

Lines changed: 12 additions & 339 deletions

File tree

api/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
"@fastify/accepts": "5.0.2",
99
"@fastify/cookie": "11.0.1",
1010
"@fastify/csrf-protection": "7.1.0",
11-
"@fastify/multipart": "^9.0.3",
1211
"@fastify/oauth2": "8.1.2",
1312
"@fastify/swagger": "9.4.0",
1413
"@fastify/swagger-ui": "5.2.0",

api/src/app.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import {
4848
import { isObjectID } from './utils/validation';
4949
import { getLogger } from './utils/logger';
5050
import {
51-
examEnvironmentMultipartRoutes,
5251
examEnvironmentOpenRoutes,
5352
examEnvironmentValidatedTokenRoutes
5453
} from './exam-environment/routes/exam-environment';
@@ -214,7 +213,6 @@ export const build = async (
214213
fastify.addHook('onRequest', fastify.authorizeExamEnvironmentToken);
215214

216215
void fastify.register(examEnvironmentValidatedTokenRoutes);
217-
void fastify.register(examEnvironmentMultipartRoutes);
218216
done();
219217
});
220218
void fastify.register(examEnvironmentOpenRoutes);

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

Lines changed: 0 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Static } from '@fastify/type-provider-typebox';
33
import jwt from 'jsonwebtoken';
44

55
import {
6-
createFetchMock,
76
createSuperRequest,
87
defaultUserId,
98
devLogin,
@@ -587,99 +586,6 @@ describe('/exam-environment/', () => {
587586
});
588587
});
589588

590-
describe('POST /exam-environment/screenshot', () => {
591-
afterEach(async () => {
592-
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.deleteMany();
593-
});
594-
595-
it('should return 400 if request is not multipart form data', async () => {
596-
const res = await superPost('/exam-environment/screenshot').set(
597-
'exam-environment-authorization-token',
598-
examEnvironmentAuthorizationToken
599-
);
600-
601-
expect(res.status).toBe(400);
602-
expect(res.body).toStrictEqual({
603-
code: 'FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT',
604-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
605-
message: expect.any(String)
606-
});
607-
});
608-
609-
it('should return 400 if image is missing', async () => {
610-
const res = await superPost('/exam-environment/screenshot')
611-
.set(
612-
'exam-environment-authorization-token',
613-
examEnvironmentAuthorizationToken
614-
)
615-
.attach('file', '');
616-
617-
expect(res.status).toBe(400);
618-
expect(res.body).toStrictEqual({
619-
code: 'FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT',
620-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
621-
message: expect.any(String)
622-
});
623-
});
624-
625-
it('should return 404 if there is no ongoing exam attempt', async () => {
626-
const res = await superPost('/exam-environment/screenshot')
627-
.set(
628-
'exam-environment-authorization-token',
629-
examEnvironmentAuthorizationToken
630-
)
631-
.attach('file', Buffer.from([]));
632-
633-
expect(res.status).toBe(404);
634-
expect(res.body).toStrictEqual({
635-
code: 'FCC_ERR_EXAM_ENVIRONMENT_EXAM_ATTEMPT',
636-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
637-
message: expect.any(String)
638-
});
639-
});
640-
641-
it('should return 400 if image is of wrong format', async () => {
642-
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
643-
data: mock.examAttempt
644-
});
645-
646-
const res = await superPost('/exam-environment/screenshot')
647-
.set(
648-
'exam-environment-authorization-token',
649-
examEnvironmentAuthorizationToken
650-
)
651-
.attach('file', Buffer.from([]));
652-
653-
expect(res.status).toBe(400);
654-
expect(res.body).toStrictEqual({
655-
code: 'FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT',
656-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
657-
message: expect.any(String)
658-
});
659-
});
660-
661-
it('should return 200 if request is valid and send image to screenshot upload service', async () => {
662-
// Mock image upload service response
663-
const imageUploadRes = createFetchMock({ ok: true });
664-
jest.spyOn(globalThis, 'fetch').mockImplementation(imageUploadRes);
665-
666-
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
667-
data: mock.examAttempt
668-
});
669-
670-
const res = await superPost('/exam-environment/screenshot')
671-
.set(
672-
'exam-environment-authorization-token',
673-
examEnvironmentAuthorizationToken
674-
)
675-
.attach('file', Buffer.from([0xff, 0xd8, 0xff, 0xff]));
676-
677-
expect(res.status).toBe(200);
678-
expect(res.body).toStrictEqual({});
679-
expect(globalThis.fetch).toHaveBeenCalled();
680-
});
681-
});
682-
683589
describe('GET /exam-environment/exams', () => {
684590
it('should return 200', async () => {
685591
const res = await superGet('/exam-environment/exams').set(
@@ -1030,17 +936,6 @@ describe('/exam-environment/', () => {
1030936
});
1031937
});
1032938

1033-
describe('POST /exam-environment/screenshot', () => {
1034-
it('should return 403', async () => {
1035-
const res = await superPost('/exam-environment/screenshot').set(
1036-
'exam-environment-authorization-token',
1037-
'invalid-token'
1038-
);
1039-
1040-
expect(res.status).toBe(403);
1041-
});
1042-
});
1043-
1044939
describe('GET /exam-environment/token-meta', () => {
1045940
it('should reject invalid tokens', async () => {
1046941
const res = await superGet('/exam-environment/token-meta').set(

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

Lines changed: 1 addition & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
/* eslint-disable jsdoc/require-returns, jsdoc/require-param */
22
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
3-
import fastifyMultipart from '@fastify/multipart';
43
import { PrismaClientValidationError } from '@prisma/client/runtime/library';
54
import { type FastifyInstance, type FastifyReply } from 'fastify';
65
import { ExamEnvironmentExamModerationStatus } from '@prisma/client';
76
import jwt from 'jsonwebtoken';
87

98
import * as schemas from '../schemas';
109
import { mapErr, syncMapErr, UpdateReqType } from '../../utils';
11-
import { JWT_SECRET, SCREENSHOT_SERVICE_LOCATION } from '../../utils/env';
10+
import { JWT_SECRET } from '../../utils/env';
1211
import {
1312
checkPrerequisites,
1413
constructEnvExamAttempt,
@@ -65,29 +64,6 @@ export const examEnvironmentValidatedTokenRoutes: FastifyPluginCallbackTypebox =
6564
done();
6665
};
6766

68-
/**
69-
* Wrapper for endpoints related to the exam environment desktop app.
70-
*
71-
* Requires multipart form data to be supported.
72-
*/
73-
export const examEnvironmentMultipartRoutes: FastifyPluginCallbackTypebox = (
74-
fastify,
75-
_options,
76-
done
77-
) => {
78-
void fastify.register(fastifyMultipart);
79-
80-
fastify.post(
81-
'/exam-environment/screenshot',
82-
{
83-
schema: schemas.examEnvironmentPostScreenshot
84-
// bodyLimit: 1024 * 1024 * 5 // 5MiB
85-
},
86-
postScreenshotHandler
87-
);
88-
done();
89-
};
90-
9167
/**
9268
* Wrapper for endpoints related to the exam environment desktop app.
9369
*
@@ -684,155 +660,6 @@ async function postExamAttemptHandler(
684660
return reply.code(200).send();
685661
}
686662

687-
/**
688-
* Handles screenshots, sending them to the screenshot service for storage.
689-
*
690-
* Requires token to be validated.
691-
*/
692-
async function postScreenshotHandler(
693-
this: FastifyInstance,
694-
req: UpdateReqType<typeof schemas.examEnvironmentPostScreenshot>,
695-
reply: FastifyReply
696-
) {
697-
const logger = this.log.child({ req });
698-
logger.info({ userId: req.user?.id });
699-
const isMultipart = req.isMultipart();
700-
701-
if (!isMultipart) {
702-
logger.warn('Request is not multipart form data.');
703-
void reply.code(400);
704-
return reply.send(
705-
ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT(
706-
'Request is not multipart form data.'
707-
)
708-
);
709-
}
710-
711-
const user = req.user!;
712-
const imgData = await req.file();
713-
714-
if (!imgData) {
715-
logger.warn('No image provided.');
716-
void reply.code(400);
717-
return reply.send(
718-
ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT('No image provided.')
719-
);
720-
}
721-
722-
const maybeAttempts = await mapErr(
723-
this.prisma.examEnvironmentExamAttempt.findMany({
724-
where: {
725-
userId: user.id
726-
}
727-
})
728-
);
729-
730-
if (maybeAttempts.hasError) {
731-
logger.error(maybeAttempts.error);
732-
this.Sentry.captureException(maybeAttempts.error);
733-
void reply.code(500);
734-
return reply.send(
735-
ERRORS.FCC_ERR_EXAM_ENVIRONMENT(JSON.stringify(maybeAttempts.error))
736-
);
737-
}
738-
739-
const attempts = maybeAttempts.data;
740-
741-
if (attempts.length === 0) {
742-
logger.warn('No exam attempts found for user.');
743-
void reply.code(404);
744-
return reply.send(
745-
ERRORS.FCC_ERR_EXAM_ENVIRONMENT_EXAM_ATTEMPT(
746-
`No exam attempts found for user '${user.id}'.`
747-
)
748-
);
749-
}
750-
751-
const latestAttempt = attempts.reduce((latest, current) =>
752-
latest.startTimeInMS > current.startTimeInMS ? latest : current
753-
);
754-
755-
const maybeExam = await mapErr(
756-
this.prisma.examEnvironmentExam.findUnique({
757-
where: {
758-
id: latestAttempt.examId
759-
},
760-
select: {
761-
id: true,
762-
config: true
763-
}
764-
})
765-
);
766-
767-
if (maybeExam.hasError) {
768-
logger.error(maybeExam.error);
769-
this.Sentry.captureException(maybeExam.error);
770-
void reply.code(500);
771-
return reply.send(
772-
ERRORS.FCC_ERR_EXAM_ENVIRONMENT(JSON.stringify(maybeExam.error))
773-
);
774-
}
775-
776-
const exam = maybeExam.data;
777-
778-
if (exam === null) {
779-
const error = {
780-
data: {
781-
examId: latestAttempt.examId,
782-
attemptId: latestAttempt.id
783-
},
784-
message: 'Unreachable. Attempt could not be related to an exam.'
785-
};
786-
logger.error(error.data, error.message);
787-
this.Sentry.captureException(error.data);
788-
void reply.code(500);
789-
return reply.send(
790-
ERRORS.FCC_ENOENT_EXAM_ENVIRONMENT_MISSING_EXAM(error.message)
791-
);
792-
}
793-
794-
const isAttemptExpired =
795-
latestAttempt.startTimeInMS + exam.config.totalTimeInMS < Date.now();
796-
if (isAttemptExpired) {
797-
logger.warn(
798-
{ examAttemptId: latestAttempt.id },
799-
'Attempt has exceeded submission time.'
800-
);
801-
void reply.code(403);
802-
return reply.send(
803-
ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_EXAM_ATTEMPT(
804-
'Attempt has exceeded submission time.'
805-
)
806-
);
807-
}
808-
809-
const imgBinary = await imgData.toBuffer();
810-
811-
// Verify image is JPG using magic number
812-
if (imgBinary[0] !== 0xff || imgBinary[1] !== 0xd8 || imgBinary[2] !== 0xff) {
813-
logger.warn('Invalid image format');
814-
void reply.code(400);
815-
return reply.send(
816-
ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT('Invalid image format.')
817-
);
818-
}
819-
820-
void reply.code(200).send();
821-
822-
const uploadData = {
823-
image: imgBinary.toString('base64'),
824-
examAttemptId: latestAttempt.id
825-
};
826-
827-
await fetch(`${SCREENSHOT_SERVICE_LOCATION}/upload`, {
828-
method: 'POST',
829-
headers: {
830-
'Content-Type': 'application/json'
831-
},
832-
body: JSON.stringify(uploadData)
833-
});
834-
}
835-
836663
async function getExams(
837664
this: FastifyInstance,
838665
req: UpdateReqType<typeof schemas.examEnvironmentExams>,

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@ export {
44
examEnvironmentGetExamAttempt
55
} from './exam-environment-exam-attempt';
66
export { examEnvironmentPostExamGeneratedExam } from './exam-environment-exam-generated-exam';
7-
export { examEnvironmentPostScreenshot } from './screenshot';
87
export { examEnvironmentTokenMeta } from './token-meta';
98
export { examEnvironmentExams } from './exam-environment-exams';

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

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

api/src/exam-environment/utils/errors.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,7 @@ export const ERRORS = {
3939
'FCC_ENOENT_EXAM_ENVIRONMENT_GENERATED_EXAM',
4040
'%s'
4141
),
42-
FCC_EINVAL_EXAM_ID: createError('FCC_EINVAL_EXAM_ID', '%s'),
43-
FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT: createError(
44-
'FCC_EINVAL_EXAM_ENVIRONMENT_SCREENSHOT',
45-
'%s'
46-
)
42+
FCC_EINVAL_EXAM_ID: createError('FCC_EINVAL_EXAM_ID', '%s')
4743
};
4844

4945
/**

0 commit comments

Comments
 (0)