216 lines
9.0 KiB
JavaScript
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');
|
|
});
|
|
});
|
|
});
|