From 229fbbdd27580af84095fec23e743dea33f6e20f Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Sat, 2 Dec 2023 08:02:12 +0100 Subject: [PATCH] feat: support classes for HTTP management Signed-off-by: Marc Nuri --- .../mockwebserver/http/Dispatcher.java | 24 +++ .../fabric8/mockwebserver/http/Headers.java | 120 +++++++++++++++ .../mockwebserver/http/MockHttpResponse.java | 142 ++++++++++++++++++ .../mockwebserver/http/MockWebSocket.java | 26 ++++ .../http/MockWebSocketListener.java | 54 +++++++ .../mockwebserver/http/RecordedRequest.java | 77 ++++++++++ 6 files changed, 443 insertions(+) create mode 100644 junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Dispatcher.java create mode 100644 junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Headers.java create mode 100644 junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockHttpResponse.java create mode 100644 junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocket.java create mode 100644 junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocketListener.java create mode 100644 junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/RecordedRequest.java diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Dispatcher.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Dispatcher.java new file mode 100644 index 00000000000..ab98c911d9d --- /dev/null +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Dispatcher.java @@ -0,0 +1,24 @@ +/** + * 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.http; + +public abstract class Dispatcher { + + public abstract MockHttpResponse dispatch(RecordedRequest request); + + public void shutdown() { + } +} diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Headers.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Headers.java new file mode 100644 index 00000000000..b0c52103dce --- /dev/null +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/Headers.java @@ -0,0 +1,120 @@ +/** + * 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.http; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Headers { + + private final Map> headers; + + Headers(Map> headers) { + this.headers = headers; + } + + public final List headers(String key) { + return Collections.unmodifiableList(headers.getOrDefault(key, Collections.emptyList())); + } + + public final Map> headers() { + return Collections.unmodifiableMap(headers); + } + + public final String header(String key) { + final List values = headers(key); + if (values.size() == 1) { + return values.get(0); + } + if (values.isEmpty()) { + return null; + } + return String.join(",", values); + } + + public final Builder newBuilder() { + return new Builder(new LinkedHashMap<>(headers)); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Headers mockHttpHeaders; + + private Builder() { + this(new LinkedHashMap<>()); + } + + private Builder(Map> headers) { + this.mockHttpHeaders = new Headers(headers); + ; + } + + public Headers build() { + return mockHttpHeaders; + } + + public Builder add(String header) { + final int index = header.indexOf(":"); + if (index == -1) { + throw new IllegalArgumentException("Unexpected header: " + header); + } + return add(header.substring(0, index).trim(), header.substring(index + 1)); + } + + public Builder add(String key, String value) { + mockHttpHeaders.headers.compute(key, (k, values) -> { + if (values == null) { + values = new ArrayList<>(); + } + values.add(value); + return values; + }); + return this; + } + + public Builder addAll(Iterable> headers) { + for (Map.Entry header : headers) { + add(header.getKey(), header.getValue()); + } + return this; + } + + public Builder removeAll(String key) { + mockHttpHeaders.headers.remove(key); + return this; + } + + public Builder setHeader(String key, String value) { + final List values = new ArrayList<>(); + values.add(value); + mockHttpHeaders.headers.put(key, values); + return this; + } + + public Builder setHeaders(Headers headers) { + this.mockHttpHeaders.headers.clear(); + this.mockHttpHeaders.headers.putAll(headers.headers()); + return this; + } + + } +} diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockHttpResponse.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockHttpResponse.java new file mode 100644 index 00000000000..00eb13d68a8 --- /dev/null +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockHttpResponse.java @@ -0,0 +1,142 @@ +/** + * 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.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +public class MockHttpResponse { + + private Headers.Builder headers; + private int code; + private byte[] body; + private Duration bodyDelay; + private MockWebSocketListener webSocketListener; + + public MockHttpResponse() { + this.headers = Headers.builder(); + } + + public List headers(String key) { + return headers.build().headers(key); + } + + public Map> headers() { + return headers.build().headers(); + } + + public String header(String key) { + return headers.build().header(key); + } + + public int code() { + return code; + } + + public byte[] body() { + return body; + } + + public MockHttpResponse setResponseCode(int code) { + this.code = code; + return this; + } + + public MockHttpResponse setBody(byte[] body) { + this.body = body; + return this; + } + + public MockHttpResponse setChunkedBody(byte[] body, int maxChunkSize) { + removeHeader("Content-Length"); + setHeader("Transfer-encoding", "chunked"); + + try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + int offset = 0; + while (offset < body.length) { + final int chunkSize = Math.min(body.length - offset, maxChunkSize); + baos.write(Integer.toHexString(chunkSize).getBytes(StandardCharsets.UTF_8)); + baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); + baos.write(body, offset, chunkSize); + baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); + offset += chunkSize; + } + baos.write("0\r\n".getBytes(StandardCharsets.UTF_8)); // Last chunk. Trailers follow! + + this.body = baos.toByteArray(); + return this; + } catch (IOException ex) { + throw new IllegalArgumentException("Invalid chunked body provided", ex); + } + } + + public Duration getBodyDelay() { + return bodyDelay; + } + + public MockHttpResponse setBodyDelay(Duration bodyDelay) { + this.bodyDelay = bodyDelay; + return this; + } + + public MockHttpResponse clearHeaders() { + headers = Headers.builder(); + return this; + } + + public MockHttpResponse addHeader(String header) { + headers.add(header); + return this; + } + + public MockHttpResponse addHeader(String name, Object value) { + headers.add(name, String.valueOf(value)); + return this; + } + + public MockHttpResponse setHeaders(Headers headers) { + this.headers = headers.newBuilder(); + return this; + } + + public MockHttpResponse setHeader(String name, Object value) { + headers.setHeader(name, String.valueOf(value)); + return this; + } + + public MockHttpResponse removeHeader(String name) { + headers.removeAll(name); + return this; + } + + public MockHttpResponse withWebSocketUpgrade(MockWebSocketListener listener) { + // TODO: Check if this is necessary with Vert.x + // setStatus("HTTP/1.1 101 Switching Protocols"); + // setHeader("Connection", "Upgrade"); + // setHeader("Upgrade", "websocket"); + body = null; + webSocketListener = listener; + return this; + } + + public MockWebSocketListener getWebSocketListener() { + return webSocketListener; + } +} diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocket.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocket.java new file mode 100644 index 00000000000..a7b4e9da794 --- /dev/null +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocket.java @@ -0,0 +1,26 @@ +/** + * 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.http; + +public interface MockWebSocket { + RecordedRequest request(); + + boolean send(String text); + + boolean send(byte[] bytes); + + boolean close(int code, String reason); +} diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocketListener.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocketListener.java new file mode 100644 index 00000000000..e27b0ee5c86 --- /dev/null +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/MockWebSocketListener.java @@ -0,0 +1,54 @@ +/** + * 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.http; + +public interface MockWebSocketListener { + + /** + * Invoked when a web socket has been accepted by the remote peer and may begin transmitting + * messages. + */ + default void onOpen(MockWebSocket webSocket, MockHttpResponse response) { + } + + default void onMessage(MockWebSocket webSocket, String text) { + } + + default void onMessage(MockWebSocket webSocket, byte[] bytes) { + } + + /** + * Invoked when the remote peer has indicated that no more incoming messages will be + * transmitted. + */ + default void onClosing(MockWebSocket webSocket, int code, String reason) { + } + + /** + * Invoked when both peers have indicated that no more messages will be transmitted and the + * connection has been successfully released. No further calls to this listener will be made. + */ + default void onClosed(MockWebSocket webSocket, int code, String reason) { + } + + /** + * Invoked when a web socket has been closed due to an error reading from or writing to the + * network. Both outgoing and incoming messages may have been lost. No further calls to this + * listener will be made. + */ + default void onFailure(MockWebSocket webSocket, Throwable error, MockHttpResponse response) { + } +} diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/RecordedRequest.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/RecordedRequest.java new file mode 100644 index 00000000000..a9c59e86d42 --- /dev/null +++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/http/RecordedRequest.java @@ -0,0 +1,77 @@ +/** + * 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.http; + +import io.fabric8.mockwebserver.dsl.HttpMethod; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class RecordedRequest { + + private final UUID id; + private final HttpMethod method; + private final String path; + private final Headers headers; + private final byte[] body; + + public RecordedRequest(HttpMethod method, String path, Headers headers, byte[] body) { + this.id = UUID.randomUUID(); + this.method = method; + this.path = path; + this.headers = headers; + this.body = body; + } + + public UUID id() { + return id; + } + + public String getPath() { + return path; + } + + public HttpMethod getMethod() { + return method; + } + + public byte[] getBody() { + return body; + } + + public String getUtf8Body() { + return body != null ? new String(body, StandardCharsets.UTF_8) : null; + } + + public String method() { + return method.toString(); + } + + public List headers(String key) { + return headers.headers(key); + } + + public Map> headers() { + return headers.headers(); + } + + public String header(String key) { + return headers.header(key); + } + +}