Skip to content

Commit 4fca5bb

Browse files
authored
Merge pull request #45 from LabsCrypt/fix/ci-typecheck-and-test-mocks
fix(ci): green typecheck + tests (mock completeness + real remittance-auth/rbac bug fixes)
2 parents 1972183 + 5e19d99 commit 4fca5bb

10 files changed

Lines changed: 137 additions & 60 deletions

src/__tests__/adminDisputeController.test.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const mockQuery: jest.MockedFunction<
1919
jest.unstable_mockModule("../db/connection.js", () => ({
2020
default: { query: mockQuery },
2121
query: mockQuery,
22+
getClient: jest.fn<() => Promise<unknown>>(),
23+
withTransaction: jest.fn<
24+
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
25+
>((fn) => fn({ query: mockQuery, release: jest.fn() })),
26+
pool: { query: mockQuery },
2227
closePool: jest.fn(),
2328
}));
2429

@@ -33,13 +38,15 @@ jest.unstable_mockModule("../services/cacheService.js", () => ({
3338

3439
jest.unstable_mockModule("../services/sorobanService.js", () => ({
3540
sorobanService: {
36-
ping: jest.fn().mockResolvedValue("ok"),
41+
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
3742
},
3843
}));
3944

4045
jest.unstable_mockModule("../services/notificationService.js", () => ({
4146
notificationService: {
42-
createNotification: jest.fn().mockResolvedValue(undefined),
47+
createNotification: jest
48+
.fn<() => Promise<void>>()
49+
.mockResolvedValue(undefined),
4350
},
4451
}));
4552

@@ -48,7 +55,7 @@ const { default: app } = await import("../app.js");
4855

4956
const mockedQuery = mockQuery;
5057

51-
const bearer = (publicKey: string) => ({
58+
const bearer = (publicKey: string, _role?: string) => ({
5259
Authorization: `Bearer ${generateJwtToken(publicKey)}`,
5360
});
5461

@@ -339,7 +346,7 @@ describe("POST /api/admin/disputes/:disputeId/resolve", () => {
339346
status: "resolved", // Already resolved
340347
};
341348

342-
mockedQuery.mockResolvedValueOnce({
349+
mockedQuery.mockResolvedValue({
343350
rows: [], // No open dispute found
344351
rowCount: 0,
345352
});
@@ -449,7 +456,7 @@ describe("POST /api/admin/disputes/:disputeId/reject", () => {
449456
});
450457

451458
it("should reject resolution on already-resolved dispute", async () => {
452-
mockedQuery.mockResolvedValueOnce({
459+
mockedQuery.mockResolvedValue({
453460
rows: [], // No open dispute found
454461
rowCount: 0,
455462
});

src/__tests__/authController.test.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ import { jest } from "@jest/globals";
33
import { Keypair } from "@stellar/stellar-sdk";
44

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

710
jest.unstable_mockModule("../db/connection.js", () => ({
811
default: { query: jest.fn() },
912
query: jest.fn(),
13+
getClient: jest.fn<() => Promise<unknown>>(),
14+
withTransaction: jest.fn<
15+
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
16+
>((fn) => fn({ query: jest.fn(), release: jest.fn() })),
17+
pool: { query: jest.fn() },
1018
closePool: jest.fn(),
1119
}));
1220

@@ -21,7 +29,7 @@ jest.unstable_mockModule("../services/cacheService.js", () => ({
2129

2230
jest.unstable_mockModule("../services/sorobanService.js", () => ({
2331
sorobanService: {
24-
ping: jest.fn().mockResolvedValue("ok"),
32+
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
2533
},
2634
}));
2735

@@ -34,6 +42,7 @@ beforeEach(() => {
3442

3543
afterAll(() => {
3644
delete process.env.JWT_SECRET;
45+
delete process.env.DISABLE_RATE_LIMIT;
3746
});
3847

3948
// ---------------------------------------------------------------------------
@@ -66,7 +75,7 @@ describe("POST /api/auth/challenge", () => {
6675
expect(message).toContain("Nonce:");
6776
expect(message).toContain("Timestamp:");
6877
expect(message).toContain(nonce);
69-
expect(message).toContain(timestamp);
78+
expect(message).toContain(String(timestamp));
7079
});
7180

7281
it("should reject missing publicKey", async () => {
@@ -223,7 +232,7 @@ describe("POST /api/auth/login", () => {
223232

224233
expect(response.status).toBe(200);
225234
expect(response.body.success).toBe(true);
226-
expect(response.body.token).toBeDefined();
235+
expect(response.body.data.token).toBeDefined();
227236
expect(response.headers["set-cookie"]).toBeDefined();
228237
});
229238

@@ -241,9 +250,9 @@ describe("POST /api/auth/login", () => {
241250
});
242251

243252
expect(response.status).toBe(200);
244-
expect(response.body.token).toBeDefined();
253+
expect(response.body.data.token).toBeDefined();
245254
// Token should be a valid JWT (three parts separated by dots)
246-
expect(response.body.token.split(".").length).toBe(3);
255+
expect(response.body.data.token.split(".").length).toBe(3);
247256
});
248257

249258
it("should set secure cookie with JWT", async () => {
@@ -261,7 +270,9 @@ describe("POST /api/auth/login", () => {
261270

262271
expect(response.status).toBe(200);
263272
expect(response.headers["set-cookie"]).toBeDefined();
264-
const setCookie = response.headers["set-cookie"][0];
273+
const setCookie = (
274+
response.headers["set-cookie"] as unknown as string[]
275+
)[0];
265276
expect(setCookie).toContain("HttpOnly");
266277
expect(setCookie).toContain("Max-Age");
267278
});
@@ -375,7 +386,7 @@ describe("Auth Controller - Happy Path Scenarios", () => {
375386
});
376387

377388
expect(loginResponse.status).toBe(200);
378-
expect(loginResponse.body.token).toBeDefined();
389+
expect(loginResponse.body.data.token).toBeDefined();
379390
});
380391

381392
it("should reject login with stale message", async () => {
@@ -448,6 +459,6 @@ describe("Auth Controller - Happy Path Scenarios", () => {
448459

449460
expect(login1.status).toBe(200);
450461
expect(login2.status).toBe(200);
451-
expect(login1.body.token).not.toBe(login2.body.token);
462+
expect(login1.body.data.token).not.toBe(login2.body.data.token);
452463
});
453464
});

src/__tests__/notificationController.test.ts

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ process.env.JWT_SECRET = "test-jwt-secret-min-32-chars-long!!";
1313
jest.unstable_mockModule("../db/connection.js", () => ({
1414
default: { query: jest.fn() },
1515
query: jest.fn(),
16+
getClient: jest.fn<() => Promise<unknown>>(),
17+
withTransaction: jest.fn<
18+
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
19+
>((fn) => fn({ query: jest.fn(), release: jest.fn() })),
20+
pool: { query: jest.fn() },
1621
closePool: jest.fn(),
1722
}));
1823

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

2833
jest.unstable_mockModule("../services/sorobanService.js", () => ({
2934
sorobanService: {
30-
ping: jest.fn().mockResolvedValue("ok"),
35+
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
3136
},
3237
}));
3338

3439
const mockNotificationService = {
35-
getNotificationsForUser: jest.fn(),
36-
getUnreadCount: jest.fn(),
37-
markRead: jest.fn(),
38-
markAllRead: jest.fn(),
39-
subscribe: jest.fn(),
40-
createNotification: jest.fn(),
40+
getNotificationsForUser: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
41+
getUnreadCount: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
42+
markRead: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
43+
markAllRead: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
44+
subscribe: jest.fn<(...args: unknown[]) => unknown>(),
45+
createNotification: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
4146
};
4247

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

60+
/**
61+
* SSE endpoints keep the connection open indefinitely, so a normal awaited
62+
* supertest request never resolves. This helper opens the stream, resolves once
63+
* the response headers have arrived (by which point the controller has already
64+
* run its synchronous on-connect logic: getNotificationsForUser + subscribe),
65+
* then aborts the underlying request so the test can complete.
66+
*/
67+
const sseRequest = (
68+
publicKey?: string,
69+
): Promise<{ status: number; headers: Record<string, string> }> =>
70+
new Promise((resolve, reject) => {
71+
const req = request(app).get("/api/notifications/stream");
72+
if (publicKey) {
73+
req.set(bearer(publicKey));
74+
}
75+
req
76+
.buffer(false)
77+
.parse((res, callback) => {
78+
const httpRes = res as unknown as {
79+
statusCode: number;
80+
headers: Record<string, string>;
81+
};
82+
resolve({ status: httpRes.statusCode, headers: httpRes.headers });
83+
// Abort the streaming connection; we already have what we need.
84+
(res as unknown as { destroy: () => void }).destroy();
85+
callback(null, {});
86+
})
87+
.end((err) => {
88+
// A forcibly-destroyed socket surfaces as an aborted error; ignore it.
89+
if (err && !/aborted|socket hang up|ECONNRESET/i.test(err.message)) {
90+
reject(err);
91+
}
92+
});
93+
});
94+
5595
beforeEach(() => {
5696
jest.clearAllMocks();
5797
});
@@ -355,9 +395,7 @@ describe("GET /api/notifications/stream", () => {
355395
mockNotificationService.getNotificationsForUser.mockResolvedValueOnce([]);
356396
mockNotificationService.subscribe.mockReturnValueOnce(mockUnsubscribe);
357397

358-
const response = await request(app)
359-
.get("/api/notifications/stream")
360-
.set(bearer(TEST_USER));
398+
const response = await sseRequest(TEST_USER);
361399

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

384-
const response = await request(app)
385-
.get("/api/notifications/stream")
386-
.set(bearer(TEST_USER));
422+
const response = await sseRequest(TEST_USER);
387423

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

399-
const response = await request(app)
400-
.get("/api/notifications/stream")
401-
.set(bearer(TEST_USER));
435+
const response = await sseRequest(TEST_USER);
402436

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

414-
await request(app).get("/api/notifications/stream").set(bearer(TEST_USER));
448+
await sseRequest(TEST_USER);
415449

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

427-
const response = await request(app)
428-
.get("/api/notifications/stream")
429-
.set(bearer(TEST_USER));
461+
const response = await sseRequest(TEST_USER);
430462

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

484-
await request(app).get("/api/notifications/stream").set(bearer(TEST_USER));
516+
await sseRequest(TEST_USER);
485517

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

551-
const response = await request(app)
552-
.get("/api/notifications/stream")
553-
.set(bearer(TEST_USER));
583+
const response = await sseRequest(TEST_USER);
554584

555585
expect(response.status).toBe(200);
556586
expect(mockNotificationService.getNotificationsForUser).toHaveBeenCalled();

src/__tests__/remittanceController.test.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ const mockQuery: jest.MockedFunction<
1818
jest.unstable_mockModule("../db/connection.js", () => ({
1919
default: { query: mockQuery },
2020
query: mockQuery,
21+
getClient: jest.fn<() => Promise<unknown>>(),
22+
withTransaction: jest.fn<
23+
(fn: (client: unknown) => Promise<unknown>) => Promise<unknown>
24+
>((fn) => fn({ query: mockQuery, release: jest.fn() })),
25+
pool: { query: mockQuery },
2126
closePool: jest.fn(),
2227
}));
2328

@@ -40,21 +45,23 @@ const mockSubmitSignedTx =
4045
jest.unstable_mockModule("../services/sorobanService.js", () => ({
4146
sorobanService: {
4247
submitSignedTx: mockSubmitSignedTx,
43-
ping: jest.fn().mockResolvedValue("ok"),
48+
ping: jest.fn<() => Promise<string>>().mockResolvedValue("ok"),
4449
},
4550
}));
4651

4752
jest.unstable_mockModule("../services/notificationService.js", () => ({
4853
notificationService: {
49-
createNotification: jest.fn().mockResolvedValue(undefined),
54+
createNotification: jest
55+
.fn<() => Promise<void>>()
56+
.mockResolvedValue(undefined),
5057
},
5158
}));
5259

5360
const mockRemittanceService = {
54-
createRemittance: jest.fn(),
55-
getRemittances: jest.fn(),
56-
getRemittance: jest.fn(),
57-
updateRemittanceStatus: jest.fn(),
61+
createRemittance: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
62+
getRemittances: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
63+
getRemittance: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
64+
updateRemittanceStatus: jest.fn<(...args: unknown[]) => Promise<unknown>>(),
5865
};
5966

6067
jest.unstable_mockModule("../services/remittanceService.js", () => ({
@@ -239,7 +246,7 @@ describe("GET /api/remittances", () => {
239246
expect(mockRemittanceService.getRemittances).toHaveBeenCalledWith(
240247
TEST_SENDER,
241248
expect.anything(),
242-
expect.anything(),
249+
null,
243250
"completed",
244251
);
245252
});

0 commit comments

Comments
 (0)