feat(api): daily challenge by month endpoint (#61400)

This commit is contained in:
Tom
2025-09-23 07:49:37 -05:00
committed by GitHub
parent b2817c28de
commit e6066a1d50
3 changed files with 208 additions and 24 deletions
@@ -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
};