Skip to content

Commit 8676b2b

Browse files
committed
Improve integration test helpers and scenario assertions
Add status code support to mock API, add error checking to helper functions, and tighten assertions in S13, S17, S25, and S29 scenarios.
1 parent 5583c2d commit 8676b2b

6 files changed

Lines changed: 44 additions & 48 deletions

File tree

integration/helpers.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,13 @@ function post(
105105
});
106106
}
107107

108-
async function setMockResponse(path: string, response: unknown): Promise<void> {
109-
await fetch(`${MOCK_API_URL}/mock/set-response`, {
108+
async function setMockResponse(path: string, response: unknown, status = 200): Promise<void> {
109+
const result = await fetch(`${MOCK_API_URL}/mock/set-response`, {
110110
method: 'POST',
111111
headers: { 'Content-Type': 'application/json' },
112-
body: JSON.stringify({ path, response }),
112+
body: JSON.stringify({ path, response, status }),
113113
});
114+
if (!result.ok) throw new Error(`Failed to set mock response: ${String(result.status)}`);
114115
}
115116

116117
interface RecordedCall {
@@ -122,11 +123,13 @@ interface RecordedCall {
122123

123124
async function getMockCalls(): Promise<readonly RecordedCall[]> {
124125
const response = await fetch(`${MOCK_API_URL}/mock/calls`);
126+
if (!response.ok) throw new Error(`Failed to get mock calls: ${String(response.status)}`);
125127
return (await response.json()) as RecordedCall[];
126128
}
127129

128130
async function resetMock(): Promise<void> {
129-
await fetch(`${MOCK_API_URL}/mock/reset`, { method: 'POST' });
131+
const result = await fetch(`${MOCK_API_URL}/mock/reset`, { method: 'POST' });
132+
if (!result.ok) throw new Error(`Failed to reset mock: ${String(result.status)}`);
130133
}
131134

132135
export {

integration/mock-api.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ function startMockApi(port = 5123): MockApiHandle {
6161

6262
// Control endpoints
6363
if (url.pathname === '/mock/set-response' && request.method === 'POST') {
64-
const payload = JSON.parse(body) as { path: string; response: unknown };
65-
overrides.set(payload.path, payload.response); // eslint-disable-line functional/immutable-data
64+
const payload = JSON.parse(body) as { path: string; response: unknown; status?: number };
65+
overrides.set(payload.path, payload); // eslint-disable-line functional/immutable-data
6666
return Response.json({ ok: true });
6767
}
6868
if (url.pathname === '/mock/calls' && request.method === 'GET') {
@@ -79,8 +79,10 @@ function startMockApi(port = 5123): MockApiHandle {
7979
calls.push({ url: request.url, method: request.method, headers, body }); // eslint-disable-line functional/immutable-data
8080

8181
// Return override if set, otherwise default
82-
const override = overrides.get(url.pathname);
83-
if (override !== undefined) return Response.json(override);
82+
const override = overrides.get(url.pathname) as { response: unknown; status?: number } | undefined;
83+
if (override !== undefined) {
84+
return Response.json(override.response, { status: override.status ?? 200 });
85+
}
8486

8587
return Response.json(findDefaultResponse(url.pathname));
8688
},

integration/scenarios/s13-plugin-api-call.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ describe('S13 — Plugin hooks — onBeforeApiCall / onAfterApiCall', () => {
4848
const body = (await response.json()) as { data: string };
4949

5050
expect(response.status).toBe(200);
51-
// The encoded value should reflect 99.9 * 100 = 9990, not the mock's 22.5 * 100 = 2250
52-
expect(body.data).toBeDefined();
51+
// 99.9 * 100 = 9990 as int256 → 0x...270e (9990 in hex)
52+
// The mock returns 22.5 which would encode to 2250 (0x...08ca). The plugin overrides to 99.9.
53+
expect(body.data).toContain('270e');
5354
});
5455

5556
test('plugin returning undefined passes through', async () => {

integration/scenarios/s17-upstream-failure.test.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,26 @@ afterAll(() => {
1414
beforeEach(() => resetMock());
1515

1616
describe('S17 — Upstream API failure handling', () => {
17-
test('upstream 500 returns airnode 502', async () => {
18-
await setMockResponse('/current.json', '__STATUS_500__');
19-
// The mock returns JSON — but processResponse will fail because the shape is wrong
20-
// Actually we need the mock to return a 500 status. Let me use the default mock
21-
// which returns valid JSON — instead set a response that breaks encoding.
22-
await setMockResponse('/current.json', { error: 'internal server error' });
17+
test('upstream returning wrong shape returns 502', async () => {
18+
await setMockResponse('/current.json', { completely: { wrong: 'shape' } });
2319

2420
const endpointId = findEndpointId(ctx.endpointMap, 'WeatherAPI', 'currentTemp');
2521
const response = await post(ctx.baseUrl, endpointId, { q: 'London' });
2622
const body = (await response.json()) as { error: string };
2723

28-
// processResponse fails because $.current.temp_c doesn't exist → 502
2924
expect(response.status).toBe(502);
30-
expect(body.error).not.toContain('temp_c'); // error details not leaked
25+
expect(body.error).toBe('Internal processing error');
3126
});
3227

33-
test('upstream returning wrong shape returns 502', async () => {
34-
await setMockResponse('/current.json', { completely: { wrong: 'shape' } });
28+
test('upstream returning non-JSON returns 502', async () => {
29+
// The mock always returns JSON, but we can set a response that the Airnode
30+
// treats as an error because the encoded data fails processing
31+
await setMockResponse('/current.json', 'not json', 200);
3532

3633
const endpointId = findEndpointId(ctx.endpointMap, 'WeatherAPI', 'currentTemp');
3734
const response = await post(ctx.baseUrl, endpointId, { q: 'Berlin' });
38-
const body = (await response.json()) as { error: string };
3935

4036
expect(response.status).toBe(502);
41-
expect(body.error).toBe('Internal processing error');
4237
});
4338

4439
test('error details are not leaked to the client', async () => {
@@ -49,8 +44,8 @@ describe('S17 — Upstream API failure handling', () => {
4944
const body = (await response.json()) as { error: string };
5045

5146
expect(response.status).toBe(502);
52-
// Should be a generic message, not the JSONPath error or stack trace
5347
expect(body.error).not.toContain('$.');
5448
expect(body.error).not.toContain('stack');
49+
expect(body.error).not.toContain('temp_c');
5550
});
5651
});

integration/scenarios/s25-concurrent-requests.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ describe('S25 — Concurrent request handling', () => {
4343
expect(r.status).toBe(200);
4444
}
4545

46-
// The mock should have been called at most a few times (first request populates cache)
46+
// At least one request must hit the API (cache was empty), but with caching
47+
// enabled, not all 5 should hit the upstream — some should be served from cache
4748
const calls = await getMockCalls();
48-
// With cache, subsequent requests shouldn't hit the API
49-
// First request hits the API, rest may hit cache depending on timing
50-
expect(calls.length).toBeLessThanOrEqual(5);
49+
expect(calls.length).toBeGreaterThanOrEqual(1);
50+
expect(calls.length).toBeLessThan(5);
5151
});
5252
});

integration/scenarios/s29-x402-payment.test.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,26 @@ afterAll(() => {
1212
ctx.stop();
1313
});
1414

15-
describe('S29 — x402 payment auth', () => {
16-
test('returns 402 with payment details when no proof header', () => {
17-
// RandomAPI has no auth — we need an endpoint with x402 auth.
18-
// Since the complete config doesn't have x402, we test via unit tests.
19-
// This integration test verifies the pipeline returns 402 correctly
20-
// by checking the response format from the unit test coverage.
21-
// Full integration requires a live chain for payment verification.
22-
23-
// For now, verify the 402 flow works at the pipeline level
24-
// by confirming that endpoints without payment proof return properly.
25-
// The unit tests in auth.test.ts cover the x402 logic thoroughly.
26-
expect(true).toBe(true);
15+
describe('S29 — Multi-method auth integration', () => {
16+
test('endpoint with multi-method auth rejects unauthenticated requests', async () => {
17+
// coinMarketData has auth: [nftKey, apiKey]
18+
const endpointId = findEndpointId(ctx.endpointMap, 'CoinGecko', 'coinMarketData');
19+
const response = await post(ctx.baseUrl, endpointId, { coinId: 'bitcoin' });
20+
21+
expect(response.status).toBe(401);
2722
});
2823

29-
test('multi-method: x402 + apiKey fallback works via API key', async () => {
30-
// coinMarketData has auth: [nftKey, apiKey] — similar multi-method pattern
31-
// This tests that multi-method auth with fallback works in integration
24+
test('endpoint with multi-method auth accepts API key fallback', async () => {
3225
const endpointId = findEndpointId(ctx.endpointMap, 'CoinGecko', 'coinMarketData');
26+
const response = await post(ctx.baseUrl, endpointId, { coinId: 'bitcoin' }, { 'X-Api-Key': 'test-client-key' });
27+
28+
expect(response.status).toBe(200);
29+
});
3330

34-
// Without any auth — should fail
35-
const r1 = await post(ctx.baseUrl, endpointId, { coinId: 'bitcoin' });
36-
expect(r1.status).toBe(401);
31+
test('unknown endpoint returns 404', async () => {
32+
const fakeId = '0x0000000000000000000000000000000000000000000000000000000000000001';
33+
const response = await post(ctx.baseUrl, fakeId, {});
3734

38-
// With API key fallback — should succeed
39-
const r2 = await post(ctx.baseUrl, endpointId, { coinId: 'bitcoin' }, { 'X-Api-Key': 'test-client-key' });
40-
expect(r2.status).toBe(200);
35+
expect(response.status).toBe(404);
4136
});
4237
});

0 commit comments

Comments
 (0)