mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): daily challenge by month endpoint (#61400)
This commit is contained in:
@@ -92,18 +92,29 @@ describe('/daily-coding-challenge', () => {
|
||||
});
|
||||
|
||||
it('should return 400 for an invalid date format', async () => {
|
||||
const res = await superRequest(
|
||||
'/daily-coding-challenge/date/invalid-format',
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
const invalidFormats = [
|
||||
'invalid-format',
|
||||
'2025-07',
|
||||
'07-18-2025',
|
||||
'25-07-18',
|
||||
'2025-7-18',
|
||||
'2025-07-8'
|
||||
];
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'Invalid date format. Please use YYYY-MM-DD.'
|
||||
});
|
||||
for (const invalidFormat of invalidFormats) {
|
||||
const res = await superRequest(
|
||||
`/daily-coding-challenge/date/${invalidFormat}`,
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'Invalid date format. Please use YYYY-MM-DD.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should return 404 for a date without a challenge', async () => {
|
||||
@@ -188,6 +199,82 @@ describe('/daily-coding-challenge', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /daily-coding-challenge/month/:month', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
|
||||
data: mockChallenges
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.deleteMany();
|
||||
});
|
||||
|
||||
it('should return 400 for invalid month format', async () => {
|
||||
const invalidFormats = ['invalid-month', '2025-13', '2025-1', '25-07'];
|
||||
|
||||
for (const invalidFormat of invalidFormats) {
|
||||
const res = await superRequest(
|
||||
`/daily-coding-challenge/month/${invalidFormat}`,
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'Invalid date format. Please use YYYY-MM.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should return { id, date, challengeNumber, title } for all available challenges of the given month up to today US Central', async () => {
|
||||
const currentMonth = todayUsCentral.toISOString().slice(0, 7);
|
||||
|
||||
const res = await superRequest(
|
||||
`/daily-coding-challenge/month/${currentMonth}`,
|
||||
{
|
||||
method: 'GET'
|
||||
}
|
||||
).send({});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
|
||||
// Should include yesterday's and today's challenges, but not tomorrow's
|
||||
const expectedResponse = [
|
||||
{
|
||||
id: todaysChallenge.id,
|
||||
challengeNumber: todaysChallenge.challengeNumber,
|
||||
date: todaysChallenge.date.toISOString(),
|
||||
title: todaysChallenge.title
|
||||
},
|
||||
{
|
||||
id: yesterdaysChallenge.id,
|
||||
challengeNumber: yesterdaysChallenge.challengeNumber,
|
||||
date: yesterdaysChallenge.date.toISOString(),
|
||||
title: yesterdaysChallenge.title
|
||||
}
|
||||
];
|
||||
|
||||
expect(res.body).toHaveLength(2);
|
||||
expect(res.body).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return 404 when no challenges exist for the given month', async () => {
|
||||
const res = await superRequest('/daily-coding-challenge/month/2024-01', {
|
||||
method: 'GET'
|
||||
}).send({});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body).toEqual({
|
||||
type: 'error',
|
||||
message: 'No challenges found.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /daily-coding-challenge/all', () => {
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.dailyCodingChallenges.createMany({
|
||||
|
||||
@@ -111,6 +111,79 @@ export const dailyCodingChallengeRoutes: FastifyPluginCallbackTypebox = (
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/daily-coding-challenge/month/:month',
|
||||
{
|
||||
schema: schemas.dailyCodingChallenge.month
|
||||
},
|
||||
async (req, reply) => {
|
||||
const logger = fastify.log.child({ req, res: reply });
|
||||
logger.info('Received request for month of daily coding challenges', {
|
||||
month: req.params.month
|
||||
});
|
||||
|
||||
const { month } = req.params;
|
||||
|
||||
try {
|
||||
// Month is guaranteed YYYY-MM format from schema validation
|
||||
const parts = month.split('-');
|
||||
const parsedYear = parseInt(parts[0]!, 10);
|
||||
const parsedMonth = parseInt(parts[1]!, 10);
|
||||
|
||||
// Validate month range
|
||||
if (parsedMonth < 1 || parsedMonth > 12) {
|
||||
logger.warn('Invalid month value requested', { month });
|
||||
return reply.status(400).send({
|
||||
type: 'error',
|
||||
message: 'Invalid date format. Please use YYYY-MM.'
|
||||
});
|
||||
}
|
||||
|
||||
const monthStart = new Date(Date.UTC(parsedYear, parsedMonth - 1, 1));
|
||||
const monthEnd = new Date(Date.UTC(parsedYear, parsedMonth, 1));
|
||||
const todayUsCentral = getUtcMidnight(getNowUsCentral());
|
||||
|
||||
const challenges = await fastify.prisma.dailyCodingChallenges.findMany({
|
||||
where: {
|
||||
date: {
|
||||
gte: monthStart,
|
||||
lt: monthEnd,
|
||||
lte: todayUsCentral
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
date: 'desc'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
challengeNumber: true,
|
||||
date: true,
|
||||
title: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!challenges || challenges.length === 0) {
|
||||
logger.warn('No challenges found for month', { month });
|
||||
return reply
|
||||
.status(404)
|
||||
.send({ type: 'error', message: 'No challenges found.' });
|
||||
}
|
||||
|
||||
const response = challenges.map(challenge => ({
|
||||
...challenge,
|
||||
date: challenge.date.toISOString()
|
||||
}));
|
||||
|
||||
return reply.send(response);
|
||||
} catch (error) {
|
||||
logger.error(error, 'Failed to get monthly daily coding challenges.');
|
||||
await reply
|
||||
.status(500)
|
||||
.send({ type: 'error', message: 'Internal server error.' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/daily-coding-challenge/all',
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ const challengeLanguage = Type.Object({
|
||||
)
|
||||
});
|
||||
|
||||
const singleChallenge = Type.Object({
|
||||
const singleChallengeResponse = Type.Object({
|
||||
id: Type.String({ format: 'objectid', maxLength: 24, minLength: 24 }),
|
||||
date: Type.String({ format: 'date-time' }),
|
||||
challengeNumber: Type.Number(),
|
||||
@@ -30,7 +30,7 @@ const date = {
|
||||
date: Type.String({ format: 'date' })
|
||||
}),
|
||||
response: {
|
||||
200: singleChallenge,
|
||||
200: singleChallengeResponse,
|
||||
400: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Invalid date format. Please use YYYY-MM-DD.')
|
||||
@@ -48,7 +48,7 @@ const date = {
|
||||
|
||||
const today = {
|
||||
response: {
|
||||
200: singleChallenge,
|
||||
200: singleChallengeResponse,
|
||||
404: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Challenge not found.')
|
||||
@@ -60,16 +60,39 @@ const today = {
|
||||
}
|
||||
};
|
||||
|
||||
const manyChallengesResponse = Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
date: Type.String({ format: 'date-time' }),
|
||||
challengeNumber: Type.Number(),
|
||||
title: Type.String()
|
||||
})
|
||||
);
|
||||
|
||||
const month = {
|
||||
params: Type.Object({
|
||||
month: Type.String({ pattern: '^\\d{4}-\\d{2}$' })
|
||||
}),
|
||||
response: {
|
||||
200: manyChallengesResponse,
|
||||
400: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Invalid date format. Please use YYYY-MM.')
|
||||
}),
|
||||
404: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('No challenges found.')
|
||||
}),
|
||||
500: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('Internal server error.')
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const all = {
|
||||
response: {
|
||||
200: Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
date: Type.String({ format: 'date-time' }),
|
||||
challengeNumber: Type.Number(),
|
||||
title: Type.String()
|
||||
})
|
||||
),
|
||||
200: manyChallengesResponse,
|
||||
404: Type.Object({
|
||||
type: Type.Literal('error'),
|
||||
message: Type.Literal('No challenges found.')
|
||||
@@ -100,6 +123,7 @@ const newest = {
|
||||
export const dailyCodingChallenge = {
|
||||
date,
|
||||
today,
|
||||
newest,
|
||||
all
|
||||
month,
|
||||
all,
|
||||
newest
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user