Skip to content

Commit 1f58707

Browse files
authored
Merge pull request #1307 from luciachizaram/fix/1228-1231-1232-1233
fix: stale closure in price service, date validation, error handling
2 parents 5dfc3bb + 0cdef9c commit 1f58707

8 files changed

Lines changed: 416 additions & 30 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { NextRequest } from 'next/server';
3+
4+
vi.mock('@/lib/auditLog', () => ({
5+
default: {
6+
getAuditEntries: vi.fn(() => []),
7+
},
8+
}));
9+
10+
const { GET } = await import('./route');
11+
12+
function request(query = '') {
13+
return new NextRequest(`http://localhost/api/admin-audit${query}`);
14+
}
15+
16+
describe('GET /api/admin-audit', () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
});
20+
21+
it('returns 200 with default pagination', async () => {
22+
const res = await GET(request());
23+
const body = await res.json();
24+
25+
expect(res.status).toBe(200);
26+
expect(body).toMatchObject({
27+
entries: [],
28+
total: 0,
29+
limit: 100,
30+
offset: 0,
31+
hasMore: false,
32+
});
33+
});
34+
35+
it('returns 400 for invalid startDate', async () => {
36+
const res = await GET(request('?startDate=not-a-date'));
37+
const body = await res.json();
38+
39+
expect(res.status).toBe(400);
40+
expect(body.error).toContain('startDate');
41+
});
42+
43+
it('returns 400 for invalid endDate', async () => {
44+
const res = await GET(request('?endDate=zzz'));
45+
const body = await res.json();
46+
47+
expect(res.status).toBe(400);
48+
expect(body.error).toContain('endDate');
49+
});
50+
51+
it('accepts valid ISO dates', async () => {
52+
const res = await GET(
53+
request('?startDate=2025-01-01T00:00:00Z&endDate=2025-12-31T23:59:59Z'),
54+
);
55+
expect(res.status).toBe(200);
56+
});
57+
58+
it('clamps limit to max 1000', async () => {
59+
const res = await GET(request('?limit=9999'));
60+
const body = await res.json();
61+
62+
expect(res.status).toBe(200);
63+
expect(body.limit).toBe(1000);
64+
});
65+
66+
it('defaults limit to 100 for non-numeric input', async () => {
67+
const res = await GET(request('?limit=abc'));
68+
const body = await res.json();
69+
70+
expect(res.status).toBe(200);
71+
expect(body.limit).toBe(100);
72+
});
73+
74+
it('defaults offset to 0 for non-numeric input', async () => {
75+
const res = await GET(request('?offset=abc'));
76+
const body = await res.json();
77+
78+
expect(res.status).toBe(200);
79+
expect(body.offset).toBe(0);
80+
});
81+
82+
it('returns 405 for POST', async () => {
83+
const { POST } = await import('./route');
84+
const res = await POST();
85+
expect(res.status).toBe(405);
86+
});
87+
});

Dechat/dex_with_fiat_frontend/src/app/api/admin-audit/route.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,34 +47,35 @@ export async function GET(request: NextRequest) {
4747

4848
const startDate = searchParams.get('startDate');
4949
if (startDate) {
50-
try {
51-
filter.startDate = new Date(startDate);
52-
} catch {
50+
const parsed = new Date(startDate);
51+
if (Number.isNaN(parsed.getTime())) {
5352
return NextResponse.json(
5453
{ error: 'Invalid startDate format. Use ISO 8601 format.' },
5554
{ status: 400 }
5655
);
5756
}
57+
filter.startDate = parsed;
5858
}
5959

6060
const endDate = searchParams.get('endDate');
6161
if (endDate) {
62-
try {
63-
filter.endDate = new Date(endDate);
64-
} catch {
62+
const parsed = new Date(endDate);
63+
if (Number.isNaN(parsed.getTime())) {
6564
return NextResponse.json(
6665
{ error: 'Invalid endDate format. Use ISO 8601 format.' },
6766
{ status: 400 }
6867
);
6968
}
69+
filter.endDate = parsed;
7070
}
7171

7272
// Pagination parameters
73-
const limit = Math.min(
74-
parseInt(searchParams.get('limit') || '100', 10),
75-
1000 // Max limit
76-
);
77-
const offset = Math.max(parseInt(searchParams.get('offset') || '0', 10), 0);
73+
const rawLimit = parseInt(searchParams.get('limit') || '100', 10);
74+
const limit = Number.isFinite(rawLimit)
75+
? Math.min(Math.max(rawLimit, 1), 1000)
76+
: 100;
77+
const rawOffset = parseInt(searchParams.get('offset') || '0', 10);
78+
const offset = Number.isFinite(rawOffset) ? Math.max(rawOffset, 0) : 0;
7879

7980
// Retrieve filtered entries
8081
const allEntries = AuditLogService.getAuditEntries(filter);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
const { GET } = await import('./route');
4+
5+
describe('GET /api/health', () => {
6+
it('returns 200 with status ok', async () => {
7+
const res = await GET();
8+
const body = await res.json();
9+
10+
expect(res.status).toBe(200);
11+
expect(body.status).toBe('ok');
12+
expect(body.timestamp).toBeDefined();
13+
// timestamp should be a valid ISO date
14+
expect(Number.isNaN(new Date(body.timestamp).getTime())).toBe(false);
15+
});
16+
});

Dechat/dex_with_fiat_frontend/src/app/api/health/route.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@ import { NextResponse } from 'next/server';
33
export const runtime = 'edge';
44

55
export async function GET() {
6-
return NextResponse.json(
7-
{
8-
status: 'ok',
9-
timestamp: new Date().toISOString(),
10-
},
11-
{ status: 200 },
12-
);
6+
try {
7+
return NextResponse.json(
8+
{
9+
status: 'ok',
10+
timestamp: new Date().toISOString(),
11+
},
12+
{ status: 200 },
13+
);
14+
} catch (error) {
15+
return NextResponse.json(
16+
{
17+
status: 'error',
18+
timestamp: new Date().toISOString(),
19+
message: error instanceof Error ? error.message : 'Health check failed',
20+
},
21+
{ status: 503 },
22+
);
23+
}
1324
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { NextRequest } from 'next/server';
3+
import { z } from 'zod';
4+
5+
const mockSchema = z.object({
6+
source: z.string().min(1),
7+
reason: z.string().min(1),
8+
amount: z.string().min(1),
9+
recipient: z.string().min(1),
10+
reference: z.string().optional(),
11+
});
12+
13+
vi.mock('@/lib/telemetry', () => ({
14+
telemetry: {
15+
extractTraceFromHeaders: () => ({ traceId: 'trace', spanId: 'parent' }),
16+
createSpan: () => ({ spanId: 'span' }),
17+
addLog: vi.fn(),
18+
finishSpan: vi.fn(),
19+
},
20+
}));
21+
22+
vi.mock('@sentry/nextjs', () => ({
23+
captureException: vi.fn(),
24+
}));
25+
26+
vi.mock('@/lib/rateLimit', () => ({
27+
applyRateLimit: vi.fn(() => null),
28+
getClientIp: vi.fn(() => '127.0.0.1'),
29+
}));
30+
31+
vi.mock('@/lib/transferStore', () => ({
32+
setTransferStatus: vi.fn(),
33+
}));
34+
35+
vi.mock('@/lib/payout/providers/registry', () => ({
36+
getPayoutProvider: () => ({
37+
initiateTransfer: vi.fn().mockResolvedValue({ reference: 'ref-123' }),
38+
}),
39+
}));
40+
41+
vi.mock('@/lib/apiSchemas', () => ({
42+
initiateTransferSchema: mockSchema,
43+
}));
44+
45+
const { POST } = await import('./route');
46+
47+
function makeRequest(body: unknown) {
48+
return new NextRequest(
49+
new Request('http://localhost/api/initiate-transfer', {
50+
method: 'POST',
51+
body: JSON.stringify(body),
52+
headers: { 'content-type': 'application/json' },
53+
}),
54+
);
55+
}
56+
57+
describe('POST /api/initiate-transfer', () => {
58+
beforeEach(() => {
59+
vi.clearAllMocks();
60+
});
61+
62+
it('returns 400 for malformed JSON body', async () => {
63+
const badReq = new NextRequest(
64+
new Request('http://localhost/api/initiate-transfer', {
65+
method: 'POST',
66+
body: 'not json{{{',
67+
headers: { 'content-type': 'application/json' },
68+
}),
69+
);
70+
71+
const res = await POST(badReq);
72+
const body = await res.json();
73+
74+
expect(res.status).toBe(400);
75+
expect(body.success).toBe(false);
76+
expect(body.message).toContain('Invalid JSON');
77+
});
78+
79+
it('returns 400 for validation failures', async () => {
80+
const req = makeRequest({ source: '', reason: '', amount: '', recipient: '' });
81+
const res = await POST(req);
82+
const body = await res.json();
83+
84+
expect(res.status).toBe(400);
85+
expect(body.success).toBe(false);
86+
});
87+
88+
it('returns 200 for valid request', async () => {
89+
const req = makeRequest({
90+
source: 'wallet',
91+
reason: 'payment',
92+
amount: '100',
93+
recipient: 'GABC123',
94+
});
95+
const res = await POST(req);
96+
const body = await res.json();
97+
98+
expect(res.status).toBe(200);
99+
expect(body.success).toBe(true);
100+
});
101+
});

Dechat/dex_with_fiat_frontend/src/app/api/initiate-transfer/route.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,20 @@ export async function POST(request: NextRequest) {
2525
endpoint: '/api/initiate-transfer',
2626
});
2727

28-
const body = await request.json();
28+
let body: unknown;
29+
try {
30+
body = await request.json();
31+
} catch {
32+
telemetry.addLog(span.spanId, 'warn', 'Malformed JSON body');
33+
telemetry.finishSpan(span.spanId, {
34+
success: false,
35+
error: 'Invalid JSON body',
36+
});
37+
return NextResponse.json(
38+
{ success: false, message: 'Invalid JSON in request body.' },
39+
{ status: 400 },
40+
);
41+
}
2942

3043
// Validate with Zod
3144
const validationResult = initiateTransferSchema.safeParse(body);
@@ -94,7 +107,6 @@ export async function POST(request: NextRequest) {
94107
data,
95108
});
96109
} catch (error: unknown) {
97-
// Capture error in Sentry
98110
Sentry.captureException(error, {
99111
tags: {
100112
endpoint: '/api/initiate-transfer',
@@ -107,13 +119,14 @@ export async function POST(request: NextRequest) {
107119
},
108120
});
109121

122+
const errorMessage =
123+
error instanceof Error ? error.message : 'Unknown error';
124+
110125
telemetry.addLog(
111126
span.spanId,
112127
'error',
113128
'Unhandled error in transfer initiation',
114-
{
115-
error: error instanceof Error ? error.message : 'Unknown error',
116-
},
129+
{ error: errorMessage },
117130
);
118131

119132
console.error('Initiate transfer error:', error);

0 commit comments

Comments
 (0)