From af4474a789327dc5e21a7f6156213ca3869016d2 Mon Sep 17 00:00:00 2001 From: Dirk Bolte Date: Sat, 23 Dec 2023 15:47:19 +0100 Subject: [PATCH] bug!: fix updateCount handling (#110) - introduce a pseudo-transaction which writes updates when a request ends - introduce a transactionManager for this - move locking to transactionManager (and remove some unneeded locks) - update tests as the updateCount is only increased once now ## References #102 ## Submitter checklist - [ ] Recommended: Join [WireMock Slack](https://slack.wiremock.org/) to get any help in `#help-contributing` or a project-specific channel like `#wiremock-java` - [ ] 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 | 20 +- .../extensions/state/StateExtension.java | 8 +- .../extensions/DeleteStateEventListener.java | 209 ++++++++++-------- .../extensions/RecordStateEventListener.java | 116 ++++++---- .../extensions/StateHandlerbarHelper.java | 6 +- .../state/extensions/StateRequestMatcher.java | 9 +- .../extensions/TransactionEventListener.java | 59 +++++ .../state/internal/ContextManager.java | 127 ++++++----- .../state/internal/ExtensionLogger.java | 17 ++ .../state/internal/TransactionManager.java | 85 +++++++ .../{ => api}/DeleteStateParameters.java | 2 +- .../{ => api}/RecordStateParameters.java | 2 +- .../StateRequestMatcherParameters.java | 2 +- .../state/internal/{ => model}/Context.java | 24 +- .../{ => model}/ContextTemplateModel.java | 7 +- .../{ => model}/ResponseTemplateModel.java | 2 +- .../state/internal/model/Transaction.java | 37 ++++ .../StubMappingLoadingExampleTest.java | 11 +- .../state/functionality/AbstractTestBase.java | 7 +- .../DeleteStateEventListenerTest.java | 170 ++++++++------ .../RecordStateEventListenerTest.java | 67 +++++- .../StateRequestMatcherTest.java | 12 +- ...teTemplateHelperProviderExtensionTest.java | 4 +- 23 files changed, 676 insertions(+), 327 deletions(-) create mode 100644 src/main/java/org/wiremock/extensions/state/extensions/TransactionEventListener.java create mode 100644 src/main/java/org/wiremock/extensions/state/internal/TransactionManager.java rename src/main/java/org/wiremock/extensions/state/internal/{ => api}/DeleteStateParameters.java (98%) rename src/main/java/org/wiremock/extensions/state/internal/{ => api}/RecordStateParameters.java (97%) rename src/main/java/org/wiremock/extensions/state/internal/{ => api}/StateRequestMatcherParameters.java (98%) rename src/main/java/org/wiremock/extensions/state/internal/{ => model}/Context.java (76%) rename src/main/java/org/wiremock/extensions/state/internal/{ => model}/ContextTemplateModel.java (88%) rename src/main/java/org/wiremock/extensions/state/internal/{ => model}/ResponseTemplateModel.java (96%) create mode 100644 src/main/java/org/wiremock/extensions/state/internal/model/Transaction.java diff --git a/README.md b/README.md index 6079ccd..a409cbf 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ The state is recorded in `serveEventListeners` of a stub. The following function - to delete a selective property, set it to `null` (as string). - `list` : stores a state in a list. Can be used to prepend/append new states to an existing list. List elements cannot be modified (only read/deleted). -`state` and `list` can be used in the same `ServeEventListener` (would count as two updates). Adding multiple `recordState` `ServeEventListener` is supported. +`state` and `list` can be used in the same `ServeEventListener` (would count as ONE updates). Adding multiple `recordState` `ServeEventListener` is supported. The following parameters have to be provided: @@ -803,7 +803,9 @@ For documentation on using these matchers, check the [WireMock documentation](ht ### Context update count match -Whenever the serve event listener `recordState` is processed, the internal context update counter is increased. The number can be used +Whenever a request with a serve event listener `recordState` or `deleteState` is processed, the internal context update counter is increased. +The update count is increased by one whenever there is at least one change to a context (so: property adding/change, list entry addition/deletion). Multiple +event listeners with multiple changes of a single context within a single request only result in an increase by one. for request matching as well. The following matchers are available: - `updateCountEqualTo` @@ -1070,6 +1072,20 @@ body file with handlebars to ignore a missing property: } ``` +# Distributed setups and concurrency + +This extension is at the moment not optimized for distributed setups or high degrees concurrency. While it will basically work, there are some limitations +that should be held into account: + +- The store used for storing the state is on instance-level only + - while it can be exchanged for a distributed store, any atomicity assurance on instance level is not replicated to the distributed setup. Thus concurrent operations on different instances might result in state overwrites +- Lock-level is basically the whole context store + - while the lock time is kept small, this can still impact measurements when being used in load tests +- Single updates to contexts (property additions or changes, list entry additions or deletions) are atomic on instance level +- Concurrent requests are currently allowed to change the same context. Atomicity prevents overwrites but does not provide something like a transaction, so: the context can change while a request is performed + +For any kind of usage with parallel write requests, it's recommended to use a different context for each parallel stream. + # Debugging In general, you can increase verbosity, either by [register a notifier](https://wiremock.org/3.x/docs/configuration/#notification-logging) diff --git a/src/main/java/org/wiremock/extensions/state/StateExtension.java b/src/main/java/org/wiremock/extensions/state/StateExtension.java index ea16254..ce756df 100644 --- a/src/main/java/org/wiremock/extensions/state/StateExtension.java +++ b/src/main/java/org/wiremock/extensions/state/StateExtension.java @@ -24,7 +24,9 @@ import org.wiremock.extensions.state.extensions.RecordStateEventListener; import org.wiremock.extensions.state.extensions.StateRequestMatcher; import org.wiremock.extensions.state.extensions.StateTemplateHelperProviderExtension; +import org.wiremock.extensions.state.extensions.TransactionEventListener; import org.wiremock.extensions.state.internal.ContextManager; +import org.wiremock.extensions.state.internal.TransactionManager; import java.util.Collections; import java.util.List; @@ -52,15 +54,18 @@ public class StateExtension implements ExtensionFactory { private final StateTemplateHelperProviderExtension stateTemplateHelperProviderExtension; private final RecordStateEventListener recordStateEventListener; private final DeleteStateEventListener deleteStateEventListener; + private final TransactionEventListener transactionEventListener; private final StateRequestMatcher stateRequestMatcher; public StateExtension(Store store) { - var contextManager = new ContextManager(store); + var transactionManager = new TransactionManager(store); + var contextManager = new ContextManager(store, transactionManager); this.stateTemplateHelperProviderExtension = new StateTemplateHelperProviderExtension(contextManager); var templateEngine = new TemplateEngine(stateTemplateHelperProviderExtension.provideTemplateHelpers(), null, Collections.emptySet(), false); this.recordStateEventListener = new RecordStateEventListener(contextManager, templateEngine); this.deleteStateEventListener = new DeleteStateEventListener(contextManager, templateEngine); + this.transactionEventListener = new TransactionEventListener(transactionManager); this.stateRequestMatcher = new StateRequestMatcher(contextManager, templateEngine); } @@ -69,6 +74,7 @@ public List create(WireMockServices services) { return List.of( recordStateEventListener, deleteStateEventListener, + transactionEventListener, stateRequestMatcher, stateTemplateHelperProviderExtension ); diff --git a/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java b/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java index 099227b..d12ddfa 100644 --- a/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java +++ b/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java @@ -24,9 +24,9 @@ import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import org.apache.commons.lang3.StringUtils; import org.wiremock.extensions.state.internal.ContextManager; -import org.wiremock.extensions.state.internal.DeleteStateParameters; -import org.wiremock.extensions.state.internal.ResponseTemplateModel; import org.wiremock.extensions.state.internal.StateExtensionMixin; +import org.wiremock.extensions.state.internal.api.DeleteStateParameters; +import org.wiremock.extensions.state.internal.model.ResponseTemplateModel; import java.util.List; import java.util.Map; @@ -72,122 +72,139 @@ public void beforeResponseSent(ServeEvent serveEvent, Parameters parameters) { "response", ResponseTemplateModel.from(serveEvent.getResponse()) ); var configuration = Json.mapToObject(parameters, DeleteStateParameters.class); - Optional.ofNullable(configuration.getList()).ifPresentOrElse( - listConfig -> handleListDeletion(listConfig, createContextName(model, configuration.getContext()), model), - () -> handleContextDeletion(configuration, model) - ); + new ListenerInstance(serveEvent.getId().toString(), model, configuration).run(); } - private void handleContextDeletion(DeleteStateParameters configuration, Map model) { - if (configuration.getContext() != null) { - deleteContext(configuration.getContext(), model); - } else if (configuration.getContexts() != null) { - deleteContexts(configuration.getContexts(), model); - } else if (configuration.getContextsMatching() != null) { - deleteContextsMatching(configuration.getContextsMatching(), model); - } else { - throw createConfigurationError("Missing/invalid configuration for context deletion"); - } + private String renderTemplate(Object context, String value) { + return templateEngine.getUncachedTemplate(value).apply(context); } - private void deleteContexts(List rawContexts, Map model) { + private class ListenerInstance { + private final String requestId; + private final DeleteStateParameters configuration; + private final Map model; - var contexts = rawContexts.stream().map(it -> renderTemplate(model, it)).collect(Collectors.toList()); - contextManager.onEach(context -> { - if(contexts.contains(context.getContextName())) { - contextManager.deleteContext(context.getContextName()); + ListenerInstance(String requestId, Map model, DeleteStateParameters configuration) { + this.requestId = requestId; + this.model = model; + this.configuration = configuration; + } + + public void run() { + Optional.ofNullable(configuration.getList()).ifPresentOrElse( + listConfig -> handleListDeletion(listConfig, createContextName(configuration.getContext())), + this::handleContextDeletion + ); + } + + private void handleContextDeletion() { + if (configuration.getContext() != null) { + deleteContext(configuration.getContext()); + } else if (configuration.getContexts() != null) { + deleteContexts(configuration.getContexts()); + } else if (configuration.getContextsMatching() != null) { + deleteContextsMatching(configuration.getContextsMatching()); + } else { + throw createConfigurationError("Missing/invalid configuration for context deletion"); } - }); - } + } + + private void deleteContexts(List rawContexts) { - private void deleteContextsMatching(String rawRegex, Map model) { - try { - var regex = renderTemplate(model, rawRegex); - var pattern = Pattern.compile(regex); - contextManager.onEach(context -> { - if(pattern.matcher(context.getContextName()).matches()) { - contextManager.deleteContext(context.getContextName()); + var contexts = rawContexts.stream().map(it -> renderTemplate(model, it)).collect(Collectors.toList()); + contextManager.onEach(requestId, context -> { + if (contexts.contains(context.getContextName())) { + contextManager.deleteContext(requestId, context.getContextName()); } }); - } catch (PatternSyntaxException ex) { - throw createConfigurationError("Missing/invalid configuration for context deletion: %s", ex.getMessage()); } - } - private void deleteContext(String rawContext, Map model) { - contextManager.deleteContext(createContextName(model, rawContext)); - } + private void deleteContextsMatching(String rawRegex) { + try { + var regex = renderTemplate(model, rawRegex); + var pattern = Pattern.compile(regex); + contextManager.onEach(requestId, context -> { + if (pattern.matcher(context.getContextName()).matches()) { + contextManager.deleteContext(requestId, context.getContextName()); + } + }); + } catch (PatternSyntaxException ex) { + throw createConfigurationError("Missing/invalid configuration for context deletion: %s", ex.getMessage()); + } + } - private void handleListDeletion(DeleteStateParameters.ListParameters listConfig, String contextName, Map model) { - if (Boolean.TRUE.equals(listConfig.getDeleteFirst())) { - deleteFirst(contextName); - } else if (Boolean.TRUE.equals(listConfig.getDeleteLast())) { - deleteLast(contextName); - } else if (StringUtils.isNotBlank(listConfig.getDeleteIndex())) { - deleteIndex(listConfig, contextName, model); - } else if (listConfig.getDeleteWhere() != null && - listConfig.getDeleteWhere().getProperty() != null && - listConfig.getDeleteWhere().getValue() != null - ) { - deleteWhere(listConfig, contextName, model); - } else { - throw createConfigurationError("Missing/invalid configuration for list entry deletion"); + private void deleteContext(String rawContext) { + contextManager.deleteContext(requestId, createContextName(rawContext)); } - } - private Long deleteFirst(String contextName) { - return contextManager.createOrUpdateContextList(contextName, maps -> { - if (!maps.isEmpty()) maps.removeFirst(); - logger().info(contextName, "list::deleteFirst"); - }); - } + private void handleListDeletion(DeleteStateParameters.ListParameters listConfig, String contextName) { + if (Boolean.TRUE.equals(listConfig.getDeleteFirst())) { + deleteFirst(contextName); + } else if (Boolean.TRUE.equals(listConfig.getDeleteLast())) { + deleteLast(contextName); + } else if (StringUtils.isNotBlank(listConfig.getDeleteIndex())) { + deleteIndex(listConfig, contextName); + } else if (listConfig.getDeleteWhere() != null && + listConfig.getDeleteWhere().getProperty() != null && + listConfig.getDeleteWhere().getValue() != null + ) { + deleteWhere(listConfig, contextName); + } else { + throw createConfigurationError("Missing/invalid configuration for list entry deletion"); + } + } - private void deleteLast(String contextName) { - contextManager.createOrUpdateContextList(contextName, maps -> { - if (!maps.isEmpty()) maps.removeLast(); - logger().info(contextName, "list::deleteLast"); - }); - } + private void deleteFirst(String contextName) { + contextManager.createOrUpdateContextList(requestId, contextName, maps -> { + if (!maps.isEmpty()) maps.removeFirst(); + logger().info(contextName, "list::deleteFirst"); + }); + } - private void deleteIndex(DeleteStateParameters.ListParameters listConfig, String contextName, Map model) { - try { - var index = Integer.parseInt(renderTemplate(model, listConfig.getDeleteIndex())); - contextManager.createOrUpdateContextList(contextName, list -> { - list.remove(index); - logger().info(contextName, String.format("list::deleteIndex(%d)", index)); + private void deleteLast(String contextName) { + contextManager.createOrUpdateContextList(requestId, contextName, maps -> { + if (!maps.isEmpty()) maps.removeLast(); + logger().info(contextName, "list::deleteLast"); }); - } catch (IndexOutOfBoundsException | NumberFormatException e) { - logger().info(contextName, String.format("Unknown or unparsable list index: '%s' - ignoring", listConfig.getDeleteIndex())); } - } - private void deleteWhere(DeleteStateParameters.ListParameters listConfig, String contextName, Map model) { - var property = renderTemplate(model, listConfig.getDeleteWhere().getProperty()); - var value = renderTemplate(model, listConfig.getDeleteWhere().getValue()); - contextManager.createOrUpdateContextList(contextName, list -> { - var iterator = list.iterator(); - while (iterator.hasNext()) { - var element = iterator.next(); - if (Objects.equals(element.getOrDefault(property, null), value)) { - iterator.remove(); - logger().info(contextName, String.format("list::deleteWhere(property=%s)", property)); - break; - } + private void deleteIndex(DeleteStateParameters.ListParameters listConfig, String contextName) { + try { + var index = Integer.parseInt(renderTemplate(model, listConfig.getDeleteIndex())); + contextManager.createOrUpdateContextList(requestId, contextName, list -> { + list.remove(index); + logger().info(contextName, String.format("list::deleteIndex(%d)", index)); + }); + } catch (IndexOutOfBoundsException | NumberFormatException e) { + logger().info(contextName, String.format("Unknown or unparsable list index: '%s' - ignoring", listConfig.getDeleteIndex())); } - }); - } + } - private String createContextName(Map model, String rawContext) { - var context = Optional.ofNullable(rawContext).filter(StringUtils::isNotBlank) - .map(it -> renderTemplate(model, it)) - .orElseThrow(() -> new ConfigurationException("no context specified")); - if (StringUtils.isBlank(context)) { - throw createConfigurationError("Context cannot be blank"); + private void deleteWhere(DeleteStateParameters.ListParameters listConfig, String contextName) { + var property = renderTemplate(model, listConfig.getDeleteWhere().getProperty()); + var value = renderTemplate(model, listConfig.getDeleteWhere().getValue()); + contextManager.createOrUpdateContextList(requestId, contextName, list -> { + var iterator = list.iterator(); + while (iterator.hasNext()) { + var element = iterator.next(); + if (Objects.equals(element.getOrDefault(property, null), value)) { + iterator.remove(); + logger().info(contextName, String.format("list::deleteWhere(property=%s)", property)); + break; + } + } + }); + } + + private String createContextName(String rawContext) { + var context = Optional.ofNullable(rawContext).filter(StringUtils::isNotBlank) + .map(it -> renderTemplate(model, it)) + .orElseThrow(() -> new ConfigurationException("no context specified")); + if (StringUtils.isBlank(context)) { + throw createConfigurationError("Context cannot be blank"); + } + return context; } - return context; - } - private String renderTemplate(Object context, String value) { - return templateEngine.getUncachedTemplate(value).apply(context); } } diff --git a/src/main/java/org/wiremock/extensions/state/extensions/RecordStateEventListener.java b/src/main/java/org/wiremock/extensions/state/extensions/RecordStateEventListener.java index 1843f18..e373613 100644 --- a/src/main/java/org/wiremock/extensions/state/extensions/RecordStateEventListener.java +++ b/src/main/java/org/wiremock/extensions/state/extensions/RecordStateEventListener.java @@ -24,9 +24,9 @@ import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import org.apache.commons.lang3.StringUtils; import org.wiremock.extensions.state.internal.ContextManager; -import org.wiremock.extensions.state.internal.RecordStateParameters; -import org.wiremock.extensions.state.internal.ResponseTemplateModel; import org.wiremock.extensions.state.internal.StateExtensionMixin; +import org.wiremock.extensions.state.internal.api.RecordStateParameters; +import org.wiremock.extensions.state.internal.model.ResponseTemplateModel; import java.util.Map; import java.util.Optional; @@ -57,9 +57,7 @@ public void beforeResponseSent(ServeEvent serveEvent, Parameters parameters) { "response", ResponseTemplateModel.from(serveEvent.getResponse()) ); var configuration = Json.mapToObject(parameters, RecordStateParameters.class); - var contextName = createContextName(model, parameters); - handleState(contextName, model, configuration); - handleList(contextName, model, configuration); + new ListenerInstance(serveEvent.getId().toString(), model, configuration).run(); } @Override @@ -72,57 +70,77 @@ public boolean applyGlobally() { return false; } - private void handleState(String contextName, Map model, RecordStateParameters parameters) { - Optional.ofNullable(parameters.getState()) - .ifPresent(configuration -> - contextManager.createOrUpdateContextState(contextName, getPropertiesFromConfiguration(model, configuration)) - ); - } - private Map getPropertiesFromConfiguration(Map model, Map configuration) { - return configuration.entrySet() - .stream() - .map(entry -> Map.entry(entry.getKey(), renderTemplate(model, entry.getValue()))) - .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + private String renderTemplate(Object context, String value) { + return templateEngine.getUncachedTemplate(value).apply(context); } - private void handleList(String contextName, Map model, RecordStateParameters parameters) { - Optional.ofNullable(parameters.getList()) - .ifPresent(listConfiguration -> { - Optional.ofNullable(listConfiguration.getAddFirst()) - .ifPresent(configuration -> addFirst(contextName, model, configuration)); - Optional.ofNullable(listConfiguration.getAddLast()) - .ifPresent(configuration -> addLast(contextName, model, configuration)); - } - ); - } + private class ListenerInstance { + private final String requestId; + private final RecordStateParameters parameters; + private final Map model; + private final String contextName; + + ListenerInstance(String requestId, Map model, RecordStateParameters parameters) { + this.requestId = requestId; + this.model = model; + this.parameters = parameters; + this.contextName = createContextName(); + } - private Long addFirst(String contextName, Map model, Map configuration) { - return contextManager.createOrUpdateContextList(contextName, list -> { - list.addFirst(getPropertiesFromConfiguration(model, configuration)); - logger().info(contextName, "list::addFirst"); - }); - } + void run() { + handleState(); + handleList(); + } - private Long addLast(String contextName, Map model, Map configuration) { - return contextManager.createOrUpdateContextList(contextName, list -> { - list.addLast(getPropertiesFromConfiguration(model, configuration)); - logger().info(contextName, "list::addLast"); - }); - } + private String createContextName() { + var rawContext = Optional.ofNullable(parameters.getContext()) + .filter(StringUtils::isNotBlank) + .orElseThrow(() -> new ConfigurationException("no context specified")); + String context = renderTemplate(model, rawContext); + if (StringUtils.isBlank(context)) { + throw createConfigurationError("context cannot be blank"); + } + return context; + } - private String createContextName(Map model, Parameters parameters) { - var rawContext = Optional.ofNullable(parameters.getString("context")) - .filter(StringUtils::isNotBlank) - .orElseThrow(() -> new ConfigurationException("no context specified")); - String context = renderTemplate(model, rawContext); - if (StringUtils.isBlank(context)) { - throw createConfigurationError("context cannot be blank"); + private void handleState() { + Optional.ofNullable(parameters.getState()) + .ifPresent(configuration -> + contextManager.createOrUpdateContextState(requestId, contextName, getPropertiesFromConfiguration(configuration)) + ); } - return context; - } - private String renderTemplate(Object context, String value) { - return templateEngine.getUncachedTemplate(value).apply(context); + private Map getPropertiesFromConfiguration(Map configuration) { + return configuration.entrySet() + .stream() + .map(entry -> Map.entry(entry.getKey(), renderTemplate(model, entry.getValue()))) + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private void handleList() { + Optional.ofNullable(parameters.getList()) + .ifPresent(listConfiguration -> { + Optional.ofNullable(listConfiguration.getAddFirst()) + .ifPresent(this::addFirst); + Optional.ofNullable(listConfiguration.getAddLast()) + .ifPresent(this::addLast); + } + ); + } + + private void addFirst(Map configuration) { + contextManager.createOrUpdateContextList(requestId, contextName, list -> { + list.addFirst(getPropertiesFromConfiguration(configuration)); + logger().info(contextName, "list::addFirst"); + }); + } + + private void addLast(Map configuration) { + contextManager.createOrUpdateContextList(requestId, contextName, list -> { + list.addLast(getPropertiesFromConfiguration(configuration)); + logger().info(contextName, "list::addLast"); + }); + } } } 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 4b28db0..44d4381 100644 --- a/src/main/java/org/wiremock/extensions/state/extensions/StateHandlerbarHelper.java +++ b/src/main/java/org/wiremock/extensions/state/extensions/StateHandlerbarHelper.java @@ -22,7 +22,7 @@ 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.model.Context; import org.wiremock.extensions.state.internal.ContextManager; import java.util.ArrayList; @@ -84,7 +84,7 @@ public Object apply(Object o, Options options) { } private Optional getProperty(String contextName, String property, String defaultValue) { - return contextManager.getContext(contextName) + return contextManager.getContextCopy(contextName) .map(context -> Stream.of(SpecialProperties.values()) .filter(it -> it.name().equals(property)) @@ -111,7 +111,7 @@ private Optional convertToPropertySpecificDefault(String contextName, St } private Optional getList(String contextName, String list) { - return contextManager.getContext(contextName) + return contextManager.getContextCopy(contextName) .flatMap(context -> { try { return Optional.of(JsonPath.read(context.getList(), list)); diff --git a/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java b/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java index 30aafa9..e76c765 100644 --- a/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java +++ b/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java @@ -21,13 +21,12 @@ import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel; import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine; import com.github.tomakehurst.wiremock.http.Request; -import com.github.tomakehurst.wiremock.matching.ContentPattern; import com.github.tomakehurst.wiremock.matching.MatchResult; import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; import com.github.tomakehurst.wiremock.matching.StringValuePattern; -import org.wiremock.extensions.state.internal.Context; +import org.wiremock.extensions.state.internal.model.Context; import org.wiremock.extensions.state.internal.ContextManager; -import org.wiremock.extensions.state.internal.ContextTemplateModel; +import org.wiremock.extensions.state.internal.model.ContextTemplateModel; import org.wiremock.extensions.state.internal.StateExtensionMixin; import java.util.Arrays; @@ -107,7 +106,7 @@ public MatchResult match(Request request, Parameters parameters) { } private MatchResult hasContext(Map model, Parameters parameters, String template) { - return contextManager.getContext(renderTemplate(model, template)) + return contextManager.getContextCopy(renderTemplate(model, template)) .map(context -> { List> matchers = getMatchers(parameters); if (matchers.isEmpty()) { @@ -131,7 +130,7 @@ private MatchResult calculateMatch(Map model, Context context, L private MatchResult hasNotContext(Map model, String template) { var context = renderTemplate(model, template); - if (contextManager.getContext(context).isEmpty()) { + if (contextManager.getContextCopy(context).isEmpty()) { logger().info(context, "hasNotContext matched"); return MatchResult.exactMatch(); } else { diff --git a/src/main/java/org/wiremock/extensions/state/extensions/TransactionEventListener.java b/src/main/java/org/wiremock/extensions/state/extensions/TransactionEventListener.java new file mode 100644 index 0000000..6019ab1 --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/extensions/TransactionEventListener.java @@ -0,0 +1,59 @@ +/* + * 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.extensions; + +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.extension.ServeEventListener; +import com.github.tomakehurst.wiremock.stubbing.ServeEvent; +import org.wiremock.extensions.state.internal.ContextManager; +import org.wiremock.extensions.state.internal.StateExtensionMixin; +import org.wiremock.extensions.state.internal.TransactionManager; + +/** + * Persist transaction-related information in the context. + *

+ * DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead. + * + * @see org.wiremock.extensions.state.StateExtension + */ +public class TransactionEventListener implements ServeEventListener, StateExtensionMixin { + + private final TransactionManager transactionManager; + + + public TransactionEventListener(TransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public String getName() { + return "stateTransaction"; + } + + @Override + public boolean applyGlobally() { + return true; + } + + @Override + public void afterComplete(ServeEvent serveEvent, Parameters parameters) { + + String requestId = serveEvent.getId().toString(); + var contextNames = transactionManager.getContextNamesByRequestId(requestId); + contextNames.forEach((contextName) -> transactionManager.deleteTransaction(requestId, contextName)); + + } +} diff --git a/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java b/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java index f94456e..b8feae5 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java +++ b/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java @@ -16,6 +16,7 @@ package org.wiremock.extensions.state.internal; import com.github.tomakehurst.wiremock.store.Store; +import org.wiremock.extensions.state.internal.model.Context; import java.util.LinkedList; import java.util.Map; @@ -27,10 +28,13 @@ public class ContextManager { + private final String CONTEXT_KEY_PREFIX = "context:"; private final Store store; + private final TransactionManager transactionManager; - public ContextManager(Store store) { + public ContextManager(Store store, TransactionManager transactionManager) { this.store = store; + this.transactionManager = transactionManager; } private static Supplier createNewContext(String contextName) { @@ -38,54 +42,73 @@ private static Supplier createNewContext(String contextName) { return () -> new Context(contextName); } - public Object getState(String contextName, String property) { - synchronized (store) { - return store.get(contextName).map(it -> ((Context) it).getProperties().get(property)).orElse(null); - } - } - /** * Searches for the context by the given name. * * @param contextName The context name to search for. * @return Optional with a copy of the context - or empty. */ - public Optional getContext(String contextName) { - synchronized (store) { - return getSafeContextCopy(contextName); - } + public Optional getContextCopy(String contextName) { + return getSafeContextCopy(contextName); } - public void deleteContext(String contextName) { - synchronized (store) { - store.remove(contextName); + /** + * Deletes a context by its name. + * + * @param requestId ID of the request performing this action. + * @param contextName Name of the context to delete. + */ + public void deleteContext(String requestId, String contextName) { + transactionManager.withTransaction(requestId, contextName, (transaction) -> { + store.remove(createContextKey(contextName)); logger().info(contextName, "deleted"); - } + }); } - public void onEach(Consumer consumer) { - synchronized(store) { - store.getAllKeys().forEach(contextName -> { - getSafeContextCopy(contextName).ifPresent(consumer); + /** + * Iterates over all contexts, passing a safe copy to the consumer. + *

+ * Silently ignores non-existing contexts. + * + * @param requestId ID of the request performing this action. + * @param consumer Action to be performed on the copy of the context. + */ + public void onEach(String requestId, Consumer consumer) { + store.getAllKeys() + .filter(it -> it.startsWith(CONTEXT_KEY_PREFIX)) + .forEach(key -> { + var contextName = getContextNameFromContextKey(key); + transactionManager + .withTransaction( + requestId, + contextName, + (transaction) -> { + getSafeContextCopy(contextName).ifPresent(consumer); + }); }); - } } - public void deleteAllContexts() { - synchronized (store) { - logger().info("allContexts", "deleted"); - store.clear(); - } + public void deleteAllContexts(String requestId) { + store.getAllKeys() + .filter(it -> it.startsWith(CONTEXT_KEY_PREFIX)) + .forEach(key -> { + transactionManager + .withTransaction( + requestId, + getContextNameFromContextKey(key), + (transaction) -> { + store.remove(key); + }); + logger().info("allContexts", "deleted"); + }); } - public Long createOrUpdateContextState(String contextName, Map properties) { - synchronized (store) { - var context = store.get(contextName) + public void createOrUpdateContextState(String requestId, String contextName, Map properties) { + transactionManager.withTransaction(requestId, contextName, (transaction) -> { + var contextKey = createContextKey(contextName); + var context = store.get(contextKey) .map(it -> (Context) it) - .map(it -> { - it.incUpdateCount(); - return it; - }).orElseGet(createNewContext(contextName)); + .orElseGet(createNewContext(contextName)); properties.forEach((k, v) -> { if (v.equals("null")) { context.getProperties().remove(k); @@ -95,38 +118,36 @@ public Long createOrUpdateContextState(String contextName, Map p logger().info(contextName, String.format("property '%s' updated", k)); } }); - store.put(contextName, context); - return context.getUpdateCount(); - } + transaction.recordWrite(context::incUpdateCount); + store.put(contextKey, context); + }); } - public Long createOrUpdateContextList(String contextName, Consumer>> consumer) { - synchronized (store) { - var context = store.get(contextName) + public void createOrUpdateContextList(String requestId, String contextName, Consumer>> consumer) { + transactionManager.withTransaction(requestId, contextName, (transaction) -> { + var contextKey = createContextKey(contextName); + var context = store.get(contextKey) .map(it -> (Context) it) - .map(it -> { - it.incUpdateCount(); - return it; - }).orElseGet(createNewContext(contextName)); + .orElseGet(createNewContext(contextName)); consumer.accept(context.getList()); - store.put(contextName, context); - return context.getUpdateCount(); - } + transaction.recordWrite(context::incUpdateCount); + store.put(contextKey, context); + }); } public Long numUpdates(String contextName) { - synchronized (store) { - return store.get(contextName).map(it -> ((Context) it).getUpdateCount()).orElse(0L); - } + return store.get(createContextKey(contextName)).map(it -> ((Context) it).getUpdateCount()).orElse(0L); + } + + private String getContextNameFromContextKey(String key) { + return key.substring(CONTEXT_KEY_PREFIX.length()); } - public Long numReads(String contextName) { - synchronized (store) { - return store.get(contextName).map(it -> ((Context) it).getMatchCount()).orElse(0L); - } + public String createContextKey(String contextName) { + return CONTEXT_KEY_PREFIX + contextName; } private Optional getSafeContextCopy(String contextName) { - return store.get(contextName).map(it -> (Context) it).map(Context::new); + return store.get(createContextKey(contextName)).map(it -> (Context) it).map(Context::new); } } diff --git a/src/main/java/org/wiremock/extensions/state/internal/ExtensionLogger.java b/src/main/java/org/wiremock/extensions/state/internal/ExtensionLogger.java index f2cae93..71fb1c3 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/ExtensionLogger.java +++ b/src/main/java/org/wiremock/extensions/state/internal/ExtensionLogger.java @@ -1,5 +1,22 @@ +/* + * 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.internal; +import org.wiremock.extensions.state.internal.model.Context; + import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; public class ExtensionLogger { diff --git a/src/main/java/org/wiremock/extensions/state/internal/TransactionManager.java b/src/main/java/org/wiremock/extensions/state/internal/TransactionManager.java new file mode 100644 index 0000000..40e7595 --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/internal/TransactionManager.java @@ -0,0 +1,85 @@ +/* + * 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.internal; + +import com.github.tomakehurst.wiremock.store.Store; +import org.wiremock.extensions.state.internal.model.Transaction; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +public class TransactionManager { + + private final String TRANSACTION_KEY_PREFIX = "transaction:"; + private final Store store; + + public TransactionManager(Store store) { + this.store = store; + } + + public T withTransaction(String requestId, String contextName, Function function) { + var transactionKey = createTransactionKey(requestId); + synchronized (store) { + @SuppressWarnings("unchecked") var requestTransactions = store.get(transactionKey).map(it -> (Map) it).orElse(new HashMap<>()); + var contextTransaction = requestTransactions.getOrDefault(contextName, new Transaction(contextName)); + try { + return function.apply(contextTransaction); + } finally { + requestTransactions.put(contextName, contextTransaction); + store.put(transactionKey, requestTransactions); + } + } + } + + public void withTransaction(String requestId, String contextName, Consumer consumer) { + var transactionKey = createTransactionKey(requestId); + synchronized (store) { + @SuppressWarnings("unchecked") var requestTransactions = store.get(transactionKey).map(it -> (Map) it).orElse(new HashMap<>()); + var contextTransaction = requestTransactions.getOrDefault(contextName, new Transaction(contextName)); + try { + consumer.accept(contextTransaction); + } finally { + requestTransactions.put(contextName, contextTransaction); + store.put(transactionKey, requestTransactions); + } + } + } + + public void deleteTransaction(String requestId, String contextName) { + var transactionKey = createTransactionKey(requestId); + synchronized (store) { + @SuppressWarnings("unchecked") var requestTransactions = store.get(transactionKey).map(it -> (Map) it).orElse(new HashMap<>()); + requestTransactions.remove(contextName); + } + } + + public Set getContextNamesByRequestId(String requestId) { + var transactionKey = createTransactionKey(requestId); + synchronized (store) { + @SuppressWarnings("unchecked") var requestTransactions = store.get(transactionKey).map(it -> (Map) it).orElse(new HashMap<>()); + return requestTransactions.keySet(); + } + } + + private String createTransactionKey(String requestId) { + return TRANSACTION_KEY_PREFIX + requestId; + } + + +} diff --git a/src/main/java/org/wiremock/extensions/state/internal/DeleteStateParameters.java b/src/main/java/org/wiremock/extensions/state/internal/api/DeleteStateParameters.java similarity index 98% rename from src/main/java/org/wiremock/extensions/state/internal/DeleteStateParameters.java rename to src/main/java/org/wiremock/extensions/state/internal/api/DeleteStateParameters.java index f4f18f4..406eba5 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/DeleteStateParameters.java +++ b/src/main/java/org/wiremock/extensions/state/internal/api/DeleteStateParameters.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.wiremock.extensions.state.internal; +package org.wiremock.extensions.state.internal.api; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/src/main/java/org/wiremock/extensions/state/internal/RecordStateParameters.java b/src/main/java/org/wiremock/extensions/state/internal/api/RecordStateParameters.java similarity index 97% rename from src/main/java/org/wiremock/extensions/state/internal/RecordStateParameters.java rename to src/main/java/org/wiremock/extensions/state/internal/api/RecordStateParameters.java index 94b69ac..0dc95a5 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/RecordStateParameters.java +++ b/src/main/java/org/wiremock/extensions/state/internal/api/RecordStateParameters.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.wiremock.extensions.state.internal; +package org.wiremock.extensions.state.internal.api; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/src/main/java/org/wiremock/extensions/state/internal/StateRequestMatcherParameters.java b/src/main/java/org/wiremock/extensions/state/internal/api/StateRequestMatcherParameters.java similarity index 98% rename from src/main/java/org/wiremock/extensions/state/internal/StateRequestMatcherParameters.java rename to src/main/java/org/wiremock/extensions/state/internal/api/StateRequestMatcherParameters.java index d5b8d4e..1d68ee8 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/StateRequestMatcherParameters.java +++ b/src/main/java/org/wiremock/extensions/state/internal/api/StateRequestMatcherParameters.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.wiremock.extensions.state.internal; +package org.wiremock.extensions.state.internal.api; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/src/main/java/org/wiremock/extensions/state/internal/Context.java b/src/main/java/org/wiremock/extensions/state/internal/model/Context.java similarity index 76% rename from src/main/java/org/wiremock/extensions/state/internal/Context.java rename to src/main/java/org/wiremock/extensions/state/internal/model/Context.java index c97a242..e354012 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/Context.java +++ b/src/main/java/org/wiremock/extensions/state/internal/model/Context.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.wiremock.extensions.state.internal; +package org.wiremock.extensions.state.internal.model; import java.util.HashMap; import java.util.LinkedList; @@ -22,13 +22,11 @@ public class Context { - private static final int MAX_IDS = 10; private final String contextName; private final Map properties = new HashMap<>(); private final LinkedList> list = new LinkedList<>(); private final LinkedList requests = new LinkedList<>(); - private Long updateCount = 1L; - private Long matchCount = 0L; + private Long updateCount = 0L; public Context(Context other) { this.contextName = other.contextName; @@ -36,7 +34,6 @@ public Context(Context other) { this.list.addAll(other.list.stream().map(HashMap::new).collect(Collectors.toList())); this.requests.addAll(other.requests); this.updateCount = other.updateCount; - this.matchCount = other.matchCount; } public Context(String contextName) { @@ -51,28 +48,11 @@ public Long getUpdateCount() { return updateCount; } - public Long getMatchCount() { - return matchCount; - } - public Long incUpdateCount() { updateCount = updateCount + 1; return updateCount; } - public Long incMatchCount(String requestId) { - if (requests.contains(requestId)) { - return matchCount; - } else { - requests.add(requestId); - if (requests.size() > MAX_IDS) { - requests.removeFirst(); - } - matchCount = matchCount + 1; - return matchCount; - } - } - public Map getProperties() { return properties; } diff --git a/src/main/java/org/wiremock/extensions/state/internal/ContextTemplateModel.java b/src/main/java/org/wiremock/extensions/state/internal/model/ContextTemplateModel.java similarity index 88% rename from src/main/java/org/wiremock/extensions/state/internal/ContextTemplateModel.java rename to src/main/java/org/wiremock/extensions/state/internal/model/ContextTemplateModel.java index 8f35426..691e7a4 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/ContextTemplateModel.java +++ b/src/main/java/org/wiremock/extensions/state/internal/model/ContextTemplateModel.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.wiremock.extensions.state.internal; +package org.wiremock.extensions.state.internal.model; +@SuppressWarnings("unused") public final class ContextTemplateModel { private final Context context; @@ -28,10 +29,6 @@ public static ContextTemplateModel from(Context context) { } - public Long getReadCount() { - return context.getMatchCount(); - } - public Long getUpdateCount() { return context.getUpdateCount(); } diff --git a/src/main/java/org/wiremock/extensions/state/internal/ResponseTemplateModel.java b/src/main/java/org/wiremock/extensions/state/internal/model/ResponseTemplateModel.java similarity index 96% rename from src/main/java/org/wiremock/extensions/state/internal/ResponseTemplateModel.java rename to src/main/java/org/wiremock/extensions/state/internal/model/ResponseTemplateModel.java index f09c458..87c98b1 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/ResponseTemplateModel.java +++ b/src/main/java/org/wiremock/extensions/state/internal/model/ResponseTemplateModel.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.wiremock.extensions.state.internal; +package org.wiremock.extensions.state.internal.model; import com.github.tomakehurst.wiremock.common.ListOrSingle; import com.github.tomakehurst.wiremock.http.LoggedResponse; diff --git a/src/main/java/org/wiremock/extensions/state/internal/model/Transaction.java b/src/main/java/org/wiremock/extensions/state/internal/model/Transaction.java new file mode 100644 index 0000000..05b59c3 --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/internal/model/Transaction.java @@ -0,0 +1,37 @@ +/* + * 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.internal.model; + +import java.util.function.Consumer; + +public class Transaction { + private final String contextName; + private Boolean writeRecorded = false; + + public Transaction(String contextName) { + this.contextName = contextName; + } + + public void recordWrite(Runnable runnable) { + if(!writeRecorded) { + runnable.run(); + writeRecorded = true; + } + } + public String getContextName() { + return contextName; + } +} diff --git a/src/test/java/org/wiremock/extensions/state/examples/StubMappingLoadingExampleTest.java b/src/test/java/org/wiremock/extensions/state/examples/StubMappingLoadingExampleTest.java index e6d6a81..21d229c 100644 --- a/src/test/java/org/wiremock/extensions/state/examples/StubMappingLoadingExampleTest.java +++ b/src/test/java/org/wiremock/extensions/state/examples/StubMappingLoadingExampleTest.java @@ -18,6 +18,7 @@ import org.wiremock.extensions.state.CaffeineStore; import org.wiremock.extensions.state.StateExtension; import org.wiremock.extensions.state.internal.ContextManager; +import org.wiremock.extensions.state.internal.TransactionManager; import java.net.URI; import java.time.Duration; @@ -40,7 +41,8 @@ public class StubMappingLoadingExampleTest { private static WireMockServer wireMockServer; private static final Store store = new CaffeineStore(); - static final ContextManager contextManager = new ContextManager(store); + private static final TransactionManager transactionManager = new TransactionManager(store); + private static final ContextManager contextManager = new ContextManager(store, transactionManager); @BeforeAll @@ -57,7 +59,6 @@ public static void initWithTempDir() { wireMockServer = new WireMockServer(options); wireMockServer.start(); WireMock.configureFor(wireMockServer.port()); - Stubbing wm = wireMockServer; Locale.setDefault(Locale.ENGLISH); WireMock.create().port(wireMockServer.port()).build(); @@ -76,7 +77,7 @@ void testSimpleContext() throws Exception { .statusCode(HttpStatus.SC_CREATED); awaitAndAssert(() -> Assertions.assertThat(contextManager.numUpdates(contextId)).isEqualTo(1)); - assertThat(store.get(contextId).isPresent(), is(true)); + assertThat(store.get(contextManager.createContextKey(contextId)).isPresent(), is(true)); given() @@ -87,7 +88,7 @@ void testSimpleContext() throws Exception { .body("result", equalTo(entityId)); awaitAndAssert(() -> Assertions.assertThat(contextManager.numUpdates(contextId)).isEqualTo(1)); - assertThat(store.get(contextId).isPresent(), is(true)); + assertThat(store.get(contextManager.createContextKey(contextId)).isPresent(), is(true)); given() .accept(ContentType.JSON) @@ -96,7 +97,7 @@ void testSimpleContext() throws Exception { .statusCode(HttpStatus.SC_OK); awaitAndAssert(() -> Assertions.assertThat(contextManager.numUpdates(contextId)).isEqualTo(0)); - assertThat(store.get(contextId).isPresent(), is(false)); + assertThat(store.get(contextManager.createContextKey(contextId)).isPresent(), is(false)); } @Test diff --git a/src/test/java/org/wiremock/extensions/state/functionality/AbstractTestBase.java b/src/test/java/org/wiremock/extensions/state/functionality/AbstractTestBase.java index c21ad3e..2a4a74f 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/AbstractTestBase.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/AbstractTestBase.java @@ -28,8 +28,10 @@ import org.wiremock.extensions.state.CaffeineStore; import org.wiremock.extensions.state.StateExtension; import org.wiremock.extensions.state.internal.ContextManager; +import org.wiremock.extensions.state.internal.TransactionManager; import java.time.Duration; +import java.util.UUID; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -41,7 +43,8 @@ public class AbstractTestBase { protected static final ObjectMapper mapper = new ObjectMapper(); protected static final CaffeineStore store = new CaffeineStore(); - protected static final ContextManager contextManager = new ContextManager(store); + protected static final TransactionManager transactionManager = new TransactionManager(store); + protected static final ContextManager contextManager = new ContextManager(store, transactionManager); @RegisterExtension public static WireMockExtension wm = WireMockExtension.newInstance() @@ -60,7 +63,7 @@ void setupAll() { @BeforeEach void setupBase() { wm.resetAll(); - contextManager.deleteAllContexts(); + contextManager.deleteAllContexts(UUID.randomUUID().toString()); } protected void assertContextNumUpdates(String context, int expected) { diff --git a/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java b/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java index 54467fd..6fd301a 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java @@ -30,6 +30,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -159,13 +160,14 @@ public void setup() { postContext(contextName, Map.of(property, valueOne)); postContext(contextName, Map.of(property, valueTwo)); postContext(contextName, Map.of(property, valueThree)); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(3); assertThat(context.getList().get(0)).containsEntry(property, valueOne); assertThat(context.getList().get(1)).containsEntry(property, valueTwo); assertThat(context.getList().get(2)).containsEntry(property, valueThree); + assertThat(context.getUpdateCount()).isEqualTo(3); }); } @@ -183,17 +185,27 @@ public void setup() { void test_deleteFirstOne() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueOne))); } + @DisplayName("updates numUpdates") + @Test + void test_updatesCounter() { + getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); + + assertThat(contextManager.getContextCopy(contextName)) + .isPresent() + .hasValueSatisfying((context) -> assertThat(context.getUpdateCount()).isEqualTo(4)); + } + @DisplayName("does not delete other entries") @Test void test_doesNotDeleteOther() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -209,7 +221,7 @@ void test_canDeleteAll() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).isEmpty()); } @@ -225,7 +237,7 @@ void test_canDeleteReAdded() { postContext(contextName, Map.of(property, valueTwo)); postContext(contextName, Map.of(property, valueFour)); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -249,17 +261,27 @@ public void setup() { void test_deleteLastOne() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueThree))); } + @DisplayName("updates numUpdates") + @Test + void test_updatesCounter() { + getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); + + assertThat(contextManager.getContextCopy(contextName)) + .isPresent() + .hasValueSatisfying((context) -> assertThat(context.getUpdateCount()).isEqualTo(4)); + } + @DisplayName("does not delete other entries") @Test void test_doesNotDeleteOther() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -275,7 +297,7 @@ void test_canDeleteAll() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).isEmpty()); } @@ -291,7 +313,7 @@ void test_canDeleteReAdded() { postContext(contextName, Map.of(property, valueTwo)); postContext(contextName, Map.of(property, valueFour)); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -315,17 +337,28 @@ public void setup() { void test_deleteIndexZero() { getContext(contextName + "/0", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueOne))); } + @DisplayName("updates numUpdates") + @Test + void test_updatesCounter() { + getContext(contextName + "/0", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); + + assertThat(contextManager.getContextCopy(contextName)) + .isPresent() + .hasValueSatisfying((context) -> assertThat(context.getUpdateCount()).isEqualTo(4)); + } + + @DisplayName("can delete middle entry") @Test void test_deleteIndexOne() { getContext(contextName + "/1", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueTwo))); } @@ -335,7 +368,7 @@ void test_deleteIndexOne() { void test_deleteIndexTwo() { getContext(contextName + "/2", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueThree))); } @@ -347,7 +380,7 @@ void test_doesNotDeleteOther() { getContext(contextName + "/1", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -363,7 +396,7 @@ void test_canDeleteAll() { getContext(contextName + "/1", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); getContext(contextName + "/0", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).isEmpty()); } @@ -379,7 +412,7 @@ void test_canDeleteReAdded() { postContext(contextName, Map.of(property, valueTwo)); postContext(contextName, Map.of(property, valueFour)); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -403,17 +436,28 @@ public void setup() { void test_deleteIndexZero() { getContext(contextName + "/" + valueOne, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueOne))); } + @DisplayName("updates numUpdates") + @Test + void test_updatesCounter() { + getContext(contextName + "/" + valueOne, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); + + assertThat(contextManager.getContextCopy(contextName)) + .isPresent() + .hasValueSatisfying((context) -> assertThat(context.getUpdateCount()).isEqualTo(4)); + } + + @DisplayName("can delete middle entry") @Test void test_deleteIndexOne() { getContext(contextName + "/" + valueTwo, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueTwo))); } @@ -423,7 +467,7 @@ void test_deleteIndexOne() { void test_deleteIndexTwo() { getContext(contextName + "/" + valueThree, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).noneSatisfy(it -> assertThat(it).containsEntry(property, valueThree))); } @@ -435,7 +479,7 @@ void test_doesNotDeleteOther() { getContext(contextName + "/" + valueTwo, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -451,7 +495,7 @@ void test_canDeleteAll() { getContext(contextName + "/" + valueOne, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); getContext(contextName + "/" + valueThree, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> assertThat(context.getList()).isEmpty()); } @@ -467,7 +511,7 @@ void test_canDeleteReAdded() { postContext(contextName, Map.of(property, valueTwo)); postContext(contextName, Map.of(property, valueFour)); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying((context) -> { assertThat(context.getList()).hasSize(2); @@ -502,7 +546,7 @@ void setup() { void test_deleteContext() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)).isEmpty(); + assertThat(contextManager.getContextCopy(contextName)).isEmpty(); } @DisplayName("double deletion does not cause an error") @@ -511,7 +555,7 @@ void test_deleteContextTwice() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)).isEmpty(); + assertThat(contextManager.getContextCopy(contextName)).isEmpty(); } @DisplayName("does not delete other contexts") @@ -519,7 +563,7 @@ void test_deleteContextTwice() { void test_doesNotDeleteOther() { getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(otherContextName)).isPresent(); + assertThat(contextManager.getContextCopy(otherContextName)).isPresent(); } } @@ -537,9 +581,9 @@ void setup() { postContext(contextNameOne, Map.of()); postContext(contextNameTwo, Map.of()); postContext(contextNameThree, Map.of()); - assertThat(contextManager.getContext(contextNameOne)).isPresent(); - assertThat(contextManager.getContext(contextNameTwo)).isPresent(); - assertThat(contextManager.getContext(contextNameThree)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameOne)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameThree)).isPresent(); } @@ -550,7 +594,7 @@ void test_deleteContextsSingle() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameTwo)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isEmpty(); } @DisplayName("deletes multiple context") @@ -560,9 +604,9 @@ void test_deleteContextsMultiple() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameOne)).isEmpty(); - assertThat(contextManager.getContext(contextNameTwo)).isPresent(); - assertThat(contextManager.getContext(contextNameThree)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameOne)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameThree)).isEmpty(); } @DisplayName("deletes all context") @@ -572,7 +616,7 @@ void test_deleteContextsAll() { getContext(contextNameTwo, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameTwo)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isEmpty(); } @DisplayName("double deletion does not cause an error") @@ -582,9 +626,9 @@ void test_deleteContextsTwice() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameTwo)).isEmpty(); - assertThat(contextManager.getContext(contextNameOne)).isPresent(); - assertThat(contextManager.getContext(contextNameThree)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameOne)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameThree)).isPresent(); } @DisplayName("does not delete other contexts") @@ -594,9 +638,9 @@ void test_doesNotDeleteOther() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameTwo)).isEmpty(); - assertThat(contextManager.getContext(contextNameOne)).isPresent(); - assertThat(contextManager.getContext(contextNameThree)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameOne)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameThree)).isPresent(); } } @@ -614,9 +658,9 @@ void setup() { postContext(contextNameOne, Map.of()); postContext(contextNameTwo, Map.of()); postContext(contextNameThree, Map.of()); - assertThat(contextManager.getContext(contextNameOne)).isPresent(); - assertThat(contextManager.getContext(contextNameTwo)).isPresent(); - assertThat(contextManager.getContext(contextNameThree)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameOne)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameThree)).isPresent(); } @@ -627,7 +671,7 @@ void test_deleteContextsSingle() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameOne)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameOne)).isEmpty(); } @DisplayName("deletes multiple context") @@ -637,9 +681,9 @@ void test_deleteContextsMultiple() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameOne)).isPresent(); - assertThat(contextManager.getContext(contextNameTwo)).isEmpty(); - assertThat(contextManager.getContext(contextNameThree)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameOne)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameThree)).isEmpty(); } @DisplayName("deletes all context") @@ -649,9 +693,9 @@ void test_deleteContextsAll() { getContext(contextNameTwo, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameOne)).isEmpty(); - assertThat(contextManager.getContext(contextNameTwo)).isEmpty(); - assertThat(contextManager.getContext(contextNameThree)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameOne)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameThree)).isEmpty(); } @DisplayName("double deletion does not cause an error") @@ -661,9 +705,9 @@ void test_deleteContextsTwice() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameOne)).isEmpty(); - assertThat(contextManager.getContext(contextNameTwo)).isPresent(); - assertThat(contextManager.getContext(contextNameThree)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameOne)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameThree)).isPresent(); } @DisplayName("does not delete other contexts") @@ -673,9 +717,9 @@ void test_doesNotDeleteOther() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextNameTwo)).isEmpty(); - assertThat(contextManager.getContext(contextNameOne)).isPresent(); - assertThat(contextManager.getContext(contextNameThree)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameTwo)).isEmpty(); + assertThat(contextManager.getContextCopy(contextNameOne)).isPresent(); + assertThat(contextManager.getContextCopy(contextNameThree)).isPresent(); } } } @@ -704,7 +748,7 @@ public void test_ignoreUnknownExtraProperty() { ); getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)).isEmpty(); + assertThat(contextManager.getContextCopy(contextName)).isEmpty(); } private void assertListConfigurationError() { @@ -746,7 +790,7 @@ public void test_missingArray() { assertContextDeletionConfigurationError(); - assertThat(contextManager.getContext(contextName)).isPresent(); + assertThat(contextManager.getContextCopy(contextName)).isPresent(); } @DisplayName("ignores empty array") @@ -756,7 +800,7 @@ public void test_emptyArray() { getContext("any", HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(contextName)).isPresent(); + assertThat(contextManager.getContextCopy(contextName)).isPresent(); } } @@ -773,7 +817,7 @@ public void test_missingConfig() { assertContextDeletionConfigurationError(); - assertThat(contextManager.getContext(contextName)).isPresent(); + assertThat(contextManager.getContextCopy(contextName)).isPresent(); } @DisplayName("fails on invalid regex") @@ -786,7 +830,7 @@ public void test_invalidRegex() { assertContextDeletionConfigurationError(); - assertThat(contextManager.getContext(contextName)).isPresent(); + assertThat(contextManager.getContextCopy(contextName)).isPresent(); } } @@ -936,14 +980,14 @@ public List test_doesNotDeleteOther() { return checks.entrySet().stream().map(entry -> DynamicTest.dynamicTest(entry.getKey(), () -> { wm.resetAll(); - contextManager.deleteAllContexts(); + contextManager.deleteAllContexts(UUID.randomUUID().toString()); createPostStubList(Map.of("listValue", "{{jsonPath request.body '$.listValue'}}")); createGetStubList(Map.of(entry.getKey(), entry.getValue())); postContext(otherContextName, body); - assertThat(contextManager.getContext(otherContextName)).isPresent(); + assertThat(contextManager.getContextCopy(otherContextName)).isPresent(); getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(otherContextName)).isPresent().hasValueSatisfying(it -> { + assertThat(contextManager.getContextCopy(otherContextName)).isPresent().hasValueSatisfying(it -> { assertThat(it.getList()).hasSize(1).first().asInstanceOf(MAP).containsAllEntriesOf(body); }); }) @@ -976,7 +1020,7 @@ public void test_doesNotDeleteOther() { postContext(otherContextName, Map.of()); getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); - assertThat(contextManager.getContext(otherContextName)).isPresent(); + assertThat(contextManager.getContextCopy(otherContextName)).isPresent(); } } } diff --git a/src/test/java/org/wiremock/extensions/state/functionality/RecordStateEventListenerTest.java b/src/test/java/org/wiremock/extensions/state/functionality/RecordStateEventListenerTest.java index cb4d2ef..efee3e2 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/RecordStateEventListenerTest.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/RecordStateEventListenerTest.java @@ -22,7 +22,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -34,7 +33,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; class RecordStateEventListenerTest extends AbstractTestBase { @@ -43,6 +41,7 @@ class RecordStateEventListenerTest extends AbstractTestBase { void setup() { createStatePostStub(); createListPostStub(); + createStateAndListPostStub(); } private void postRequest(String path, String contextValueOne, String contextValueTwo) { @@ -124,6 +123,45 @@ private void createListPostStub() { ); } + private void createStateAndListPostStub() { + wm.stubFor( + WireMock.post(urlPathMatching("/stateAndList/[^/]+")) + .willReturn( + WireMock.ok() + .withHeader("content-type", "application/json") + .withBody("{}") + ) + .withServeEventListener( + "recordState", + Parameters.from( + Map.of( + "context", "{{request.pathSegments.[1]}}", + "list", Map.of( + "addFirst", Map.of( + "stateValueOne", "{{jsonPath request.body '$.contextValueOne'}}", + "stateValueTwo", "{{jsonPath request.body '$.contextValueTwo'}}" + ) + ) + ) + ) + ) + .withServeEventListener( + "recordState", + Parameters.from( + Map.of( + "context", "{{request.pathSegments.[1]}}", + "state", Map.of( + "stateValueOne", "{{jsonPath request.body '$.contextValueOne'}}", + "stateValueTwoWithoutDefault", "{{jsonPath request.body '$.contextValueTwo'}}", + "stateValueTwoWithDefault", "{{jsonPath request.body '$.contextValueTwo' default='stateValueTwoDefaultValue'}}", + "previousStateValueTwo", "{{state context=request.pathSegments.[1] property='stateValueTwoWithoutDefault' default='noPrevious'}}" + ) + ) + ) + ) + ); + } + @Nested public class State { @@ -148,7 +186,7 @@ public void test_stateUsesNullOrDefaultIfNoValueIsMissingSpecified_ok() { assertThat(contextManager.numUpdates(contextName)).isEqualTo(1); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying(it -> { assertThat(it.getProperties()) @@ -168,7 +206,7 @@ public void test_stateIsOverwritten_ok() { assertThat(contextManager.numUpdates(contextName)).isEqualTo(2); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying(it -> { assertThat(it.getProperties()) @@ -189,7 +227,7 @@ public void test_stateCanAccessPreviousState_ok() { assertThat(contextManager.numUpdates(contextName)).isEqualTo(2); - assertThat(contextManager.getContext(contextName)) + assertThat(contextManager.getContextCopy(contextName)) .isPresent() .hasValueSatisfying(it -> { assertThat(it.getProperties()) @@ -217,7 +255,7 @@ public void test_otherStateIsWritten_ok() { } private void assertContext(String contextNameTwo, String stateValueOne, String stateValueTwoWithoutDefault, String stateValueTwoWithDefault, String statePrevious) { - assertThat(contextManager.getContext(contextNameTwo)) + assertThat(contextManager.getContextCopy(contextNameTwo)) .isPresent() .hasValueSatisfying(it -> { assertThat(it.getList()).isEmpty(); @@ -264,7 +302,7 @@ public void test_stateIsAppended_ok() { } private void assertContext(String contextNameTwo, Integer size, Integer index, String stateValueOne, String stateValueTwo) { - assertThat(contextManager.getContext(contextNameTwo)) + assertThat(contextManager.getContextCopy(contextNameTwo)) .isPresent() .hasValueSatisfying(it -> { assertThat(it.getList()) @@ -323,11 +361,22 @@ void test_differentContext_ok() { assertContextNumUpdates(contextTwo, 1); } - @Disabled + @Test + void test_addListEntry_ok() { + var contextOne = RandomStringUtils.randomAlphabetic(5); + + postRequest("list", contextOne, "one"); + postRequest("list", contextOne, "one"); + + assertContextNumUpdates("first-" + contextOne, 2); + } + @DisplayName("update count only increased by one when both property and list are updated") @Test void test_updatePropertyAndList_incOne() { - throw new NotImplementedException(); + var context = RandomStringUtils.randomAlphabetic(5); + postRequest("stateAndList", context, "one"); + assertContextNumUpdates(context, 1); } } } \ No newline at end of file diff --git a/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java b/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java index 43d50e0..8cf6909 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java @@ -534,7 +534,7 @@ public class UpdateCountEqualTo { @DisplayName("succeeds on matching count") @Test void test_countMatches_ok() { - createGetStub("updateCountEqualTo", "4"); + createGetStub("updateCountEqualTo", "2"); getAndAssertContextMatcher(context, HttpStatus.SC_OK); } @@ -550,7 +550,7 @@ void test_countInvalid_fail() { @DisplayName("fails on non-matching count") @Test void test_countDoesNotMatch_fail() { - createGetStub("updateCountEqualTo", "2"); + createGetStub("updateCountEqualTo", "1"); getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); } @@ -563,7 +563,7 @@ public class UpdateCountLessThan { @DisplayName("succeeds on matching count") @Test void test_countMatches_ok() { - createGetStub("updateCountLessThan", "5"); + createGetStub("updateCountLessThan", "3"); getAndAssertContextMatcher(context, HttpStatus.SC_OK); } @@ -579,7 +579,7 @@ void test_countInvalid_fail() { @DisplayName("fails on non-matching count") @Test void test_countDoesNotMatch_fail() { - createGetStub("updateCountLessThan", "4"); + createGetStub("updateCountLessThan", "2"); getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); } @@ -592,7 +592,7 @@ public class UpdateCountMoreThan { @DisplayName("succeeds on matching count") @Test void test_countMatches_ok() { - createGetStub("updateCountMoreThan", "3"); + createGetStub("updateCountMoreThan", "1"); getAndAssertContextMatcher(context, HttpStatus.SC_OK); } @@ -608,7 +608,7 @@ void test_countInvalid_fail() { @DisplayName("fails on non-matching count") @Test void test_countDoesNotMatch_fail() { - createGetStub("updateCountMoreThan", "4"); + createGetStub("updateCountMoreThan", "2"); getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); } 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 59eb1b2..da623bb 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/StateTemplateHelperProviderExtensionTest.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/StateTemplateHelperProviderExtensionTest.java @@ -286,7 +286,7 @@ public class MissingProperty { public void setup() { createContextStatePostStub(Map.of()); postContext(contextName, Map.of()); - assertThat(contextManager.getContext(contextName)).isPresent(); + assertThat(contextManager.getContextCopy(contextName)).isPresent(); } @DisplayName("when no default is specified returns empty string") @@ -331,7 +331,7 @@ public class MissingList { public void setup() { createContextStatePostStub(Map.of()); postContext(contextName, Map.of()); - assertThat(contextManager.getContext(contextName)).isPresent(); + assertThat(contextManager.getContextCopy(contextName)).isPresent(); } @DisplayName("when accessing first element")