Skip to content

Commit ed8790e

Browse files
authored
feat(mockwebserver): Vert.x based MockWebServer implementation
Signed-off-by: Marc Nuri <[email protected]>
1 parent 278d9fe commit ed8790e

13 files changed

+1055
-14
lines changed

junit/mockwebserver/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
<groupId>io.fabric8</groupId>
4848
<artifactId>zjsonpatch</artifactId>
4949
</dependency>
50+
<dependency>
51+
<!-- Self Signed certificates can't be generated by all JREs enforce the BouncyCastle for that purpose -->
52+
<groupId>org.bouncycastle</groupId>
53+
<artifactId>bcpkix-jdk18on</artifactId>
54+
<scope>compile</scope>
55+
</dependency>
5056
<dependency>
5157
<groupId>io.vertx</groupId>
5258
<artifactId>vertx-web-client</artifactId>
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.mockwebserver;
17+
18+
import io.fabric8.mockwebserver.http.Dispatcher;
19+
import io.fabric8.mockwebserver.http.HttpUrl;
20+
import io.fabric8.mockwebserver.http.MockResponse;
21+
import io.fabric8.mockwebserver.http.QueueDispatcher;
22+
import io.fabric8.mockwebserver.http.RecordedHttpConnection;
23+
import io.fabric8.mockwebserver.http.RecordedRequest;
24+
import io.fabric8.mockwebserver.vertx.HttpServerRequestHandler;
25+
import io.fabric8.mockwebserver.vertx.Protocol;
26+
import io.netty.handler.ssl.ClientAuth;
27+
import io.vertx.core.AsyncResult;
28+
import io.vertx.core.Future;
29+
import io.vertx.core.Handler;
30+
import io.vertx.core.Vertx;
31+
import io.vertx.core.http.HttpServer;
32+
import io.vertx.core.http.HttpServerOptions;
33+
import io.vertx.core.net.NetServerOptions;
34+
import io.vertx.core.net.SelfSignedCertificate;
35+
36+
import java.io.Closeable;
37+
import java.io.IOException;
38+
import java.net.InetAddress;
39+
import java.net.InetSocketAddress;
40+
import java.net.Proxy;
41+
import java.util.ArrayList;
42+
import java.util.Arrays;
43+
import java.util.HashSet;
44+
import java.util.List;
45+
import java.util.concurrent.BlockingQueue;
46+
import java.util.concurrent.CompletableFuture;
47+
import java.util.concurrent.ExecutionException;
48+
import java.util.concurrent.LinkedBlockingQueue;
49+
import java.util.concurrent.TimeUnit;
50+
import java.util.concurrent.TimeoutException;
51+
import java.util.concurrent.atomic.AtomicInteger;
52+
import java.util.logging.Level;
53+
import java.util.logging.Logger;
54+
import java.util.stream.Collectors;
55+
56+
import static io.vertx.core.net.SSLOptions.DEFAULT_ENABLED_SECURE_TRANSPORT_PROTOCOLS;
57+
58+
public class MockWebServer implements Closeable {
59+
60+
private static final String[] SUPPORTED_WEBSOCKET_SUB_PROTOCOLS = new String[] {
61+
"v1.channel.k8s.io", "v2.channel.k8s.io", "v3.channel.k8s.io", "v4.channel.k8s.io"
62+
};
63+
64+
private static final Logger logger = Logger.getLogger(MockWebServer.class.getName());
65+
66+
private final Vertx vertx;
67+
private final BlockingQueue<RecordedRequest> requestQueue;
68+
private final AtomicInteger requestCount;
69+
private final List<MockWebServerListener> listeners;
70+
private Dispatcher dispatcher;
71+
private ClientAuth clientAuth;
72+
private final List<String> enabledSecuredTransportProtocols;
73+
private boolean ssl;
74+
private SelfSignedCertificate selfSignedCertificate;
75+
private HttpServer httpServer;
76+
private int port;
77+
private InetAddress inetAddress;
78+
private String hostName;
79+
private List<Protocol> protocols;
80+
private boolean started;
81+
82+
public MockWebServer() {
83+
vertx = Vertx.vertx();
84+
requestQueue = new LinkedBlockingQueue<>();
85+
requestCount = new AtomicInteger();
86+
listeners = new ArrayList<>();
87+
dispatcher = new QueueDispatcher();
88+
clientAuth = ClientAuth.NONE;
89+
enabledSecuredTransportProtocols = new ArrayList<>();
90+
enabledSecuredTransportProtocols.addAll(DEFAULT_ENABLED_SECURE_TRANSPORT_PROTOCOLS);
91+
protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1);
92+
}
93+
94+
private void before() {
95+
if (started) {
96+
return;
97+
}
98+
start();
99+
}
100+
101+
public void start() {
102+
start(NetServerOptions.DEFAULT_PORT);
103+
}
104+
105+
public void start(int port) {
106+
start(InetAddress.getLoopbackAddress(), port);
107+
}
108+
109+
public synchronized void start(InetAddress inetAddress, int port) {
110+
if (started) {
111+
throw new IllegalStateException("start() already called");
112+
}
113+
this.started = true;
114+
this.inetAddress = inetAddress;
115+
this.hostName = inetAddress.getHostName().equals("127.0.0.1") ? "localhost" : inetAddress.getHostName();
116+
final HttpServerOptions options = new HttpServerOptions()
117+
.setHost(inetAddress.getHostAddress())
118+
.setPort(port)
119+
.setAlpnVersions(protocols.stream().map(Protocol::getHttpVersion).collect(Collectors.toList()))
120+
.setWebSocketSubProtocols(Arrays.asList(SUPPORTED_WEBSOCKET_SUB_PROTOCOLS))
121+
.setHandle100ContinueAutomatically(true);
122+
if (ssl) {
123+
selfSignedCertificate = SelfSignedCertificate.create(getHostName());
124+
options
125+
.setSsl(true)
126+
.setEnabledSecureTransportProtocols(new HashSet<>(enabledSecuredTransportProtocols))
127+
.setTrustOptions(selfSignedCertificate.trustOptions())
128+
.setKeyCertOptions(selfSignedCertificate.keyCertOptions());
129+
}
130+
httpServer = vertx.createHttpServer(options);
131+
httpServer.connectionHandler(event -> {
132+
final RecordedHttpConnection connection = new RecordedHttpConnection(
133+
event.remoteAddress(), event.localAddress(), ssl);
134+
listeners.forEach(listener -> listener.onConnection(connection));
135+
event.closeHandler(res -> listeners.forEach(listener -> listener.onConnectionClosed(connection)));
136+
});
137+
httpServer.requestHandler(new HttpServerRequestHandler(vertx) {
138+
@Override
139+
protected MockResponse onHttpRequest(RecordedRequest request) {
140+
requestCount.incrementAndGet();
141+
requestQueue.add(request);
142+
final MockResponse response = dispatcher.dispatch(request);
143+
info("received request: %s and responded: %s", request.toString(), response.toString());
144+
return response;
145+
}
146+
});
147+
await(httpServer.listen(), "Unable to start MockWebServer");
148+
this.port = httpServer.actualPort();
149+
info("starting to accept connections on %s", getHostName());
150+
}
151+
152+
public synchronized void shutdown() {
153+
if (!started) {
154+
return;
155+
}
156+
if (httpServer == null) {
157+
throw new IllegalStateException("shutdown() before start()");
158+
}
159+
dispatcher.shutdown();
160+
final Future<Void> httpClose = httpServer.close();
161+
Handler<AsyncResult<Void>> onComplete = v -> {
162+
vertx.close();
163+
info("done accepting connections");
164+
};
165+
if (httpClose.isComplete()) {
166+
onComplete.handle(httpClose);
167+
} else {
168+
httpClose.onComplete(onComplete);
169+
await(httpClose, "Unable to close MockWebServer");
170+
}
171+
}
172+
173+
@Override
174+
public void close() throws IOException {
175+
shutdown();
176+
}
177+
178+
public int getPort() {
179+
before();
180+
return port;
181+
}
182+
183+
public String getHostName() {
184+
before();
185+
return hostName;
186+
}
187+
188+
public Proxy toProxyAddress() {
189+
before();
190+
final InetSocketAddress address = new InetSocketAddress(getHostName(), getPort());
191+
return new Proxy(Proxy.Type.HTTP, address);
192+
}
193+
194+
public SelfSignedCertificate getSelfSignedCertificate() {
195+
return selfSignedCertificate;
196+
}
197+
198+
public HttpUrl url(String path) {
199+
if (path.startsWith("/")) {
200+
path = path.substring(1);
201+
}
202+
final String schema = ssl ? "https" : "http";
203+
return HttpUrl.parse(schema + "://" + getHostName() + ":" + getPort() + "/" + path);
204+
}
205+
206+
public RecordedRequest takeRequest() throws InterruptedException {
207+
return requestQueue.take();
208+
}
209+
210+
public RecordedRequest takeRequest(long timeout, TimeUnit unit) throws InterruptedException {
211+
return requestQueue.poll(timeout, unit);
212+
}
213+
214+
public int getRequestCount() {
215+
return requestCount.get();
216+
}
217+
218+
public void useHttps() {
219+
this.ssl = true;
220+
}
221+
222+
public void enqueue(MockResponse response) {
223+
if (dispatcher instanceof QueueDispatcher) {
224+
((QueueDispatcher) dispatcher).enqueueResponse(response);
225+
} else {
226+
throw new IllegalStateException("Dispatcher is not a QueueDispatcher");
227+
}
228+
}
229+
230+
public void addListener(MockWebServerListener listener) {
231+
listeners.add(listener);
232+
}
233+
234+
public void setDispatcher(Dispatcher dispatcher) {
235+
this.dispatcher = dispatcher;
236+
}
237+
238+
public void setProtocols(List<Protocol> protocols) {
239+
this.protocols = protocols;
240+
}
241+
242+
private static <T> T await(Future<T> vertxFuture, String errorMessage) {
243+
final CompletableFuture<T> future = new CompletableFuture<>();
244+
vertxFuture.onComplete(r -> {
245+
if (r.succeeded()) {
246+
future.complete(r.result());
247+
} else {
248+
future.completeExceptionally(r.cause());
249+
}
250+
});
251+
try {
252+
return future.get(10, TimeUnit.SECONDS);
253+
} catch (InterruptedException e) {
254+
Thread.currentThread().interrupt();
255+
throw new IllegalStateException(e);
256+
} catch (ExecutionException | TimeoutException e) {
257+
throw new IllegalStateException(errorMessage, e);
258+
}
259+
}
260+
261+
private void info(String log, String... parameters) {
262+
if (logger.isLoggable(Level.INFO)) {
263+
final String formatMessage = "%s " + log;
264+
final String[] allParams = Arrays.copyOf(new String[] { toString() }, parameters.length + 1);
265+
System.arraycopy(parameters, 0, allParams, 1, parameters.length);
266+
logger.info(String.format(formatMessage, allParams));
267+
}
268+
}
269+
270+
@Override
271+
public String toString() {
272+
return "MockWebServer[" + getPort() + "]";
273+
}
274+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.mockwebserver;
17+
18+
import io.fabric8.mockwebserver.http.RecordedHttpConnection;
19+
20+
public interface MockWebServerListener {
21+
default void onConnection(RecordedHttpConnection connection) {
22+
}
23+
24+
default void onConnectionClosed(RecordedHttpConnection connection) {
25+
}
26+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (C) 2015 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.fabric8.mockwebserver.http;
17+
18+
import io.vertx.core.net.SocketAddress;
19+
20+
import java.util.Objects;
21+
22+
public class RecordedHttpConnection {
23+
24+
private final SocketAddress remoteAddress;
25+
private final SocketAddress localAddress;
26+
private final boolean ssl;
27+
28+
public RecordedHttpConnection(SocketAddress remoteAddress, SocketAddress localAddress, boolean ssl) {
29+
this.remoteAddress = remoteAddress;
30+
this.localAddress = localAddress;
31+
this.ssl = ssl;
32+
}
33+
34+
public SocketAddress getRemoteAddress() {
35+
return remoteAddress;
36+
}
37+
38+
public SocketAddress getLocalAddress() {
39+
return localAddress;
40+
}
41+
42+
public boolean isSsl() {
43+
return ssl;
44+
}
45+
46+
@Override
47+
public boolean equals(Object o) {
48+
if (this == o)
49+
return true;
50+
if (o == null || getClass() != o.getClass())
51+
return false;
52+
RecordedHttpConnection that = (RecordedHttpConnection) o;
53+
return ssl == that.ssl && Objects.equals(remoteAddress, that.remoteAddress)
54+
&& Objects.equals(localAddress, that.localAddress);
55+
}
56+
57+
@Override
58+
public int hashCode() {
59+
return Objects.hash(remoteAddress, localAddress, ssl);
60+
}
61+
}

junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/WebSocketListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void onMessage(WebSocket webSocket, String text) {
4242
public void onMessage(WebSocket webSocket, ByteString bytes) {
4343
}
4444

45-
public final void onMessage(WebSocket webSocket, byte[] bytes) {
45+
public void onMessage(WebSocket webSocket, byte[] bytes) {
4646
onMessage(webSocket, ByteString.of(bytes));
4747
}
4848

junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/vertx/HttpServerRequestHandler.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public final void handle(HttpServerRequest event) {
5353
}
5454
body.onFailure(exceptionHandler);
5555
body.onSuccess(bodyBuffer -> {
56-
5756
final RecordedRequest request = new RecordedRequest(
5857
event.version().alpnName().toUpperCase(Locale.ROOT),
5958
HttpMethod.fromVertx(event.method()),

0 commit comments

Comments
 (0)