Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions backend/src/__tests__/adminDisputePagination.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
import type { NextFunction, Request, Response } from "express";

type MockQueryResult = { rows: Record<string, unknown>[]; rowCount: number };

const mockQuery: jest.MockedFunction<
(sql: string, params?: unknown[]) => Promise<MockQueryResult>
> = jest.fn();

jest.unstable_mockModule("../db/connection.js", () => ({
query: mockQuery,
getClient: jest.fn(),
}));

const { listLoanDisputes } = await import("../controllers/adminDisputeController.js");

Check failure on line 15 in backend/src/__tests__/adminDisputePagination.test.ts

View workflow job for this annotation

GitHub Actions / backend

Insert `⏎·`

const flushAsync = async (): Promise<void> =>
new Promise((resolve) => setImmediate(resolve));

const createMockResponse = (): Response =>
({
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
}) as unknown as Response;

function disputeRow(id: number, status: string, created_at: string) {
return {
id,
loan_id: 100 + id,
borrower: "GBORROWER",
status,
reason: "Test reason",
created_at,
};
}

describe("listLoanDisputes pagination", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("returns disputes with default limit and status=open", async () => {
const rows = [
disputeRow(3, "open", "2026-05-28T10:00:00.000Z"),
disputeRow(2, "open", "2026-05-27T10:00:00.000Z"),
disputeRow(1, "open", "2026-05-26T10:00:00.000Z"),
];
// LIMIT is default 50 + 1 = 51 — fewer rows than limit, no next page
mockQuery.mockResolvedValueOnce({ rows, rowCount: 3 });

const req = { query: {} } as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

listLoanDisputes(req, res, next as unknown as NextFunction);
await flushAsync();

expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
page_info: expect.objectContaining({
limit: 50,
count: 3,
has_next: false,
next_cursor: null,
}),
}),
);
});

it("returns next_cursor when there are more results than limit", async () => {
const rows = Array.from({ length: 51 }, (_, i) =>
disputeRow(
100 - i,
"open",
new Date(2026, 4, 28, 10, 0, 0, -i * 60_000).toISOString(),
),
);
// 51 rows for limit=50 + 1 check
mockQuery.mockResolvedValueOnce({ rows, rowCount: 51 });

const req = { query: {} } as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

listLoanDisputes(req, res, next as unknown as NextFunction);
await flushAsync();

const jsonCall = (res.json as jest.Mock).mock.calls[0]?.[0] as Record<string, unknown>;

Check failure on line 89 in backend/src/__tests__/adminDisputePagination.test.ts

View workflow job for this annotation

GitHub Actions / backend

Replace `string,·unknown` with `⏎······string,⏎······unknown⏎····`
expect(jsonCall.success).toBe(true);
const pageInfo = jsonCall.page_info as Record<string, unknown>;
expect(pageInfo.has_next).toBe(true);
expect(typeof pageInfo.next_cursor).toBe("string");
expect((jsonCall.data as unknown[]).length).toBe(50);
});

it("enforces max page size (capped at 100)", async () => {
const rows = Array.from({ length: 100 }, (_, i) =>
disputeRow(
i,
"open",
new Date(2026, 4, 28, 10, 0, 0, -i * 60_000).toISOString(),
),
);
mockQuery.mockResolvedValueOnce({ rows, rowCount: 100 });

const req = { query: { limit: "500" } } as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

listLoanDisputes(req, res, next as unknown as NextFunction);
await flushAsync();

const jsonCall = (res.json as jest.Mock).mock.calls[0]?.[0] as Record<string, unknown>;

Check failure on line 114 in backend/src/__tests__/adminDisputePagination.test.ts

View workflow job for this annotation

GitHub Actions / backend

Replace `string,·unknown` with `⏎······string,⏎······unknown⏎····`
const pageInfo = jsonCall.page_info as Record<string, unknown>;
// limit should be capped at 100
expect(pageInfo.limit).toBe(100);
});

it("filters by status correctly", async () => {
const rows = [

Check failure on line 121 in backend/src/__tests__/adminDisputePagination.test.ts

View workflow job for this annotation

GitHub Actions / backend

Replace `⏎······disputeRow(1,·"resolved",·"2026-05-28T10:00:00.000Z"),⏎····` with `disputeRow(1,·"resolved",·"2026-05-28T10:00:00.000Z")`
disputeRow(1, "resolved", "2026-05-28T10:00:00.000Z"),
];
mockQuery.mockResolvedValueOnce({ rows, rowCount: 1 });

const req = { query: { status: "resolved" } } as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

listLoanDisputes(req, res, next as unknown as NextFunction);
await flushAsync();

// Verify SQL includes status filter
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("WHERE status = $1"),
expect.arrayContaining(["resolved"]),
);
});

it("includes all statuses when status=all", async () => {
const rows = [
disputeRow(3, "open", "2026-05-28T10:00:00.000Z"),
disputeRow(2, "resolved", "2026-05-27T10:00:00.000Z"),
disputeRow(1, "rejected", "2026-05-26T10:00:00.000Z"),
];
mockQuery.mockResolvedValueOnce({ rows, rowCount: 3 });

const req = { query: { status: "all" } } as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

listLoanDisputes(req, res, next as unknown as NextFunction);
await flushAsync();

// No WHERE clause for status
expect(mockQuery).toHaveBeenCalledWith(
expect.not.stringContaining("WHERE status"),
expect.any(Array),
);
});

it("uses cursor pagination when cursor is provided", async () => {
const rows = [
disputeRow(2, "open", "2026-05-27T10:00:00.000Z"),
disputeRow(1, "open", "2026-05-26T10:00:00.000Z"),
];
mockQuery.mockResolvedValueOnce({ rows, rowCount: 2 });

const req = {
query: { cursor: "2026-05-28T10:00:00.000Z" },
} as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

listLoanDisputes(req, res, next as unknown as NextFunction);
await flushAsync();

expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("created_at < $2"),
expect.arrayContaining(["2026-05-28T10:00:00.000Z"]),
);
});

it("orders newest-first by default", async () => {
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });

const req = { query: {} } as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

listLoanDisputes(req, res, next as unknown as NextFunction);
await flushAsync();

expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining("ORDER BY created_at DESC"),
expect.any(Array),
);
});
});
113 changes: 113 additions & 0 deletions backend/src/__tests__/poolController.emergencyWithdraw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
import type { NextFunction, Request, Response } from "express";

const mockBuildEmergencyWithdrawTx = jest.fn<

Check failure on line 4 in backend/src/__tests__/poolController.emergencyWithdraw.test.ts

View workflow job for this annotation

GitHub Actions / backend

Insert `⏎·`
(

Check failure on line 5 in backend/src/__tests__/poolController.emergencyWithdraw.test.ts

View workflow job for this annotation

GitHub Actions / backend

Insert `··`
providerPublicKey: string,

Check failure on line 6 in backend/src/__tests__/poolController.emergencyWithdraw.test.ts

View workflow job for this annotation

GitHub Actions / backend

Insert `··`
tokenAddress: string,

Check failure on line 7 in backend/src/__tests__/poolController.emergencyWithdraw.test.ts

View workflow job for this annotation

GitHub Actions / backend

Replace `····` with `······`
shares: number,

Check failure on line 8 in backend/src/__tests__/poolController.emergencyWithdraw.test.ts

View workflow job for this annotation

GitHub Actions / backend

Insert `··`
) => Promise<{ unsignedTxXdr: string; networkPassphrase: string }>

Check failure on line 9 in backend/src/__tests__/poolController.emergencyWithdraw.test.ts

View workflow job for this annotation

GitHub Actions / backend

Insert `··`
>();

jest.unstable_mockModule("../services/sorobanService.js", () => ({
sorobanService: {
buildEmergencyWithdrawTx: mockBuildEmergencyWithdrawTx,
getSharePrice: jest.fn(),
},
}));

jest.unstable_mockModule("../db/connection.js", () => ({
query: jest.fn(),
getClient: jest.fn(),
}));

jest.unstable_mockModule("../services/cacheService.js", () => ({
cacheService: {
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(undefined),
},
}));

const { emergencyWithdrawFromPool } =
await import("../controllers/poolController.js");

const flushAsync = async (): Promise<void> =>
new Promise((resolve) => setImmediate(resolve));

const createMockResponse = (): Response =>
({
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
}) as unknown as Response;

describe("emergencyWithdrawFromPool", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("builds an unsigned emergency withdraw transaction", async () => {
mockBuildEmergencyWithdrawTx.mockResolvedValue({
unsignedTxXdr: "AAAAAgAAAAtlbWVyZ2VuY3lfd2l0aGRyYXc=",
networkPassphrase: "Test SDF Network ; September 2015",
});

const req = {
body: {
depositorPublicKey: "GDEPOSITOR123",
token: "GTOKEN456",
shares: 500,
},
user: { publicKey: "GDEPOSITOR123" },
} as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

emergencyWithdrawFromPool(req, res, next as unknown as NextFunction);
await flushAsync();

expect(mockBuildEmergencyWithdrawTx).toHaveBeenCalledWith(
"GDEPOSITOR123",
"GTOKEN456",
500,
);
expect(res.json).toHaveBeenCalledWith({
success: true,
unsignedTxXdr: "AAAAAgAAAAtlbWVyZ2VuY3lfd2l0aGRyYXc=",
networkPassphrase: "Test SDF Network ; September 2015",
});
});

it("rejects when depositorPublicKey does not match JWT", async () => {
const req = {
body: {
depositorPublicKey: "GWRONGKEY",
token: "GTOKEN456",
shares: 500,
},
user: { publicKey: "GDEPOSITOR123" },
} as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

emergencyWithdrawFromPool(req, res, next as unknown as NextFunction);
await flushAsync();

expect(mockBuildEmergencyWithdrawTx).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.objectContaining({ statusCode: 403 }));
});

it("rejects when required fields are missing", async () => {
const req = {
body: { depositorPublicKey: "GDEPOSITOR123" },
user: { publicKey: "GDEPOSITOR123" },
} as unknown as Request;
const res = createMockResponse();
const next = jest.fn<(err?: unknown) => void>();

emergencyWithdrawFromPool(req, res, next as unknown as NextFunction);
await flushAsync();

expect(mockBuildEmergencyWithdrawTx).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.objectContaining({ statusCode: 400 }));
});
});
Loading
Loading