package com.elepy.tests.http; import com.elepy.exceptions.ElepyErrorMessage; import com.elepy.exceptions.ElepyException; import com.elepy.http.HttpService; import com.elepy.uploads.FileUpload; import com.google.common.net.HttpHeaders; import com.mashape.unirest.http.Unirest; import com.mashape.unirest.http.exceptions.UnirestException; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; public abstract class HttpServiceTest { private HttpService service; private HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build(); @BeforeEach void setUp() { service = httpService(); service.port(3030); } public abstract HttpService httpService(); @AfterEach void tearDown() { service.stop(); } @Test void can_StartService_without_Exception() { service.get("/", context -> context.result("Hello World")); assertDoesNotThrow(service::ignite); } @Test void can_handleGET() { service.get("/test", (request, response) -> response.result("hi")); service.ignite(); assertResponseReturns("get", "/test", "hi"); } @Test void can_serveRawBytes() throws IOException { service.get("/cv", ctx -> { ctx.response().result(IOUtils.toByteArray(inputStream("cv.pdf"))); }); service.ignite(); assertResponseReturns("get", "/cv", IOUtils.toByteArray(inputStream("cv.pdf")), HttpResponse.BodyHandlers.ofByteArray()); } @Test void can_handleStaticFiles() throws IOException, InterruptedException { service.staticFiles("static"); service.get("/test", ctx -> ctx.result("hi")); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/doggo.jpg")) .build(); final HttpResponse<InputStream> send = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); //assert it doesn't interfere with existing request. assertResponseReturns("get", "/test", "hi"); assertThat(IOUtils.contentEquals(send.body(), inputStream("static/doggo.jpg"))).as("Static file returning a wrong input stream").isTrue(); } @Test void can_handlePOST() { service.post("/test", (request, response) -> response.result("hi")); service.ignite(); assertResponseReturns("post", "/test", "hi"); } @Test void can_handlePUT() { service.put("/test", ctx -> ctx.result("hi")); service.ignite(); assertResponseReturns("put", "/test", "hi"); } @Test void can_handlePATCH() { service.patch("/test", ctx -> ctx.result("hi")); service.ignite(); assertResponseReturns("patch", "/test", "hi"); } @Test void can_handleDELETE() { service.delete("/test", ctx -> ctx.result("hi")); service.ignite(); assertResponseReturns("delete", "/test", "hi"); } @Test void can_handleMultipleRoutes() { service.get("/testGET", (request, response) -> response.result("hiGET")); service.post("/testPOST", (request, response) -> response.result("hiPOST")); service.put("/testPUT", ctx -> ctx.result("hiPUT")); service.patch("/testPATCH", ctx -> ctx.result("hiPATCH")); service.delete("/testDELETE", ctx -> ctx.result("hiDELETE")); service.ignite(); assertResponseReturns("delete", "/testDELETE", "hiDELETE"); assertResponseReturns("patch", "/testPATCH", "hiPATCH"); assertResponseReturns("put", "/testPUT", "hiPUT"); assertResponseReturns("post", "/testPOST", "hiPOST"); assertResponseReturns("get", "/testGET", "hiGET"); } @Test void can_handleException_inRoute() throws IOException, InterruptedException { service.get("/testException", (request, response) -> { response.result("Exception not handled"); throw new ElepyException("Exception handled", 400); }); service.exception(ElepyException.class, (e, context) -> { context.result(e.getMessage()); context.status(e.getStatus()); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/testException")).GET().build(); final HttpResponse<String> send = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); assertThat(send.body()).isEqualTo("Exception handled"); assertThat(send.statusCode()).isEqualTo(400); } @Test void can_handleException_inBefore() throws IOException, InterruptedException { service.before(context -> { throw new ElepyException("Exception handled", 400); }); service.get("/testException2", (request, response) -> { response.result("Exception not handled"); }); service.exception(ElepyErrorMessage.class, (e, context) -> { context.result(e.getMessage()); context.status(e.getStatus()); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/testException2")).GET().build(); final HttpResponse<String> send = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); assertThat(send.body()).isEqualTo("Exception handled"); assertThat(send.statusCode()).isEqualTo(400); } @Test void can_handleException_inAfter() throws IOException, InterruptedException { service.after(context -> { throw new ElepyException("Exception handled", 400); }); service.get("/testException2", (request, response) -> { response.result("Exception not handled"); }); service.exception(ElepyErrorMessage.class, (e, context) -> { context.result(e.getMessage()); context.status(e.getStatus()); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/testException2")).GET().build(); final HttpResponse<String> send = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); assertThat(send.body()).isEqualTo("Exception handled"); assertThat(send.statusCode()).isEqualTo(400); } @Test void requests_haveProper_QueryString() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); service.get("/queryString", (request, response) -> queryString.set(request.queryString())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString?q=a&a=b")).GET().build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("q=a&a=b")); } @Test void requests_haveProper_Method() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); service.get("/queryString", (request, response) -> queryString.set(request.method())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString?q=a&a=b")).GET().build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("GET")); } @Test void requests_haveProper_Scheme() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); service.get("/queryString", (request, response) -> queryString.set(request.scheme())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString?q=a&a=b")).GET().build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("http")); } @Test void requests_haveProper_Host() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); service.get("/queryString", (request, response) -> queryString.set(request.host())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString?q=a&a=b")).GET().build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("localhost:3030")); } @Test void requests_haveProper_Body() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); service.post("/queryString", ctx -> queryString.set(ctx.body())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString")) .POST(HttpRequest.BodyPublishers.ofString("theBody")) .build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("theBody")); } @Test void requests_haveProper_BodyAsBytes() throws IOException, InterruptedException { final byte[] theBytes = new byte[]{ 0B1010, 0B1110, 0B1011, 0B1110, 0B1111, }; AtomicReference<byte[]> queryString = new AtomicReference<>(); service.post("/queryString", ctx -> queryString.set(ctx.bodyAsBytes())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString")) .POST(HttpRequest.BodyPublishers.ofByteArray(theBytes)) .build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo(theBytes)); } @Test void requests_haveProper_IP() throws IOException, InterruptedException { final String hostAddress = InetAddress.getByName("localhost").getHostAddress(); AtomicReference<String> queryString = new AtomicReference<>(); service.post("/queryString", ctx -> queryString.set(ctx.ip())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString")) .POST(HttpRequest.BodyPublishers.ofString("theBody")) .build(); final HttpResponse<String> send = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo(hostAddress)); } @Test void requests_haveProper_URL() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); service.post("/queryString", ctx -> queryString.set(ctx.url())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString")) .POST(HttpRequest.BodyPublishers.ofString("theBody")) .build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("http://localhost:3030/queryString")); } @Test void requests_haveProper_URI() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); service.post("/queryString", ctx -> queryString.set(ctx.uri())); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString")) .POST(HttpRequest.BodyPublishers.ofString("theBody")) .build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("/queryString")); } @Test void requests_haveProper_PathParams() throws IOException, InterruptedException { AtomicReference<String> queryString = new AtomicReference<>(); AtomicReference<Map<String, String>> pathParams = new AtomicReference<>(); service.post("/:pathparam1/x/:pathparam2", ctx -> { pathParams.set(ctx.params()); queryString.set(ctx.params("pathparam2")); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString/x/thePathParam")) .POST(HttpRequest.BodyPublishers.ofString("theBody")) .build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryString, equalTo("thePathParam")); await().atMost(1, TimeUnit.SECONDS).untilAtomic(pathParams, allOf(hasEntry("pathparam1", "queryString"), hasEntry("pathparam2", "thePathParam"))); } @Test void requests_haveProper_QueryParams() throws IOException, InterruptedException { AtomicReference<String> queryParamValueQ = new AtomicReference<>(); AtomicReference<Set<String>> queryParams = new AtomicReference<>(); AtomicReference<String[]> queryParamValuesX = new AtomicReference<>(); service.get("/queryString", (request, response) -> { queryParams.set(request.queryParams()); queryParamValueQ.set(request.queryParamOrDefault("q", "NOT_FOUND")); queryParamValuesX.set(request.queryParamValues("x")); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString?q=a&x=y&x=z")).GET().build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryParamValueQ, equalTo("a")); await().atMost(1, TimeUnit.SECONDS).untilAtomic(queryParams, allOf(hasItems("q", "x"), not(hasItem("NONO")))); await().atMost(1, TimeUnit.SECONDS) .untilAtomic(queryParamValuesX, allOf( hasItemInArray("y"), hasItemInArray("z"), not(hasItemInArray("a")) )); } @Test void requests_haveProper_Headers() throws IOException, InterruptedException { AtomicReference<String> headerQ = new AtomicReference<>(); AtomicReference<Set<String>> headers = new AtomicReference<>(); service.get("/queryString", (request, response) -> { headers.set(request.headers()); headerQ.set(request.headers("q")); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString?q=a&x=y&x=z")).GET() .headers("x", "y", "q", "a").build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(headerQ, equalTo("a")); await().atMost(1, TimeUnit.SECONDS).untilAtomic(headers, allOf(hasItems("q", "x"), not(hasItem("NONO")))); } @Test void requests_haveProper_Attributes() throws IOException, InterruptedException { AtomicReference<String> attribute = new AtomicReference<>(); service.before("/queryString", (request, response) -> { request.attribute("attribute", "theAttribute"); }); service.get("/queryString", (request, response) -> attribute.set(request.attribute("attribute"))); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString?q=a&x=y&x=z")).GET() .headers("x", "y", "q", "a").build(); httpClient.send(request, HttpResponse.BodyHandlers.ofString()); await().atMost(1, TimeUnit.SECONDS).untilAtomic(attribute, equalTo("theAttribute")); } @Test void requests_haveProper_FileUploads() throws IOException, UnirestException { AtomicReference<List<FileUpload>> atomicReference = new AtomicReference<>(); service.post("/elepy/uploads", (request, response) -> atomicReference.set(request.uploadedFiles("files"))); service.ignite(); final InputStream file1 = inputStream("cv.pdf"); Unirest.post("http://localhost:3030/elepy/uploads") .field("files", file1, "cv.pdf") .asString(); file1.close(); await().atMost(5, TimeUnit.SECONDS).untilAtomic(atomicReference, hasSize(1)); final List<FileUpload> fileUploads = atomicReference.get(); final FileUpload fileUpload1 = fileUploads.get(0); assertThat(IOUtils.contentEquals(fileUpload1.getContent(), inputStream("cv.pdf"))).isTrue(); } @Test void responses_haveProper_Type() throws IOException, InterruptedException { service.post("/queryString", ctx -> { ctx.response().type("application/xml"); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/queryString")) .POST(HttpRequest.BodyPublishers.ofString("theBody")) .build(); final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); assertThat(response.headers().firstValue("Content-Type").orElseThrow() .equalsIgnoreCase("application/xml")).isTrue(); } @Test void responses_RedirectProperly() throws IOException, InterruptedException { service.get("/redir", context -> { context.redirect("http://google.com"); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/redir")) .GET() .build(); final var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); assertThat(response.uri()).isEqualTo(URI.create("http://www.google.com/")); } @Test void responses_HaveProperInputStream() throws IOException, InterruptedException { service.get("/input-stream", context -> { context.response().result(inputStream("cv.pdf")); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/input-stream")) .GET() .build(); final var send = httpClient.send(request, HttpResponse .BodyHandlers.ofInputStream()); assertThat(IOUtils.contentEquals(inputStream("cv.pdf"), send.body())) .isTrue(); } @Test void responses_HaveProperInputStream_GZIP() throws IOException, InterruptedException { service.get("/input-stream", context -> { context.response().header("Content-Encoding", "gzip"); context.response().result(inputStream("logo-dark.svg")); }); service.ignite(); var request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:3030/input-stream")) .GET() .setHeader(HttpHeaders.ACCEPT_ENCODING, "gzip") .build(); final var send = httpClient.send(request, HttpResponse .BodyHandlers.ofInputStream()); assertThat(IOUtils.contentEquals(inputStream("logo-dark.svg"), new GZIPInputStream(send.body()))) .isTrue(); } private void assertResponseReturns(String method, String path, String expected) { assertResponseReturns(method, path, expected, HttpResponse.BodyHandlers.ofString()); } private <T> void assertResponseReturns(String method, String path, T expectedResult, HttpResponse.BodyHandler<T> bodyHandler) { var request = HttpRequest.newBuilder() .uri(URI.create(String.format("http://localhost:3030%s", path))) .method(method.toUpperCase(), HttpRequest.BodyPublishers.noBody()) .build(); try { final HttpResponse<T> send = httpClient.send(request, bodyHandler); assertThat(send.body()).isEqualTo(expectedResult); assertThat(send.statusCode()).isEqualTo(200); } catch (IOException e) { throw new AssertionError("network error", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new AssertionError("thread error", e); } } private InputStream inputStream(String name) { return Optional.ofNullable(this.getClass().getResourceAsStream("/" + name)) .orElseThrow(() -> new AssertionFailedError(String.format("The file '%s' can't be found in resources", name))); } }