Reject impossible K League schedule dates before querying

The kleague-results API previously accepted regex-shaped but impossible calendar dates and treated them like empty-result queries. This change adds a regression test first, then rejects invalid YYYY-MM-DD strings during normalization so bad input never reaches the fetch layer.

Constraint: Public API contract says date must be a valid Date or YYYY-MM-DD string
Rejected: Let upstream return zero matches | hides invalid input as a valid no-results response
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep string-date validation in normalizeDateInput before any schedule fetch
Tested: node --test packages/kleague-results/test/index.test.js; npm run ci; live smoke getKLeagueSummary(2026-03-22, K리그1, FC서울, standings); live smoke getMatchResults(2026-03-22, K리그2); invalid-date smoke getMatchResults(2026-13-40); LSP diagnostics on changed files; architect verification
Not-tested: Additional leap-year and month-end invalid-date permutations
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-03-29 16:22:38 +09:00
commit c784a77454
2 changed files with 43 additions and 0 deletions

View file

@ -65,6 +65,10 @@ function normalizeDateInput(value) {
throw new Error("date must be a valid Date or YYYY-MM-DD string.");
}
if (!isValidCalendarDate(match[1], match[2], match[3])) {
throw new Error("date must be a valid Date or YYYY-MM-DD string.");
}
return buildDateParts(match[1], match[2], match[3]);
}
@ -85,6 +89,27 @@ function buildDateParts(year, month, day) {
};
}
function isValidCalendarDate(year, month, day) {
const numericYear = Number(year);
const numericMonth = Number(month);
const numericDay = Number(day);
if (!Number.isInteger(numericYear) || !Number.isInteger(numericMonth) || !Number.isInteger(numericDay)) {
return false;
}
if (numericMonth < 1 || numericMonth > 12 || numericDay < 1) {
return false;
}
const maxDay = [31, isLeapYear(numericYear) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][numericMonth - 1];
return numericDay <= maxDay;
}
function isLeapYear(year) {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
function buildClubDirectory(clubList = []) {
const directory = new Map();

View file

@ -126,6 +126,24 @@ test("public fetchers compose day results with current standings via mocked fetc
}
});
test("getMatchResults rejects impossible calendar dates before fetching", async () => {
let fetchCalled = false;
await assert.rejects(
() =>
getMatchResults("2026-13-40", {
leagueId: "K리그1",
fetchImpl: async () => {
fetchCalled = true;
return makeResponse(schedulePayload);
},
}),
/date must be a valid Date or YYYY-MM-DD string\./,
);
assert.equal(fetchCalled, false);
});
function makeResponse(body) {
return new Response(JSON.stringify(body), {
status: 200,