141 lines
4.7 KiB
JavaScript
141 lines
4.7 KiB
JavaScript
/**
|
|
* Integration test for GET /api/admin/system-health/backup-integrity.
|
|
*
|
|
* Auth + permission middleware are mocked to pass-through so the test
|
|
* focuses on the route's own behaviour: scope-param validation, the
|
|
* successResponse envelope, and that the underlying service report
|
|
* surfaces correctly in the JSON body.
|
|
*
|
|
* The verifier service itself is exercised against the real schema
|
|
* (bootCrmDb) and real filesystem — only the auth gate is stubbed.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const express = require('express');
|
|
const request = require('supertest');
|
|
|
|
const { bootCrmDb, seedMinimal } = require('./helpers/crmDb');
|
|
|
|
// Pass-through auth so we don't need to mint JWTs.
|
|
jest.mock('../../src/middleware/auth', () => ({
|
|
adminAuth: (req, _res, next) => { req.admin = { id: 1 }; next(); },
|
|
customerAuth: (_req, _res, next) => next(),
|
|
galleryAuth: (_req, _res, next) => next(),
|
|
}));
|
|
|
|
// Pass-through permissions so settings.view always allows.
|
|
jest.mock('../../src/middleware/permissions', () => ({
|
|
requirePermission: () => (_req, _res, next) => next(),
|
|
}));
|
|
|
|
jest.setTimeout(30000);
|
|
|
|
describe('GET /api/admin/system-health/backup-integrity', () => {
|
|
let cleanup;
|
|
let db;
|
|
let customerId;
|
|
let app;
|
|
let storagePath;
|
|
|
|
beforeAll(async () => {
|
|
({ db, cleanup } = await bootCrmDb());
|
|
({ customerId } = await seedMinimal(db));
|
|
storagePath = process.env.STORAGE_PATH;
|
|
|
|
// Mount the route on a minimal Express app. Cold-require after
|
|
// bootCrmDb so the route's downstream `require('../database/db')`
|
|
// sees the same db instance.
|
|
const route = require('../../src/routes/adminSystemHealth');
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use('/api/admin/system-health', route);
|
|
}, 120000);
|
|
|
|
afterAll(async () => {
|
|
if (cleanup) await cleanup();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await db('contracts').del().catch(() => {});
|
|
await db('invoices').del().catch(() => {});
|
|
await db('quotes').del().catch(() => {});
|
|
});
|
|
|
|
it('returns a report envelope when nothing references any path', async () => {
|
|
const res = await request(app).get('/api/admin/system-health/backup-integrity');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toHaveProperty('report');
|
|
expect(res.body.report.summary).toMatchObject({
|
|
totalRows: 0,
|
|
missingFiles: 0,
|
|
hashMismatches: 0,
|
|
verifiedOk: 0,
|
|
existsButNoHash: 0,
|
|
});
|
|
expect(res.body.report.scopes).toEqual(expect.arrayContaining([
|
|
'quote', 'contract', 'contract-signature', 'invoice',
|
|
]));
|
|
});
|
|
|
|
it('surfaces a missing file in the response payload', async () => {
|
|
await db('contracts').insert({
|
|
customer_account_id: customerId,
|
|
contract_number: 'C-B7-MISSING',
|
|
status: 'sent',
|
|
issue_date: '2026-01-01',
|
|
signed_pdf_path: 'business-docs/contract/2026/C-B7-MISSING.pdf',
|
|
created_at: new Date(),
|
|
});
|
|
|
|
const res = await request(app).get('/api/admin/system-health/backup-integrity');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.report.summary.missingFiles).toBe(1);
|
|
expect(res.body.report.missing[0]).toMatchObject({
|
|
table: 'contracts',
|
|
column: 'signed_pdf_path',
|
|
expectedPath: 'business-docs/contract/2026/C-B7-MISSING.pdf',
|
|
});
|
|
});
|
|
|
|
it('honours the ?scope=invoice filter', async () => {
|
|
// Seed both an invoice and a contract with missing files. With
|
|
// scope=invoice the contract row must not appear.
|
|
await db('invoices').insert({
|
|
customer_account_id: customerId,
|
|
invoice_number: 'INV-B7-SCOPE',
|
|
status: 'sent',
|
|
issue_date: '2026-01-01',
|
|
due_date: '2026-01-31',
|
|
pdf_path: 'business-docs/invoice/2026/INV-B7-SCOPE.pdf',
|
|
created_at: new Date(),
|
|
});
|
|
await db('contracts').insert({
|
|
customer_account_id: customerId,
|
|
contract_number: 'C-B7-SCOPE',
|
|
status: 'sent',
|
|
issue_date: '2026-01-01',
|
|
signed_pdf_path: 'business-docs/contract/2026/C-B7-SCOPE.pdf',
|
|
created_at: new Date(),
|
|
});
|
|
|
|
const res = await request(app)
|
|
.get('/api/admin/system-health/backup-integrity')
|
|
.query({ scope: 'invoice' });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.report.scopes).toEqual(['invoice']);
|
|
expect(res.body.report.missing.every((m) => m.table === 'invoices')).toBe(true);
|
|
});
|
|
|
|
it('rejects an unknown scope with 400 + a code', async () => {
|
|
const res = await request(app)
|
|
.get('/api/admin/system-health/backup-integrity')
|
|
.query({ scope: 'gallery' });
|
|
expect(res.status).toBe(400);
|
|
expect(res.body.code).toBe('BACKUP_INTEGRITY_UNKNOWN_SCOPE');
|
|
expect(res.body.validScopes).toEqual(expect.arrayContaining([
|
|
'quote', 'contract', 'contract-signature', 'invoice',
|
|
]));
|
|
});
|
|
});
|