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 } });