Skip to content

Commit

Permalink
feat(mockwebserver): vert.x implementation bridge classes
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa authored Oct 17, 2024
1 parent 08e54a9 commit 9ad6f43
Show file tree
Hide file tree
Showing 10 changed files with 392 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ public enum HttpMethod {
DELETE,
OPTIONS,
CONNECT,
ANY
ANY;

public static HttpMethod fromVertx(io.vertx.core.http.HttpMethod method) {
if (method != null) {
for (HttpMethod m : HttpMethod.values()) {
if (m.toString().equalsIgnoreCase(method.toString())) {
return m;
}
}
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@
package io.fabric8.mockwebserver.http;

public interface WebSocket {
/**
* Returns the original request that initiated this web socket.
*/
RecordedRequest request();

/**
* Attempts to enqueue {@code text} to be UTF-8 encoded and sent as the data of a text (type
* {@code 0x1}) message.
*/
boolean send(String text);

/**
* Attempts to enqueue {@code bytes} to be sent as a the data of a binary (type {@code 0x2})
* message.
*/
boolean send(byte[] bytes);

/**
Expand All @@ -32,5 +43,13 @@ default boolean send(ByteString bytes) {
return send(bytes.toByteArray());
}

/**
* Attempts to initiate a graceful shutdown of this web socket.
* <p>
* No more messages can be sent.
*
* @param code the status code.
* @param reason reason of closure.
*/
boolean close(int code, String reason);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.mockwebserver.vertx;

import io.fabric8.mockwebserver.dsl.HttpMethod;
import io.fabric8.mockwebserver.http.Headers;
import io.fabric8.mockwebserver.http.MockResponse;
import io.fabric8.mockwebserver.http.RecordedRequest;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;

import java.util.Locale;

public abstract class HttpServerRequestHandler implements Handler<HttpServerRequest> {

private static final String CONTENT_LENGTH = "Content-Length";
private static final String CONTENT_TYPE = "Content-Type";

private final Vertx vertx;

protected HttpServerRequestHandler(Vertx vertx) {
this.vertx = vertx;
}

protected abstract MockResponse onHttpRequest(RecordedRequest request);

@Override
public final void handle(HttpServerRequest event) {
final Handler<Throwable> exceptionHandler = err -> event.response().setStatusCode(500).setStatusMessage(err.getMessage())
.send();
event.resume();
final Future<io.vertx.core.buffer.Buffer> body;
if (hasBody(event)) {
body = event.body();
} else {
body = Future.succeededFuture(null);
}
body.onFailure(exceptionHandler);
body.onSuccess(bodyBuffer -> {

final RecordedRequest request = new RecordedRequest(
event.version().alpnName().toUpperCase(Locale.ROOT),
HttpMethod.fromVertx(event.method()),
event.uri(),
Headers.builder().addAll(event.headers()).build(),
new io.fabric8.mockwebserver.http.Buffer(bodyBuffer == null ? null : bodyBuffer.getBytes()));
final MockResponse mockResponse = onHttpRequest(request);
// WebSocket
if (mockResponse.getWebSocketListener() != null) {
event.toWebSocket()
.onFailure(exceptionHandler)
.onSuccess(new ServerWebSocketHandler(request, mockResponse));
return;
}
// Standard Http Response
final HttpServerResponse vertxResponse = event.response();
vertxResponse.setStatusCode(mockResponse.code());
mockResponse.getHeaders().toMultimap().forEach((key, values) -> vertxResponse.headers().add(key, values));
if (mockResponse.getBody() != null && mockResponse.getBody().size() > 0) {
vertxResponse.headers().add(CONTENT_LENGTH, String.valueOf(mockResponse.getBody().size()));
final io.vertx.core.buffer.Buffer toSend = io.vertx.core.buffer.Buffer.buffer(mockResponse.getBody().getBytes());
if (mockResponse.getBodyDelay() != null) {
vertx.setTimer(mockResponse.getBodyDelay().toMillis(), timerId -> vertxResponse.send(toSend));
} else {
vertxResponse.send(toSend);
}
} else {
vertxResponse.end();
}
});

}

private static boolean hasBody(HttpServerRequest event) {
final String contentLength = event.headers().get(CONTENT_LENGTH);
if (contentLength != null && !contentLength.trim().isEmpty() && Integer.parseInt(contentLength) > 0) {
return true;
}
return event.headers().contains(CONTENT_TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.mockwebserver.vertx;

import io.vertx.core.http.HttpVersion;

import java.util.Locale;

/**
* Compatibility layer for OkHttp.
*/
public enum Protocol {
HTTP_1_0(HttpVersion.HTTP_1_0, "http/1.0"),
HTTP_1_1(HttpVersion.HTTP_1_1, "http/1.1"),
HTTP_2(HttpVersion.HTTP_2, "h2", "h2_prior_knowledge", "h2c", "http/2", "http/2.0");

private final String[] protocolNames;
private final HttpVersion httpVersion;

Protocol(HttpVersion httpVersion, String... protocolNames) {
this.httpVersion = httpVersion;
this.protocolNames = protocolNames;
}

public static Protocol get(String protocol) {
for (Protocol p : Protocol.values()) {
for (String protocolName : p.protocolNames) {
if (protocolName.equals(protocol.toLowerCase(Locale.ROOT))) {
return p;
}
}
}
throw new IllegalArgumentException("Unknown protocol: " + protocol);
}

public HttpVersion getHttpVersion() {
return httpVersion;
}

@Override
public String toString() {
return protocolNames[0];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.mockwebserver.vertx;

import io.fabric8.mockwebserver.http.RecordedRequest;
import io.fabric8.mockwebserver.http.Response;
import io.fabric8.mockwebserver.http.WebSocketListener;
import io.vertx.core.Handler;
import io.vertx.core.http.ServerWebSocket;

public class ServerWebSocketHandler implements Handler<ServerWebSocket> {

private static final int WEBSOCKET_CLOSE_CODE_SERVER_ERROR = 1011;

private final RecordedRequest request;
private final Response response;

public ServerWebSocketHandler(RecordedRequest request, Response response) {
this.request = request;
this.response = response;
}

@Override
public void handle(ServerWebSocket serverWebSocket) {
final WebSocketListener wsListener = response.getWebSocketListener();
final VertxMockWebSocket mockWebSocket = new VertxMockWebSocket(request, serverWebSocket);
// Important to call onBeforeAccept before sending accept so that WebSockets get registered by dispatchers, handlers, and so on
wsListener.onBeforeAccept(mockWebSocket, response);
serverWebSocket.textMessageHandler(text -> wsListener.onMessage(mockWebSocket, text));
serverWebSocket.binaryMessageHandler(buff -> wsListener.onMessage(mockWebSocket, buff.getBytes()));
serverWebSocket.frameHandler(frame -> {
if (frame.isClose()) {
wsListener.onClosing(mockWebSocket, frame.closeStatusCode(), frame.closeReason());
}
serverWebSocket.fetch(1);
});
// use end, not close, because close is processed immediately vs. end is in frame order
serverWebSocket.endHandler(v -> wsListener.onClosed(
mockWebSocket,
serverWebSocket.closeStatusCode() == null ? WEBSOCKET_CLOSE_CODE_SERVER_ERROR
: serverWebSocket.closeStatusCode(),
serverWebSocket.closeReason()));
serverWebSocket.exceptionHandler(err -> wsListener.onFailure(mockWebSocket, err, response));
serverWebSocket.accept();
wsListener.onOpen(mockWebSocket, response);
serverWebSocket.fetch(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.mockwebserver.vertx;

import io.fabric8.mockwebserver.http.RecordedRequest;
import io.fabric8.mockwebserver.http.WebSocket;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.ServerWebSocket;

public class VertxMockWebSocket implements WebSocket {
private final RecordedRequest request;
private final ServerWebSocket webSocket;
private volatile boolean closing;

public VertxMockWebSocket(RecordedRequest request, ServerWebSocket webSocket) {
this.request = request;
this.webSocket = webSocket;
closing = false;
}

@Override
public RecordedRequest request() {
return request;
}

@Override
public boolean send(String text) {
final Future<Void> send = webSocket.writeTextMessage(text);
if (send.isComplete()) {
return send.succeeded();
}
return true;
}

@Override
public boolean send(byte[] bytes) {
final Future<Void> send = webSocket.writeBinaryMessage(Buffer.buffer(bytes));
if (send.isComplete()) {
return send.succeeded();
}
return true;
}

@Override
public synchronized boolean close(int code, String reason) {
if (!closing) {
closing = true;
webSocket.close((short) code, reason);
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.fabric8.mockwebserver.vertx

import io.vertx.core.http.HttpVersion
import spock.lang.Specification

class ProtocolTest extends Specification {

def "get should parse the right protocol HttpVersion"(String protocol, HttpVersion expectedVersion) {
expect:
Protocol.get(protocol).getHttpVersion() == expectedVersion
where:
protocol | expectedVersion
"HTTP/1.0" | HttpVersion.HTTP_1_0
"HTTP/1.1" | HttpVersion.HTTP_1_1
"h2" | HttpVersion.HTTP_2
"h2_prior_knowledge" | HttpVersion.HTTP_2
"h2c" | HttpVersion.HTTP_2
"HTTP/2.0" | HttpVersion.HTTP_2
}

def "get with invalid ALPN identifier or handled alternative should throw an exception"() {
when:
Protocol.get("HTTP/1.33.7")
then:
def exception = thrown(IllegalArgumentException)
exception.message == "Unknown protocol: HTTP/1.33.7"
}
}
Loading

0 comments on commit 9ad6f43

Please sign in to comment.