164 lines
5.9 KiB
JavaScript
164 lines
5.9 KiB
JavaScript
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
|
|
}
|
|
});
|