feat: dynamic semester data loading and automated daily update setup
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import {
|
||||
parseTimetableIds,
|
||||
parseSemesterStartDate,
|
||||
getFirstMondayAfter,
|
||||
formatDateForButton,
|
||||
getCurrentAcademicYear,
|
||||
atomicWriteFileSync,
|
||||
} from '../src/scripts/update-semester-data.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
test('Academic Year calculation', () => {
|
||||
const year = getCurrentAcademicYear();
|
||||
assert.ok(typeof year.startYear === 'number');
|
||||
assert.ok(typeof year.endYear === 'number');
|
||||
assert.equal(year.endYear, year.startYear + 1);
|
||||
});
|
||||
|
||||
test('Live HTML parsing from dm.unipi.it', async t => {
|
||||
await t.test('extracts valid timetable IDs for all courses', async () => {
|
||||
const ids = await parseTimetableIds();
|
||||
assert.ok(ids, 'Timetable IDs should not be null');
|
||||
|
||||
const expectedKeys = ['anno-1', 'anno-2', 'anno-3', 'magistrale'];
|
||||
for (const key of expectedKeys) {
|
||||
assert.ok(ids[key], `Missing timetable ID for ${key}`);
|
||||
assert.match(
|
||||
ids[key],
|
||||
/^[a-f0-9]{24}$/,
|
||||
`Timetable ID for ${key} must be a 24-char hex string: ${ids[key]}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('extracts valid semester start date from calendar page', async () => {
|
||||
const startDateStr = await parseSemesterStartDate();
|
||||
assert.ok(startDateStr, 'Semester start date string should not be null');
|
||||
assert.match(
|
||||
startDateStr,
|
||||
/^\d{1,2}\s+[a-zA-Z]+\s+\d{4}$/,
|
||||
`Date should match Italian format, got: ${startDateStr}`
|
||||
);
|
||||
|
||||
const firstMonday = getFirstMondayAfter(startDateStr);
|
||||
assert.ok(firstMonday instanceof Date);
|
||||
assert.equal(firstMonday.getUTCDay(), 1, 'First Monday must be day 1 (Monday)');
|
||||
|
||||
const buttonText = formatDateForButton(firstMonday);
|
||||
assert.match(buttonText, /^\d{1,2}\s+[a-zA-Z]+$/);
|
||||
});
|
||||
});
|
||||
|
||||
test('Cineca API verifies live parsed timetable IDs', async () => {
|
||||
const ids = await parseTimetableIds();
|
||||
assert.ok(ids, 'Need valid IDs to test Cineca API');
|
||||
|
||||
// Test the first year ID against Cineca API
|
||||
const testId = ids['anno-1'];
|
||||
const now = new Date();
|
||||
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const res = await fetch('https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json;charset=UTF-8',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mostraImpegniAnnullati: true,
|
||||
mostraIndisponibilitaTotali: false,
|
||||
linkCalendarioId: testId,
|
||||
clienteId: '628de8b9b63679f193b87046',
|
||||
pianificazioneTemplate: false,
|
||||
dataInizio: now.toISOString(),
|
||||
dataFine: nextWeek.toISOString(),
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200, `Cineca API should respond with HTTP 200 for ID ${testId}`);
|
||||
const data = await res.json();
|
||||
assert.ok(Array.isArray(data), 'Cineca response should be a JSON array of events');
|
||||
});
|
||||
|
||||
test('Atomic file write safety and schema', async () => {
|
||||
const tmpPath = path.resolve(process.cwd(), 'tests/scratch-semester-test.json');
|
||||
|
||||
try {
|
||||
const sampleData = {
|
||||
timetableIds: {
|
||||
'anno-1': '687e66d62d13e6001edd366d',
|
||||
'anno-2': '687e681301ee9300198320e6',
|
||||
'anno-3': '687e68bb01ee9300198320eb',
|
||||
'magistrale': '687e6a9f96ea080019d439d5',
|
||||
},
|
||||
semesterStartDateStr: '23 settembre 2026',
|
||||
firstMondayDate: '2026-09-28T00:00:00.000Z',
|
||||
buttonText: 'Vai al 28 settembre! 🚀',
|
||||
lastUpdated: new Date().toISOString(),
|
||||
academicYear: { startYear: 2026, endYear: 2027 },
|
||||
};
|
||||
|
||||
atomicWriteFileSync(tmpPath, JSON.stringify(sampleData, null, 2));
|
||||
|
||||
assert.ok(fs.existsSync(tmpPath), 'Output file should exist');
|
||||
const readBack = JSON.parse(fs.readFileSync(tmpPath, 'utf8'));
|
||||
assert.deepEqual(readBack.timetableIds, sampleData.timetableIds);
|
||||
assert.equal(readBack.buttonText, sampleData.buttonText);
|
||||
} finally {
|
||||
if (fs.existsSync(tmpPath)) {
|
||||
fs.unlinkSync(tmpPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('CLI command execution', async t => {
|
||||
await t.test('--check flag runs and exits cleanly', async () => {
|
||||
const { stdout, stderr } = await execFileAsync('node', [
|
||||
'src/scripts/update-semester-data.js',
|
||||
'--check',
|
||||
]);
|
||||
assert.match(stdout, /Semester data is up to date/);
|
||||
assert.equal(stderr, '');
|
||||
});
|
||||
|
||||
await t.test('--output flag writes to custom path', async () => {
|
||||
const tmpPath = path.resolve(process.cwd(), 'tests/cli-output-test.json');
|
||||
try {
|
||||
const { stdout } = await execFileAsync('node', [
|
||||
'src/scripts/update-semester-data.js',
|
||||
'--output',
|
||||
tmpPath,
|
||||
'--quiet',
|
||||
]);
|
||||
assert.ok(fs.existsSync(tmpPath), 'Target output file was created');
|
||||
const data = JSON.parse(fs.readFileSync(tmpPath, 'utf8'));
|
||||
assert.ok(data.timetableIds['anno-1']);
|
||||
} finally {
|
||||
if (fs.existsSync(tmpPath)) {
|
||||
fs.unlinkSync(tmpPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Dev server serves semester-data.json dynamically', async () => {
|
||||
try {
|
||||
const res = await fetch('http://localhost:3000/semester-data.json');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
assert.ok(data.timetableIds);
|
||||
assert.ok(data.timetableIds['anno-1']);
|
||||
assert.ok(data.buttonText);
|
||||
}
|
||||
} catch {
|
||||
// If dev server isn't running in background, test passes conditionally
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user