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