Skip to content

Commit 77113c4

Browse files
Shoaib AnsariShoaib Ansari
authored andcommitted
fix(ci): resolve backend and frontend failures
1 parent 0d2edb3 commit 77113c4

8 files changed

Lines changed: 137 additions & 47 deletions

File tree

backend/package-lock.json

Lines changed: 4 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"cron-parser": "^4.9.0",
1818
"dotenv": "^16.4.5",
1919
"express": "^4.21.0",
20+
"express-rate-limit": "^7.4.1",
2021
"node-cron": "^3.0.3",
2122
"openai": "^4.67.0",
2223
"zod": "^3.23.8"

backend/src/routes/__tests__/health.test.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { healthRouter } from '../health.js';
3-
import { Request, Response } from 'express';
3+
import { Request, Response, Router, RequestHandler } from 'express';
44
import { server as stellarServer } from '../../services/stellar.js';
55
import { getJobScheduler } from '../../jobs/index.js';
66

@@ -20,6 +20,17 @@ describe('Health Router', () => {
2020
let resJson: any;
2121
let resStatus: any;
2222

23+
const getRouteHandler = (router: Router, path: string): RequestHandler => {
24+
const layer = router.stack.find((entry) => entry.route?.path === path);
25+
const handler = layer?.route?.stack[0]?.handle;
26+
27+
if (!handler) {
28+
throw new Error(`Route handler not found for ${path}`);
29+
}
30+
31+
return handler;
32+
};
33+
2334
beforeEach(() => {
2435
vi.clearAllMocks();
2536
resJson = vi.fn();
@@ -32,13 +43,16 @@ describe('Health Router', () => {
3243

3344
describe('GET /health', () => {
3445
it('returns 200 and healthy status when all dependencies are up', async () => {
35-
vi.mocked(stellarServer.root).mockResolvedValue({} as any);
46+
const mockedStellarRoot = vi.mocked(
47+
(stellarServer as unknown as { root: () => Promise<unknown> }).root
48+
);
49+
50+
mockedStellarRoot.mockResolvedValue({});
3651
vi.mocked(getJobScheduler).mockReturnValue({} as any);
3752
process.env.OPENAI_API_KEY = 'test-key';
3853

39-
// Access the private handler (for testing purposes in vitest)
40-
const handler = (healthRouter.stack.find(s => s.route.path === '/health')?.route.stack[0].handle);
41-
await handler(mockReq as Request, mockRes as Response);
54+
const handler = getRouteHandler(healthRouter, '/health');
55+
await handler(mockReq as Request, mockRes as Response, vi.fn());
4256

4357
expect(resStatus).toHaveBeenCalledWith(200);
4458
expect(resJson).toHaveBeenCalledWith(expect.objectContaining({
@@ -52,13 +66,17 @@ describe('Health Router', () => {
5266
});
5367

5468
it('returns 200 and degraded status when OpenAI is missing', async () => {
55-
vi.mocked(stellarServer.root).mockResolvedValue({} as any);
69+
const mockedStellarRoot = vi.mocked(
70+
(stellarServer as unknown as { root: () => Promise<unknown> }).root
71+
);
72+
73+
mockedStellarRoot.mockResolvedValue({});
5674
vi.mocked(getJobScheduler).mockReturnValue({} as any);
5775
const originalKey = process.env.OPENAI_API_KEY;
5876
delete process.env.OPENAI_API_KEY;
5977

60-
const handler = (healthRouter.stack.find(s => s.route.path === '/health')?.route.stack[0].handle);
61-
await handler(mockReq as Request, mockRes as Response);
78+
const handler = getRouteHandler(healthRouter, '/health');
79+
await handler(mockReq as Request, mockRes as Response, vi.fn());
6280

6381
expect(resStatus).toHaveBeenCalledWith(200);
6482
expect(resJson).toHaveBeenCalledWith(expect.objectContaining({
@@ -74,12 +92,16 @@ describe('Health Router', () => {
7492
});
7593

7694
it('returns 503 and unhealthy status when Stellar Horizon is down', async () => {
77-
vi.mocked(stellarServer.root).mockRejectedValue(new Error('Horizon Down'));
95+
const mockedStellarRoot = vi.mocked(
96+
(stellarServer as unknown as { root: () => Promise<unknown> }).root
97+
);
98+
99+
mockedStellarRoot.mockRejectedValue(new Error('Horizon Down'));
78100
vi.mocked(getJobScheduler).mockReturnValue({} as any);
79101
process.env.OPENAI_API_KEY = 'test-key';
80102

81-
const handler = (healthRouter.stack.find(s => s.route.path === '/health')?.route.stack[0].handle);
82-
await handler(mockReq as Request, mockRes as Response);
103+
const handler = getRouteHandler(healthRouter, '/health');
104+
await handler(mockReq as Request, mockRes as Response, vi.fn());
83105

84106
expect(resStatus).toHaveBeenCalledWith(503);
85107
expect(resJson).toHaveBeenCalledWith(expect.objectContaining({
@@ -95,8 +117,8 @@ describe('Health Router', () => {
95117

96118
describe('GET /ready', () => {
97119
it('returns 200 and ready status', async () => {
98-
const handler = (healthRouter.stack.find(s => s.route.path === '/ready')?.route.stack[0].handle);
99-
await handler(mockReq as Request, mockRes as Response);
120+
const handler = getRouteHandler(healthRouter, '/ready');
121+
await handler(mockReq as Request, mockRes as Response, vi.fn());
100122

101123
expect(resStatus).toHaveBeenCalledWith(200);
102124
expect(resJson).toHaveBeenCalledWith(expect.objectContaining({

backend/src/routes/__tests__/validation.test.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { invoiceRouter } from '../invoice.js';
33
import { verificationRouter } from '../verification.js';
4-
import { Request, Response } from 'express';
4+
import { Request, Response, Router, RequestHandler } from 'express';
55

66
describe('Schema Validation', () => {
77
let mockReq: Partial<Request>;
88
let mockRes: Partial<Response>;
99
let resJson: any;
1010
let resStatus: any;
1111

12+
const getNamedRouteHandler = (router: Router, path: string, name: string): RequestHandler => {
13+
const routeLayer = router.stack.find((entry) => entry.route?.path === path);
14+
const handler = routeLayer?.route?.stack.find((entry) => entry.name === name)?.handle;
15+
16+
if (!handler) {
17+
throw new Error(`Route handler not found for ${path}:${name}`);
18+
}
19+
20+
return handler;
21+
};
22+
1223
beforeEach(() => {
1324
resJson = vi.fn();
1425
resStatus = vi.fn().mockReturnValue({ json: resJson });
@@ -21,8 +32,8 @@ describe('Schema Validation', () => {
2132
describe('Invoice Validation', () => {
2233
it('returns 400 when projectId is missing', async () => {
2334
mockReq.body = { workDescription: 'Test' };
24-
const handler = (invoiceRouter.stack.find(s => s.route?.path === '/generate')?.route.stack.find((s: any) => s.name === 'validateMiddleware')?.handle);
25-
35+
const handler = getNamedRouteHandler(invoiceRouter, '/generate', 'validateMiddleware');
36+
2637
await handler(mockReq as Request, mockRes as Response, vi.fn());
2738

2839
expect(resStatus).toHaveBeenCalledWith(400);
@@ -42,8 +53,8 @@ describe('Schema Validation', () => {
4253
milestoneDescription: 'Test',
4354
projectId: 'P1'
4455
};
45-
const handler = (verificationRouter.stack.find(s => s.route?.path === '/verify')?.route.stack.find((s: any) => s.name === 'validateMiddleware')?.handle);
46-
56+
const handler = getNamedRouteHandler(verificationRouter, '/verify', 'validateMiddleware');
57+
4758
await handler(mockReq as Request, mockRes as Response, vi.fn());
4859

4960
expect(resStatus).toHaveBeenCalledWith(400);
@@ -56,8 +67,12 @@ describe('Schema Validation', () => {
5667

5768
it('returns 400 for empty bulk verification items', async () => {
5869
mockReq.body = { items: [] };
59-
const handler = (verificationRouter.stack.find(s => s.route?.path === '/verify/batch')?.route.stack.find((s: any) => s.name === 'validateMiddleware')?.handle);
60-
70+
const handler = getNamedRouteHandler(
71+
verificationRouter,
72+
'/verify/batch',
73+
'validateMiddleware'
74+
);
75+
6176
await handler(mockReq as Request, mockRes as Response, vi.fn());
6277

6378
expect(resStatus).toHaveBeenCalledWith(400);

backend/src/routes/health.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import { getJobScheduler } from '../jobs/index.js';
44

55
export const healthRouter = Router();
66

7+
type HorizonHealthServer = {
8+
root: () => Promise<unknown>;
9+
};
10+
11+
const horizonHealthServer = stellarServer as unknown as HorizonHealthServer;
12+
713
/**
814
* @openapi
915
* /health:
@@ -26,10 +32,12 @@ healthRouter.get('/health', async (_req: Request, res: Response) => {
2632

2733
try {
2834
// 1. Stellar Horizon Check (with timeout)
29-
const stellarCheck = stellarServer.root()
35+
const stellarCheck = horizonHealthServer
36+
.root()
3037
.then(() => true)
31-
.catch((err) => {
32-
console.warn('Stellar health check failed:', err.message);
38+
.catch((err: unknown) => {
39+
const message = err instanceof Error ? err.message : String(err);
40+
console.warn('Stellar health check failed:', message);
3341
return false;
3442
});
3543

backend/src/services/invoice.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import OpenAI from 'openai';
22

3-
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
3+
let openaiClient: OpenAI | null = null;
4+
5+
const getOpenAIClient = () => {
6+
const apiKey = process.env.OPENAI_API_KEY;
7+
if (!apiKey) {
8+
throw new Error(
9+
'The OPENAI_API_KEY environment variable is missing or empty; provide it to generate invoices.'
10+
);
11+
}
12+
13+
if (!openaiClient) {
14+
openaiClient = new OpenAI({ apiKey });
15+
}
16+
17+
return openaiClient;
18+
};
419

520
interface InvoiceRequest {
621
projectId: string;
@@ -28,7 +43,7 @@ interface Invoice {
2843
export async function generateInvoice(request: InvoiceRequest): Promise<Invoice> {
2944
const id = `inv_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3045

31-
const completion = await openai.chat.completions.create({
46+
const completion = await getOpenAIClient().chat.completions.create({
3247
model: 'gpt-4o-mini',
3348
messages: [
3449
{

backend/src/services/stellar.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ const HORIZON_URL =
88

99
export const server = new StellarSdk.Horizon.Server(HORIZON_URL);
1010

11+
export class ValidationError extends Error {
12+
statusCode: number;
13+
14+
constructor(message: string, statusCode = 400) {
15+
super(message);
16+
this.name = 'ValidationError';
17+
this.statusCode = statusCode;
18+
}
19+
}
20+
1121
export function isValidStellarAddress(address: string) {
1222
if (!address?.trim()) {
1323
return false;
@@ -24,6 +34,30 @@ export function isValidTransactionHash(hash: string) {
2434
return /^[A-Fa-f0-9]{64}$/.test(hash);
2535
}
2636

37+
export function validateStellarAddress(address: string): string {
38+
if (!address?.trim()) {
39+
throw new ValidationError('Stellar address must not be empty');
40+
}
41+
42+
if (!isValidStellarAddress(address)) {
43+
throw new ValidationError('Invalid Stellar address');
44+
}
45+
46+
return address;
47+
}
48+
49+
export function validateTransactionHash(hash: string): string {
50+
if (!hash?.trim()) {
51+
throw new ValidationError('Transaction hash must not be empty');
52+
}
53+
54+
if (!isValidTransactionHash(hash)) {
55+
throw new ValidationError('Invalid transaction hash');
56+
}
57+
58+
return hash;
59+
}
60+
2761
export async function getAccountInfo(address: string) {
2862
const account = await server.loadAccount(address);
2963
return {

frontend/lib/hooks/useAgenticPay.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,14 @@ export const useAgenticPay = () => {
9898
});
9999

100100
const formattedProjects: Project[] = projectsData
101-
? projectsData.map((result: { status: string; result: unknown }) => {
102-
if (result.status === 'success' && result.result) {
103-
return formatProjectData(result.result as RawProjectData);
104-
}
105-
return null;
106-
}).filter((p): p is Project => p !== null)
101+
? projectsData
102+
.map((result) => {
103+
if (result.status === 'success' && result.result) {
104+
return formatProjectData(result.result as RawProjectData);
105+
}
106+
return null;
107+
})
108+
.filter((project): project is Project => project !== null)
107109
: [];
108110

109111
return { projects: formattedProjects, loading: loadingClient || loadingFreelancer || loadingDetails };
@@ -118,7 +120,11 @@ export const useAgenticPay = () => {
118120
query: { enabled: !!projectId }
119121
});
120122

121-
return { project: data ? formatProjectData(data) : null, loading: isLoading, refetch };
123+
return {
124+
project: data ? formatProjectData(data as RawProjectData) : null,
125+
loading: isLoading,
126+
refetch,
127+
};
122128
};
123129

124130

0 commit comments

Comments
 (0)