geocrop-platform./apps/nextgen/evidence/smoke.js

145 lines
7.8 KiB
JavaScript

/**
* Smoke test for enrollment workflow + bulk marks CSV upload.
* Runs against a live server on http://127.0.0.1:3001.
*
* Usage: node evidence/smoke.js
*/
const fs = require('fs');
const path = require('path');
// Use 'localhost' (resolves to both IPv4 and IPv6); on this Windows host the
// Node listener binds to :: (IPv6 any-address), so 127.0.0.1 fails.
const BASE = 'http://localhost:3001';
const log = (...a) => console.log(...a);
const fail = (msg) => { console.error('FAIL:', msg); process.exitCode = 1; };
const pass = (msg) => log(' PASS:', msg);
async function login(email, password) {
const r = await fetch(`${BASE}/api/auth/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const body = await r.json();
if (!r.ok || !body.token) {
throw new Error(`login ${email} -> ${r.status} ${JSON.stringify(body)}`);
}
return { token: body.token, user: body.user };
}
async function api(method, path, { token, json, formData } = {}) {
const headers = {};
if (token) headers['authorization'] = `Bearer ${token}`;
let body;
if (json !== undefined) {
headers['content-type'] = 'application/json';
body = JSON.stringify(json);
} else if (formData) {
body = formData;
}
const r = await fetch(`${BASE}${path}`, { method, headers, body });
let parsed = null;
const text = await r.text();
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; }
return { status: r.status, body: parsed };
}
(async () => {
log('============================================================');
log(' Smoke test: enrollment workflow + bulk marks CSV upload');
log(` Time: ${new Date().toISOString()}`);
log('============================================================\n');
// ---- [1] Student self-enrols in class_id=2 (Grade 2) ----
// student5@school.com (id=10) is seeded in Grade 1 only; safe to enrol in Grade 2.
log('[1] Student student5@school.com self-enrols in class_id=2 (Grade 2)');
const student = await login('student5@school.com', 'student123');
log(` student token len: ${student.token.length}`);
const enrol = await api('POST', '/api/enrollments', {
token: student.token,
json: { class_id: 2 },
});
log(` HTTP ${enrol.status} -> ${JSON.stringify(enrol.body)}`);
if (enrol.status !== 201 && enrol.status !== 200) fail('expected 201/200 on enrol');
if (enrol.body.request_status !== 'pending') fail(`expected request_status=pending, got ${enrol.body.request_status}`);
if (enrol.body.status !== 'inactive') fail(`expected status=inactive, got ${enrol.body.status}`);
if (!enrol.body.requested_at) fail('expected requested_at to be set');
if (enrol.body.approved_by !== null) fail(`expected approved_by=null, got ${enrol.body.approved_by}`);
pass(`request_status=${enrol.body.request_status}, status=${enrol.body.status}`);
const enrollmentId = enrol.body.id;
// ---- [2] Admin GET /api/enrollments/pending ----
log('\n[2] Admin GET /api/enrollments/pending');
const admin = await login('admin@school.com', 'admin123');
const pending = await api('GET', '/api/enrollments/pending', { token: admin.token });
log(` HTTP ${pending.status}, ${Array.isArray(pending.body) ? pending.body.length : 'n/a'} rows`);
if (pending.status !== 200) fail('expected 200 on pending list');
if (!Array.isArray(pending.body)) fail('expected array');
const found = pending.body.find(r => r.id === enrollmentId);
if (!found) fail(`enrollment ${enrollmentId} not found in pending list`);
else pass(`enrollment ${enrollmentId} visible to admin`);
// ---- [3] Admin POST /api/enrollments/:id/approve ----
log('\n[3] Admin POST /api/enrollments/:id/approve');
const approve = await api('POST', `/api/enrollments/${enrollmentId}/approve`, { token: admin.token });
log(` HTTP ${approve.status} -> ${JSON.stringify(approve.body)}`);
if (approve.status !== 200) fail('expected 200 on approve');
if (approve.body.request_status !== 'approved') fail('expected request_status=approved');
if (approve.body.status !== 'active') fail('expected status=active');
if (!approve.body.approved_at) fail('expected approved_at to be set');
if (approve.body.approved_by !== admin.user.id) fail('expected approved_by=admin.id');
pass(`request_status=${approve.body.request_status}, approved_by=${approve.body.approved_by}`);
// ---- [4] Teacher uploads 3-row valid CSV ----
log('\n[4] Teacher uploads evidence/valid.csv (3 rows, all valid)');
const teacher = await login('teacher@school.com', 'teacher123');
const fd = new FormData();
fd.append('file', new Blob([fs.readFileSync(path.resolve(__dirname, 'valid.csv'))], { type: 'text/csv' }), 'valid.csv');
const validRes = await api('POST', '/api/marks/bulk', { token: teacher.token, formData: fd });
log(` HTTP ${validRes.status} -> ${JSON.stringify(validRes.body)}`);
if (validRes.status !== 200) fail('expected 200 on bulk valid');
if (validRes.body.inserted !== 3) fail(`expected inserted=3, got ${validRes.body.inserted}`);
if (validRes.body.updated !== 0) fail(`expected updated=0, got ${validRes.body.updated}`);
if (!Array.isArray(validRes.body.errors) || validRes.body.errors.length !== 0) fail('expected errors=[]');
pass(`inserted=${validRes.body.inserted}, updated=${validRes.body.updated}, errors=[]`);
// ---- [5] Teacher uploads 4-row mixed CSV (2 valid + 2 invalid) ----
log('\n[5] Teacher uploads evidence/mixed.csv (4 rows: 2 valid, 2 invalid)');
const fd2 = new FormData();
fd2.append('file', new Blob([fs.readFileSync(path.resolve(__dirname, 'mixed.csv'))], { type: 'text/csv' }), 'mixed.csv');
const mixedRes = await api('POST', '/api/marks/bulk', { token: teacher.token, formData: fd2 });
log(` HTTP ${mixedRes.status} -> ${JSON.stringify(mixedRes.body)}`);
if (mixedRes.status !== 200) fail('expected 200 on bulk mixed');
// 2 valid inserts/updates + 2 errors
const insUpd = (mixedRes.body.inserted || 0) + (mixedRes.body.updated || 0);
if (insUpd !== 2) fail(`expected inserted+updated=2, got inserted=${mixedRes.body.inserted} updated=${mixedRes.body.updated}`);
if (!Array.isArray(mixedRes.body.errors) || mixedRes.body.errors.length !== 2) {
fail(`expected errors=[2 items], got ${JSON.stringify(mixedRes.body.errors)}`);
} else {
const reasons = mixedRes.body.errors.map(e => e.reason).join(' | ');
pass(`inserted=${mixedRes.body.inserted}, updated=${mixedRes.body.updated}, errors=2 [${reasons}]`);
}
// ---- [6] Negative: student cannot call /marks/bulk ----
log('\n[6] Student attempts POST /api/marks/bulk (should be 403)');
const fd3 = new FormData();
fd3.append('file', new Blob([fs.readFileSync(path.resolve(__dirname, 'valid.csv'))], { type: 'text/csv' }), 'valid.csv');
const stuBulk = await api('POST', '/api/marks/bulk', { token: student.token, formData: fd3 });
log(` HTTP ${stuBulk.status} -> ${JSON.stringify(stuBulk.body)}`);
if (stuBulk.status !== 403) fail(`expected 403, got ${stuBulk.status}`);
else pass('student correctly blocked from /marks/bulk');
// ---- [7] GET /api/marks?assignment_id=1 (teacher sees all) ----
log('\n[7] GET /api/marks?assignment_id=1');
const marksList = await api('GET', '/api/marks?assignment_id=1', { token: teacher.token });
log(` HTTP ${marksList.status}, ${Array.isArray(marksList.body) ? marksList.body.length : 'n/a'} rows`);
if (marksList.status !== 200) fail('expected 200 on /marks list');
if (!Array.isArray(marksList.body)) fail('expected array');
pass(`returned ${marksList.body.length} submission rows for assignment 1`);
log('\n============================================================');
log(process.exitCode === 1 ? 'SMOKE: FAIL' : 'SMOKE: PASS');
log('============================================================');
})().catch(e => { console.error('UNCAUGHT:', e); process.exit(1); });