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
17 changes: 12 additions & 5 deletions src/__tests__/adminDisputeController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@
jest.unstable_mockModule("../db/connection.js", () => ({
default: { query: mockQuery },
query: mockQuery,
getClient: jest.fn<() => Promise<unknown>>(),
withTransaction: jest.fn<
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
>((fn) => fn({ query: mockQuery, release: jest.fn() })),
pool: { query: mockQuery },
closePool: jest.fn(),
}));

jest.unstable_mockModule("../services/cacheService.js", () => ({
cacheService: {
get: jest.fn<() => Promise<any>>().mockResolvedValue(null),

Check warning on line 32 in src/__tests__/adminDisputeController.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unexpected any. Specify a different type
set: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
delete: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
Expand All @@ -33,13 +38,15 @@

jest.unstable_mockModule("../services/sorobanService.js", () => ({
sorobanService: {
ping: jest.fn().mockResolvedValue("ok"),
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
},
}));

jest.unstable_mockModule("../services/notificationService.js", () => ({
notificationService: {
createNotification: jest.fn().mockResolvedValue(undefined),
createNotification: jest
.fn<() => Promise<void>>()
.mockResolvedValue(undefined),
},
}));

Expand All @@ -48,7 +55,7 @@

const mockedQuery = mockQuery;

const bearer = (publicKey: string) => ({
const bearer = (publicKey: string, _role?: string) => ({
Authorization: `Bearer ${generateJwtToken(publicKey)}`,
});

Expand Down Expand Up @@ -332,14 +339,14 @@
});

it("should reject resolution on already-resolved dispute", async () => {
const dispute = {

Check warning on line 342 in src/__tests__/adminDisputeController.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

'dispute' is assigned a value but never used
id: 1,
loan_id: 100,
borrower: TEST_BORROWER,
status: "resolved", // Already resolved
};

mockedQuery.mockResolvedValueOnce({
mockedQuery.mockResolvedValue({
rows: [], // No open dispute found
rowCount: 0,
});
Expand Down Expand Up @@ -449,7 +456,7 @@
});

it("should reject resolution on already-resolved dispute", async () => {
mockedQuery.mockResolvedValueOnce({
mockedQuery.mockResolvedValue({
rows: [], // No open dispute found
rowCount: 0,
});
Expand Down
27 changes: 19 additions & 8 deletions src/__tests__/authController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
import { Keypair } from "@stellar/stellar-sdk";

process.env.JWT_SECRET = "test-jwt-secret-min-32-chars-long!!";
// These tests exercise auth flows repeatedly from a single IP, which would
// otherwise trip the login rate limiter. Disable it for this suite only.
process.env.DISABLE_RATE_LIMIT = "true";

jest.unstable_mockModule("../db/connection.js", () => ({
default: { query: jest.fn() },
query: jest.fn(),
getClient: jest.fn<() => Promise<unknown>>(),
withTransaction: jest.fn<
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
>((fn) => fn({ query: jest.fn(), release: jest.fn() })),
pool: { query: jest.fn() },
closePool: jest.fn(),
}));

jest.unstable_mockModule("../services/cacheService.js", () => ({
cacheService: {
get: jest.fn<() => Promise<any>>().mockResolvedValue(null),

Check warning on line 23 in src/__tests__/authController.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unexpected any. Specify a different type
set: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
delete: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
Expand All @@ -21,7 +29,7 @@

jest.unstable_mockModule("../services/sorobanService.js", () => ({
sorobanService: {
ping: jest.fn().mockResolvedValue("ok"),
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
},
}));

Expand All @@ -34,6 +42,7 @@

afterAll(() => {
delete process.env.JWT_SECRET;
delete process.env.DISABLE_RATE_LIMIT;
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -66,7 +75,7 @@
expect(message).toContain("Nonce:");
expect(message).toContain("Timestamp:");
expect(message).toContain(nonce);
expect(message).toContain(timestamp);
expect(message).toContain(String(timestamp));
});

it("should reject missing publicKey", async () => {
Expand Down Expand Up @@ -223,7 +232,7 @@

expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.token).toBeDefined();
expect(response.body.data.token).toBeDefined();
expect(response.headers["set-cookie"]).toBeDefined();
});

Expand All @@ -241,9 +250,9 @@
});

expect(response.status).toBe(200);
expect(response.body.token).toBeDefined();
expect(response.body.data.token).toBeDefined();
// Token should be a valid JWT (three parts separated by dots)
expect(response.body.token.split(".").length).toBe(3);
expect(response.body.data.token.split(".").length).toBe(3);
});

it("should set secure cookie with JWT", async () => {
Expand All @@ -261,7 +270,9 @@

expect(response.status).toBe(200);
expect(response.headers["set-cookie"]).toBeDefined();
const setCookie = response.headers["set-cookie"][0];
const setCookie = (
response.headers["set-cookie"] as unknown as string[]
)[0];
expect(setCookie).toContain("HttpOnly");
expect(setCookie).toContain("Max-Age");
});
Expand Down Expand Up @@ -361,7 +372,7 @@
});

expect(challengeResponse.status).toBe(200);
const { message, nonce, timestamp } = challengeResponse.body.data;

Check warning on line 375 in src/__tests__/authController.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test

'nonce' is assigned a value but never used

// Sign challenge and login
const messageBytes = Buffer.from(message, "utf-8");
Expand All @@ -375,7 +386,7 @@
});

expect(loginResponse.status).toBe(200);
expect(loginResponse.body.token).toBeDefined();
expect(loginResponse.body.data.token).toBeDefined();
});

it("should reject login with stale message", async () => {
Expand Down Expand Up @@ -448,6 +459,6 @@

expect(login1.status).toBe(200);
expect(login2.status).toBe(200);
expect(login1.body.token).not.toBe(login2.body.token);
expect(login1.body.data.token).not.toBe(login2.body.data.token);
});
});
78 changes: 54 additions & 24 deletions src/__tests__/notificationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ process.env.JWT_SECRET = "test-jwt-secret-min-32-chars-long!!";
jest.unstable_mockModule("../db/connection.js", () => ({
default: { query: jest.fn() },
query: jest.fn(),
getClient: jest.fn<() => Promise<unknown>>(),
withTransaction: jest.fn<
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
>((fn) => fn({ query: jest.fn(), release: jest.fn() })),
pool: { query: jest.fn() },
closePool: jest.fn(),
}));

Expand All @@ -27,17 +32,17 @@ jest.unstable_mockModule("../services/cacheService.js", () => ({

jest.unstable_mockModule("../services/sorobanService.js", () => ({
sorobanService: {
ping: jest.fn().mockResolvedValue("ok"),
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
},
}));

const mockNotificationService = {
getNotificationsForUser: jest.fn(),
getUnreadCount: jest.fn(),
markRead: jest.fn(),
markAllRead: jest.fn(),
subscribe: jest.fn(),
createNotification: jest.fn(),
getNotificationsForUser: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
getUnreadCount: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
markRead: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
markAllRead: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
subscribe: jest.fn<(...args: unknown[]) => unknown>(),
createNotification: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
};

jest.unstable_mockModule("../services/notificationService.js", () => ({
Expand All @@ -52,6 +57,41 @@ const bearer = (publicKey: string) => ({
Authorization: `Bearer ${generateJwtToken(publicKey)}`,
});

/**
* SSE endpoints keep the connection open indefinitely, so a normal awaited
* supertest request never resolves. This helper opens the stream, resolves once
* the response headers have arrived (by which point the controller has already
* run its synchronous on-connect logic: getNotificationsForUser + subscribe),
* then aborts the underlying request so the test can complete.
*/
const sseRequest = (
publicKey?: string,
): Promise<{ status: number; headers: Record<string, string> }> =>
new Promise((resolve, reject) => {
const req = request(app).get("/api/notifications/stream");
if (publicKey) {
req.set(bearer(publicKey));
}
req
.buffer(false)
.parse((res, callback) => {
const httpRes = res as unknown as {
statusCode: number;
headers: Record<string, string>;
};
resolve({ status: httpRes.statusCode, headers: httpRes.headers });
// Abort the streaming connection; we already have what we need.
(res as unknown as { destroy: () => void }).destroy();
callback(null, {});
})
.end((err) => {
// A forcibly-destroyed socket surfaces as an aborted error; ignore it.
if (err && !/aborted|socket hang up|ECONNRESET/i.test(err.message)) {
reject(err);
}
});
});

beforeEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -355,9 +395,7 @@ describe("GET /api/notifications/stream", () => {
mockNotificationService.getNotificationsForUser.mockResolvedValueOnce([]);
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);

const response = await request(app)
.get("/api/notifications/stream")
.set(bearer(TEST_USER));
const response = await sseRequest(TEST_USER);

// SSE streams are typically handled with 200 status and event-stream content-type
expect(response.status).toBe(200);
Expand All @@ -381,9 +419,7 @@ describe("GET /api/notifications/stream", () => {
]);
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);

const response = await request(app)
.get("/api/notifications/stream")
.set(bearer(TEST_USER));
const response = await sseRequest(TEST_USER);

expect(response.status).toBe(200);
expect(
Expand All @@ -396,9 +432,7 @@ describe("GET /api/notifications/stream", () => {
mockNotificationService.getNotificationsForUser.mockResolvedValueOnce([]);
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);

const response = await request(app)
.get("/api/notifications/stream")
.set(bearer(TEST_USER));
const response = await sseRequest(TEST_USER);

expect(response.status).toBe(200);
expect(response.headers["content-type"]).toContain("text/event-stream");
Expand All @@ -411,7 +445,7 @@ describe("GET /api/notifications/stream", () => {
mockNotificationService.getNotificationsForUser.mockResolvedValueOnce([]);
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);

await request(app).get("/api/notifications/stream").set(bearer(TEST_USER));
await sseRequest(TEST_USER);

expect(mockNotificationService.subscribe).toHaveBeenCalledWith(
TEST_USER,
Expand All @@ -424,9 +458,7 @@ describe("GET /api/notifications/stream", () => {
mockNotificationService.getNotificationsForUser.mockResolvedValueOnce([]);
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);

const response = await request(app)
.get("/api/notifications/stream")
.set(bearer(TEST_USER));
const response = await sseRequest(TEST_USER);

expect(response.status).toBe(200);
});
Expand Down Expand Up @@ -481,7 +513,7 @@ describe("Notification Controller - Authorization", () => {
mockNotificationService.getNotificationsForUser.mockResolvedValueOnce([]);
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);

await request(app).get("/api/notifications/stream").set(bearer(TEST_USER));
await sseRequest(TEST_USER);

// Should subscribe with TEST_USER
expect(mockNotificationService.subscribe).toHaveBeenCalledWith(
Expand Down Expand Up @@ -548,9 +580,7 @@ describe("Notification Controller - Happy Path Scenarios", () => {
);
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);

const response = await request(app)
.get("/api/notifications/stream")
.set(bearer(TEST_USER));
const response = await sseRequest(TEST_USER);

expect(response.status).toBe(200);
expect(mockNotificationService.getNotificationsForUser).toHaveBeenCalled();
Expand Down
21 changes: 14 additions & 7 deletions src/__tests__/remittanceController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ const mockQuery: jest.MockedFunction<
jest.unstable_mockModule("../db/connection.js", () => ({
default: { query: mockQuery },
query: mockQuery,
getClient: jest.fn<() => Promise<unknown>>(),
withTransaction: jest.fn<
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
>((fn) => fn({ query: mockQuery, release: jest.fn() })),
pool: { query: mockQuery },
closePool: jest.fn(),
}));

Expand All @@ -40,21 +45,23 @@ const mockSubmitSignedTx =
jest.unstable_mockModule("../services/sorobanService.js", () => ({
sorobanService: {
submitSignedTx: mockSubmitSignedTx,
ping: jest.fn().mockResolvedValue("ok"),
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
},
}));

jest.unstable_mockModule("../services/notificationService.js", () => ({
notificationService: {
createNotification: jest.fn().mockResolvedValue(undefined),
createNotification: jest
.fn<() => Promise<void>>()
.mockResolvedValue(undefined),
},
}));

const mockRemittanceService = {
createRemittance: jest.fn(),
getRemittances: jest.fn(),
getRemittance: jest.fn(),
updateRemittanceStatus: jest.fn(),
createRemittance: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
getRemittances: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
getRemittance: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
updateRemittanceStatus: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
};

jest.unstable_mockModule("../services/remittanceService.js", () => ({
Expand Down Expand Up @@ -239,7 +246,7 @@ describe("GET /api/remittances", () => {
expect(mockRemittanceService.getRemittances).toHaveBeenCalledWith(
TEST_SENDER,
expect.anything(),
expect.anything(),
null,
"completed",
);
});
Expand Down
Loading
Loading