├── .travis.yml ├── .gitignore ├── LICENSE ├── README.md ├── src ├── main │ └── java │ │ └── net │ │ └── codestory │ │ └── rest │ │ ├── ShouldChain.java │ │ ├── Should.java │ │ ├── Response.java │ │ ├── misc │ │ └── PostBody.java │ │ ├── RestResponse.java │ │ ├── FluentRestTest.java │ │ ├── RestResponseShould.java │ │ └── RestAssert.java └── test │ └── java │ └── net │ └── codestory │ └── rest │ ├── OptionsTest.java │ ├── DeleteTest.java │ ├── HeadTest.java │ ├── AbstractTest.java │ ├── PutTest.java │ ├── PatchTest.java │ ├── PostTest.java │ └── GetTest.java ├── .github └── workflows │ └── maven.yml └── pom.xml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | install: true 4 | 5 | cache: 6 | directories: 7 | - '$HOME/.m2/repository' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | *.iws 4 | target/ 5 | pom.xml.releaseBackup 6 | release.properties 7 | snapshots 8 | coverage-error.log 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-2015 all@code-story.net 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluent 2 | 3 | Unit-test Rest applications. 4 | 5 | # Build status 6 | 7 | [![Java CI with Maven](https://github.com/CodeStory/fluent-rest-test/actions/workflows/maven.yml/badge.svg)](https://github.com/CodeStory/fluent-rest-test/actions/workflows/maven.yml) 8 | 9 | # Environment 10 | 11 | - `java-1.8` 12 | 13 | ## Maven 14 | 15 | Release versions are deployed on Maven Central: 16 | 17 | ```xml 18 | 19 | net.code-story 20 | fluent-rest-test 21 | 1.7 22 | 23 | ``` 24 | 25 | # Build 26 | 27 | ```bash 28 | mvn clean verify 29 | ``` 30 | 31 | # Deploy on Maven Central 32 | 33 | Build the release: 34 | 35 | ```bash 36 | mvn release:clean release:prepare release:perform 37 | ``` 38 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/ShouldChain.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | public interface ShouldChain extends Should { 19 | ShouldChain should(); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "master" ] 14 | pull_request: 15 | branches: [ "master" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: '17' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml 32 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/OptionsTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import org.junit.Test; 19 | 20 | public class OptionsTest extends AbstractTest { 21 | @Test 22 | public void options() { 23 | configure(routes -> routes 24 | .options("/", "Hello") 25 | ); 26 | 27 | options("/").should().respond(200).contain("Hello"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/DeleteTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.http.payload.Payload; 19 | import org.junit.Test; 20 | 21 | public class DeleteTest extends AbstractTest { 22 | @Test 23 | public void delete() { 24 | configure(routes -> routes 25 | .delete("/", () -> Payload.ok()) 26 | ); 27 | 28 | delete("/").should().respond(200); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/HeadTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.http.payload.Payload; 19 | import org.junit.Test; 20 | 21 | public class HeadTest extends AbstractTest { 22 | @Test 23 | public void head() { 24 | configure(routes -> routes 25 | .get("/get", "Hello") 26 | .head("/head", Payload.ok()) 27 | ); 28 | 29 | head("/get").should().respond(200).contain(""); 30 | head("/head").should().respond(200).contain(""); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/Should.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | public interface Should { 19 | Should not(); 20 | 21 | ShouldChain respond(int statusCode); 22 | 23 | ShouldChain succeed(); 24 | 25 | ShouldChain fail(); 26 | 27 | ShouldChain contain(String content); 28 | 29 | ShouldChain beEmpty(); 30 | 31 | ShouldChain haveType(String contentType); 32 | 33 | ShouldChain haveCookie(String name, String value); 34 | 35 | ShouldChain haveHeader(String name, String value); 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/AbstractTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.http.Configuration; 19 | import net.codestory.http.WebServer; 20 | import net.codestory.http.misc.*; 21 | 22 | import org.junit.Rule; 23 | import org.junit.rules.ExpectedException; 24 | 25 | public abstract class AbstractTest implements FluentRestTest { 26 | private static WebServer server = new WebServer() { 27 | @Override 28 | protected Env createEnv() { 29 | return Env.prod(); 30 | } 31 | }.startOnRandomPort(); 32 | 33 | @Rule 34 | public ExpectedException thrown = ExpectedException.none(); 35 | 36 | @Override 37 | public int port() { 38 | return server.port(); 39 | } 40 | 41 | protected void configure(Configuration configuration) { 42 | server.configure(configuration); 43 | } 44 | 45 | protected void shouldFail(String message) { 46 | thrown.expect(AssertionError.class); 47 | thrown.expectMessage(message); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/PutTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.http.payload.Payload; 19 | import org.junit.Test; 20 | 21 | public class PutTest extends AbstractTest { 22 | @Test 23 | public void put_empty() { 24 | configure(routes -> routes 25 | .put("/", () -> Payload.created()) 26 | ); 27 | 28 | put("/").should() 29 | .respond(201) 30 | .contain(""); 31 | } 32 | 33 | @Test 34 | public void put_body() { 35 | configure(routes -> routes 36 | .put("/", context -> new Payload("text/plain", context.request().content(), 201)) 37 | ); 38 | 39 | put("/", "").should().respond(201).contain(""); 40 | } 41 | 42 | @Test 43 | public void put_form() { 44 | configure(routes -> routes 45 | .put("/", context -> new Payload("text/plain", context.get("key1") + "&" + context.get("key2"), 201)) 46 | ); 47 | 48 | put("/", "key1", "1st", "key2", "2nd").should().respond(201).contain("1st&2nd"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/Response.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | public class Response { 23 | private final String contentType; 24 | private final String content; 25 | private final int code; 26 | private final Map> headers; 27 | 28 | public Response(String contentType, String content, int code) { 29 | this(contentType, content, code, new HashMap<>()); 30 | } 31 | 32 | public Response(String contentType, String content, int code, Map> headers) { 33 | this.contentType = contentType; 34 | this.content = content; 35 | this.code = code; 36 | this.headers = headers; 37 | } 38 | 39 | public String contentType() { 40 | return contentType; 41 | } 42 | 43 | public String content() { 44 | return content; 45 | } 46 | 47 | public int code() { 48 | return code; 49 | } 50 | 51 | public List header(String headerName) { 52 | return headers.get(headerName); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/misc/PostBody.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest.misc; 17 | 18 | import okhttp3.FormBody; 19 | import okhttp3.MediaType; 20 | import okhttp3.RequestBody; 21 | 22 | public class PostBody { 23 | private PostBody() { 24 | // Static class 25 | } 26 | 27 | public static RequestBody empty() { 28 | return RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), ""); 29 | } 30 | 31 | public static RequestBody json(String body) { 32 | return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), body); 33 | } 34 | 35 | public static RequestBody form(String firstParameterName, Object firstParameterValue, Object... parameterNameValuePairs) { 36 | FormBody.Builder form = new FormBody.Builder(); 37 | 38 | form.add(firstParameterName, firstParameterValue.toString()); 39 | for (int i = 0; i < parameterNameValuePairs.length; i += 2) { 40 | form.add(parameterNameValuePairs[i].toString(), parameterNameValuePairs[i + 1].toString()); 41 | } 42 | 43 | return form.build(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/PatchTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.http.payload.Payload; 19 | import net.codestory.http.routes.NoParamRoute; 20 | import org.junit.Test; 21 | 22 | public class PatchTest extends AbstractTest { 23 | @Test 24 | public void patch_empty() { 25 | configure(routes -> routes 26 | .patch("/", (NoParamRoute) Payload::created) 27 | ); 28 | 29 | patch("/").should() 30 | .respond(201) 31 | .contain(""); 32 | } 33 | 34 | @Test 35 | public void patch_body() { 36 | configure(routes -> routes 37 | .patch("/", context -> new Payload("text/plain", context.request().content(), 201)) 38 | ); 39 | 40 | patch("/", "").should().respond(201).contain(""); 41 | } 42 | 43 | @Test 44 | public void patch_form() { 45 | configure(routes -> routes 46 | .patch("/", context -> new Payload("text/plain", context.get("key1") + "&" + context.get("key2"), 201)) 47 | ); 48 | 49 | patch("/", "key1", "1st", "key2", "2nd").should().respond(201).contain("1st&2nd"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/PostTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.http.payload.Payload; 19 | import org.junit.Test; 20 | 21 | public class PostTest extends AbstractTest { 22 | @Test 23 | public void post_empty() { 24 | configure(routes -> routes 25 | .post("/", () -> Payload.created()) 26 | ); 27 | 28 | post("/").should() 29 | .respond(201) 30 | .contain(""); 31 | } 32 | 33 | @Test 34 | public void post_body() { 35 | configure(routes -> routes 36 | .post("/", context -> new Payload("text/plain", context.request().content(), 201)) 37 | ); 38 | 39 | post("/", "").should().respond(201).contain(""); 40 | } 41 | 42 | @Test 43 | public void post_form() { 44 | configure(routes -> routes 45 | .post("/", context -> new Payload("text/plain", context.get("key1") + "&" + context.get("key2"), 201)) 46 | ); 47 | 48 | post("/", "key1", "1st", "key2", "2nd").should().respond(201).contain("1st&2nd"); 49 | } 50 | 51 | @Test 52 | public void post_multipart_raw_one_part() { 53 | configure(routes -> routes 54 | .post("/", context -> new Payload("text/plain", "parts=" + context.parts().size(), 200)) 55 | ); 56 | postRaw("/", "multipart/form-data; boundary=boundary", "--boundary\r\n" + 57 | "Content-Disposition: form-data; name=\"field\"\r\n" + 58 | "\r\n" + 59 | "value\r\n" + 60 | "--boundary--").should().respond(200).contain("parts=1"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/RestResponse.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | 19 | import okhttp3.*; 20 | 21 | import java.io.IOException; 22 | import java.util.*; 23 | import java.util.function.Function; 24 | import java.util.stream.Collectors; 25 | 26 | class RestResponse { 27 | private final MemoryCookieJar cookieJar; 28 | private final okhttp3.Response response; 29 | 30 | private String bodyAsString; 31 | 32 | private RestResponse(MemoryCookieJar cookieJar, okhttp3.Response response) { 33 | this.cookieJar = cookieJar; 34 | this.response = response; 35 | } 36 | 37 | static RestResponse call(String url, Function configureClient, Function configureRequest) throws IOException { 38 | MemoryCookieJar cookieJar = new MemoryCookieJar(); 39 | Request.Builder request = configureRequest.apply(new Request.Builder().url(url)); 40 | OkHttpClient.Builder client = configureClient.apply(new OkHttpClient.Builder().cookieJar(cookieJar)); 41 | 42 | OkHttpClient okHttpClient = client.build(); 43 | okhttp3.Response response = okHttpClient.newCall(request.build()).execute(); 44 | 45 | return new RestResponse(cookieJar, response); 46 | } 47 | 48 | public int code() { 49 | return response.code(); 50 | } 51 | 52 | public String bodyAsString() { 53 | if (bodyAsString == null) { 54 | try { 55 | bodyAsString = response.body().string(); 56 | } catch (IOException e) { 57 | throw new RuntimeException("Unable to read response as String", e); 58 | } 59 | } 60 | return bodyAsString; 61 | } 62 | 63 | public String contentType() { 64 | return header("Content-Type"); 65 | } 66 | 67 | public String header(String name) { 68 | return response.header(name); 69 | } 70 | public Map> headers() {return response.headers().toMultimap();} 71 | 72 | public String cookie(String name) { 73 | List cookies = cookieJar.map.values().stream().flatMap(List::stream).collect(Collectors.toList()); 74 | return cookies.stream().filter(cookie -> cookie.name().equals(name)).findFirst().map(Cookie::value).orElse(null); 75 | } 76 | 77 | static class MemoryCookieJar implements CookieJar { 78 | Map> map = Collections.synchronizedMap(new HashMap<>()); 79 | @Override 80 | public void saveFromResponse(HttpUrl httpUrl, List cookies) { 81 | map.put(httpUrl, cookies); 82 | } 83 | 84 | @Override 85 | public List loadForRequest(HttpUrl httpUrl) { 86 | return map.getOrDefault(httpUrl, new ArrayList<>()); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/FluentRestTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.rest.misc.PostBody; 19 | import okhttp3.MediaType; 20 | import okhttp3.RequestBody; 21 | 22 | public interface FluentRestTest { 23 | int port(); 24 | 25 | default String baseUrl() { 26 | return "http://localhost:" + port(); 27 | } 28 | 29 | // GET 30 | default RestAssert get(String path) { 31 | return new RestAssert(baseUrl() + path); 32 | } 33 | 34 | // DELETE 35 | default RestAssert delete(String path) { 36 | return get(path).withRequest(request -> request.delete()); 37 | } 38 | 39 | // HEAD 40 | default RestAssert head(String path) { 41 | return get(path).withRequest(request -> request.head()); 42 | } 43 | 44 | // POST 45 | default RestAssert post(String path) { 46 | return get(path).withRequest(request -> request.post(PostBody.empty())); 47 | } 48 | 49 | default RestAssert post(String path, String body) { 50 | return get(path).withRequest(request -> request.post(PostBody.json(body))); 51 | } 52 | 53 | default RestAssert post(String path, String firstParameterName, Object firstParameterValue, Object... parameterNameValuePairs) { 54 | return get(path).withRequest(request -> request.post(PostBody.form(firstParameterName, firstParameterValue, parameterNameValuePairs))); 55 | } 56 | 57 | default RestAssert postRaw(String path, String contentType, String content) { 58 | return get(path).withRequest(request -> request.post(RequestBody.create(MediaType.parse(contentType), content))); 59 | } 60 | 61 | // PUT 62 | default RestAssert put(String path) { 63 | return get(path).withRequest(request -> request.put(PostBody.empty())); 64 | } 65 | 66 | default RestAssert put(String path, String body) { 67 | return get(path).withRequest(request -> request.put(PostBody.json(body))); 68 | } 69 | 70 | default RestAssert put(String path, String firstParameterName, Object firstParameterValue, Object... parameterNameValuePairs) { 71 | return get(path).withRequest(request -> request.put(PostBody.form(firstParameterName, firstParameterValue, parameterNameValuePairs))); 72 | } 73 | 74 | // OPTIONS 75 | default RestAssert options(String path) { 76 | return get(path).withRequest(request -> request.method("OPTIONS", null)); 77 | } 78 | 79 | // PATCH 80 | default RestAssert patch(String path) { 81 | return get(path).withRequest(request -> request.patch(PostBody.empty())); 82 | } 83 | 84 | default RestAssert patch(String path, String body) { 85 | return get(path).withRequest(request -> request.patch(PostBody.json(body))); 86 | } 87 | 88 | default RestAssert patch(String path, String firstParameterName, Object firstParameterValue, Object... parameterNameValuePairs) { 89 | return get(path).withRequest(request -> request.patch(PostBody.form(firstParameterName, firstParameterValue, parameterNameValuePairs))); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/RestResponseShould.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import java.util.Objects; 19 | 20 | import static java.lang.String.format; 21 | 22 | class RestResponseShould implements ShouldChain { 23 | private final RestResponse response; 24 | private final boolean negate; 25 | 26 | RestResponseShould(RestResponse response, boolean negate) { 27 | this.response = response; 28 | this.negate = negate; 29 | } 30 | 31 | // Modifiers 32 | 33 | @Override 34 | public RestResponseShould not() { 35 | return new RestResponseShould(response, !negate); 36 | } 37 | 38 | // Verifications 39 | 40 | @Override 41 | public RestResponseShould should() { 42 | return new RestResponseShould(response, false); 43 | } 44 | 45 | @Override 46 | public RestResponseShould respond(int statusCode) { 47 | return assertEquals("status code", response.code(), statusCode); 48 | } 49 | 50 | @Override 51 | public RestResponseShould succeed() { 52 | return assertBetween("status code", response.code(), 200, 299); 53 | } 54 | 55 | @Override 56 | public RestResponseShould fail() { 57 | return assertBetween("status code", response.code(), 400, 599); 58 | } 59 | 60 | @Override 61 | public RestResponseShould contain(String content) { 62 | return assertContains(response.bodyAsString(), content); 63 | } 64 | 65 | @Override 66 | public RestResponseShould beEmpty() { 67 | return assertEmpty(response.bodyAsString()); 68 | } 69 | 70 | @Override 71 | public RestResponseShould haveType(String contentType) { 72 | return assertContains(response.contentType(), contentType); 73 | } 74 | 75 | @Override 76 | public RestResponseShould haveCookie(String name, String value) { 77 | return assertEquals("cookie " + name, response.cookie(name), value); 78 | } 79 | 80 | @Override 81 | public RestResponseShould haveHeader(String name, String value) { 82 | return assertEquals("header " + name, response.header(name), value); 83 | } 84 | 85 | // Verifications 86 | private RestResponseShould assertEquals(String what, Object actual, Object expected) { 87 | if (negate == Objects.equals(expected, actual)) { 88 | throw new AssertionError(format("Expecting [%s] to be [%s]. It was [%s]", what, expected, actual)); 89 | } 90 | return this; 91 | } 92 | 93 | private RestResponseShould assertContains(String actual, String expected) { 94 | if (negate == actual.contains(expected)) { 95 | throw new AssertionError(format("Expecting [%s] to contain [%s]", actual, expected)); 96 | } 97 | return this; 98 | } 99 | 100 | private RestResponseShould assertEmpty(String actual) { 101 | if (negate == actual.isEmpty()) { 102 | throw new AssertionError(format("Expecting [%s] to be empty", actual)); 103 | } 104 | return this; 105 | } 106 | 107 | private RestResponseShould assertBetween(String what, int actual, int lowerBound, int higherBound) { 108 | if (negate == ((actual >= lowerBound) && (actual <= higherBound))) { 109 | throw new AssertionError(format("Expecting [%s] to be between [%d] and [%d]. It was [%s]", what, lowerBound, higherBound, actual)); 110 | } 111 | return this; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/net/codestory/rest/RestAssert.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | 19 | import okhttp3.*; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.function.UnaryOperator; 24 | 25 | import static java.util.function.UnaryOperator.identity; 26 | 27 | public class RestAssert { 28 | private final String url; 29 | private final UnaryOperator configureClient; 30 | private final UnaryOperator configureRequest; 31 | 32 | RestAssert(String url) { 33 | this(url, identity(), identity()); 34 | } 35 | 36 | private RestAssert(String url, UnaryOperator configureClient, UnaryOperator configureRequest) { 37 | this.url = url; 38 | this.configureRequest = configureRequest; 39 | this.configureClient = configureClient; 40 | } 41 | 42 | // Creation 43 | public RestAssert withRequest(UnaryOperator configure) { 44 | return new RestAssert(url, configureClient, request -> configure.apply(configureRequest.apply(request))); 45 | } 46 | 47 | public RestAssert withClient(UnaryOperator configure) { 48 | return new RestAssert(url, client -> configure.apply(configureClient.apply(client)), configureRequest); 49 | } 50 | 51 | // Extraction 52 | 53 | public Response response() { 54 | try { 55 | RestResponse call = call(); 56 | return new Response(call.contentType(), call.bodyAsString(), call.code(), call.headers()); 57 | } catch (IOException e) { 58 | throw new RuntimeException("Unable to query: " + url, e); 59 | } 60 | } 61 | 62 | // Configuration 63 | public RestAssert withHeader(String name, String value) { 64 | return withRequest(addHeader(name, value)); 65 | } 66 | 67 | public RestAssert withPreemptiveAuthentication(String login, String password) { 68 | return withRequest(addBasicAuthHeader(login, password)); 69 | } 70 | 71 | public RestAssert withAuthentication(String login, String password) { 72 | return withClient(setAuthenticator(new Authenticator() { 73 | AtomicInteger tries = new AtomicInteger(0); 74 | 75 | @Override 76 | public Request authenticate(Route route, okhttp3.Response response) throws IOException { 77 | if (tries.getAndIncrement() > 0) { 78 | return null; 79 | } 80 | return addBasicAuthHeader(login, password).apply(response.request().newBuilder()).build(); 81 | } 82 | })); 83 | } 84 | 85 | // Assertions 86 | public Should should() { 87 | try { 88 | return new RestResponseShould(call(), false); 89 | } catch (IOException e) { 90 | throw new RuntimeException("Unable to query: " + url, e); 91 | } 92 | } 93 | 94 | // Client modification 95 | private static UnaryOperator setAuthenticator(Authenticator authenticator) { 96 | return client -> client.authenticator(authenticator); 97 | } 98 | 99 | // Call 100 | private RestResponse call() throws IOException { 101 | return RestResponse.call(url, configureClient, configureRequest); 102 | } 103 | 104 | // Request configuration 105 | private static UnaryOperator addBasicAuthHeader(String login, String password) { 106 | return addHeader("Authorization", Credentials.basic(login, password)); 107 | } 108 | 109 | private static UnaryOperator addHeader(String name, String value) { 110 | return request -> request.addHeader(name, value); 111 | } 112 | 113 | public RestAssert withFollowRedirect(boolean b) { 114 | return withClient(setFollowRedirect(b)); 115 | } 116 | 117 | private static UnaryOperator setFollowRedirect(boolean followRedirect) { 118 | return client -> client.followRedirects(followRedirect); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/net/codestory/rest/GetTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2015 all@code-story.net 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package net.codestory.rest; 17 | 18 | import net.codestory.http.filters.basic.BasicAuthFilter; 19 | import net.codestory.http.payload.Payload; 20 | import org.junit.Test; 21 | 22 | import java.util.Collections; 23 | 24 | import static net.codestory.http.security.Users.singleUser; 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | public class GetTest extends AbstractTest { 28 | @Test 29 | public void get() { 30 | configure(routes -> routes 31 | .get("/", "hello World") 32 | ); 33 | 34 | get("/").should().respond(200).succeed().haveType("text/html").contain("hello"); 35 | } 36 | 37 | @Test 38 | public void fail_to_get() { 39 | configure(routes -> routes 40 | .get("/", "hello") 41 | ); 42 | 43 | shouldFail("Expecting [hello] to contain [good bye]"); 44 | 45 | get("/").should().contain("good bye"); 46 | } 47 | 48 | @Test 49 | public void get_with_header() { 50 | configure(routes -> routes 51 | .get("/", context -> context.header("name")) 52 | ); 53 | 54 | get("/").withHeader("name", "value").should().contain("value"); 55 | } 56 | 57 | @Test 58 | public void get_with_headers() { 59 | configure(routes -> routes 60 | .get("/", context -> context.header("first") + context.header("second")) 61 | ); 62 | 63 | get("/").withHeader("first", "1").withHeader("second", "2").should().contain("12"); 64 | } 65 | 66 | @Test 67 | public void get_with_preemptive_authentication() { 68 | configure(routes -> routes 69 | .filter(new BasicAuthFilter("/", "realm", singleUser("login", "pwd"))) 70 | .get("/", context -> "Secret") 71 | ); 72 | 73 | get("/").withPreemptiveAuthentication("login", "pwd").should().contain("Secret"); 74 | get("/").withPreemptiveAuthentication("", "").should().respond(401); 75 | get("/").should().respond(401); 76 | } 77 | 78 | @Test 79 | public void get_with_basic_authentication() { 80 | configure(routes -> routes 81 | .filter(new BasicAuthFilter("/", "realm", singleUser("login", "pwd"))) 82 | .get("/", context -> "Secret") 83 | ); 84 | 85 | get("/").withAuthentication("login", "pwd").should().contain("Secret"); 86 | get("/").withAuthentication("", "").should().respond(401); 87 | get("/").should().respond(401).fail(); 88 | } 89 | 90 | @Test 91 | public void get_cookie() { 92 | configure(routes -> routes 93 | .get("/", context -> new Payload("Hello").withCookie("name", "value")) 94 | ); 95 | 96 | get("/").should().haveCookie("name", "value"); 97 | } 98 | 99 | @Test 100 | public void get_cookies() { 101 | configure(routes -> routes 102 | .get("/", context -> new Payload("").withCookie("first", "1st").withCookie("second", "2nd")) 103 | ); 104 | 105 | get("/").should().haveCookie("first", "1st").haveCookie("second", "2nd"); 106 | } 107 | 108 | @Test 109 | public void fail_without_cookie() { 110 | configure(routes -> routes 111 | .get("/", context -> "") 112 | ); 113 | 114 | shouldFail("Expecting [cookie ??] to be [??]. It was [null]"); 115 | 116 | get("/").should().haveCookie("??", "??"); 117 | } 118 | 119 | @Test 120 | public void fail_with_wrong_cookie() { 121 | configure(routes -> routes 122 | .get("/", context -> new Payload("").withCookie("name", "value")) 123 | ); 124 | 125 | shouldFail("Expecting [cookie name] to be [??]. It was [value]"); 126 | 127 | get("/").should().haveCookie("name", "??"); 128 | } 129 | 130 | @Test 131 | public void get_header() { 132 | configure(routes -> routes 133 | .get("/", context -> new Payload("").withHeader("name", "value")) 134 | ); 135 | 136 | get("/").should().haveHeader("name", "value"); 137 | } 138 | 139 | @Test 140 | public void fail_without_header() { 141 | configure(routes -> routes 142 | .get("/", context -> new Payload("")) 143 | ); 144 | 145 | shouldFail("Expecting [header name] to be [??]. It was [null]"); 146 | 147 | get("/").should().haveHeader("name", "??"); 148 | } 149 | 150 | @Test 151 | public void fail_with_wrong_header() { 152 | configure(routes -> routes 153 | .get("/", context -> new Payload("").withHeader("name", "value")) 154 | ); 155 | 156 | shouldFail("Expecting [header name] to be [??]. It was [value]"); 157 | 158 | get("/").should().haveHeader("name", "??"); 159 | } 160 | 161 | @Test 162 | public void empty() { 163 | configure(routes -> routes 164 | .get("/", "") 165 | ); 166 | 167 | get("/").should().beEmpty(); 168 | } 169 | 170 | @Test 171 | public void not() { 172 | configure(routes -> routes 173 | .get("/", "Hello") 174 | ); 175 | 176 | get("/").should().not().beEmpty(); 177 | get("/").should().not().contain("Bye"); 178 | } 179 | 180 | @Test 181 | public void chain() { 182 | configure(routes -> routes 183 | .get("/", "Hello") 184 | ); 185 | 186 | get("/") 187 | .should().contain("Hello") 188 | .should().not().beEmpty() 189 | .should().not().contain("Bye"); 190 | } 191 | 192 | @Test 193 | public void get_response() { 194 | configure(routes -> routes 195 | .get("/", "Hello") 196 | ); 197 | 198 | Response response = get("/").response(); 199 | 200 | assertThat(response.contentType()).isEqualTo("text/html;charset=UTF-8"); 201 | assertThat(response.content()).isEqualTo("Hello"); 202 | assertThat(response.code()).isEqualTo(200); 203 | } 204 | 205 | @Test 206 | public void get_response_with_headers() { 207 | configure(routes -> routes 208 | .get("/", (context) -> { 209 | context.response().setHeader("foo", "bar"); 210 | context.response().setHeader("baz", "qux"); 211 | return "Hello foo"; 212 | }) 213 | ); 214 | Response response = get("/").response(); 215 | assertThat(response.content()).isEqualTo("Hello foo"); 216 | assertThat(response.header("foo")).isEqualTo(Collections.singletonList("bar")); 217 | assertThat(response.header("baz")).isEqualTo(Collections.singletonList("qux")); 218 | } 219 | 220 | @Test 221 | public void get_response_with_follow_redirect_false() { 222 | configure(routes -> routes 223 | .get("/", (context) -> Payload.temporaryRedirect("http://foo.bar")) 224 | ); 225 | Response response = get("/").withFollowRedirect(false).response(); 226 | assertThat(response.code()).isEqualTo(307); 227 | assertThat(response.header("Location")).isEqualTo(Collections.singletonList("http://foo.bar")); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | net.code-story 6 | fluent-rest-test 7 | 1.8-SNAPSHOT 8 | jar 9 | 10 | Fluent-Rest-Test 11 | Library to write REST tests 12 | https://github.com/CodeStory/fluent-rest-test 13 | 14 | 15 | org.sonatype.oss 16 | oss-parent 17 | 9 18 | 19 | 20 | 21 | 22 | scm:git:git@github.com:CodeStory/fluent-rest-test.git 23 | scm:git:git@github.com:CodeStory/fluent-rest-test.git 24 | scm:git:git@github.com:CodeStory/fluent-rest-test.git 25 | HEAD 26 | 27 | 28 | 29 | 30 | Apache 2 31 | http://www.apache.org/licenses/LICENSE-2.0.txt 32 | repo 33 | A business-friendly OSS license 34 | 35 | 36 | 37 | 38 | 3.0.4 39 | 40 | 41 | 42 | UTF-8 43 | 1.8 44 | 1.8 45 | true 46 | 47 | 48 | 49 | 50 | jitpack.io 51 | https://jitpack.io 52 | 53 | 54 | 55 | 56 | 57 | release 58 | 59 | 60 | 61 | maven-gpg-plugin 62 | 1.6 63 | 64 | 65 | sign-artifacts 66 | verify 67 | 68 | sign 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | readmeVersion 78 | 79 | 80 | 81 | com.google.code.maven-replacer-plugin 82 | replacer 83 | 1.5.3 84 | 85 | README.md 86 | ${project.groupId}\s*${project.artifactId}\s*)(\d|.)*()]]> 87 | $1${project.version}$3 88 | 89 | 90 | 91 | maven-scm-plugin 92 | 1.9.4 93 | 94 | README.md 95 | Update version in the README.md 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | maven-clean-plugin 108 | 2.6.1 109 | 110 | 111 | maven-compiler-plugin 112 | 3.3 113 | 114 | 115 | maven-deploy-plugin 116 | 2.8.2 117 | 118 | 119 | maven-install-plugin 120 | 2.5.2 121 | 122 | 123 | maven-jar-plugin 124 | 2.6 125 | 126 | 127 | maven-resources-plugin 128 | 2.7 129 | 130 | 131 | maven-site-plugin 132 | 3.4 133 | 134 | 135 | maven-source-plugin 136 | 2.4 137 | 138 | 139 | maven-release-plugin 140 | 2.5.2 141 | 142 | 143 | maven-surefire-plugin 144 | 2.18.1 145 | 146 | 147 | 148 | 149 | 150 | org.sonatype.plugins 151 | nexus-staging-maven-plugin 152 | 1.6.6 153 | true 154 | 155 | ossrh 156 | https://oss.sonatype.org/ 157 | true 158 | 159 | 160 | 161 | maven-release-plugin 162 | 163 | release 164 | -Psonatype-oss-release,readmeVersion 165 | replacer:replace scm:checkin 166 | 167 | 168 | 169 | maven-source-plugin 170 | 171 | 172 | attach-sources 173 | 174 | jar 175 | 176 | 177 | 178 | 179 | 180 | false 181 | com.mycila.maven-license-plugin 182 | maven-license-plugin 183 | 1.9.0 184 | 185 |
${project.basedir}/LICENSE
186 | true 187 | true 188 | true 189 | 190 | **/*.java 191 | 192 | 193 | JAVADOC_STYLE 194 | 195 |
196 | 197 | 198 | enforce-license-headers 199 | validate 200 | 201 | check 202 | 203 | 204 | 205 |
206 |
207 |
208 | 209 | 210 | 211 | com.squareup.okhttp3 212 | okhttp 213 | 3.14.2 214 | 215 | 216 | 217 | 218 | junit 219 | junit 220 | 4.12 221 | test 222 | 223 | 224 | com.github.CodeStory 225 | fluent-http 226 | 4cf7708 227 | jar 228 | 229 | 230 | org.assertj 231 | assertj-core 232 | 3.2.0 233 | test 234 | 235 | 236 |
237 | --------------------------------------------------------------------------------