Add remediation plan and resolution date history tracking
New table compliance_item_history stores an append-only audit trail of changes to resolution_date and remediation_plan. The current values remain on compliance_items for fast VCL reporting queries (no double-counting). Backend: - Migration: creates compliance_item_history with indexes - PATCH /items/:hostname/metadata: records old→new in history before updating, accepts optional change_reason field (max 500 chars) - GET /items/:hostname: returns history array (last 10 entries, newest first) - POST /vcl/bulk-commit: records history for each changed field per hostname Frontend: - ComplianceDetailPanel: added change reason input below Save button - Added Change History section showing field changes with timestamps, usernames, old→new values, and reasons - Re-fetches detail after save to show updated history immediately Tests updated to match new transaction-based PATCH flow.
This commit is contained in:
@@ -112,7 +112,27 @@ beforeEach(() => {
|
||||
|
||||
describe('PATCH /items/:hostname/metadata', () => {
|
||||
it('happy path — updates resolution_date and remediation_plan', async () => {
|
||||
mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 2 });
|
||||
// Mock client.query: first call = SELECT current values, second+ = INSERT history / UPDATE
|
||||
const mockClient = {
|
||||
query: jest.fn()
|
||||
.mockResolvedValueOnce({ rows: [{ resolution_date: null, remediation_plan: null }], rowCount: 1 }) // SELECT current
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (resolution_date)
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (remediation_plan)
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 2 }) // UPDATE items
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 }), // COMMIT
|
||||
release: jest.fn(),
|
||||
};
|
||||
// Override connect to return our mock client
|
||||
mockPool.connect.mockResolvedValueOnce(mockClient);
|
||||
// The first call from the handler is BEGIN, then SELECT, then inserts, then UPDATE, then COMMIT
|
||||
mockClient.query = jest.fn()
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [{ resolution_date: null, remediation_plan: null }], rowCount: 1 }) // SELECT current values
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (resolution_date)
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 }) // INSERT history (remediation_plan)
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 2 }) // UPDATE items
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 }); // COMMIT
|
||||
|
||||
const res = await request(server, 'PATCH', '/api/compliance/items/srv-001/metadata', {
|
||||
resolution_date: '2026-06-15',
|
||||
@@ -143,7 +163,14 @@ describe('PATCH /items/:hostname/metadata', () => {
|
||||
});
|
||||
|
||||
it('returns 404 when hostname not found', async () => {
|
||||
mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const mockClient = {
|
||||
query: jest.fn()
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 }) // SELECT current values — empty = not found
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 }), // ROLLBACK
|
||||
release: jest.fn(),
|
||||
};
|
||||
mockPool.connect.mockResolvedValueOnce(mockClient);
|
||||
|
||||
const res = await request(server, 'PATCH', '/api/compliance/items/nonexistent-host/metadata', {
|
||||
resolution_date: '2026-06-15',
|
||||
@@ -277,6 +304,9 @@ describe('Integration: full bulk upload flow (preview → commit)', () => {
|
||||
};
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({}) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [{ hostname: 'srv-001', resolution_date: null, remediation_plan: null }] }) // SELECT current values for all hostnames
|
||||
.mockResolvedValueOnce({ rowCount: 1 }) // INSERT history (resolution_date)
|
||||
.mockResolvedValueOnce({ rowCount: 1 }) // INSERT history (remediation_plan)
|
||||
.mockResolvedValueOnce({ rowCount: 1 }) // UPDATE srv-001
|
||||
.mockResolvedValueOnce({}); // COMMIT
|
||||
|
||||
|
||||
Reference in New Issue
Block a user