diff --git a/spotbugs.exclude.xml b/spotbugs.exclude.xml index 20e59c4..8726b13 100644 --- a/spotbugs.exclude.xml +++ b/spotbugs.exclude.xml @@ -40,21 +40,6 @@ - - - - - - - - - - - - - - - @@ -96,4 +81,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/io/github/nstdio/http/ext/BinaryMetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/BinaryMetadataSerializer.java new file mode 100644 index 0000000..dd0e9b7 --- /dev/null +++ b/src/main/java/io/github/nstdio/http/ext/BinaryMetadataSerializer.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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.github.nstdio.http.ext; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.net.URI; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse.ResponseInfo; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.net.http.HttpRequest.BodyPublishers.noBody; + +class BinaryMetadataSerializer implements MetadataSerializer { + private final StreamFactory streamFactory; + + BinaryMetadataSerializer(StreamFactory streamFactory) { + this.streamFactory = streamFactory; + } + + @Override + public void write(CacheEntryMetadata metadata, Path path) { + try (var out = new ObjectOutputStream(streamFactory.output(path))) { + out.writeObject(new ExternalizableMetadata(metadata)); + } catch (IOException ignored) { + } + } + + @Override + public CacheEntryMetadata read(Path path) { + try (var input = new ObjectInputStream(streamFactory.input(path))) { + return ((ExternalizableMetadata) input.readObject()).metadata; + } catch (IOException | ClassNotFoundException ignored) { + return null; + } + } + + static final class ExternalizableMetadata implements Externalizable { + private static final long serialVersionUID = 15052410042022L; + private CacheEntryMetadata metadata; + + public ExternalizableMetadata() { + } + + ExternalizableMetadata(CacheEntryMetadata metadata) { + this.metadata = metadata; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeLong(metadata.requestTime()); + out.writeLong(metadata.responseTime()); + + out.writeObject(new ExternalizableResponseInfo(metadata.response())); + out.writeObject(new ExternalizableHttpRequest(metadata.request())); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + final long requestTime = in.readLong(); + final long responseTime = in.readLong(); + ResponseInfo responseInfo = ((ExternalizableResponseInfo) in.readObject()).responseInfo; + HttpRequest request = ((ExternalizableHttpRequest) in.readObject()).request; + + metadata = CacheEntryMetadata.of(requestTime, responseTime, responseInfo, request, Clock.systemUTC()); + } + } + + static class ExternalizableHttpRequest implements Externalizable { + private static final long serialVersionUID = 15052410042022L; + private HttpRequest request; + + public ExternalizableHttpRequest() { + } + + ExternalizableHttpRequest(HttpRequest request) { + this.request = request; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(request.uri()); + out.writeUTF(request.method()); + out.writeObject(request.version().orElse(null)); + out.writeObject(request.timeout().orElse(null)); + out.writeObject(new ExternalizableHttpHeaders(request.headers())); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + Builder builder = HttpRequest.newBuilder() + .uri((URI) in.readObject()) + .method(in.readUTF(), noBody()); + + Version v = (Version) in.readObject(); + if (v != null) { + builder.version(v); + } + Duration t = (Duration) in.readObject(); + if (t != null) { + builder.timeout(t); + } + + Map> headers = ((ExternalizableHttpHeaders) in.readObject()).headers.map(); + for (var entry : headers.entrySet()) { + for (String value : entry.getValue()) { + builder.header(entry.getKey(), value); + } + } + + request = builder.build(); + } + } + + static class ExternalizableResponseInfo implements Externalizable { + private static final long serialVersionUID = 15052410042022L; + private ResponseInfo responseInfo; + + public ExternalizableResponseInfo() { + } + + ExternalizableResponseInfo(ResponseInfo responseInfo) { + this.responseInfo = responseInfo; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeInt(responseInfo.statusCode()); + out.writeObject(new ExternalizableHttpHeaders(responseInfo.headers())); + out.writeObject(responseInfo.version()); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + responseInfo = ImmutableResponseInfo.builder() + .statusCode(in.readInt()) + .headers(((ExternalizableHttpHeaders) in.readObject()).headers) + .version((Version) in.readObject()) + .build(); + } + } + + static class ExternalizableHttpHeaders implements Externalizable { + private static final long serialVersionUID = 15052410042022L; + private static final int maxMapSize = 1024; + private static final int maxListSize = 256; + + private final boolean respectLimits; + HttpHeaders headers; + + public ExternalizableHttpHeaders() { + this(null, true); + } + + ExternalizableHttpHeaders(HttpHeaders headers) { + this(headers, true); + } + + ExternalizableHttpHeaders(HttpHeaders headers, boolean respectLimits) { + this.headers = headers; + this.respectLimits = respectLimits; + } + + private static String mapSizeExceedMessage(int mapSize) { + return String.format("The headers size exceeds max allowed number. Size: %d, Max:%d", mapSize, maxMapSize); + } + + private static String listSizeExceedMessage(String headerName, int size) { + return String.format("The values for header '%s' exceeds maximum allowed number. Size:%d, Max:%d", + headerName, size, maxListSize); + } + + private static IOException corruptedStream(String desc) { + return new IOException("Corrupted stream: " + desc); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + Map> map = headers.map(); + + int mapSize = map.size(); + if (respectLimits && mapSize > maxMapSize) { + throw new IOException(mapSizeExceedMessage(mapSize)); + } + + // write map size + out.writeInt(mapSize); + for (var entry : map.entrySet()) { + // header name + String headerName = entry.getKey(); + out.writeUTF(headerName); + + List values = entry.getValue(); + int valuesSize = values.size(); + checkValuesSize(headerName, valuesSize); + + // write values size + out.writeInt(valuesSize); + // header values + for (String value : values) out.writeUTF(value); + } + } + + @Override + public void readExternal(ObjectInput in) throws IOException { + final int mapSize = in.readInt(); + checkMapSize(mapSize); + + if (mapSize == 0) { + headers = HttpHeaders.of(Map.of(), Headers.ALLOW_ALL); + return; + } + + var map = new HashMap>(mapSize, 1.0f); + for (int i = 0; i < mapSize; i++) { + String headerName = in.readUTF(); + + int valuesSize = in.readInt(); + checkValuesSize(headerName, valuesSize); + + var values = new ArrayList(valuesSize); + for (int j = 0; j < valuesSize; j++) values.add(in.readUTF()); + + map.put(headerName, values); + } + + headers = HttpHeaders.of(map, Headers.ALLOW_ALL); + } + + private void checkValuesSize(String headerName, int valuesSize) throws IOException { + if (valuesSize <= 0) throw corruptedStream("list size should be positive"); + else if (respectLimits && valuesSize > maxListSize) + throw new IOException(listSizeExceedMessage(headerName, valuesSize)); + } + + private void checkMapSize(int mapSize) throws IOException { + if (mapSize < 0) throw corruptedStream("map size cannot be negative"); + else if (respectLimits && mapSize > maxMapSize) throw new IOException(mapSizeExceedMessage(mapSize)); + } + } +} diff --git a/src/main/java/io/github/nstdio/http/ext/Cache.java b/src/main/java/io/github/nstdio/http/ext/Cache.java index 626f9cc..df260d8 100644 --- a/src/main/java/io/github/nstdio/http/ext/Cache.java +++ b/src/main/java/io/github/nstdio/http/ext/Cache.java @@ -47,15 +47,11 @@ static InMemoryCacheBuilder newInMemoryCacheBuilder() { } /** - * Creates a new {@code DiskCacheBuilder} instance. Requires Jackson form dumping cache files on disk. + * Creates a new {@code DiskCacheBuilder} instance. * * @return the new {@code DiskCacheBuilder}. - * - * @throws IllegalStateException When Jackson (a.k.a. ObjectMapper) is not in classpath. */ static DiskCacheBuilder newDiskCacheBuilder() { - MetadataSerializer.requireAvailability(); - return new DiskCacheBuilder(); } diff --git a/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java b/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java index 2549ffb..4a3fdfb 100644 --- a/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java +++ b/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java @@ -62,7 +62,7 @@ public class ExtendedHttpClient extends HttpClient { * @return an {@code ExtendedHttpClient.Builder} */ public static ExtendedHttpClient.Builder newBuilder() { - return new ExtendedHttpClient.Builder(); + return new ExtendedHttpClient.Builder(HttpClient.newBuilder()); } /** @@ -200,7 +200,7 @@ private Sender syncSender() { return ctx -> { try { return completedFuture(delegate.send(ctx.request(), ctx.bodyHandler())); - } catch (IOException | InterruptedException e) { + } catch (Throwable e) { return CompletableFuture.failedFuture(e); } }; @@ -221,11 +221,12 @@ interface Sender extends Function diff --git a/src/main/java/io/github/nstdio/http/ext/GsonMetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/GsonMetadataSerializer.java deleted file mode 100644 index 0fe5fb3..0000000 --- a/src/main/java/io/github/nstdio/http/ext/GsonMetadataSerializer.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (C) 2022 Edgar Asatryan - * - * 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.github.nstdio.http.ext; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import io.github.nstdio.http.ext.ImmutableResponseInfo.ResponseInfoBuilder; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse.ResponseInfo; -import java.nio.file.Path; -import java.time.Clock; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - -import static io.github.nstdio.http.ext.IOUtils.bufferedReader; -import static io.github.nstdio.http.ext.IOUtils.bufferedWriter; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_CODE; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_HEADERS; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST_TIME; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE_TIME; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_VERSION; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_METHOD; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_TIMEOUT; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_URI; -import static java.net.http.HttpRequest.BodyPublishers.noBody; - -class GsonMetadataSerializer implements MetadataSerializer { - private final Gson gson; - private final StreamFactory streamFactory; - - GsonMetadataSerializer() { - this(new SimpleStreamFactory()); - } - - GsonMetadataSerializer(StreamFactory streamFactory) { - var headers = new HttpHeadersTypeAdapter(); - var request = new HttpRequestTypeAdapter(headers); - var response = new ResponseInfoTypeAdapter(headers); - - this.gson = new GsonBuilder() - .disableHtmlEscaping() - .registerTypeAdapter(CacheEntryMetadata.class, new CacheEntryMetadataTypeAdapter(request, response)) - .create(); - this.streamFactory = streamFactory; - } - - @Override - public void write(CacheEntryMetadata metadata, Path path) { - try (var out = bufferedWriter(streamFactory.output(path))) { - gson.toJson(metadata, out); - } catch (IOException ignored) { - } - } - - - @Override - public CacheEntryMetadata read(Path path) { - try (var in = bufferedReader(streamFactory.input(path))) { - return gson.fromJson(in, CacheEntryMetadata.class); - } catch (IOException ignored) { - return null; - } - } - - private static class HttpHeadersTypeAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, HttpHeaders value) throws IOException { - out.beginObject(); - - for (var entry : value.map().entrySet()) { - out.name(entry.getKey()).beginArray(); - - for (String headerValue : entry.getValue()) { - out.value(headerValue); - } - - out.endArray(); - } - - out.endObject(); - } - - @Override - public HttpHeaders read(JsonReader in) throws IOException { - in.beginObject(); - var builder = new HttpHeadersBuilder(); - - while (in.hasNext()) { - String name = in.nextName(); - List values = new ArrayList<>(1); - - in.beginArray(); - while (in.hasNext()) { - String value = in.nextString(); - values.add(value); - } - in.endArray(); - - builder.setTrusted(name, values); - } - - in.endObject(); - return builder.build(); - } - } - - private static class HttpRequestTypeAdapter extends TypeAdapter { - private final TypeAdapter headersTypeAdapter; - - HttpRequestTypeAdapter(TypeAdapter headersTypeAdapter) { - this.headersTypeAdapter = headersTypeAdapter; - } - - @Override - public void write(JsonWriter out, HttpRequest value) throws IOException { - out.beginObject(); - - out.name(FILED_NAME_REQUEST_URI).value(value.uri().toASCIIString()); - out.name(FILED_NAME_REQUEST_METHOD).value(value.method()); - - String timeoutString = value.timeout().map(Duration::toString).orElse(null); - if (timeoutString != null) { - out.name(FILED_NAME_REQUEST_TIMEOUT).value(timeoutString); - } - Integer versionOrd = value.version().map(Enum::ordinal).orElse(null); - if (versionOrd != null) { - out.name(FIELD_NAME_VERSION).value(versionOrd); - } - - out.name(FIELD_NAME_HEADERS); - headersTypeAdapter.write(out, value.headers()); - - out.endObject(); - } - - @Override - public HttpRequest read(JsonReader in) throws IOException { - in.beginObject(); - HttpRequest.Builder builder = HttpRequest.newBuilder(); - - while (in.hasNext()) { - switch (in.nextName()) { - case FILED_NAME_REQUEST_METHOD: - builder.method(in.nextString(), noBody()); - break; - case FILED_NAME_REQUEST_TIMEOUT: - builder.timeout(Duration.parse(in.nextString())); - break; - case FIELD_NAME_VERSION: - int version = in.nextInt(); - builder.version(HttpClient.Version.values()[version]); - break; - case FILED_NAME_REQUEST_URI: - builder.uri(URI.create(in.nextString())); - break; - case FIELD_NAME_HEADERS: - HttpHeaders headers = headersTypeAdapter.read(in); - headers.map().forEach((name, values) -> values.forEach(value -> builder.header(name, value))); - break; - } - } - - in.endObject(); - - return builder.build(); - } - } - - private static class ResponseInfoTypeAdapter extends TypeAdapter { - private final TypeAdapter headersTypeAdapter; - - ResponseInfoTypeAdapter(TypeAdapter headersTypeAdapter) { - this.headersTypeAdapter = headersTypeAdapter; - } - - @Override - public void write(JsonWriter out, ResponseInfo value) throws IOException { - out.beginObject(); - - out.name(FIELD_NAME_CODE).value(value.statusCode()); - out.name(FIELD_NAME_VERSION).value(value.version().ordinal()); - - out.name(FIELD_NAME_HEADERS); - headersTypeAdapter.write(out, value.headers()); - - out.endObject(); - } - - @Override - public ResponseInfo read(JsonReader in) throws IOException { - in.beginObject(); - ResponseInfoBuilder builder = ImmutableResponseInfo.builder(); - String fieldName; - - while (in.hasNext()) { - fieldName = in.nextName(); - - switch (fieldName) { - case FIELD_NAME_CODE: - builder.statusCode(in.nextInt()); - break; - case FIELD_NAME_VERSION: - int version = in.nextInt(); - builder.version(HttpClient.Version.values()[version]); - break; - case FIELD_NAME_HEADERS: - HttpHeaders headers = headersTypeAdapter.read(in); - builder.headers(headers); - break; - } - } - - in.endObject(); - - return builder.build(); - } - } - - private static class CacheEntryMetadataTypeAdapter extends TypeAdapter { - private final TypeAdapter requestTypeAdapter; - private final TypeAdapter responseTypeAdapter; - - CacheEntryMetadataTypeAdapter(TypeAdapter requestTypeAdapter, TypeAdapter responseTypeAdapter) { - this.requestTypeAdapter = requestTypeAdapter; - this.responseTypeAdapter = responseTypeAdapter; - } - - @Override - public void write(JsonWriter out, CacheEntryMetadata value) throws IOException { - out.beginObject(); - - out.name(FIELD_NAME_REQUEST_TIME).value(value.requestTime()); - out.name(FIELD_NAME_RESPONSE_TIME).value(value.responseTime()); - - out.name(FIELD_NAME_REQUEST); - requestTypeAdapter.write(out, value.request()); - - out.name(FIELD_NAME_RESPONSE); - responseTypeAdapter.write(out, value.response()); - - out.endObject(); - } - - @Override - public CacheEntryMetadata read(JsonReader in) throws IOException { - in.beginObject(); - - long requestTime = -1; - long responseTime = -1; - HttpRequest request = null; - ResponseInfo response = null; - - while (in.hasNext()) { - switch (in.nextName()) { - case FIELD_NAME_REQUEST_TIME: - requestTime = in.nextLong(); - break; - case FIELD_NAME_RESPONSE_TIME: - responseTime = in.nextLong(); - break; - case FIELD_NAME_REQUEST: - request = requestTypeAdapter.read(in); - break; - case FIELD_NAME_RESPONSE: - response = responseTypeAdapter.read(in); - break; - } - } - - in.endObject(); - return CacheEntryMetadata.of(requestTime, responseTime, response, request, Clock.systemUTC()); - } - } -} diff --git a/src/main/java/io/github/nstdio/http/ext/IOUtils.java b/src/main/java/io/github/nstdio/http/ext/IOUtils.java index 7d53fd3..1841365 100644 --- a/src/main/java/io/github/nstdio/http/ext/IOUtils.java +++ b/src/main/java/io/github/nstdio/http/ext/IOUtils.java @@ -16,20 +16,12 @@ package io.github.nstdio.http.ext; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.Closeable; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; -import static java.nio.charset.StandardCharsets.UTF_8; - class IOUtils { private IOUtils() { } @@ -75,12 +67,4 @@ static boolean createFile(Path path) { return false; } } - - static BufferedReader bufferedReader(InputStream in) { - return new BufferedReader(new InputStreamReader(in, UTF_8)); - } - - static BufferedWriter bufferedWriter(OutputStream out) { - return new BufferedWriter(new OutputStreamWriter(out, UTF_8)); - } } diff --git a/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java deleted file mode 100644 index 237bc21..0000000 --- a/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright (C) 2022 Edgar Asatryan - * - * 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.github.nstdio.http.ext; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.databind.type.CollectionType; -import com.fasterxml.jackson.databind.type.MapType; -import com.fasterxml.jackson.databind.type.TypeFactory; -import io.github.nstdio.http.ext.ImmutableResponseInfo.ResponseInfoBuilder; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpHeaders; -import java.net.http.HttpRequest; -import java.net.http.HttpRequest.Builder; -import java.net.http.HttpResponse.ResponseInfo; -import java.nio.file.Path; -import java.time.Clock; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_CODE; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_HEADERS; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST_TIME; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE_TIME; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_VERSION; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_METHOD; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_TIMEOUT; -import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_URI; -import static java.net.http.HttpRequest.BodyPublishers.noBody; -import static java.nio.file.StandardOpenOption.CREATE; -import static java.nio.file.StandardOpenOption.READ; -import static java.nio.file.StandardOpenOption.WRITE; - -class JacksonMetadataSerializer implements MetadataSerializer { - - private final ObjectWriter writer; - private final ObjectReader reader; - private final StreamFactory streamFactory; - - JacksonMetadataSerializer() { - this(new SimpleStreamFactory()); - } - - JacksonMetadataSerializer(StreamFactory streamFactory) { - ObjectMapper mapper = createMapper(); - this.writer = mapper.writerFor(CacheEntryMetadata.class); - this.reader = mapper.readerFor(CacheEntryMetadata.class); - this.streamFactory = streamFactory; - } - - private static JsonMappingException unexpectedFieldException(JsonParser p, String fieldName) { - return new JsonMappingException(p, "Unexpected field name: " + fieldName); - } - - private ObjectMapper createMapper() { - ObjectMapper mapper = new ObjectMapper(); - SimpleModule simpleModule = new SimpleModule("JsonMetadataSerializer"); - - simpleModule.addSerializer(CacheEntryMetadata.class, new CacheMetadataSerializer()); - simpleModule.addDeserializer(CacheEntryMetadata.class, new CacheMetadataDeserializer()); - - simpleModule.addSerializer(HttpRequest.class, new HttpRequestSerializer()); - simpleModule.addDeserializer(HttpRequest.class, new HttpRequestDeserializer()); - - simpleModule.addSerializer(ResponseInfo.class, new ResponseInfoSerializer()); - simpleModule.addDeserializer(ResponseInfo.class, new ResponseInfoDeserializer()); - - simpleModule.addSerializer(HttpHeaders.class, new HttpHeadersSerializer()); - simpleModule.addDeserializer(HttpHeaders.class, new HttpHeadersDeserializer()); - - mapper.registerModule(simpleModule); - return mapper; - } - - @Override - public void write(CacheEntryMetadata metadata, Path path) { - try (var out = new GZIPOutputStream(streamFactory.output(path, WRITE, CREATE))) { - writer.writeValue(out, metadata); - } catch (IOException ignore) { - // noop - } - } - - @Override - public CacheEntryMetadata read(Path path) { - try (var in = new GZIPInputStream(streamFactory.input(path, READ))) { - return reader.readValue(in); - } catch (IOException e) { - return null; - } - } - - static class CacheMetadataSerializer extends StdSerializer { - private static final long serialVersionUID = 1L; - - CacheMetadataSerializer() { - super(CacheEntryMetadata.class); - } - - @Override - public void serialize(CacheEntryMetadata value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - - gen.writeNumberField(FIELD_NAME_REQUEST_TIME, value.requestTime()); - gen.writeNumberField(FIELD_NAME_RESPONSE_TIME, value.responseTime()); - gen.writeObjectField(FIELD_NAME_REQUEST, value.request()); - gen.writeObjectField(FIELD_NAME_RESPONSE, value.response()); - - gen.writeEndObject(); - } - } - - private static class CacheMetadataDeserializer extends StdDeserializer { - private static final long serialVersionUID = 1L; - private final JavaType requestType = TypeFactory.defaultInstance().constructType(HttpRequest.class); - private final JavaType responseType = TypeFactory.defaultInstance().constructType(ResponseInfo.class); - - CacheMetadataDeserializer() { - super(CacheEntryMetadata.class); - } - - @Override - public CacheEntryMetadata deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - long requestTime = -1; - long responseTime = -1; - HttpRequest request = null; - ResponseInfo response = null; - String fieldName; - - while ((fieldName = p.nextFieldName()) != null) { - switch (fieldName) { - case FIELD_NAME_REQUEST_TIME: - requestTime = p.nextLongValue(-1); - break; - case FIELD_NAME_RESPONSE_TIME: - responseTime = p.nextLongValue(-1); - break; - case FIELD_NAME_REQUEST: - p.nextToken(); - request = (HttpRequest) ctxt.findRootValueDeserializer(requestType).deserialize(p, ctxt); - break; - case FIELD_NAME_RESPONSE: - p.nextToken(); - response = (ResponseInfo) ctxt.findRootValueDeserializer(responseType).deserialize(p, ctxt); - break; - default: - throw unexpectedFieldException(p, fieldName); - } - } - - return CacheEntryMetadata.of(requestTime, responseTime, response, request, Clock.systemUTC()); - } - } - - static class HttpRequestSerializer extends StdSerializer { - private static final long serialVersionUID = 1L; - - HttpRequestSerializer() { - super(HttpRequest.class); - } - - @Override - public void serialize(HttpRequest value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeStartObject(); - gen.writeStringField(FILED_NAME_REQUEST_METHOD, value.method()); - - String timeoutString = value.timeout().map(Duration::toString).orElse(null); - if (timeoutString != null) { - gen.writeStringField(FILED_NAME_REQUEST_TIMEOUT, timeoutString); - } - - gen.writeStringField(FILED_NAME_REQUEST_URI, value.uri().toASCIIString()); - Integer versionOrd = value.version().map(Enum::ordinal).orElse(null); - if (versionOrd != null) { - gen.writeNumberField(FIELD_NAME_VERSION, versionOrd); - } - - gen.writeObjectField(FIELD_NAME_HEADERS, value.headers()); - - gen.writeEndObject(); - } - } - - static class HttpRequestDeserializer extends StdDeserializer { - private static final long serialVersionUID = 1L; - private final JavaType headersType = TypeFactory.defaultInstance().constructType(HttpHeaders.class); - - HttpRequestDeserializer() { - super(HttpRequest.class); - } - - @Override - public HttpRequest deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - Builder builder = HttpRequest.newBuilder(); - String fieldName; - - while ((fieldName = p.nextFieldName()) != null) { - switch (fieldName) { - case FILED_NAME_REQUEST_METHOD: - builder.method(p.nextTextValue(), noBody()); - break; - case FILED_NAME_REQUEST_TIMEOUT: - String timeout = p.nextTextValue(); - builder.timeout(Duration.parse(timeout)); - break; - case FIELD_NAME_VERSION: - int version = p.nextIntValue(-1); - builder.version(HttpClient.Version.values()[version]); - break; - case FILED_NAME_REQUEST_URI: - builder.uri(URI.create(p.nextTextValue())); - break; - case FIELD_NAME_HEADERS: - p.nextToken(); - HttpHeaders headers = (HttpHeaders) ctxt.findRootValueDeserializer(headersType).deserialize(p, ctxt); - headers.map().forEach((name, values) -> values.forEach(value -> builder.header(name, value))); - break; - default: - throw unexpectedFieldException(p, fieldName); - } - } - - return builder.build(); - } - } - - static class ResponseInfoSerializer extends StdSerializer { - private static final long serialVersionUID = 1L; - - ResponseInfoSerializer() { - super(ResponseInfo.class); - } - - @Override - public void serialize(ResponseInfo value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - - gen.writeNumberField(FIELD_NAME_CODE, value.statusCode()); - gen.writeNumberField(FIELD_NAME_VERSION, value.version().ordinal()); - gen.writeObjectField(FIELD_NAME_HEADERS, value.headers()); - - gen.writeEndObject(); - } - } - - static class ResponseInfoDeserializer extends StdDeserializer { - private static final long serialVersionUID = 1L; - private final JavaType headersType = TypeFactory.defaultInstance().constructType(HttpHeaders.class); - private final HttpClient.Version[] values = HttpClient.Version.values(); - - ResponseInfoDeserializer() { - super(ResponseInfo.class); - } - - @Override - public ResponseInfo deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - ResponseInfoBuilder builder = ImmutableResponseInfo.builder(); - - String fieldName; - while ((fieldName = p.nextFieldName()) != null) { - switch (fieldName) { - case FIELD_NAME_CODE: - builder.statusCode(p.nextIntValue(-1)); - break; - case FIELD_NAME_VERSION: - builder.version(values[p.nextIntValue(-1)]); - break; - case FIELD_NAME_HEADERS: - JsonDeserializer headersDeserializer = ctxt.findRootValueDeserializer(headersType); - p.nextToken(); - HttpHeaders headers = (HttpHeaders) headersDeserializer.deserialize(p, ctxt); - builder.headers(headers); - break; - default: - throw unexpectedFieldException(p, fieldName); - } - } - - return builder.build(); - } - } - - static class HttpHeadersSerializer extends StdSerializer { - private static final long serialVersionUID = 1L; - - HttpHeadersSerializer() { - super(HttpHeaders.class); - } - - @Override - public void serialize(HttpHeaders value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeStartObject(); - - Map> map = value.map(); - for (var entry : map.entrySet()) { - gen.writeFieldName(entry.getKey()); - - String[] values = entry.getValue().toArray(new String[0]); - gen.writeArray(values, 0, values.length); - } - - gen.writeEndObject(); - } - } - - static class HttpHeadersDeserializer extends StdDeserializer { - private static final long serialVersionUID = 1L; - private final MapType mapType; - - HttpHeadersDeserializer() { - super(HttpHeaders.class); - TypeFactory typeFactory = TypeFactory.defaultInstance(); - - JavaType keyType = typeFactory.constructType(String.class); - CollectionType valueType = typeFactory.constructCollectionType(ArrayList.class, String.class); - this.mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType); - } - - @Override - public HttpHeaders deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - @SuppressWarnings("unchecked") - var deserialize = (Map>) ctxt.findRootValueDeserializer(mapType).deserialize(p, ctxt); - - return HttpHeaders.of(deserialize, Headers.ALLOW_ALL); - } - } -} diff --git a/src/main/java/io/github/nstdio/http/ext/MetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/MetadataSerializer.java index 298d78a..6735471 100644 --- a/src/main/java/io/github/nstdio/http/ext/MetadataSerializer.java +++ b/src/main/java/io/github/nstdio/http/ext/MetadataSerializer.java @@ -18,26 +18,10 @@ import java.nio.file.Path; -import static io.github.nstdio.http.ext.spi.Classpath.isGsonPresent; -import static io.github.nstdio.http.ext.spi.Classpath.isJacksonPresent; - interface MetadataSerializer { - static void requireAvailability() { - if (!isJacksonPresent() && !isGsonPresent()) { - throw new IllegalStateException("In order to use disk cache please add either Jackson or Gson to your dependencies"); - } - } static MetadataSerializer findAvailable(StreamFactory streamFactory) { - if (isJacksonPresent()) { - return new JacksonMetadataSerializer(streamFactory); - } - - if (isGsonPresent()) { - return new GsonMetadataSerializer(streamFactory); - } - - return null; + return new BinaryMetadataSerializer(streamFactory); } void write(CacheEntryMetadata metadata, Path path); diff --git a/src/spiTest/kotlin/io/github/nstdio/http/ext/DiskCacheBuilderSpiTest.kt b/src/spiTest/kotlin/io/github/nstdio/http/ext/DiskCacheBuilderSpiTest.kt deleted file mode 100644 index 658c069..0000000 --- a/src/spiTest/kotlin/io/github/nstdio/http/ext/DiskCacheBuilderSpiTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2022 Edgar Asatryan - * - * 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.github.nstdio.http.ext - -import io.github.nstdio.http.ext.jupiter.DisabledIfOnClasspath -import org.assertj.core.api.Assertions -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -internal class DiskCacheBuilderSpiTest { - @Nested - @DisabledIfOnClasspath(JACKSON, GSON) - internal inner class WithoutJacksonTest { - @Test - fun shouldDescriptiveException() { - Assertions.assertThatIllegalStateException() - .isThrownBy { Cache.newDiskCacheBuilder() } - .withMessage("In order to use disk cache please add either Jackson or Gson to your dependencies") - } - } -} \ No newline at end of file diff --git a/src/spiTest/kotlin/io/github/nstdio/http/ext/MetadataSerializerSpiTest.kt b/src/spiTest/kotlin/io/github/nstdio/http/ext/MetadataSerializerSpiTest.kt deleted file mode 100644 index f837fb7..0000000 --- a/src/spiTest/kotlin/io/github/nstdio/http/ext/MetadataSerializerSpiTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2022 Edgar Asatryan - * - * 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.github.nstdio.http.ext - -import io.github.nstdio.http.ext.MetadataSerializer.findAvailable -import io.github.nstdio.http.ext.MetadataSerializer.requireAvailability -import io.github.nstdio.http.ext.jupiter.DisabledIfOnClasspath -import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.assertThatIllegalStateException -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -internal class MetadataSerializerSpiTest { - @Nested - @EnabledIfOnClasspath(GSON) - @DisabledIfOnClasspath(JACKSON) - internal inner class GsonPresentJacksonMissing { - @Test - fun `Should not throw when gson present`() { - requireAvailability() - } - - @Test - fun `Should return Gson serializer`() { - //when - val serializer = findAvailable(SimpleStreamFactory()) - - //then - assertThat(serializer).isInstanceOf(GsonMetadataSerializer::class.java) - } - } - - @Nested - @EnabledIfOnClasspath(JACKSON) - @DisabledIfOnClasspath(GSON) - internal inner class JacksonPresentGsonMissing { - @Test - fun `Should not throw when gson present`() { - requireAvailability() - } - - @Test - fun `Should return Jackson serializer`() { - //when - val serializer = findAvailable(SimpleStreamFactory()) - - //then - assertThat(serializer).isInstanceOf(JacksonMetadataSerializer::class.java) - } - } - - @Nested - @DisabledIfOnClasspath(GSON, JACKSON) - internal inner class AllMissing { - @Test - fun `Should throw nothing present`() { - assertThatIllegalStateException() - .isThrownBy { requireAvailability() } - } - - @Test - fun `Should return null`() { - //when - val serializer = findAvailable(SimpleStreamFactory()) - - //then - assertThat(serializer).isNull() - } - } -} \ No newline at end of file diff --git a/src/spiTest/kotlin/io/github/nstdio/http/ext/JacksonMetadataSerializerSpiTest.kt b/src/test/kotlin/io/github/nstdio/http/ext/BinaryMetadataSerializerTest.kt similarity index 70% rename from src/spiTest/kotlin/io/github/nstdio/http/ext/JacksonMetadataSerializerSpiTest.kt rename to src/test/kotlin/io/github/nstdio/http/ext/BinaryMetadataSerializerTest.kt index e01303f..81bfac8 100644 --- a/src/spiTest/kotlin/io/github/nstdio/http/ext/JacksonMetadataSerializerSpiTest.kt +++ b/src/test/kotlin/io/github/nstdio/http/ext/BinaryMetadataSerializerTest.kt @@ -13,14 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.github.nstdio.http.ext -import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path + +internal class BinaryMetadataSerializerTest : MetadataSerializerContract { + @TempDir + private lateinit var tempDir: Path -@EnabledIfOnClasspath(JACKSON) -internal class JacksonMetadataSerializerSpiTest : MetadataSerializerContract { override fun serializer(): MetadataSerializer { - return JacksonMetadataSerializer() + return BinaryMetadataSerializer(SimpleStreamFactory()) } + + override fun tempDir() = tempDir } \ No newline at end of file diff --git a/src/test/kotlin/io/github/nstdio/http/ext/EncryptedStreamFactoryTest.kt b/src/test/kotlin/io/github/nstdio/http/ext/EncryptedStreamFactoryTest.kt index 3a9e9a0..7e350dc 100644 --- a/src/test/kotlin/io/github/nstdio/http/ext/EncryptedStreamFactoryTest.kt +++ b/src/test/kotlin/io/github/nstdio/http/ext/EncryptedStreamFactoryTest.kt @@ -16,7 +16,6 @@ package io.github.nstdio.http.ext -import io.github.nstdio.http.ext.IOUtils.bufferedWriter import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.matchers.shouldBe @@ -36,10 +35,12 @@ import org.mockito.BDDMockito.times import org.mockito.BDDMockito.verify import org.mockito.Mockito.any import org.mockito.Mockito.mock +import java.io.BufferedWriter import java.io.EOFException import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.io.OutputStreamWriter import java.nio.charset.StandardCharsets.UTF_8 import java.nio.file.Path import java.security.InvalidKeyException @@ -70,7 +71,7 @@ class EncryptedStreamFactoryTest { val path = dir.resolve("sample") //when - bufferedWriter(writeFactory.output(path)).use { it.write(expected) } + writeFactory.output(path).toWriter().use { it.write(expected) } val decrypted = readFactory.input(path).use { it.readText() } val plain = path.readText() @@ -160,6 +161,8 @@ class EncryptedStreamFactoryTest { return String(readAllBytes(), UTF_8) } + private fun OutputStream.toWriter() = BufferedWriter(OutputStreamWriter(this, UTF_8)) + companion object { @JvmStatic fun rwData(): List { diff --git a/src/test/kotlin/io/github/nstdio/http/ext/ExtendedHttpClientTest.kt b/src/test/kotlin/io/github/nstdio/http/ext/ExtendedHttpClientTest.kt index 5cec885..235bb9c 100644 --- a/src/test/kotlin/io/github/nstdio/http/ext/ExtendedHttpClientTest.kt +++ b/src/test/kotlin/io/github/nstdio/http/ext/ExtendedHttpClientTest.kt @@ -15,45 +15,152 @@ */ package io.github.nstdio.http.ext +import io.github.nstdio.http.ext.ExtendedHttpClient.Builder +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.matchers.throwable.shouldHaveCause +import io.kotest.matchers.types.shouldBeSameInstanceAs import org.assertj.core.api.Assertions.assertThatExceptionOfType -import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.ValueSource -import org.mockito.BDDMockito -import org.mockito.Mockito +import org.mockito.BDDMockito.given +import org.mockito.BDDMockito.inOrder +import org.mockito.Mockito.any +import org.mockito.Mockito.mock import java.io.IOException +import java.net.Authenticator +import java.net.CookieHandler +import java.net.ProxySelector import java.net.SocketTimeoutException import java.net.URI import java.net.http.HttpClient +import java.net.http.HttpClient.Version import java.net.http.HttpRequest import java.net.http.HttpResponse.BodyHandler import java.net.http.HttpResponse.BodyHandlers.ofString import java.time.Clock +import java.time.Duration +import java.util.concurrent.CompletionException +import java.util.concurrent.Executor +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLParameters internal class ExtendedHttpClientTest { - private var client: ExtendedHttpClient? = null - private var mockHttpClient: HttpClient? = null + private lateinit var client: ExtendedHttpClient + private lateinit var mockHttpClient: HttpClient - @BeforeEach - fun setUp() { - mockHttpClient = Mockito.mock(HttpClient::class.java) - client = ExtendedHttpClient(mockHttpClient, NullCache.INSTANCE, Clock.systemUTC()) + @BeforeEach + fun setUp() { + mockHttpClient = mock(HttpClient::class.java) + client = ExtendedHttpClient(mockHttpClient, NullCache.INSTANCE, Clock.systemUTC()) + } + + @ParameterizedTest + @ValueSource(classes = [IOException::class, InterruptedException::class, SocketTimeoutException::class]) + fun shouldPropagateExceptions(th: Class) { + //given + val request = HttpRequest.newBuilder().uri(URI.create("https://example.com")).build() + given(mockHttpClient.send(any(), any>())).willThrow(th) + + //when + then + assertThatExceptionOfType(th) + .isThrownBy { client.send(request, ofString()) } + } + + @ParameterizedTest + @MethodSource("notUnwrappedExceptions") + fun `Should throw CompletionException with cause`(th: Throwable) { + //given + val request = HttpRequest.newBuilder().uri(URI.create("https://example.com")).build() + given(mockHttpClient.send(any(), any>())).willThrow(th) + + + //when + then + shouldThrowExactly { client.send(request, ofString()) } + .shouldHaveCause { it.shouldBeSameInstanceAs(th) } + } + + @Test + fun `Should forward calls to delegate`() { + //when + client.cookieHandler() + client.connectTimeout() + client.followRedirects() + client.proxy() + client.sslContext() + client.sslParameters() + client.authenticator() + client.version() + client.executor() + client.newWebSocketBuilder() + + //then + val inOrder = inOrder(mockHttpClient) + inOrder.verify(mockHttpClient).cookieHandler() + inOrder.verify(mockHttpClient).connectTimeout() + inOrder.verify(mockHttpClient).followRedirects() + inOrder.verify(mockHttpClient).proxy() + inOrder.verify(mockHttpClient).sslContext() + inOrder.verify(mockHttpClient).sslParameters() + inOrder.verify(mockHttpClient).authenticator() + inOrder.verify(mockHttpClient).version() + inOrder.verify(mockHttpClient).executor() + inOrder.verify(mockHttpClient).newWebSocketBuilder() + } + + @Nested + inner class BuilderTest { + @Test + fun `Should forward calls to delegate`() { + //given + val mockDelegate = mock(HttpClient.Builder::class.java) + val mockCookieHandler = mock(CookieHandler::class.java) + val mockSSLContext = mock(SSLContext::class.java) + val mockSSLParameters = mock(SSLParameters::class.java) + val mockProxySelector = mock(ProxySelector::class.java) + val mockAuthenticator = mock(Authenticator::class.java) + val mockExecutor = mock(Executor::class.java) + val builder = Builder(mockDelegate) + + //when + builder + .cookieHandler(mockCookieHandler) + .connectTimeout(Duration.ofSeconds(30)) + .sslContext(mockSSLContext) + .sslParameters(mockSSLParameters) + .version(Version.HTTP_2) + .priority(500) + .followRedirects(HttpClient.Redirect.ALWAYS) + .proxy(mockProxySelector) + .authenticator(mockAuthenticator) + .executor(mockExecutor) + + //then + val inOrder = inOrder(mockDelegate) + inOrder.verify(mockDelegate).cookieHandler(mockCookieHandler) + inOrder.verify(mockDelegate).connectTimeout(Duration.ofSeconds(30)) + inOrder.verify(mockDelegate).sslContext(mockSSLContext) + inOrder.verify(mockDelegate).sslParameters(mockSSLParameters) + inOrder.verify(mockDelegate).version(Version.HTTP_2) + inOrder.verify(mockDelegate).priority(500) + inOrder.verify(mockDelegate).followRedirects(HttpClient.Redirect.ALWAYS) + inOrder.verify(mockDelegate).proxy(mockProxySelector) + inOrder.verify(mockDelegate).authenticator(mockAuthenticator) + inOrder.verify(mockDelegate).executor(mockExecutor) } + } - @ParameterizedTest - @ValueSource(classes = [IOException::class, InterruptedException::class, IllegalStateException::class, RuntimeException::class, OutOfMemoryError::class, SocketTimeoutException::class]) - @Throws( - Exception::class - ) - fun shouldPropagateExceptions(th: Class) { - //given - val request = HttpRequest.newBuilder().uri(URI.create("https://example.com")).build() - BDDMockito.given(mockHttpClient!!.send(Mockito.any(), Mockito.any>())).willThrow(th) - - //when + then - assertThatExceptionOfType(th) - .isThrownBy { client!!.send(request, ofString()) } - assertThrows(th) { client!!.send(request, ofString()) } + companion object { + @JvmStatic + fun notUnwrappedExceptions(): List { + return listOf( + RuntimeException("abc"), + IllegalStateException("abc"), + OutOfMemoryError("abcd") + ) } + } } \ No newline at end of file diff --git a/src/test/kotlin/io/github/nstdio/http/ext/ExternalizableHttpHeadersTest.kt b/src/test/kotlin/io/github/nstdio/http/ext/ExternalizableHttpHeadersTest.kt new file mode 100644 index 0000000..a5bb276 --- /dev/null +++ b/src/test/kotlin/io/github/nstdio/http/ext/ExternalizableHttpHeadersTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2022 Edgar Asatryan + * + * 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.github.nstdio.http.ext + +import io.github.nstdio.http.ext.BinaryMetadataSerializer.ExternalizableHttpHeaders +import io.github.nstdio.http.ext.Headers.ALLOW_ALL +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.matchers.shouldBe +import io.kotest.matchers.throwable.shouldHaveMessage +import io.kotest.property.Arb +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.string +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ValueSource +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.ObjectInput +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.OutputStream.nullOutputStream +import java.net.http.HttpHeaders + +internal class ExternalizableHttpHeadersTest { + + @ParameterizedTest + @MethodSource("httpHeaders") + fun `Should round robin proper headers`(expected: HttpHeaders) { + //given + val out = ByteArrayOutputStream() + + //when + ObjectOutputStream(out).use { it.writeObject(ExternalizableHttpHeaders(expected)) } + + val e = out.toObjectInput().readObject() as ExternalizableHttpHeaders + + //then + e.headers.shouldBe(expected) + } + + @Test + fun `Should throw when large headers`() { + //given + val maxHeadersSize = 2048 + val headers = Arb.list(Arb.string(16..32), maxHeadersSize..3000) + .map { + val map = hashMapOf>() + map.putAll(it.zip(Arb.list(Arb.list(Arb.string(), 1..1), it.size..it.size).next())) + map + } + .map { HttpHeaders.of(it, ALLOW_ALL) } + .next() + + val headersSize = headers.map().size + + //when + then + shouldThrowExactly { nullOutput().writeObject(ExternalizableHttpHeaders(headers)) } + .shouldHaveMessage( + "The headers size exceeds max allowed number. Size: $headersSize, Max:1024" + ) + + val out = ByteArrayOutputStream() + ObjectOutputStream(out).use { it.writeObject(ExternalizableHttpHeaders(headers, false)) } + + shouldThrowExactly { out.toObjectInput().readObject() } + .shouldHaveMessage( + "The headers size exceeds max allowed number. Size: $headersSize, Max:1024" + ) + } + + @Test + fun `Should throw when large header values occures`() { + //given + val maxValuesSize = 256 + val headerName = "Content-Type" + val headers = Arb.list(Arb.string(3..15), maxValuesSize..1000) + .map { mapOf(headerName to it) } + .map { HttpHeaders.of(it, ALLOW_ALL) } + .next() + val valuesSize = headers.allValues(headerName).size + + //when + then + shouldThrowExactly { + nullOutput().writeObject(ExternalizableHttpHeaders(headers)) + }.shouldHaveMessage( + "The values for header '$headerName' exceeds maximum allowed number. Size:$valuesSize, Max:256" + ) + + val out = ByteArrayOutputStream() + ObjectOutputStream(out).use { it.writeObject(ExternalizableHttpHeaders(headers, false)) } + + shouldThrowExactly { out.toObjectInput().readObject() } + .shouldHaveMessage( + "The values for header '$headerName' exceeds maximum allowed number. Size:$valuesSize, Max:256" + ) + } + + @Test + fun `Should throw if map size is negative`() { + //given + val bytes = byteArrayOf( + // header + -84, -19, 0, 5, 115, 114, 0, 76, 105, 111, 46, 103, 105, 116, 104, 117, 98, 46, + 110, 115, 116, 100, 105, 111, 46, 104, 116, 116, 112, 46, 101, 120, 116, 46, 66, + 105, 110, 97, 114, 121, 77, 101, 116, 97, 100, 97, 116, 97, 83, 101, 114, 105, 97, + 108, 105, 122, 101, 114, 36, 69, 120, 116, 101, 114, 110, 97, 108, 105, 122, 97, + 98, 108, 101, 72, 116, 116, 112, 72, 101, 97, 100, 101, 114, 115, 0, 0, 13, -80, + -87, -115, -74, -90, 12, 0, 0, 120, 112, 119, 4, + // map size + -1, -1, -1, -42, // decimal: -42 + // block end + 120 + ) + + //when + shouldThrowExactly { bytes.toObjectInput().readObject() } + .shouldHaveMessage("Corrupted stream: map size cannot be negative") + } + + @ParameterizedTest + @ValueSource(ints = [0, -1, -100, Int.MIN_VALUE]) + fun `Should throw if list size is invalid`(listSize: Int) { + //given + val out = ByteArrayOutputStream() + val objOut = ObjectOutputStream(out) + + objOut.use { + it.writeInt(15) // map size + it.writeUTF("content-type") // header value + it.writeInt(listSize) + } + + //when + shouldThrowExactly { ExternalizableHttpHeaders().readExternal(out.toObjectInput()) } + .shouldHaveMessage("Corrupted stream: list size should be positive") + } + + private fun ByteArrayOutputStream.toObjectInput(): ObjectInput = + ObjectInputStream(ByteArrayInputStream(toByteArray())) + + private fun ByteArray.toObjectInput(): ObjectInput = ObjectInputStream(ByteArrayInputStream(this)) + + private fun nullOutput() = ObjectOutputStream(nullOutputStream()) + + companion object { + @JvmStatic + fun httpHeaders(): List { + return listOf( + HttpHeaders.of(hashMapOf("Content-Type" to listOf("application/json")), Headers.ALLOW_ALL), + HttpHeaders.of( + hashMapOf( + "Host" to listOf("www.random.org"), + "User-Agent" to listOf("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0"), + "Accept" to listOf( + "text/html", + "application/xhtml+xml", + "application/xml;q=0.9", + "image/avif", + "image/webp", + "*/*;q=0.8" + ), + "Accept-Language" to listOf("en-US,en;q=0.5"), + "Accept-Encoding" to listOf("gzip, deflate, br"), + "Referer" to listOf("https://www.random.org/integers/"), + "Connection" to listOf("keep-alive"), + "Upgrade-Insecure-Requests" to listOf("1"), + "Sec-Fetch-Dest" to listOf("document"), + "Sec-Fetch-Mode" to listOf("navigate"), + "Sec-Fetch-Site" to listOf("same-origin"), + "Sec-Fetch-User" to listOf("?1"), + "Cache-Control" to listOf("max-age=0"), + ), Headers.ALLOW_ALL + ), + HttpHeaders.of(hashMapOf(), Headers.ALLOW_ALL) + ) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/nstdio/http/ext/IOUtilsTest.kt b/src/test/kotlin/io/github/nstdio/http/ext/IOUtilsTest.kt index b29dec8..a9887c7 100644 --- a/src/test/kotlin/io/github/nstdio/http/ext/IOUtilsTest.kt +++ b/src/test/kotlin/io/github/nstdio/http/ext/IOUtilsTest.kt @@ -17,6 +17,7 @@ package io.github.nstdio.http.ext import io.github.nstdio.http.ext.IOUtils.createFile +import io.kotest.matchers.booleans.shouldBeFalse import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -83,4 +84,13 @@ class IOUtilsTest { //then assertTrue(created) } + + @Test + fun `should return false if cannot create`(@TempDir temp: Path) { + //given + val path = temp.resolve("``````///dsadksaihfu///2e1```") + + //then + createFile(path).shouldBeFalse() + } } \ No newline at end of file diff --git a/src/spiTest/kotlin/io/github/nstdio/http/ext/GsonMetadataSerializerSpiTest.kt b/src/test/kotlin/io/github/nstdio/http/ext/InMemoryCacheEntryTest.kt similarity index 71% rename from src/spiTest/kotlin/io/github/nstdio/http/ext/GsonMetadataSerializerSpiTest.kt rename to src/test/kotlin/io/github/nstdio/http/ext/InMemoryCacheEntryTest.kt index 8c52706..4afbd57 100644 --- a/src/spiTest/kotlin/io/github/nstdio/http/ext/GsonMetadataSerializerSpiTest.kt +++ b/src/test/kotlin/io/github/nstdio/http/ext/InMemoryCacheEntryTest.kt @@ -13,14 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.github.nstdio.http.ext -import io.github.nstdio.http.ext.jupiter.EnabledIfOnClasspath +internal class InMemoryCacheEntryTest { -@EnabledIfOnClasspath(GSON) -internal class GsonMetadataSerializerSpiTest : MetadataSerializerContract { - override fun serializer(): MetadataSerializer { - return GsonMetadataSerializer() - } } \ No newline at end of file diff --git a/src/test/kotlin/io/github/nstdio/http/ext/InMemoryCacheTest.kt b/src/test/kotlin/io/github/nstdio/http/ext/InMemoryCacheTest.kt index 24c76a5..129231a 100644 --- a/src/test/kotlin/io/github/nstdio/http/ext/InMemoryCacheTest.kt +++ b/src/test/kotlin/io/github/nstdio/http/ext/InMemoryCacheTest.kt @@ -28,181 +28,181 @@ import java.util.stream.Collectors.toList import java.util.stream.IntStream internal class InMemoryCacheTest { - private var cache: InMemoryCache? = null - - @BeforeEach - fun setUp() { - cache = InMemoryCache(512, -1) - } - - @Test - @DisplayName("Should be able to cache with Vary header") - fun simple() { - //given - val uris = uris(4) - val acceptHeaders = listOf("text/plain", "application/json", "text/csv", "text/*") - val maxItems = uris.size * acceptHeaders.size - 1 - cache = InMemoryCache(maxItems, -1) - for (uri in uris) { - for (acceptHeader in acceptHeaders) { - val request = HttpRequest.newBuilder(uri) - .headers("Accept", acceptHeader) - .build() - val responseHeaders = java.util.Map.of( - "Content-Type", acceptHeader, - "Content-Length", "0", - "Vary", "Accept" - ) - val e = cacheEntry(responseHeaders, request) - - //when - cache!!.put(request, e) - - //then - assertThat(cache!![request]).isSameAs(e) - } - } - assertThat(cache!!.mapSize()).isEqualTo(uris.size) - assertThat(cache!!.multimapSize()).isEqualTo(maxItems) - } - - @Test - fun shouldWorkAsLRU() { - //given - val maxItems = 2 - cache = InMemoryCache(maxItems, -1) - val requests = uris(10) - .stream() - .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } - .collect(toList()) - val expected = requests.subList(requests.size - maxItems, requests.size) - for (request in requests) { - val e = cacheEntry(java.util.Map.of(), request) - cache!!.put(request, e) - - //then - assertThat(cache!![request]).isSameAs(e) - } - for (r in expected) { - assertThat(cache!![r]).isNotNull - } - } - - @Test - fun shouldRespectSizeConstraints() { - //given - val maxBytes = 16 - cache = InMemoryCache(512, maxBytes.toLong()) - val bodySize = 4 - val requests = uris(6) - .stream() - .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } - .collect(toList()) - val expectedEntriesCount = maxBytes / bodySize - val notInCache = requests.subList(0, requests.size - expectedEntriesCount) - val inCache = requests.subList(requests.size - expectedEntriesCount, requests.size) + private lateinit var cache: InMemoryCache + + @BeforeEach + fun setUp() { + cache = InMemoryCache(512, -1) + } + + @Test + @DisplayName("Should be able to cache with Vary header") + fun simple() { + //given + val uris = uris(4) + val acceptHeaders = listOf("text/plain", "application/json", "text/csv", "text/*") + val maxItems = uris.size * acceptHeaders.size - 1 + cache = InMemoryCache(maxItems, -1) + for (uri in uris) { + for (acceptHeader in acceptHeaders) { + val request = HttpRequest.newBuilder(uri) + .headers("Accept", acceptHeader) + .build() + val responseHeaders = java.util.Map.of( + "Content-Type", acceptHeader, + "Content-Length", "0", + "Vary", "Accept" + ) + val e = cacheEntry(responseHeaders, request) //when - for (request in requests) { - val metadata = metadata(request, Helpers.responseInfo(java.util.Map.of())) - val e = InMemoryCacheEntry(ByteArray(bodySize), metadata) - cache!!.put(request, e) - } + cache.put(request, e) //then - assertThat(cache!!.bytes()).isLessThanOrEqualTo(maxBytes.toLong()) - assertThat(cache!!.multimapSize()) - .isLessThanOrEqualTo(expectedEntriesCount) - assertThat(notInCache).isNotEmpty.allMatch { r: HttpRequest? -> cache!![r] == null } - assertThat(inCache).isNotEmpty.allMatch { r: HttpRequest? -> cache!![r] != null } + assertThat(cache[request]).isSameAs(e) + } } - - @Test - fun shouldEvictMultipleEldestToPutBigOne() { - //given - val maxBytes = 16 - val bodySize = 8 - cache = InMemoryCache(512, maxBytes.toLong()) - val requests = uris(2) - .stream() - .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } - .collect(toList()) - val r1 = HttpRequest.newBuilder(URI.create("https://testurl.com")).build() - val e1 = InMemoryCacheEntry(ByteArray(maxBytes), metadata(r1)) - - //when - for (r in requests) { - cache!!.put(r, InMemoryCacheEntry(ByteArray(bodySize), metadata(r))) - } - cache!!.put(r1, e1) - - //then - assertThat(cache!!.bytes()).isEqualTo(maxBytes.toLong()) - assertThat(cache!!.multimapSize()).isEqualTo(1) - assertThat(requests).allMatch { r: HttpRequest? -> cache!![r] == null } - assertThat(cache!![r1]).isSameAs(e1) + assertThat(cache.mapSize()).isEqualTo(uris.size) + assertThat(cache.multimapSize()).isEqualTo(maxItems) + } + + @Test + fun shouldWorkAsLRU() { + //given + val maxItems = 2 + cache = InMemoryCache(maxItems, -1) + val requests = uris(10) + .stream() + .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } + .collect(toList()) + val expected = requests.subList(requests.size - maxItems, requests.size) + for (request in requests) { + val e = cacheEntry(java.util.Map.of(), request) + cache.put(request, e) + + //then + assertThat(cache[request]).isSameAs(e) + } + for (r in expected) { + assertThat(cache[r]).isNotNull + } + } + + @Test + fun shouldRespectSizeConstraints() { + //given + val maxBytes = 16 + cache = InMemoryCache(512, maxBytes.toLong()) + val bodySize = 4 + val requests = uris(6) + .stream() + .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } + .collect(toList()) + val expectedEntriesCount = maxBytes / bodySize + val notInCache = requests.subList(0, requests.size - expectedEntriesCount) + val inCache = requests.subList(requests.size - expectedEntriesCount, requests.size) + + //when + for (request in requests) { + val metadata = metadata(request, Helpers.responseInfo(java.util.Map.of())) + val e = InMemoryCacheEntry(ByteArray(bodySize), metadata) + cache.put(request, e) } - @Test - fun shouldNotEvictWhenNewOneExceedsLimit() { - //given - val maxBytes = 16 - val bodySize = 4 - cache = InMemoryCache(512, maxBytes.toLong()) - val requests = uris(4) - .stream() - .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } - .collect(toList()) - val r1 = HttpRequest.newBuilder(URI.create("https://testurl.com")).build() - val e1 = InMemoryCacheEntry(ByteArray(maxBytes + 1), metadata(r1)) - - //when - for (r in requests) { - cache!!.put(r, InMemoryCacheEntry(ByteArray(bodySize), metadata(r))) - } - cache!!.put(r1, e1) - - //then - assertThat(cache!!.bytes()).isEqualTo(maxBytes.toLong()) - assertThat(cache!!.multimapSize()).isEqualTo(4) - assertThat(requests).allMatch { r: HttpRequest? -> cache!![r] != null } - assertThat(cache!![r1]).isNull() - cache!!.evictAll() - assertThat(cache!!.bytes()).isZero - assertThat(cache!!.multimapSize()).isZero - assertThat(cache!!.mapSize()).isZero + //then + assertThat(cache.bytes()).isLessThanOrEqualTo(maxBytes.toLong()) + assertThat(cache.multimapSize()) + .isLessThanOrEqualTo(expectedEntriesCount) + assertThat(notInCache).isNotEmpty.allMatch { r: HttpRequest? -> cache[r] == null } + assertThat(inCache).isNotEmpty.allMatch { r: HttpRequest? -> cache[r] != null } + } + + @Test + fun shouldEvictMultipleEldestToPutBigOne() { + //given + val maxBytes = 16 + val bodySize = 8 + cache = InMemoryCache(512, maxBytes.toLong()) + val requests = uris(2) + .stream() + .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } + .collect(toList()) + val r1 = HttpRequest.newBuilder(URI.create("https://testurl.com")).build() + val e1 = InMemoryCacheEntry(ByteArray(maxBytes), metadata(r1)) + + //when + for (r in requests) { + cache.put(r, InMemoryCacheEntry(ByteArray(bodySize), metadata(r))) + } + cache.put(r1, e1) + + //then + assertThat(cache.bytes()).isEqualTo(maxBytes.toLong()) + assertThat(cache.multimapSize()).isEqualTo(1) + assertThat(requests).allMatch { r: HttpRequest? -> cache[r] == null } + assertThat(cache[r1]).isSameAs(e1) + } + + @Test + fun shouldNotEvictWhenNewOneExceedsLimit() { + //given + val maxBytes = 16 + val bodySize = 4 + cache = InMemoryCache(512, maxBytes.toLong()) + val requests = uris(4) + .stream() + .map { uri: URI? -> HttpRequest.newBuilder(uri).build() } + .collect(toList()) + val r1 = HttpRequest.newBuilder(URI.create("https://testurl.com")).build() + val e1 = InMemoryCacheEntry(ByteArray(maxBytes + 1), metadata(r1)) + + //when + for (r in requests) { + cache.put(r, InMemoryCacheEntry(ByteArray(bodySize), metadata(r))) + } + cache.put(r1, e1) + + //then + assertThat(cache.bytes()).isEqualTo(maxBytes.toLong()) + assertThat(cache.multimapSize()).isEqualTo(4) + assertThat(requests).allMatch { r: HttpRequest? -> cache[r] != null } + assertThat(cache[r1]).isNull() + cache.evictAll() + assertThat(cache.bytes()).isZero + assertThat(cache.multimapSize()).isZero + assertThat(cache.mapSize()).isZero + } + + private fun metadata( + request: HttpRequest, + info: ResponseInfo = Helpers.responseInfo(java.util.Map.of()) + ): CacheEntryMetadata { + return CacheEntryMetadata(0, 0, info, request, SYSTEM_CLOCK) + } + + companion object { + private val SYSTEM_CLOCK = Clock.systemDefaultZone() + fun cacheEntry(headers: Map?, r: HttpRequest?): InMemoryCacheEntry { + return cacheEntry(0, headers, r) } - private fun metadata( - request: HttpRequest, - info: ResponseInfo = Helpers.responseInfo(java.util.Map.of()) - ): CacheEntryMetadata { - return CacheEntryMetadata(0, 0, info, request, SYSTEM_CLOCK) + private fun cacheEntry( + responseTimeMs: Long, + headers: Map?, + r: HttpRequest? + ): InMemoryCacheEntry { + return InMemoryCacheEntry( + ByteArray(0), + CacheEntryMetadata(0, responseTimeMs, Helpers.responseInfo(headers), r, Clock.systemDefaultZone()) + ) } - companion object { - private val SYSTEM_CLOCK = Clock.systemDefaultZone() - fun cacheEntry(headers: Map?, r: HttpRequest?): InMemoryCacheEntry { - return cacheEntry(0, headers, r) - } - - private fun cacheEntry( - responseTimeMs: Long, - headers: Map?, - r: HttpRequest? - ): InMemoryCacheEntry { - return InMemoryCacheEntry( - ByteArray(0), - CacheEntryMetadata(0, responseTimeMs, Helpers.responseInfo(headers), r, Clock.systemDefaultZone()) - ) - } - - fun uris(size: Int): List { - val baseUri = URI.create("http://example.com/") - return IntStream.rangeClosed(1, size) - .mapToObj { it.toString() } - .map { str: String? -> baseUri.resolve(str) } - .collect(toList()) - } + fun uris(size: Int): List { + val baseUri = URI.create("http://example.com/") + return IntStream.rangeClosed(1, size) + .mapToObj { it.toString() } + .map { baseUri.resolve(it) } + .collect(toList()) } + } } \ No newline at end of file diff --git a/src/spiTest/kotlin/io/github/nstdio/http/ext/MetadataSerializerContract.kt b/src/test/kotlin/io/github/nstdio/http/ext/MetadataSerializerContract.kt similarity index 56% rename from src/spiTest/kotlin/io/github/nstdio/http/ext/MetadataSerializerContract.kt rename to src/test/kotlin/io/github/nstdio/http/ext/MetadataSerializerContract.kt index ed416c0..ce6fff2 100644 --- a/src/spiTest/kotlin/io/github/nstdio/http/ext/MetadataSerializerContract.kt +++ b/src/test/kotlin/io/github/nstdio/http/ext/MetadataSerializerContract.kt @@ -20,8 +20,11 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import java.net.URI import java.net.http.HttpClient +import java.net.http.HttpClient.Version.HTTP_2 import java.net.http.HttpRequest import java.nio.file.Path import java.time.Clock @@ -34,6 +37,11 @@ internal interface MetadataSerializerContract { */ fun serializer(): MetadataSerializer + /** + * The metadata serializer under the test. + */ + fun tempDir(): Path + @Test fun `Should return null when cannot read`(@TempDir dir: Path) { //given @@ -47,29 +55,13 @@ internal interface MetadataSerializerContract { assertNull(metadata) } - @Test - fun `Should write and read`(@TempDir dir: Path) { + @ParameterizedTest + @MethodSource("metadata") + fun `Should write and read`(metadata: CacheEntryMetadata) { //given + val dir = tempDir() val file = dir.resolve("abc") - val responseInfo = ImmutableResponseInfo.builder() - .headers( - HttpHeadersBuilder() - .add("test", "1") - .add("test", "2") - .build() - ) - .statusCode(200) - .version(HttpClient.Version.HTTP_1_1) - .build() - val request = HttpRequest.newBuilder(URI.create("https://example.com")) - .header("abc", "1") - .header("abc", "2") - .header("abcd", "11") - .header("abcd", "22") - .version(HttpClient.Version.HTTP_2) - .timeout(Duration.ofSeconds(30)) - .build() - val metadata = CacheEntryMetadata(10, 15, responseInfo, request, Clock.systemUTC()) + val ser = serializer() //when @@ -84,4 +76,44 @@ internal interface MetadataSerializerContract { assertThat(actual.response().version()).isEqualTo(metadata.response().version()) assertThat(actual.response().headers()).isEqualTo(metadata.response().headers()) } + + companion object { + @JvmStatic + fun metadata(): List { + val metadata1 = CacheEntryMetadata( + 10, 15, ImmutableResponseInfo.builder() + .headers( + HttpHeadersBuilder() + .add("test", "1") + .add("test", "2") + .build() + ) + .statusCode(200) + .version(HttpClient.Version.HTTP_1_1) + .build(), HttpRequest.newBuilder(URI.create("https://example.com")) + .header("abc", "1") + .header("abc", "2") + .header("abcd", "11") + .header("abcd", "22") + .version(HTTP_2) + .timeout(Duration.ofSeconds(30)) + .build(), Clock.systemUTC() + ) + + val metadata2 = CacheEntryMetadata( + 10, 15, ImmutableResponseInfo.builder() + .headers(HttpHeadersBuilder().build()) + .statusCode(200) + .version(HTTP_2) + .build(), HttpRequest.newBuilder(URI.create("https://example.com")) + .header("abc", "1") + .header("abcd", "22") + .build(), Clock.systemUTC() + ) + + return listOf( + metadata1, metadata2 + ) + } + } } \ No newline at end of file