From 7a336137c2544129ec31cb2df8bb2ab475c1874e Mon Sep 17 00:00:00 2001 From: Dirk Bolte Date: Wed, 16 Aug 2023 21:28:57 +0200 Subject: [PATCH] Support for iterating over list property (#34) - adding special property `list` - make special property handling more generic - added example test cases - extended README - added wiremock logo to the top ## References - TODO ## Submitter checklist - [ ] The PR request is well described and justified, including the body and the references - [ ] The PR title represents the desired changelog entry - [ ] The repository's code style is followed (see the contributing guide) - [ ] Test coverage that demonstrates that the change works as expected - [ ] For new features, there's necessary documentation in this pull request or in a subsequent PR to [wiremock.org](https://github.com/wiremock/wiremock.org) --- README.md | 41 ++- .../extensions/StateHandlerbarHelper.java | 86 +++-- .../StateExtensionListExampleTest.java | 57 ++-- .../StateExtensionQueueExampleTest.java | 194 ++++++++++++ ...teTemplateHelperProviderExtensionTest.java | 297 +++++++++++------- 5 files changed, 509 insertions(+), 166 deletions(-) create mode 100644 src/test/java/org/wiremock/extensions/state/examples/StateExtensionQueueExampleTest.java diff --git a/README.md b/README.md index f74740a..3601b7c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # WireMock State extension +

+ + WireMock Logo + +

+ Adds support to transport state across different stubs. ## Feature summary @@ -701,6 +707,8 @@ The handler has the following parameters: - `property='updateCount` retrieves the number of updates to a certain state. The number matches the one described in [Context update count match](#context-update-count-match) - `property='listSize` retrieves the number of entries of `list` + - `property='list` get the whole list as array, e.g. to use it with [handlebars #each](https://handlebarsjs.com/guide/builtin-helpers.html#each) + - this property always has a default value (empty list), which can be overwritten with a JSON list - `list`: Getting an entry of the context's `list`, identified via a JSON path. Examples: - getting the first state in the list: `list='[0].myProperty` - getting the last state in the list: `list='[-1].myProperty` @@ -709,11 +717,42 @@ The handler has the following parameters: You have to choose either `property` or `list` (otherwise, you will get a configuration error). -To retrieve a full body, use: `{{{state context=request.pathSegments.[1] property='fullBody'}}}` . +To retrieve a full body, use tripple braces: `{{{state context=request.pathSegments.[1] property='fullBody'}}}` . When registering this extension, this helper is available via WireMock's [response templating](https://wiremock.org/3.x/docs/response-templating/) as well as in all configuration options of this extension. +### List operations + +You can use [handlebars #each](https://handlebarsjs.com/guide/builtin-helpers.html#each) to build a full JSON response with the current list's content. + +Things to consider: + +- this syntax only works with `body`. It DOES NOT work with `jsonBody` + - as this might get ugly, consider using `bodyFileName` / `withBodyFile()` have proper indentation +- the default response for non-existant context as well as non-existant list in a context is an empty list. These states cannot be differentiated here + - if you still want a different response, consider using a [StateRequestMatcher](#negative-context-exists-match) +- the default value for this property has to be a valid JSON list - otherwise you will get an error log and the empty list response +- JSON does not allow trailing commas, so in order to create a valid JSON list, use `{{#unless @last}},{{/unless}` before `{{/each}}` + + +Example: +```json +{ + "request" : { + "urlPathPattern" : "/listing", + "method" : "GET" + }, + "response" : { + "status" : 200, + "body" : "[\n{{# each (state context=list property='list' default='[]') }} {\n \"id\": \"{{id}}\",\n \"firstName\": \"{{firstName}}\",\n \"firstName\": \"{{firstName}}\" }{{#unless @last}},{{/unless}}\n{{/each}}]", + "headers" : { + "content-type" : "application/json" + } + } +} +``` + ### Error handling Missing Helper properties as well as unknown context properties are reported as error. Wiremock renders them in the field, itself, so there won't be an diff --git a/src/main/java/org/wiremock/extensions/state/extensions/StateHandlerbarHelper.java b/src/main/java/org/wiremock/extensions/state/extensions/StateHandlerbarHelper.java index ef23716..31ed5c2 100644 --- a/src/main/java/org/wiremock/extensions/state/extensions/StateHandlerbarHelper.java +++ b/src/main/java/org/wiremock/extensions/state/extensions/StateHandlerbarHelper.java @@ -16,13 +16,19 @@ package org.wiremock.extensions.state.extensions; import com.github.jknack.handlebars.Options; +import com.github.tomakehurst.wiremock.common.Json; +import com.github.tomakehurst.wiremock.common.JsonException; import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.HandlebarsHelper; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; import org.apache.commons.lang3.StringUtils; +import org.wiremock.extensions.state.internal.Context; import org.wiremock.extensions.state.internal.ContextManager; +import java.util.ArrayList; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; @@ -57,35 +63,36 @@ public Object apply(Object o, Options options) { return handleError("Either 'property' or 'list' has to be set"); } if (StringUtils.isNotBlank(property)) { - return getProperty(contextName, property) - .orElseGet(() -> - Optional - .ofNullable(defaultValue) - .orElseGet(() -> handleError(String.format("No state for context %s, property %s found", contextName, property))) - ); + return getProperty(contextName, property, defaultValue) + .orElseGet(() -> handleError(String.format("No state for context %s, property %s found", contextName, property))); } else { return getList(contextName, list) .orElseGet(() -> - Optional - .ofNullable(defaultValue) - .orElseGet(() -> handleError(String.format("No state for context %s, list %s found", contextName, list))) - ); + Optional.ofNullable(defaultValue) + .orElseGet(() -> handleError(String.format("No state for context %s, list %s found", contextName, list))) + ); } } - private Optional getProperty(String contextName, String property) { + private Optional getProperty(String contextName, String property, String defaultValue) { return contextManager.getContext(contextName) - .map(context -> { - if ("updateCount" .equals(property)) { - return context.getUpdateCount(); - } else if ("listSize" .equals(property)) { - return context.getList().size(); - } else { - return context.getProperties().get(property); - } - } - ); + .map(context -> + Stream.of(SpecialProperties.values()) + .filter(it -> it.name().equals(property)) + .findFirst() + .map(it -> it.getFromContext(context)) + .orElseGet(() -> context.getProperties().get(property)) + ) + .or(() -> convertToPropertySpecificDefault(property, defaultValue)); + } + + private Optional convertToPropertySpecificDefault(String property, String defaultValue) { + return Stream.of(SpecialProperties.values()) + .filter(it -> it.name().equals(property)) + .findFirst() + .map(it -> it.convertDefaultValue(defaultValue)) + .or(() -> Optional.ofNullable(defaultValue)); } private Optional getList(String contextName, String list) { @@ -99,4 +106,41 @@ private Optional getList(String contextName, String list) { } }); } + + private enum SpecialProperties { + updateCount(Context::getUpdateCount, it -> it), + listSize((context) -> context.getList().size(), it -> it), + @SuppressWarnings("rawtypes") list( + Context::getList, + (defaultValue) -> Optional.ofNullable(defaultValue) + .map(it -> { + try { + return Json.read(it, ArrayList.class); + } catch (JsonException ex) { + notifier().error("default for list property is not a JSON list - fallback to empty list: " + defaultValue); + return null; + } + }) + .or(() -> Optional.of(new ArrayList())) + .map(it -> (Object) it) + .get() + ); + + private final Function contextExtractor; + private final Function defaultConverter; + + SpecialProperties(Function contextExtractor, Function defaultConverter) { + this.contextExtractor = contextExtractor; + this.defaultConverter = defaultConverter; + } + + public Object getFromContext(Context context) { + return contextExtractor.apply(context); + } + + public Object convertDefaultValue(String defaultValue) { + return defaultConverter.apply(defaultValue); + } + } + } diff --git a/src/test/java/org/wiremock/extensions/state/examples/StateExtensionListExampleTest.java b/src/test/java/org/wiremock/extensions/state/examples/StateExtensionListExampleTest.java index 6e3e385..e9f2aa5 100644 --- a/src/test/java/org/wiremock/extensions/state/examples/StateExtensionListExampleTest.java +++ b/src/test/java/org/wiremock/extensions/state/examples/StateExtensionListExampleTest.java @@ -47,13 +47,13 @@ import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; /** - * Sample test for creating a mock for a queue with java. + * Sample test for creating a mock for a listing with java. */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Execution(SAME_THREAD) class StateExtensionListExampleTest { - private static final String TEST_URL = "/test"; + private static final String TEST_URL = "/listing"; private static final Store store = new CaffeineStore(); private static final ObjectMapper mapper = new ObjectMapper(); @@ -77,7 +77,7 @@ public void setup() throws JsonProcessingException { } @Test - public void testQueue() { + public void testList() { var firstNameOne = RandomStringUtils.randomAlphabetic(5); var lastNameOne = RandomStringUtils.randomAlphabetic(5); var firstNameTwo = RandomStringUtils.randomAlphabetic(5); @@ -113,17 +113,13 @@ public void testQueue() { .get(assertDoesNotThrow(() -> new URI(wm.getRuntimeInfo().getHttpBaseUrl() + TEST_URL))) .then() .statusCode(HttpStatus.SC_OK) - .body("id", Matchers.equalTo(idOne)) - .body("firstName", Matchers.equalTo(firstNameOne)) - .body("lastName", Matchers.equalTo(lastNameOne)); - given() - .accept(ContentType.JSON) - .get(assertDoesNotThrow(() -> new URI(wm.getRuntimeInfo().getHttpBaseUrl() + TEST_URL))) - .then() - .statusCode(HttpStatus.SC_OK) - .body("id", Matchers.equalTo(idTwo)) - .body("firstName", Matchers.equalTo(firstNameTwo)) - .body("lastName", Matchers.equalTo(lastNameTwo)); + .body("$", Matchers.hasSize(2)) + .body("[0].id", Matchers.equalTo(idOne)) + .body("[0].firstName", Matchers.equalTo(firstNameOne)) + .body("[0].lastName", Matchers.equalTo(lastNameOne)) + .body("[1].id", Matchers.equalTo(idTwo)) + .body("[1].firstName", Matchers.equalTo(firstNameTwo)) + .body("[1].lastName", Matchers.equalTo(lastNameTwo)); } @@ -149,7 +145,7 @@ private void createPostStub() throws JsonProcessingException { "recordState", Parameters.from( Map.of( - "context", "queue", + "context", "list", "list", Map.of( "addLast", Map.of( "id", "{{jsonPath response.body '$.id'}}", @@ -163,33 +159,24 @@ private void createPostStub() throws JsonProcessingException { ); } - private void createGetStub() throws JsonProcessingException { + private void createGetStub() { wm.stubFor( get(urlPathMatching(TEST_URL)) .willReturn( WireMock.ok() .withHeader("content-type", "application/json") - .withJsonBody( - mapper.readTree( - mapper.writeValueAsString(Map.of( - "id", "{{state context='queue' list='[0].id'}}", - "firstName", "{{state context='queue' list='[0].firstName'}}", - "lastName", "{{state context='queue' list='[0].lastName'}}" - ) - ) - ) + .withBody( + "[\n" + + "{{#each (state context='list' property='list' default='[]') }}" + + " {\n" + + " \"id\": \"{{id}}\",\n" + + " \"firstName\": \"{{firstName}}\",\n" + + " \"lastName\": \"{{lastName}}\"" + + " }{{#unless @last}},{{/unless}}\n" + + "{{/each}}" + + "]" ) ) - .withServeEventListener( - "deleteState", - Parameters.from( - Map.of( - "context", "queue", - "list", Map.of("deleteFirst", true) - ) - ) - ) - ); } } \ No newline at end of file diff --git a/src/test/java/org/wiremock/extensions/state/examples/StateExtensionQueueExampleTest.java b/src/test/java/org/wiremock/extensions/state/examples/StateExtensionQueueExampleTest.java new file mode 100644 index 0000000..aed8c97 --- /dev/null +++ b/src/test/java/org/wiremock/extensions/state/examples/StateExtensionQueueExampleTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2023 Dirk Bolte + * + * 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 org.wiremock.extensions.state.examples; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.ConsoleNotifier; +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.store.Store; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.http.HttpStatus; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.parallel.Execution; +import org.wiremock.extensions.state.CaffeineStore; +import org.wiremock.extensions.state.StateExtension; + +import java.net.URI; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; + +/** + * Sample test for creating a mock for a queue with java. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Execution(SAME_THREAD) +class StateExtensionQueueExampleTest { + + private static final String TEST_URL = "/queue"; + private static final Store store = new CaffeineStore(); + private static final ObjectMapper mapper = new ObjectMapper(); + + @RegisterExtension + public static WireMockExtension wm = WireMockExtension.newInstance() + .options( + wireMockConfig().dynamicPort().dynamicHttpsPort().templatingEnabled(true).globalTemplating(true) + .extensions(new StateExtension(store)) + .notifier(new ConsoleNotifier(true)) + ) + .build(); + + + @BeforeEach + public void setup() throws JsonProcessingException { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + createGetStub(); + createPostStub(); + + wm.saveMappings(); + } + + @Test + public void testQueue() { + var firstNameOne = RandomStringUtils.randomAlphabetic(5); + var lastNameOne = RandomStringUtils.randomAlphabetic(5); + var firstNameTwo = RandomStringUtils.randomAlphabetic(5); + var lastNameTwo = RandomStringUtils.randomAlphabetic(5); + + var idOne = given() + .accept(ContentType.JSON) + .body(Map.of("firstName", firstNameOne, "lastName", lastNameOne)) + .post(assertDoesNotThrow(() -> new URI(wm.getRuntimeInfo().getHttpBaseUrl() + TEST_URL))) + .then() + .statusCode(HttpStatus.SC_OK) + .body("id", Matchers.notNullValue()) + .body("firstName", Matchers.equalTo(firstNameOne)) + .body("lastName", Matchers.equalTo(lastNameOne)) + .extract() + .body() + .jsonPath().get("id"); + var idTwo = given() + .accept(ContentType.JSON) + .body(Map.of("firstName", firstNameTwo, "lastName", lastNameTwo)) + .post(assertDoesNotThrow(() -> new URI(wm.getRuntimeInfo().getHttpBaseUrl() + TEST_URL))) + .then() + .statusCode(HttpStatus.SC_OK) + .body("id", Matchers.notNullValue()) + .body("firstName", Matchers.equalTo(firstNameTwo)) + .body("lastName", Matchers.equalTo(lastNameTwo)) + .extract() + .body() + .jsonPath().get("id"); + + given() + .accept(ContentType.JSON) + .get(assertDoesNotThrow(() -> new URI(wm.getRuntimeInfo().getHttpBaseUrl() + TEST_URL))) + .then() + .statusCode(HttpStatus.SC_OK) + .body("id", Matchers.equalTo(idOne)) + .body("firstName", Matchers.equalTo(firstNameOne)) + .body("lastName", Matchers.equalTo(lastNameOne)); + given() + .accept(ContentType.JSON) + .get(assertDoesNotThrow(() -> new URI(wm.getRuntimeInfo().getHttpBaseUrl() + TEST_URL))) + .then() + .statusCode(HttpStatus.SC_OK) + .body("id", Matchers.equalTo(idTwo)) + .body("firstName", Matchers.equalTo(firstNameTwo)) + .body("lastName", Matchers.equalTo(lastNameTwo)); + } + + + private void createPostStub() throws JsonProcessingException { + wm.stubFor( + post(urlPathMatching(TEST_URL)) + .willReturn( + WireMock.ok() + .withHeader("content-type", "application/json") + .withJsonBody( + mapper.readTree( + mapper.writeValueAsString( + Map.of( + "id", "{{randomValue length=32 type='ALPHANUMERIC' uppercase=false}}", + "firstName", "{{jsonPath request.body '$.firstName'}}", + "lastName", "{{jsonPath request.body '$.lastName'}}" + ) + ) + ) + ) + ) + .withServeEventListener( + "recordState", + Parameters.from( + Map.of( + "context", "queue", + "list", Map.of( + "addLast", Map.of( + "id", "{{jsonPath response.body '$.id'}}", + "firstName", "{{jsonPath request.body '$.firstName'}}", + "lastName", "{{jsonPath request.body '$.lastName'}}" + ) + ) + ) + ) + ) + ); + } + + private void createGetStub() throws JsonProcessingException { + wm.stubFor( + get(urlPathMatching(TEST_URL)) + .willReturn( + WireMock.ok() + .withHeader("content-type", "application/json") + .withJsonBody( + mapper.readTree( + mapper.writeValueAsString(Map.of( + "id", "{{state context='queue' list='[0].id'}}", + "firstName", "{{state context='queue' list='[0].firstName'}}", + "lastName", "{{state context='queue' list='[0].lastName'}}" + ) + ) + ) + ) + ) + .withServeEventListener( + "deleteState", + Parameters.from( + Map.of( + "context", "queue", + "list", Map.of("deleteFirst", true) + ) + ) + ) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/wiremock/extensions/state/functionality/StateTemplateHelperProviderExtensionTest.java b/src/test/java/org/wiremock/extensions/state/functionality/StateTemplateHelperProviderExtensionTest.java index 9469638..4e1a743 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/StateTemplateHelperProviderExtensionTest.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/StateTemplateHelperProviderExtensionTest.java @@ -20,8 +20,11 @@ import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.extension.Parameters; import io.restassured.http.ContentType; +import io.restassured.response.ValidatableResponse; +import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.RandomStringUtils; import org.apache.http.HttpStatus; +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -36,6 +39,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -126,109 +130,6 @@ void test_unknownProperty_fail() throws JsonProcessingException { ); } - @Nested - public class Property { - - @BeforeEach - void setup() { - createPostStub(); - createGetStub(); - } - - @Test - void test_returnsStateFromPreviousRequest_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("state", contextValue, "one"); - getAndAssertContextValue("state", contextValue, contextValue, "one", "0"); - } - - @Test - void test_defaults_returnsStateFromPreviousRequest_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("state", contextValue, "one"); - getAndAssertContextValue("state/default", contextValue, contextValue, "one", "0"); - } - - @Test - void test_returnsFullBodyFromPreviousRequest_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("state", contextValue, "one"); - getAndAssertFullBody(contextValue); - } - - @Test - void test_differentStatesSupported_ok() { - var contextValueOne = RandomStringUtils.randomAlphabetic(5); - var contextValueTwo = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("state", contextValueOne, "one"); - postAndAssertContextValue("state", contextValueTwo, "one"); - getAndAssertContextValue("state", contextValueOne, contextValueOne, "one", "0"); - getAndAssertContextValue("state", contextValueTwo, contextValueTwo, "one", "0"); - } - - - } - @Nested - public class List { - @BeforeEach - void setup() { - createPostStub(); - createGetStub(); - } - - @Test - void test_returnsListElement_oneItem_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("list", contextValue, "one"); - - getAndAssertContextValue("list/0", contextValue, contextValue, "one", "1"); - } - - @Test - void test_defaults_knownItem_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("list", contextValue, "one"); - - getAndAssertContextValue("list/default/0", contextValue, contextValue, "one", "1"); - } - - @Test - void test_defaults_unknownItem_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("list", contextValue, "one"); - - getAndAssertContextValue("list/default/1", contextValue, "defaultStateValueOne", "defaultStateValueTwo", "1"); - } - - @Test - void test_returnsListElement_multipleItems_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("list", contextValue, "one"); - postAndAssertContextValue("list", contextValue, "two"); - postAndAssertContextValue("list", contextValue, "three"); - - getAndAssertContextValue("list/1", contextValue, contextValue, "two", "3"); - } - @Test - void test_returnsSingleListElement_lastItem_ok() { - var contextValue = RandomStringUtils.randomAlphabetic(5); - - postAndAssertContextValue("list", contextValue, "one"); - postAndAssertContextValue("list", contextValue, "two"); - postAndAssertContextValue("list", contextValue, "three"); - - getAndAssertContextValue("list/-1", contextValue, contextValue, "three", "3"); - } - - } private void createGetStub() { wm.stubFor( get(urlPathMatching("/state/[^/]+")) @@ -288,7 +189,7 @@ private void createGetStub() { "valueOne", "{{state context=request.pathSegments.[2] list=(join '[' request.pathSegments.[1] '].stateValueOne' '')}}", "valueTwo", "{{state context=request.pathSegments.[2] list=(join '[' request.pathSegments.[1] '].stateValueTwo' '')}}", "listSize", "{{state context=request.pathSegments.[2] property='listSize'}}", - "unknown", "{{state context=request.pathSegments.[1] property='unknown' default='defaultUnknown'}}" + "unknown", "{{state context=request.pathSegments.[2] property='unknown' default='defaultUnknown'}}" ) ) ) @@ -296,6 +197,40 @@ private void createGetStub() { ) ); + wm.stubFor( + get(urlPathMatching("/list/allNoDefault/[^/]+")) + .willReturn( + WireMock.ok() + .withHeader("content-type", "application/json") + .withBody( + "[\n" + + "{{# each (state context=request.pathSegments.[2] property='list') }}" + + " {\n" + + " }{{#unless @last}},{{/unless}}\n" + + "{{/each}}" + + "]" + ) + ) + ); + + wm.stubFor( + get(urlPathMatching("/list/all/[^/]+")) + .willReturn( + WireMock.ok() + .withHeader("content-type", "application/json") + .withBody( + "[\n" + + "{{#each (state context=request.pathSegments.[2] property='list' default='[{\"stateValueOne\": \"defaultValueOne\",\"stateValueTwo\": \"defaultValueTwo\"}]') }}" + + " {\n" + + " \"valueOne\": \"{{stateValueOne}}\",\n" + + " \"valueTwo\": \"{{stateValueTwo}}\"" + + " }{{#unless @last}},{{/unless}}\n" + + "{{/each}}" + + "]" + ) + ) + ); + wm.stubFor( get(urlPathMatching("/list/default/[^/]+/[^/]+")) .willReturn( @@ -381,11 +316,7 @@ private void createPostStub() { } private void getAndAssertContextValue(String path, String context, String valueOne, String valueTwo, String listSize) { - given() - .accept(ContentType.JSON) - .get(assertDoesNotThrow(() -> new URI(String.format("%s/%s/%s", wm.getRuntimeInfo().getHttpBaseUrl(), path, context)))) - .then() - .statusCode(HttpStatus.SC_OK) + getAndAssertOk(path, context) .body("valueOne", equalTo(valueOne)) .body("valueTwo", equalTo(valueTwo)) .body("listSize", equalTo(listSize)) @@ -393,6 +324,14 @@ private void getAndAssertContextValue(String path, String context, String valueO .body("other", nullValue()); } + private ValidatableResponse getAndAssertOk(String path, String context) { + return given() + .accept(ContentType.JSON) + .get(assertDoesNotThrow(() -> new URI(String.format("%s/%s/%s", wm.getRuntimeInfo().getHttpBaseUrl(), path, context)))) + .then() + .statusCode(HttpStatus.SC_OK); + } + private void getAndAssertFullBody(String contextValue) { given() .accept(ContentType.JSON) @@ -416,4 +355,144 @@ private void postAndAssertContextValue(String path, String contextValueOne, Stri .then() .statusCode(HttpStatus.SC_OK); } + + @Nested + public class Property { + + @BeforeEach + void setup() { + createPostStub(); + createGetStub(); + } + + @Test + void test_returnsStateFromPreviousRequest_ok() { + var contextValue = RandomStringUtils.randomAlphabetic(5); + + postAndAssertContextValue("state", contextValue, "one"); + getAndAssertContextValue("state", contextValue, contextValue, "one", "0"); + } + + @Test + void test_defaults_returnsStateFromPreviousRequest_ok() { + var contextValue = RandomStringUtils.randomAlphabetic(5); + + postAndAssertContextValue("state", contextValue, "one"); + getAndAssertContextValue("state/default", contextValue, contextValue, "one", "0"); + } + + @Test + void test_returnsFullBodyFromPreviousRequest_ok() { + var contextValue = RandomStringUtils.randomAlphabetic(5); + + postAndAssertContextValue("state", contextValue, "one"); + getAndAssertFullBody(contextValue); + } + + @Test + void test_differentStatesSupported_ok() { + var contextValueOne = RandomStringUtils.randomAlphabetic(5); + var contextValueTwo = RandomStringUtils.randomAlphabetic(5); + + postAndAssertContextValue("state", contextValueOne, "one"); + postAndAssertContextValue("state", contextValueTwo, "one"); + getAndAssertContextValue("state", contextValueOne, contextValueOne, "one", "0"); + getAndAssertContextValue("state", contextValueTwo, contextValueTwo, "one", "0"); + } + + + } + + @Nested + public class List { + + private final String contextValue = RandomStringUtils.randomAlphabetic(5); + + @BeforeEach + void setup() { + createPostStub(); + createGetStub(); + } + + @Test + void test_returnsListElement_oneItem_ok() { + postAndAssertContextValue("list", contextValue, "one"); + + getAndAssertContextValue("list/0", contextValue, contextValue, "one", "1"); + } + + @Test + void test_defaults_knownItem_ok() { + postAndAssertContextValue("list", contextValue, "one"); + + getAndAssertContextValue("list/default/0", contextValue, contextValue, "one", "1"); + } + + @Test + void test_defaults_unknownItem_ok() { + postAndAssertContextValue("list", contextValue, "one"); + + getAndAssertContextValue("list/default/1", contextValue, "defaultStateValueOne", "defaultStateValueTwo", "1"); + } + + @Test + void test_returnsListElement_multipleItems_ok() { + postAndAssertContextValue("list", contextValue, "one"); + postAndAssertContextValue("list", contextValue, "two"); + postAndAssertContextValue("list", contextValue, "three"); + + getAndAssertContextValue("list/1", contextValue, contextValue, "two", "3"); + } + + @Test + void test_returnsSingleListElement_lastItem_ok() { + postAndAssertContextValue("list", contextValue, "one"); + postAndAssertContextValue("list", contextValue, "two"); + postAndAssertContextValue("list", contextValue, "three"); + + getAndAssertContextValue("list/-1", contextValue, contextValue, "three", "3"); + } + + @Nested + public class CompleteList { + @Test + void test_returnsCompleteList_noContext_emptyList() { + getAndAssertOk("list/allNoDefault", contextValue) + .body("$", Matchers.empty()); + } + + @Test + void test_returnsCompleteList_defaultFilled_ok() { + getAndAssertOk("list/all", contextValue) + .body("$", hasSize(1)) + .body("[0].valueOne", equalTo("defaultValueOne")) + .body("[0].valueTwo", equalTo("defaultValueTwo")); + } + + @Test + void test_returnsCompleteList_emptyList_ok() { + postAndAssertContextValue("state", contextValue, "one"); + + getAndAssertContextValue("state", contextValue, contextValue, "one", "0"); + getAndAssertOk("list/all", contextValue) + .body("$", Matchers.empty()); + } + + @Test + void test_returnsCompleteList_entries_ok() { + postAndAssertContextValue("list", contextValue, "one"); + postAndAssertContextValue("list", contextValue, "two"); + postAndAssertContextValue("list", contextValue, "three"); + + getAndAssertOk("list/all", contextValue) + .body("$", hasSize(3)) + .body("[0].valueOne", equalTo(contextValue)) + .body("[0].valueTwo", equalTo("one")) + .body("[1].valueOne", equalTo(contextValue)) + .body("[1].valueTwo", equalTo("two")) + .body("[2].valueOne", equalTo(contextValue)) + .body("[2].valueTwo", equalTo("three")); + } + } + } } \ No newline at end of file