geocrop-platform./apps/nextgen/server/tests/srs-phase1-routes.test.js

216 lines
9.0 KiB
JavaScript

// Tests for the Phase 1 wiring routes added to fees.controller.js and
// transfers.controller.js. Validates FR-SMS4 (sub-paths) and FR-XFER2
// (graduation export).
const { pointAtDevDb } = require('./setup');
pointAtDevDb();
const request = require('supertest');
const app = require('../src/index');
describe('SRS Phase 1 wiring — FR-SMS4 sub-paths + FR-XFER2 graduation', () => {
let adminToken;
let studentToken;
let parentToken;
beforeAll(async () => {
const r1 = await request(app).post('/api/auth/login').send({ email: 'admin@school.com', password: 'admin123' });
adminToken = r1.body?.token;
const r2 = await request(app).post('/api/auth/login').send({ email: 'student@school.com', password: 'student123' });
studentToken = r2.body?.token;
const r3 = await request(app).post('/api/auth/login').send({ email: 'parent@school.com', password: 'parent123' });
parentToken = r3.body?.token;
});
// ==================== FR-SMS4: /api/fees/structure ====================
describe('GET /api/fees/structure', () => {
it('returns 200 with the full fee structure for an admin', async () => {
const res = await request(app)
.get('/api/fees/structure')
.set('Authorization', `Bearer ${adminToken}`);
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('academic_year');
expect(res.body).toHaveProperty('fee_groups');
expect(res.body).toHaveProperty('fee_plans');
expect(res.body).toHaveProperty('active_discounts');
expect(res.body).toHaveProperty('outstanding_by_class');
expect(Array.isArray(res.body.fee_groups)).toBe(true);
expect(Array.isArray(res.body.fee_plans)).toBe(true);
expect(Array.isArray(res.body.active_discounts)).toBe(true);
expect(Array.isArray(res.body.outstanding_by_class)).toBe(true);
});
it('returns 200 for a student (read-only meta is public-ish)', async () => {
const res = await request(app)
.get('/api/fees/structure')
.set('Authorization', `Bearer ${studentToken}`);
expect(res.status).toBe(200);
});
it('returns 401 without a token', async () => {
const res = await request(app).get('/api/fees/structure');
expect(res.status).toBe(401);
});
it('honours the academic_year query param', async () => {
const res = await request(app)
.get('/api/fees/structure?academic_year=2025')
.set('Authorization', `Bearer ${adminToken}`);
expect(res.status).toBe(200);
expect(res.body.academic_year).toBe('2025');
});
});
// ==================== FR-SMS4: /api/fees/invoices ====================
describe('GET /api/fees/invoices', () => {
it('returns 200 with an array (admin)', async () => {
const res = await request(app)
.get('/api/fees/invoices')
.set('Authorization', `Bearer ${adminToken}`);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
it('returns 403 for a student (finance roles only)', async () => {
const res = await request(app)
.get('/api/fees/invoices')
.set('Authorization', `Bearer ${studentToken}`);
expect(res.status).toBe(403);
});
it('returns 401 without a token', async () => {
const res = await request(app).get('/api/fees/invoices');
expect(res.status).toBe(401);
});
});
// ==================== FR-XFER2: /api/xfer/graduation ====================
describe('POST /api/xfer/graduation', () => {
it('returns 200 with a complete graduation package for admin', async () => {
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ student_id: 4, target_institution: 'University of Zimbabwe' });
expect(res.status).toBe(200);
expect(res.body.package_type).toBe('graduation_export');
expect(res.body.schema_version).toBe('1.0');
expect(res.body.student.first_name).toBeTruthy();
expect(res.body).toHaveProperty('academic_history');
expect(res.body).toHaveProperty('attendance_summary');
expect(res.body).toHaveProperty('disciplinary_record');
expect(res.body).toHaveProperty('financial_clearance');
expect(res.body).toHaveProperty('signature');
expect(res.body.signature.signature_digest).toMatch(/^[a-f0-9]{64}$/);
});
it('returns 400 when target_institution is missing', async () => {
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ student_id: 4 });
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/target_institution/);
});
it('returns 400 when both student_id and student_uid are missing', async () => {
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ target_institution: 'X' });
expect(res.status).toBe(400);
});
it('returns 404 when the student does not exist', async () => {
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ student_id: 99999, target_institution: 'X' });
expect(res.status).toBe(404);
});
it('returns 403 when a student tries to read another student', async () => {
// student@school.com resolves to id 4 in the dev seed; pick a
// different student (id 9 = a generated 'Test User' student from
// the e2e seed) to exercise the cross-student RBAC check.
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${studentToken}`)
.send({ student_id: 9, target_institution: 'X' });
expect(res.status).toBe(403);
});
it('accepts a consent_id and gates on consent_status', async () => {
// First, create a consent row
const Database = require('better-sqlite3');
const db = new Database(process.env.DB_PATH);
db.pragma('foreign_keys = ON');
// Ensure table exists (the controller creates it on first hit)
const exists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='transfer_consents'").get();
if (!exists) {
// Hit the controller first to create the table
await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ student_id: 4, target_institution: 'X' });
db.close();
const db2 = new Database(process.env.DB_PATH);
const { v4: uuidv4 } = require('uuid');
const consentUid = uuidv4();
db2.prepare(`
INSERT INTO transfer_consents (uid, student_id, requested_by, target_school, consent_status, sync_status)
VALUES (?, 4, 'audit@test', 'University of Zimbabwe', 'pending', 'pending')
`).run(consentUid);
db2.close();
// Now try to export with that pending consent — must be 409
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ student_id: 4, target_institution: 'University of Zimbabwe', consent_id: consentUid });
expect(res.status).toBe(409);
expect(res.body.consent_status).toBe('pending');
} else {
// Table already exists; insert a pending consent directly
const { v4: uuidv4 } = require('uuid');
const consentUid = uuidv4();
db.prepare(`
INSERT INTO transfer_consents (uid, student_id, requested_by, target_school, consent_status, sync_status)
VALUES (?, 4, 'audit@test', 'University of Zimbabwe', 'pending', 'pending')
`).run(consentUid);
db.close();
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ student_id: 4, target_institution: 'University of Zimbabwe', consent_id: consentUid });
expect(res.status).toBe(409);
expect(res.body.consent_status).toBe('pending');
}
});
it('passes when consent is approved (signature includes consent metadata)', async () => {
const Database = require('better-sqlite3');
const db = new Database(process.env.DB_PATH);
const { v4: uuidv4 } = require('uuid');
const consentUid = uuidv4();
db.prepare(`
INSERT INTO transfer_consents (uid, student_id, requested_by, target_school, consent_status, signed_at, signature_hash, sync_status)
VALUES (?, 4, 'audit@test', 'University of Zimbabwe', 'approved', datetime('now','localtime'), 'abc123', 'pending')
`).run(consentUid);
db.close();
const res = await request(app)
.post('/api/xfer/graduation')
.set('Authorization', `Bearer ${adminToken}`)
.send({ student_id: 4, target_institution: 'University of Zimbabwe', consent_id: consentUid });
expect(res.status).toBe(200);
expect(res.body.consent).toBeTruthy();
expect(res.body.consent.consent_id).toBe(consentUid);
expect(res.body.consent.consent_status).toBe('approved');
});
});
});