Skip to content

Commit 404e419

Browse files
add upstream proxy tests
1 parent b260375 commit 404e419

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package com.testingbot.tunnel.proxy;
2+
3+
import com.github.tomakehurst.wiremock.WireMockServer;
4+
import com.github.tomakehurst.wiremock.client.WireMock;
5+
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
6+
import com.github.tomakehurst.wiremock.http.RequestMethod;
7+
import com.testingbot.tunnel.App;
8+
import org.apache.http.HttpHost;
9+
import org.apache.http.client.config.RequestConfig;
10+
import org.apache.http.client.methods.CloseableHttpResponse;
11+
import org.apache.http.client.methods.HttpGet;
12+
import org.apache.http.impl.client.CloseableHttpClient;
13+
import org.apache.http.impl.client.HttpClients;
14+
import org.apache.http.util.EntityUtils;
15+
import org.eclipse.jetty.server.Server;
16+
import org.eclipse.jetty.server.ServerConnector;
17+
import org.eclipse.jetty.servlet.ServletContextHandler;
18+
import org.eclipse.jetty.servlet.ServletHolder;
19+
import org.junit.jupiter.api.AfterEach;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
23+
import java.io.IOException;
24+
import java.util.Base64;
25+
26+
import static com.github.tomakehurst.wiremock.client.WireMock.*;
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatCode;
29+
30+
/**
31+
* Tests for upstream proxy functionality (--proxy and --proxy-userpwd options)
32+
*
33+
* These tests verify that the tunnel correctly configures and uses an upstream
34+
* proxy when specified via command-line arguments, for both HTTP and HTTPS traffic.
35+
*/
36+
class UpstreamProxyTest {
37+
38+
private App app;
39+
private WireMockServer upstreamProxy;
40+
private WireMockServer targetServer;
41+
private Server localProxyServer;
42+
private int localProxyPort;
43+
44+
@BeforeEach
45+
void setUp() throws Exception {
46+
app = new App();
47+
app.setClientKey("test_key");
48+
app.setClientSecret("test_secret");
49+
50+
// Start upstream proxy mock (simulates the --proxy server)
51+
upstreamProxy = new WireMockServer(WireMockConfiguration.options()
52+
.dynamicPort());
53+
upstreamProxy.start();
54+
WireMock.configureFor("localhost", upstreamProxy.port());
55+
56+
// Start target server mock
57+
targetServer = new WireMockServer(WireMockConfiguration.options()
58+
.dynamicPort());
59+
targetServer.start();
60+
}
61+
62+
@AfterEach
63+
void tearDown() throws Exception {
64+
if (localProxyServer != null && localProxyServer.isStarted()) {
65+
localProxyServer.stop();
66+
}
67+
if (upstreamProxy != null && upstreamProxy.isRunning()) {
68+
upstreamProxy.stop();
69+
}
70+
if (targetServer != null && targetServer.isRunning()) {
71+
targetServer.stop();
72+
}
73+
}
74+
75+
private void startLocalProxyWithUpstream(String proxyHost, String proxyAuth) throws Exception {
76+
localProxyServer = new Server(0);
77+
ServletContextHandler context = new ServletContextHandler();
78+
context.setContextPath("/");
79+
80+
ServletHolder servletHolder = new ServletHolder(new TunnelProxyServlet());
81+
if (proxyHost != null) {
82+
servletHolder.setInitParameter("proxy", proxyHost);
83+
}
84+
if (proxyAuth != null) {
85+
servletHolder.setInitParameter("proxyAuth", proxyAuth);
86+
}
87+
servletHolder.setInitParameter("jetty", "8087");
88+
89+
context.addServlet(servletHolder, "/*");
90+
localProxyServer.setHandler(context);
91+
localProxyServer.start();
92+
93+
localProxyPort = ((ServerConnector) localProxyServer.getConnectors()[0]).getLocalPort();
94+
}
95+
96+
@Test
97+
void httpRequest_shouldRouteThroughUpstreamProxy() throws Exception {
98+
// Given: Target server configured to respond
99+
targetServer.stubFor(get(urlPathEqualTo("/api/test"))
100+
.willReturn(aResponse()
101+
.withStatus(200)
102+
.withHeader("Content-Type", "application/json")
103+
.withBody("{\"message\":\"success\"}")));
104+
105+
// And: Upstream proxy configured to forward requests
106+
upstreamProxy.stubFor(any(urlMatching(".*"))
107+
.willReturn(aResponse().proxiedFrom("http://localhost:" + targetServer.port())));
108+
109+
// And: Local proxy with upstream configuration
110+
startLocalProxyWithUpstream("localhost:" + upstreamProxy.port(), null);
111+
112+
// When: Making HTTP request through local proxy
113+
HttpHost proxy = new HttpHost("localhost", localProxyPort);
114+
RequestConfig config = RequestConfig.custom()
115+
.setProxy(proxy)
116+
.build();
117+
118+
try (CloseableHttpClient client = HttpClients.custom()
119+
.setDefaultRequestConfig(config)
120+
.build()) {
121+
122+
HttpGet request = new HttpGet("http://localhost:" + targetServer.port() + "/api/test");
123+
try (CloseableHttpResponse response = client.execute(request)) {
124+
125+
// Then: Request should succeed
126+
assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
127+
String body = EntityUtils.toString(response.getEntity());
128+
assertThat(body).contains("success");
129+
130+
// And: Upstream proxy should have received the request
131+
upstreamProxy.verify(getRequestedFor(urlPathEqualTo("/api/test")));
132+
}
133+
}
134+
}
135+
136+
@Test
137+
void httpRequest_withProxyAuth_shouldSendAuthHeader() throws Exception {
138+
// Given: Upstream proxy requiring authentication
139+
String expectedAuth = "Basic " + Base64.getEncoder().encodeToString("testuser:testpass".getBytes());
140+
141+
upstreamProxy.stubFor(any(urlMatching(".*"))
142+
.withHeader("Proxy-Authorization", equalTo(expectedAuth))
143+
.willReturn(aResponse()
144+
.withStatus(200)
145+
.withBody("Authenticated")));
146+
147+
upstreamProxy.stubFor(any(urlMatching(".*"))
148+
.withHeader("Proxy-Authorization", absent())
149+
.willReturn(aResponse()
150+
.withStatus(407)
151+
.withHeader("Proxy-Authenticate", "Basic realm=\"test\"")));
152+
153+
// And: Local proxy configured with auth
154+
startLocalProxyWithUpstream("localhost:" + upstreamProxy.port(), "testuser:testpass");
155+
156+
// When: Making HTTP request through authenticated proxy
157+
HttpHost proxy = new HttpHost("localhost", localProxyPort);
158+
RequestConfig config = RequestConfig.custom()
159+
.setProxy(proxy)
160+
.build();
161+
162+
try (CloseableHttpClient client = HttpClients.custom()
163+
.setDefaultRequestConfig(config)
164+
.build()) {
165+
166+
HttpGet request = new HttpGet("http://example.com/test");
167+
try (CloseableHttpResponse response = client.execute(request)) {
168+
169+
// Then: Request should succeed with auth
170+
assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
171+
172+
// And: Upstream proxy should have received auth header
173+
upstreamProxy.verify(getRequestedFor(urlPathEqualTo("/test"))
174+
.withHeader("Proxy-Authorization", equalTo(expectedAuth)));
175+
}
176+
}
177+
}
178+
179+
@Test
180+
void httpsConnect_shouldConfigureCustomConnectHandler() throws Exception {
181+
// Given: App configured with upstream proxy for HTTPS
182+
app.setProxy("proxy.example.com:8080");
183+
184+
// When: Creating CustomConnectHandler
185+
CustomConnectHandler handler = new CustomConnectHandler(app);
186+
187+
// Then: Handler should be configured for CONNECT tunneling through upstream
188+
assertThat(handler).isNotNull();
189+
190+
// This handler will forward CONNECT requests to the upstream proxy
191+
// In real usage: client -> local proxy (CustomConnectHandler) -> upstream proxy -> target
192+
}
193+
194+
@Test
195+
void httpsConnect_withProxyAuth_shouldSendAuthInConnect() throws Exception {
196+
// Given: Upstream proxy requiring auth for CONNECT
197+
String expectedAuth = "Basic " + Base64.getEncoder().encodeToString("user:pass".getBytes());
198+
199+
upstreamProxy.stubFor(request("CONNECT", urlMatching(".*"))
200+
.withHeader("Proxy-Authorization", equalTo(expectedAuth))
201+
.willReturn(aResponse().withStatus(200)));
202+
203+
upstreamProxy.stubFor(request("CONNECT", urlMatching(".*"))
204+
.withHeader("Proxy-Authorization", absent())
205+
.willReturn(aResponse()
206+
.withStatus(407)
207+
.withHeader("Proxy-Authenticate", "Basic realm=\"secure\"")));
208+
209+
// And: Local proxy with auth
210+
app.setProxy("localhost:" + upstreamProxy.port());
211+
app.setProxyAuth("user:pass");
212+
CustomConnectHandler handler = new CustomConnectHandler(app);
213+
214+
// Then: Handler should be configured with auth for CONNECT requests
215+
assertThat(handler).isNotNull();
216+
}
217+
218+
@Test
219+
void proxyConfiguration_shouldHandleHostnameWithoutPort() {
220+
// Given: Proxy hostname without port
221+
app.setProxy("proxy.example.com");
222+
CustomConnectHandler handler = new CustomConnectHandler(app);
223+
224+
// Then: Should default to port 80
225+
assertThat(handler).isNotNull();
226+
assertThat(app.getProxy()).isEqualTo("proxy.example.com");
227+
}
228+
229+
@Test
230+
void proxyConfiguration_shouldHandleIPv4Address() throws Exception {
231+
// Given: IPv4 address as proxy
232+
startLocalProxyWithUpstream("127.0.0.1:" + upstreamProxy.port(), null);
233+
234+
// Then: Should configure successfully
235+
assertThat(localProxyPort).isGreaterThan(0);
236+
}
237+
238+
@Test
239+
void proxyConfiguration_shouldStoreCredentialsSeparately() {
240+
// Given: App with proxy and auth
241+
app.setProxy("proxy.example.com:8080");
242+
app.setProxyAuth("myuser:mypassword");
243+
244+
// Then: Both should be stored independently
245+
assertThat(app.getProxy()).isEqualTo("proxy.example.com:8080");
246+
assertThat(app.getProxyAuth()).isEqualTo("myuser:mypassword");
247+
}
248+
249+
@Test
250+
void customConnectHandler_shouldInitializeWithoutProxy() {
251+
// Given: No proxy configured
252+
CustomConnectHandler handler = new CustomConnectHandler(app);
253+
254+
// Then: Handler should initialize successfully
255+
assertThat(handler).isNotNull();
256+
}
257+
258+
@Test
259+
void multipleRequests_shouldReuseUpstreamProxyConnection() throws Exception {
260+
// Given: Target and upstream proxy configured
261+
targetServer.stubFor(get(urlPathMatching("/request.*"))
262+
.willReturn(aResponse()
263+
.withStatus(200)
264+
.withBody("OK")));
265+
266+
upstreamProxy.stubFor(any(urlMatching(".*"))
267+
.willReturn(aResponse().proxiedFrom("http://localhost:" + targetServer.port())));
268+
269+
startLocalProxyWithUpstream("localhost:" + upstreamProxy.port(), null);
270+
271+
// When: Making multiple requests
272+
HttpHost proxy = new HttpHost("localhost", localProxyPort);
273+
RequestConfig config = RequestConfig.custom().setProxy(proxy).build();
274+
275+
try (CloseableHttpClient client = HttpClients.custom()
276+
.setDefaultRequestConfig(config)
277+
.build()) {
278+
279+
for (int i = 0; i < 3; i++) {
280+
HttpGet request = new HttpGet("http://localhost:" + targetServer.port() + "/request" + i);
281+
try (CloseableHttpResponse response = client.execute(request)) {
282+
assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200);
283+
}
284+
}
285+
286+
// Then: All requests should go through upstream proxy
287+
upstreamProxy.verify(3, anyRequestedFor(urlPathMatching("/request.*")));
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)