diff --git a/src/__tests__/loanDispute.test.ts b/src/__tests__/loanDispute.test.ts index 324ca10..f00f72a 100644 --- a/src/__tests__/loanDispute.test.ts +++ b/src/__tests__/loanDispute.test.ts @@ -280,4 +280,61 @@ describe("Loan Dispute/Appeal Mechanism", () => { expect(res.status).toBe(200); expect(res.body.success).toBe(true); }); + + it("should allow admin to resolve dispute via JWT and log audit", async () => { + const adminToken = jwt.sign( + { publicKey: TEST_PUBLIC_KEY, role: "admin", scopes: [] }, + process.env.JWT_SECRET!, + { algorithm: "HS256", expiresIn: "1h" }, + ); + + mockQuery + .mockResolvedValueOnce(dbOk()) // [1] INSERT audit_logs (middleware runs first) + .mockResolvedValueOnce( + dbRows([ + { + id: disputeId, + loan_id: LOAN_ID, + borrower: TEST_PUBLIC_KEY, + status: "open", + }, + ]), + ) // [2] SELECT dispute + .mockResolvedValueOnce(dbOk("UPDATE")) // [3] UPDATE dispute + .mockResolvedValueOnce(dbOk()); // [4] INSERT contract_events + + const res = await request(app) + .post(`/api/admin/disputes/${disputeId}/resolve`) + .set("Authorization", `Bearer ${adminToken}`) + .send({ + action: "confirm", + resolution: "JWT valid default.", + token: "super_secret_token", + }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + + // Wait for the async audit log query to fire + await new Promise((resolve) => setTimeout(resolve, 50)); + + const calls = mockQuery.mock.calls; + const auditLogCall = calls.find( + (call: any[]) => + typeof call[0] === "string" && + call[0].includes("INSERT INTO audit_logs"), + ); + + expect(auditLogCall).toBeDefined(); + if (auditLogCall) { + expect(auditLogCall[1][0]).toBe(TEST_PUBLIC_KEY); + expect(auditLogCall[1][1]).toContain("POST /disputes"); + expect(auditLogCall[1][2]).toBe(`DisputeID:${disputeId}`); + + const payloadString = auditLogCall[1][3]; + expect(payloadString).toContain("JWT valid default."); + expect(payloadString).toContain("[REDACTED]"); + expect(payloadString).not.toContain("super_secret_token"); + } + }); }); diff --git a/src/middleware/auditLog.ts b/src/middleware/auditLog.ts index 3153737..42faea0 100644 --- a/src/middleware/auditLog.ts +++ b/src/middleware/auditLog.ts @@ -36,6 +36,7 @@ function extractTarget(req: Request): string | undefined { // Check common path parameters if (req.params.id) return `ID:${req.params.id}`; if (req.params.loanId) return `LoanID:${req.params.loanId}`; + if (req.params.disputeId) return `DisputeID:${req.params.disputeId}`; if (req.params.address) return `Address:${req.params.address}`; if (req.params.userId) return `UserID:${req.params.userId}`; if (req.params.borrower) return `Borrower:${req.params.borrower}`; diff --git a/src/routes/adminRoutes.ts b/src/routes/adminRoutes.ts index 6475dce..3a6ffb5 100644 --- a/src/routes/adminRoutes.ts +++ b/src/routes/adminRoutes.ts @@ -101,12 +101,14 @@ router.post( "/disputes/:disputeId/resolve", requireJwtAuth, requireRoles("admin"), + auditLog, resolveLoanDispute, ); router.post( "/disputes/:disputeId/reject", requireJwtAuth, requireRoles("admin"), + auditLog, rejectLoanDispute, );