Skip to content

Commit 29cd2d2

Browse files
moT01huyenltnguyen
andauthored
feat(api): daily challenge api (freeCodeCamp#61346)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
1 parent 3bcd6bb commit 29cd2d2

13 files changed

Lines changed: 678 additions & 22 deletions

File tree

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"ajv": "8.12.0",
2222
"ajv-formats": "2.1.1",
2323
"date-fns": "4.1.0",
24+
"date-fns-tz": "3.2.0",
2425
"dotenv": "16.4.5",
2526
"fast-uri": "2.3.0",
2627
"fastify": "5.2.0",

api/prisma/schema.prisma

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,33 @@ type SurveyResponse {
491491

492492
// ----------------------
493493

494+
model DailyCodingChallenges {
495+
id String @id @default(auto()) @map("_id") @db.ObjectId
496+
challengeNumber Int
497+
date DateTime
498+
title String
499+
description String
500+
javascript DailyCodingChallengeApiLanguage
501+
python DailyCodingChallengeApiLanguage
502+
}
503+
504+
type DailyCodingChallengeApiLanguage {
505+
tests DailyCodingChallengeApiLanguageTests[]
506+
challengeFiles DailyCodingChallengeApiLanguageChallengeFiles[]
507+
}
508+
509+
type DailyCodingChallengeApiLanguageTests {
510+
text String
511+
testString String
512+
}
513+
514+
type DailyCodingChallengeApiLanguageChallengeFiles {
515+
contents String
516+
fileKey String
517+
}
518+
519+
// ----------------------
520+
494521
model ExamEnvironmentExamModeration {
495522
id String @id @default(auto()) @map("_id") @db.ObjectId
496523
/// Whether or not the item is approved

api/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
examEnvironmentOpenRoutes,
5353
examEnvironmentValidatedTokenRoutes
5454
} from './exam-environment/routes/exam-environment';
55+
import { dailyCodingChallengeRoutes } from './daily-coding-challenge/routes/daily-coding-challenge';
5556

5657
type FastifyInstanceWithTypeProvider = FastifyInstance<
5758
RawServerDefault,
@@ -231,6 +232,7 @@ export const build = async (
231232
void fastify.register(publicRoutes.deprecatedEndpoints);
232233
void fastify.register(publicRoutes.statusRoute);
233234
void fastify.register(publicRoutes.unsubscribeDeprecated);
235+
void fastify.register(dailyCodingChallengeRoutes);
234236

235237
return fastify;
236238
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Endpoints to get daily coding challenge info. Daily challenge submission still lives in the main part of the API.
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import { addDays } from 'date-fns';
2+
3+
import { setupServer, superRequest } from '../../../jest.utils';
4+
import { getNowUsCentral, getUtcMidnight } from '../utils/helpers';
5+
6+
function dateToDateParam(date: Date): string {
7+
return date.toISOString().split('T')[0] as string;
8+
}
9+
10+
const todayUsCentral = getNowUsCentral();
11+
const todayUtcMidnight = getUtcMidnight(todayUsCentral);
12+
const todayDateParam = dateToDateParam(todayUtcMidnight);
13+
14+
const yesterdayUsCentral = addDays(todayUsCentral, -1);
15+
const yesterdayUtcMidnight = getUtcMidnight(yesterdayUsCentral);
16+
17+
const twoDaysAgoUsCentral = addDays(todayUsCentral, -2);
18+
const twoDaysAgoUtcMidnight = getUtcMidnight(twoDaysAgoUsCentral);
19+
const twoDaysAgoDateParam = dateToDateParam(twoDaysAgoUtcMidnight);
20+
21+
const tomorrowUsCentral = addDays(todayUsCentral, 1);
22+
const tomorrowUtcMidnight = getUtcMidnight(tomorrowUsCentral);
23+
const tomorrowDateParam = dateToDateParam(tomorrowUtcMidnight);
24+
25+
const yesterdaysChallenge = {
26+
id: '111111111111111111111111',
27+
challengeNumber: 1,
28+
date: yesterdayUtcMidnight,
29+
title: "Yesterday's Challenge",
30+
description: "Yesterday's Description",
31+
javascript: {
32+
tests: [{ text: 'JS Test Yesterday', testString: 'jsTestYesterday()' }],
33+
challengeFiles: [{ contents: 'JS Files Yesterday', fileKey: 'scriptjs' }]
34+
},
35+
python: {
36+
tests: [{ text: 'Py Test Yesterday', testString: 'py_test_yesterday()' }],
37+
challengeFiles: [{ contents: 'Py Files Yesterday', fileKey: 'mainpy' }]
38+
}
39+
};
40+
41+
const todaysChallenge = {
42+
id: '222222222222222222222222',
43+
challengeNumber: 2,
44+
date: todayUtcMidnight,
45+
title: "Today's Challenge",
46+
description: "Today's Description",
47+
javascript: {
48+
tests: [{ text: 'JS Test Today', testString: 'jsTestToday()' }],
49+
challengeFiles: [{ contents: 'JS Files Today', fileKey: 'scriptjs' }]
50+
},
51+
python: {
52+
tests: [{ text: 'Py Test Today', testString: 'py_test_today()' }],
53+
challengeFiles: [{ contents: 'Py Files Today', fileKey: 'mainpy' }]
54+
}
55+
};
56+
57+
const tomorrowsChallenge = {
58+
id: '333333333333333333333333',
59+
challengeNumber: 3,
60+
date: tomorrowUtcMidnight,
61+
title: "Tomorrow's Challenge",
62+
description: "Tomorrow's Description",
63+
javascript: {
64+
tests: [{ text: 'JS Test Tomorrow', testString: 'jsTestTomorrow()' }],
65+
challengeFiles: [{ contents: 'JS Files Tomorrow', fileKey: 'scriptjs' }]
66+
},
67+
python: {
68+
tests: [{ text: 'Py Test Tomorrow', testString: 'py_test_tomorrow()' }],
69+
challengeFiles: [{ contents: 'Py Files Tomorrow', fileKey: 'mainpy' }]
70+
}
71+
};
72+
73+
const mockChallenges = [
74+
tomorrowsChallenge,
75+
todaysChallenge,
76+
yesterdaysChallenge
77+
];
78+
79+
describe('/daily-coding-challenge', () => {
80+
setupServer();
81+
82+
describe('GET /daily-coding-challenge/date/:date', () => {
83+
beforeEach(async () => {
84+
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
85+
data: mockChallenges
86+
});
87+
});
88+
89+
afterEach(async () => {
90+
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
91+
});
92+
93+
it('should return 400 for an invalid date format', async () => {
94+
const res = await superRequest(
95+
'/daily-coding-challenge/date/invalid-format',
96+
{
97+
method: 'GET'
98+
}
99+
).send({});
100+
101+
expect(res.status).toBe(400);
102+
expect(res.body).toEqual({
103+
type: 'error',
104+
message: 'Invalid date format. Please use YYYY-MM-DD.'
105+
});
106+
});
107+
108+
it('should return 404 for a date without a challenge', async () => {
109+
const res = await superRequest(
110+
`/daily-coding-challenge/date/${twoDaysAgoDateParam}`,
111+
{
112+
method: 'GET'
113+
}
114+
).send({});
115+
116+
expect(res.status).toBe(404);
117+
expect(res.body).toEqual({
118+
type: 'error',
119+
message: 'Challenge not found.'
120+
});
121+
});
122+
123+
it('should return a challenge for a valid date', async () => {
124+
const res = await superRequest(
125+
`/daily-coding-challenge/date/${todayDateParam}`,
126+
{
127+
method: 'GET'
128+
}
129+
).send({});
130+
131+
expect(res.status).toBe(200);
132+
expect(res.body).toMatchObject({
133+
...todaysChallenge,
134+
date: todaysChallenge.date.toISOString()
135+
});
136+
});
137+
138+
it('should not return a challenge for a future date relative to US Central', async () => {
139+
const res = await superRequest(
140+
`/daily-coding-challenge/date/${tomorrowDateParam}`,
141+
{
142+
method: 'GET'
143+
}
144+
).send({});
145+
expect(res.body).toEqual({
146+
type: 'error',
147+
message: 'Challenge not found.'
148+
});
149+
});
150+
});
151+
152+
describe('GET /daily-coding-challenge/today', () => {
153+
beforeEach(async () => {
154+
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
155+
data: mockChallenges
156+
});
157+
});
158+
159+
afterEach(async () => {
160+
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
161+
});
162+
163+
it("should return today's challenge", async () => {
164+
const res = await superRequest('/daily-coding-challenge/today', {
165+
method: 'GET'
166+
}).send({});
167+
168+
expect(res.status).toBe(200);
169+
expect(res.body).toMatchObject({
170+
...todaysChallenge,
171+
date: todaysChallenge.date.toISOString()
172+
});
173+
});
174+
175+
it('should return 404 when no challenge exists for today', async () => {
176+
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
177+
178+
const res = await superRequest('/daily-coding-challenge/today', {
179+
method: 'GET'
180+
}).send({});
181+
182+
expect(res.status).toBe(404);
183+
expect(res.body).toEqual({
184+
type: 'error',
185+
message: 'Challenge not found.'
186+
});
187+
});
188+
});
189+
190+
describe('GET /daily-coding-challenge/all', () => {
191+
beforeEach(async () => {
192+
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
193+
data: mockChallenges
194+
});
195+
});
196+
197+
afterEach(async () => {
198+
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
199+
});
200+
201+
it('should return { _id, date, challengeNumber, title } for all challenges up to today US Central', async () => {
202+
const res = await superRequest('/daily-coding-challenge/all', {
203+
method: 'GET'
204+
}).send({});
205+
206+
expect(res.status).toBe(200);
207+
expect(Array.isArray(res.body)).toBe(true);
208+
209+
// Should include yesterday's and today's challenges, but not tomorrow's
210+
const expectedResponse = [
211+
{
212+
id: todaysChallenge.id,
213+
challengeNumber: todaysChallenge.challengeNumber,
214+
date: todaysChallenge.date.toISOString(),
215+
title: todaysChallenge.title
216+
},
217+
{
218+
id: yesterdaysChallenge.id,
219+
challengeNumber: yesterdaysChallenge.challengeNumber,
220+
date: yesterdaysChallenge.date.toISOString(),
221+
title: yesterdaysChallenge.title
222+
}
223+
];
224+
225+
expect(res.body).toHaveLength(2);
226+
expect(res.body).toEqual(expectedResponse);
227+
});
228+
229+
it('should return 404 when no challenges exist', async () => {
230+
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
231+
232+
const res = await superRequest('/daily-coding-challenge/all', {
233+
method: 'GET'
234+
}).send({});
235+
236+
expect(res.status).toBe(404);
237+
expect(res.body).toEqual({
238+
type: 'error',
239+
message: 'No challenges found.'
240+
});
241+
});
242+
});
243+
244+
describe('GET /daily-coding-challenge/newest', () => {
245+
beforeEach(async () => {
246+
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
247+
data: [yesterdaysChallenge, todaysChallenge, tomorrowsChallenge]
248+
});
249+
});
250+
251+
afterEach(async () => {
252+
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
253+
});
254+
255+
it('should return { date } of the newest challenge in the database', async () => {
256+
const res = await superRequest('/daily-coding-challenge/newest', {
257+
method: 'GET'
258+
}).send({});
259+
260+
expect(res.status).toBe(200);
261+
expect(res.body).toEqual({
262+
date: tomorrowsChallenge.date.toISOString()
263+
});
264+
});
265+
266+
it('should return 404 when no challenges exist', async () => {
267+
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
268+
269+
const res = await superRequest('/daily-coding-challenge/newest', {
270+
method: 'GET'
271+
}).send({});
272+
273+
expect(res.status).toBe(404);
274+
expect(res.body).toEqual({
275+
type: 'error',
276+
message: 'No challenges found.'
277+
});
278+
});
279+
});
280+
});

0 commit comments

Comments
 (0)