From 1eb43debec47ab6d6a3277929848dc60246589af Mon Sep 17 00:00:00 2001 From: AbuJulaybeeb Date: Mon, 22 Jun 2026 14:34:32 +0100 Subject: [PATCH 1/2] fix: attach auditLog middleware to JWT admin dispute endpoints --- src/__tests__/loanDispute.test.ts | 52 +++++++++++++++++++++++++++++++ src/middleware/auditLog.ts | 1 + src/routes/adminRoutes.ts | 2 ++ 3 files changed, 55 insertions(+) diff --git a/src/__tests__/loanDispute.test.ts b/src/__tests__/loanDispute.test.ts index 324ca10..beef49e 100644 --- a/src/__tests__/loanDispute.test.ts +++ b/src/__tests__/loanDispute.test.ts @@ -280,4 +280,56 @@ 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, ); From 4050e0386fbce7633c0283cececa2526925438e9 Mon Sep 17 00:00:00 2001 From: AbuJulaybeeb Date: Wed, 24 Jun 2026 11:29:36 +0100 Subject: [PATCH 2/2] fix: format loanDispute.test.ts with prettier --- src/__tests__/loanDispute.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/__tests__/loanDispute.test.ts b/src/__tests__/loanDispute.test.ts index beef49e..f00f72a 100644 --- a/src/__tests__/loanDispute.test.ts +++ b/src/__tests__/loanDispute.test.ts @@ -306,7 +306,11 @@ describe("Loan Dispute/Appeal Mechanism", () => { 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" }); + .send({ + action: "confirm", + resolution: "JWT valid default.", + token: "super_secret_token", + }); expect(res.status).toBe(200); expect(res.body.success).toBe(true); @@ -317,7 +321,8 @@ describe("Loan Dispute/Appeal Mechanism", () => { const calls = mockQuery.mock.calls; const auditLogCall = calls.find( (call: any[]) => - typeof call[0] === "string" && call[0].includes("INSERT INTO audit_logs"), + typeof call[0] === "string" && + call[0].includes("INSERT INTO audit_logs"), ); expect(auditLogCall).toBeDefined(); @@ -325,7 +330,7 @@ describe("Loan Dispute/Appeal Mechanism", () => { 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]");