Files
freecodecamp--freecodecamp/api/src/routes/apps/classroom.test.ts
T
wehub-resource-sync dde272c4b8
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:55:53 +08:00

335 lines
10 KiB
TypeScript

import { describe, test, expect, afterEach, vi } from 'vitest';
vi.mock('../../utils/env', async importOriginal => {
const actual = await importOriginal<typeof import('../../utils/env.js')>();
return {
...actual,
TPA_API_BEARER_TOKEN: 'test-classroom-api-secret',
FCC_ENABLE_CLASSROOM: true
};
});
import request from 'supertest';
import { createUserInput } from '../../utils/create-user.js';
import {
defaultUserEmail,
defaultUserId,
resetDefaultUser,
setupServer
} from '../../../vitest.utils.js';
const BEARER_TOKEN = 'test-classroom-api-secret';
const classroomUserEmail = 'student1@example.com';
const nonClassroomUserEmail = 'student2@example.com';
const classroomUserId = '000000000000000000000001';
const nonClassroomUserId = '000000000000000000000002';
function post(url: string) {
return request(fastifyTestInstance.server)
.post(url)
.set('authorization', `Bearer ${BEARER_TOKEN}`);
}
describe('classroom routes', () => {
setupServer();
afterEach(async () => {
vi.restoreAllMocks();
await fastifyTestInstance.prisma.user.deleteMany({
where: { email: { in: [classroomUserEmail, nonClassroomUserEmail] } }
});
await resetDefaultUser();
});
describe('Without bearer token', () => {
test('POST get-user-id returns 401', async () => {
const res = await request(fastifyTestInstance.server)
.post('/apps/classroom/get-user-id')
.send({ email: 'someone@example.com' });
expect(res.status).toBe(401);
expect(res.body).toStrictEqual({ error: 'Bearer token is required' });
});
test('POST get-user-data returns 401', async () => {
const res = await request(fastifyTestInstance.server)
.post('/apps/classroom/get-user-data')
.send({ userIds: [defaultUserId] });
expect(res.status).toBe(401);
expect(res.body).toStrictEqual({ error: 'Bearer token is required' });
});
});
describe('With wrong bearer token', () => {
test('POST get-user-id returns 401', async () => {
const res = await request(fastifyTestInstance.server)
.post('/apps/classroom/get-user-id')
.set('authorization', 'Bearer wrong-key')
.send({ email: 'someone@example.com' });
expect(res.status).toBe(401);
expect(res.body).toStrictEqual({ error: 'Invalid bearer token' });
});
test('POST get-user-data returns 401', async () => {
const res = await request(fastifyTestInstance.server)
.post('/apps/classroom/get-user-data')
.set('authorization', 'Bearer wrong-key')
.send({ userIds: [defaultUserId] });
expect(res.status).toBe(401);
expect(res.body).toStrictEqual({ error: 'Invalid bearer token' });
});
});
describe('Authenticated with API key', () => {
describe('POST /apps/classroom/get-user-id', () => {
test('returns 400 for missing email', async () => {
const res = await post('/apps/classroom/get-user-id').send({});
expect(res.status).toBe(400);
});
test('returns 400 for invalid email format', async () => {
const res = await post('/apps/classroom/get-user-id').send({
email: 'not-an-email'
});
expect(res.status).toBe(400);
});
test('returns 200 with empty userId when no classroom account matches email', async () => {
const count = vi.fn();
const originalSentry = fastifyTestInstance.Sentry;
fastifyTestInstance.Sentry = {
...originalSentry,
metrics: { ...originalSentry.metrics, count }
};
const res = await post('/apps/classroom/get-user-id').send({
email: defaultUserEmail
});
fastifyTestInstance.Sentry = originalSentry;
expect(res.status).toBe(200);
expect(res.body).toStrictEqual({ userId: '' });
expect(count).toHaveBeenCalledWith('classroom.user_looked_up', 1, {
attributes: { result: 'not_found' }
});
});
test('returns 200 with userId for a classroom account', async () => {
await fastifyTestInstance.prisma.user.update({
where: { id: defaultUserId },
data: { isClassroomAccount: true }
});
const count = vi.fn();
const originalSentry = fastifyTestInstance.Sentry;
fastifyTestInstance.Sentry = {
...originalSentry,
metrics: { ...originalSentry.metrics, count }
};
const res = await post('/apps/classroom/get-user-id').send({
email: defaultUserEmail
});
fastifyTestInstance.Sentry = originalSentry;
expect(res.status).toBe(200);
expect(res.body).toStrictEqual({ userId: defaultUserId });
expect(count).toHaveBeenCalledWith('classroom.user_looked_up', 1, {
attributes: { result: 'found' }
});
});
test('returns 500 and captures when the database query fails', async () => {
const originalSentry = fastifyTestInstance.Sentry;
const captureException = vi.fn();
fastifyTestInstance.Sentry = { ...originalSentry, captureException };
const original = fastifyTestInstance.prisma.user.findFirst;
fastifyTestInstance.prisma.user.findFirst = vi
.fn()
.mockRejectedValue(new Error('test')) as typeof original;
const res = await post('/apps/classroom/get-user-id').send({
email: defaultUserEmail
});
fastifyTestInstance.prisma.user.findFirst = original;
fastifyTestInstance.Sentry = originalSentry;
expect(res.status).toBe(500);
expect(res.body).toStrictEqual({
error: 'Failed to retrieve user id'
});
expect(captureException).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ message: 'test' })
);
});
});
describe('POST /apps/classroom/get-user-data', () => {
test('returns 400 when more than 50 userIds are provided', async () => {
const tooMany = Array.from(
{ length: 51 },
(_, i) => `${String(i).padStart(24, '0')}`
);
const res = await post('/apps/classroom/get-user-data').send({
userIds: tooMany
});
expect(res.status).toBe(400);
});
test('returns 200 with empty data for empty userIds array', async () => {
const res = await post('/apps/classroom/get-user-data').send({
userIds: []
});
expect(res.status).toBe(200);
expect(res.body).toStrictEqual({ data: {} });
});
test('returns data only for classroom accounts', async () => {
const now = Date.now();
await fastifyTestInstance.prisma.user.update({
where: { id: defaultUserId },
data: {
isClassroomAccount: true,
completedChallenges: [
{
id: 'challenge-default',
completedDate: now,
files: []
}
]
}
});
await fastifyTestInstance.prisma.user.create({
data: {
...createUserInput(classroomUserEmail),
id: classroomUserId,
isClassroomAccount: true,
completedChallenges: [
{
id: 'challenge-student',
completedDate: now + 1,
files: []
}
]
}
});
await fastifyTestInstance.prisma.user.create({
data: {
...createUserInput(nonClassroomUserEmail),
id: nonClassroomUserId,
isClassroomAccount: false,
completedChallenges: []
}
});
const res = await post('/apps/classroom/get-user-data').send({
userIds: [defaultUserId, classroomUserId, nonClassroomUserId]
});
expect(res.status).toBe(200);
const responseBody = res.body as {
data: Record<
string,
Array<{ id: string; completedDate: number }> | undefined
>;
};
expect(Object.keys(responseBody.data)).toEqual(
expect.arrayContaining([defaultUserId, classroomUserId])
);
expect(responseBody.data).not.toHaveProperty(nonClassroomUserId);
expect(responseBody.data[defaultUserId]?.[0]).toStrictEqual({
id: 'challenge-default',
completedDate: now
});
expect(responseBody.data[classroomUserId]?.[0]).toStrictEqual({
id: 'challenge-student',
completedDate: now + 1
});
});
test('response contains only id and completedDate', async () => {
const now = Date.now();
await fastifyTestInstance.prisma.user.update({
where: { id: defaultUserId },
data: {
isClassroomAccount: true,
completedChallenges: [
{
id: 'challenge-shape-test',
completedDate: now,
solution: 'http://example.com/solution',
files: [
{
contents: 'some code',
ext: 'js',
key: 'indexjs',
name: 'index'
}
]
}
]
}
});
const res = await post('/apps/classroom/get-user-data').send({
userIds: [defaultUserId]
});
expect(res.status).toBe(200);
const responseBody = res.body as {
data: Record<string, Array<Record<string, unknown>>>;
};
const challenge = responseBody.data[defaultUserId]![0]!;
expect(Object.keys(challenge)).toStrictEqual(['id', 'completedDate']);
});
test('returns 500 and captures when the database query fails', async () => {
const originalSentry = fastifyTestInstance.Sentry;
const captureException = vi.fn();
fastifyTestInstance.Sentry = { ...originalSentry, captureException };
const original = fastifyTestInstance.prisma.user.findMany;
fastifyTestInstance.prisma.user.findMany = vi
.fn()
.mockRejectedValue(new Error('test')) as typeof original;
const res = await post('/apps/classroom/get-user-data').send({
userIds: [defaultUserId]
});
fastifyTestInstance.prisma.user.findMany = original;
fastifyTestInstance.Sentry = originalSentry;
expect(res.status).toBe(500);
expect(res.body).toStrictEqual({
error: 'Failed to retrieve user data'
});
expect(captureException).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ message: 'test' })
);
});
});
});
});