Skip to content

Commit 50cf050

Browse files
committed
added test coverage
Signed-off-by: Simeon Nakov <[email protected]>
1 parent 6894c37 commit 50cf050

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
4+
import Axios, { AxiosInstance, AxiosResponse } from 'axios';
5+
import { expect } from 'chai';
6+
import { Server } from 'http';
7+
import Koa from 'koa';
8+
9+
import { ConfigServiceTestHelper } from '../../../config-service/tests/configServiceTestHelper';
10+
11+
ConfigServiceTestHelper.appendEnvsFromPath(__dirname + '/test.env');
12+
13+
import { overrideEnvsInMochaDescribe } from '../../../relay/tests/helpers';
14+
import RelayCalls from '../../tests/helpers/constants';
15+
16+
describe('X-Forwarded-For Header Integration Tests', function () {
17+
// Test with rate limiting enabled and a low limit to make testing easier
18+
overrideEnvsInMochaDescribe({
19+
RATE_LIMIT_DISABLED: false,
20+
TIER_2_RATE_LIMIT: 3, // Low limit for easy testing
21+
});
22+
23+
let testServer: Server;
24+
let testClient: AxiosInstance;
25+
let app: Koa<Koa.DefaultState, Koa.DefaultContext>;
26+
27+
// Simple static test IPs - each test uses different IP ranges to avoid conflicts
28+
const TEST_IP_A = '192.168.1.100';
29+
const TEST_IP_B = '192.168.2.100';
30+
const TEST_IP_C = '192.168.3.100';
31+
const TEST_IP_D = '192.168.4.100';
32+
const TEST_IP_E = '192.168.5.100';
33+
const TEST_METHOD = RelayCalls.ETH_ENDPOINTS.ETH_CHAIN_ID;
34+
35+
before(function () {
36+
app = require('../../src/server').default;
37+
testServer = app.listen(ConfigService.get('E2E_SERVER_PORT'));
38+
testClient = createTestClient();
39+
});
40+
41+
after(function () {
42+
testServer.close((err) => {
43+
if (err) {
44+
console.error(err);
45+
}
46+
});
47+
});
48+
49+
this.timeout(10000);
50+
51+
function createTestClient(port = ConfigService.get('E2E_SERVER_PORT')) {
52+
return Axios.create({
53+
baseURL: 'http://localhost:' + port,
54+
responseType: 'json' as const,
55+
headers: {
56+
'Content-Type': 'application/json',
57+
},
58+
method: 'POST',
59+
timeout: 5 * 1000,
60+
});
61+
}
62+
63+
function createRequestWithIP(id: string, ip: string) {
64+
return {
65+
id: id,
66+
jsonrpc: '2.0',
67+
method: TEST_METHOD,
68+
params: [null],
69+
};
70+
}
71+
72+
async function makeRequestWithForwardedIP(ip: string, id: string = '1') {
73+
return testClient.post('/', createRequestWithIP(id, ip), {
74+
headers: {
75+
'X-Forwarded-For': ip,
76+
},
77+
});
78+
}
79+
80+
async function makeRequestWithoutForwardedIP(id: string = '1') {
81+
return testClient.post('/', createRequestWithIP(id, ''));
82+
}
83+
84+
it('should use X-Forwarded-For header IP for rate limiting when app.proxy is true', async function () {
85+
// Make requests up to the rate limit for IP_A using X-Forwarded-For header
86+
const responses: AxiosResponse[] = [];
87+
88+
// Make requests within the limit (TIER_2_RATE_LIMIT = 3)
89+
for (let i = 1; i <= 3; i++) {
90+
const response = await makeRequestWithForwardedIP(TEST_IP_A, i.toString());
91+
responses.push(response);
92+
93+
expect(response.status).to.eq(200);
94+
expect(response.data.result).to.be.equal(ConfigService.get('CHAIN_ID'));
95+
}
96+
97+
// The next request should be rate limited for IP_A
98+
try {
99+
await makeRequestWithForwardedIP(TEST_IP_A, '4');
100+
expect.fail('Expected rate limit to be exceeded');
101+
} catch (error: any) {
102+
expect(error.response.status).to.eq(429);
103+
expect(error.response.data.error.code).to.eq(-32605); // IP Rate Limit Exceeded
104+
expect(error.response.data.error.message).to.include('IP Rate limit exceeded');
105+
}
106+
});
107+
108+
it('should treat different X-Forwarded-For IPs independently', async function () {
109+
// First, exhaust the rate limit for TEST_IP_B
110+
for (let i = 1; i <= 3; i++) {
111+
await makeRequestWithForwardedIP(TEST_IP_B, `b${i}`);
112+
}
113+
114+
// Verify TEST_IP_B is rate limited
115+
try {
116+
await makeRequestWithForwardedIP(TEST_IP_B, 'b4');
117+
expect.fail('Expected rate limit to be exceeded for TEST_IP_B');
118+
} catch (error: any) {
119+
expect(error.response.status).to.eq(429);
120+
expect(error.response.data.error.code).to.eq(-32605);
121+
}
122+
123+
// Now make requests with TEST_IP_C - should not be rate limited
124+
for (let i = 1; i <= 3; i++) {
125+
const response = await makeRequestWithForwardedIP(TEST_IP_C, `c${i}`);
126+
expect(response.status).to.eq(200);
127+
expect(response.data.result).to.be.equal(ConfigService.get('CHAIN_ID'));
128+
}
129+
130+
// TEST_IP_C should also get rate limited after hitting its own limit
131+
try {
132+
await makeRequestWithForwardedIP(TEST_IP_C, 'c4');
133+
expect.fail('Expected rate limit to be exceeded for TEST_IP_C');
134+
} catch (error: any) {
135+
expect(error.response.status).to.eq(429);
136+
expect(error.response.data.error.code).to.eq(-32605);
137+
}
138+
});
139+
140+
it('should use actual client IP when X-Forwarded-For header is not present', async function () {
141+
// Make requests without X-Forwarded-For header
142+
// These should use the actual client IP and have their own rate limit
143+
for (let i = 1; i <= 3; i++) {
144+
const response = await makeRequestWithoutForwardedIP(i.toString());
145+
expect(response.status).to.eq(200);
146+
expect(response.data.result).to.be.equal(ConfigService.get('CHAIN_ID'));
147+
}
148+
149+
// The next request should be rate limited for the actual client IP
150+
try {
151+
await makeRequestWithoutForwardedIP('4');
152+
expect.fail('Expected rate limit to be exceeded for actual client IP');
153+
} catch (error: any) {
154+
expect(error.response.status).to.eq(429);
155+
expect(error.response.data.error.code).to.eq(-32605);
156+
}
157+
});
158+
159+
it('should handle multiple IPs in X-Forwarded-For header (use first IP)', async function () {
160+
// X-Forwarded-For can contain multiple IPs: "client, proxy1, proxy2"
161+
// Koa should use the first IP (leftmost) as the client IP
162+
const multipleIPs = `${TEST_IP_D}, 10.0.0.1, 10.0.0.2`;
163+
164+
// Make requests with multiple IPs in the header
165+
for (let i = 1; i <= 3; i++) {
166+
const response = await testClient.post('/', createRequestWithIP(i.toString(), TEST_IP_D), {
167+
headers: {
168+
'X-Forwarded-For': multipleIPs,
169+
},
170+
});
171+
172+
expect(response.status).to.eq(200);
173+
expect(response.data.result).to.be.equal(ConfigService.get('CHAIN_ID'));
174+
}
175+
176+
// Should be rate limited based on the first IP (TEST_IP_D)
177+
try {
178+
await testClient.post('/', createRequestWithIP('4', TEST_IP_D), {
179+
headers: {
180+
'X-Forwarded-For': multipleIPs,
181+
},
182+
});
183+
expect.fail('Expected rate limit to be exceeded for first IP in X-Forwarded-For');
184+
} catch (error: any) {
185+
expect(error.response.status).to.eq(429);
186+
expect(error.response.data.error.code).to.eq(-32605);
187+
}
188+
});
189+
190+
it('should properly handle X-Forwarded-For header with different request patterns', async function () {
191+
// Make requests with X-Forwarded-For header
192+
for (let i = 1; i <= 3; i++) {
193+
const response = await testClient.post('/', createRequestWithIP(i.toString(), TEST_IP_E), {
194+
headers: {
195+
'X-Forwarded-For': TEST_IP_E,
196+
},
197+
});
198+
199+
expect(response.status).to.eq(200);
200+
expect(response.data.result).to.be.equal(ConfigService.get('CHAIN_ID'));
201+
}
202+
203+
// Next request should be rate limited
204+
try {
205+
await testClient.post('/', createRequestWithIP('4', TEST_IP_E), {
206+
headers: {
207+
'X-Forwarded-For': TEST_IP_E,
208+
},
209+
});
210+
expect.fail('Expected rate limit to be exceeded');
211+
} catch (error: any) {
212+
expect(error.response.status).to.eq(429);
213+
expect(error.response.data.error.code).to.eq(-32605);
214+
}
215+
});
216+
});

packages/server/tests/integration/server.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ describe('RPC Server', function () {
4040
let populatePreconfiguredSpendingPlansSpy: sinon.SinonSpy;
4141
let app: Koa<Koa.DefaultState, Koa.DefaultContext>;
4242

43+
overrideEnvsInMochaDescribe({
44+
RATE_LIMIT_DISABLED: true,
45+
});
46+
4347
before(function () {
48+
// Set up spy BEFORE requiring the server module to catch the constructor call
4449
populatePreconfiguredSpendingPlansSpy = sinon.spy(Relay.prototype, <any>'populatePreconfiguredSpendingPlans');
50+
51+
// Clear the module cache to ensure a fresh server instance
52+
delete require.cache[require.resolve('../../src/server')];
53+
4554
app = require('../../src/server').default;
4655
testServer = app.listen(ConfigService.get('E2E_SERVER_PORT'));
4756
testClient = BaseTest.createTestClient();
@@ -53,6 +62,7 @@ describe('RPC Server', function () {
5362
});
5463

5564
after(function () {
65+
populatePreconfiguredSpendingPlansSpy.restore();
5666
testServer.close((err) => {
5767
if (err) {
5868
console.error(err);

0 commit comments

Comments
 (0)