Skip to content

Commit

Permalink
Allow accessing the current state in this extension's configuration o…
Browse files Browse the repository at this point in the history
…ptions (#21)

- create common initialization for standalone and manual registration of
      this extension
- register all handlers of this extension in the common template engine
      of this extension
- extend documentation

## References

none

<!-- References to relevant GitHub issues and pull requests, esp.
upstream and downstream changes -->

## 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)

<!--
Put an `x` into the [ ] to show you have filled the information.
The template comes from
https://github.com/wiremock/.github/blob/main/.github/pull_request_template.md
You can override it by creating .github/pull_request_template.md in your
own repository
-->
  • Loading branch information
dirkbolte authored Jul 28, 2023
1 parent a4ef05b commit c442f31
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 49 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,46 @@ To append a state to a list:
}
```

### Accessing the previous state

You can use the `state` helper to temporarily access the previous state. Use the `state` helper in the same way as you would use it when you [retrieve a state](#retrieve-a-state).

**Note:** This extension does not keep a history in itself but it's an effect of the evaluation order.
As templates are evaluated before the state is written, the state you access in `recordState` is the one before you store the new one
(so there might be none - you might want to use `default` for these cases). In case you have multiple `recordState` `serveEventListeners`, you will have new states
being created in between, thus the previous state is the last stored one (so: not the one before the request).

1. listener 1 is executed
1. accesses state n
2. stores state n+1
2. listener 2 is executed
1. accesses state n+1
2. stores state n+2

The evaluation order of listeners within a stub as well as across stubs is not guaranteed.

```json
{
"request": {},
"response": {},
"serveEventListeners": [
{
"name": "recordState",
"parameters": {
"context": "{{jsonPath response.body '$.id'}}",
"state": {
"id": "{{jsonPath response.body '$.id'}}",
"firstName": "{{jsonPath request.body '$.firstName'}}",
"lastName": "{{jsonPath request.body '$.lastName'}}",
"birthName": "{{state context='$.id' property='lastName' default=''}}"
}
}
}
]
}
```


## Deleting a state

Similar to recording a state, its deletion can be initiated in `serveEventListeners` of a stub.
Expand Down Expand Up @@ -660,6 +700,9 @@ You have to choose either `property` or `list` (otherwise, you will get a config

To retrieve a full body, use: `{{{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.

### 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,42 +15,16 @@
*/
package org.wiremock.extensions.state;

import com.github.tomakehurst.wiremock.extension.Extension;
import com.github.tomakehurst.wiremock.extension.ExtensionFactory;
import com.github.tomakehurst.wiremock.extension.WireMockServices;
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
import com.github.tomakehurst.wiremock.store.Store;
import org.wiremock.extensions.state.extensions.DeleteStateEventListener;
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.internal.ContextManager;

import java.util.Collections;
import java.util.List;

/**
* Factory to register all extensions for handling state for standalone service.
*
* Uses {@link org.wiremock.extensions.state.CaffeineStore}.
* <p>
* Uses {@link org.wiremock.extensions.state.CaffeineStore} as store.
*
* @see CaffeineStore
*/
public class StandaloneStateExtension implements ExtensionFactory {

private final TemplateEngine templateEngine = new TemplateEngine(Collections.emptyMap(), null, Collections.emptySet(), false);

private final Store<String, Object> store = new CaffeineStore();

private final ContextManager contextManager = new ContextManager(store);;
public class StandaloneStateExtension extends StateExtension {

@Override
public List<Extension> create(WireMockServices services) {
return List.of(
new RecordStateEventListener(contextManager, templateEngine),
new DeleteStateEventListener(contextManager, templateEngine),
new StateRequestMatcher(contextManager, templateEngine),
new StateTemplateHelperProviderExtension(contextManager)
);
public StandaloneStateExtension() {
super(new CaffeineStore());
}
}
23 changes: 15 additions & 8 deletions src/main/java/org/wiremock/extensions/state/StateExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,28 @@
*/
public class StateExtension implements ExtensionFactory {

private final TemplateEngine templateEngine = new TemplateEngine(Collections.emptyMap(), null, Collections.emptySet(), false);

private final ContextManager contextManager;
private final StateTemplateHelperProviderExtension stateTemplateHelperProviderExtension;
private final RecordStateEventListener recordStateEventListener;
private final DeleteStateEventListener deleteStateEventListener;
private final StateRequestMatcher stateRequestMatcher;

public StateExtension(Store<String, Object> store) {
this.contextManager = new ContextManager(store);
var contextManager = new ContextManager(store);
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.stateRequestMatcher = new StateRequestMatcher(contextManager, templateEngine);
}

@Override
public List<Extension> create(WireMockServices services) {
return List.of(
new RecordStateEventListener(contextManager, templateEngine),
new DeleteStateEventListener(contextManager, templateEngine),
new StateRequestMatcher(contextManager, templateEngine),
new StateTemplateHelperProviderExtension(contextManager)
recordStateEventListener,
deleteStateEventListener,
stateRequestMatcher,
stateTemplateHelperProviderExtension
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Context {

Expand All @@ -30,6 +30,15 @@ public class Context {
private Long updateCount = 1L;
private Long matchCount = 0L;

public Context(Context other) {
this.contextName = other.contextName;
this.properties.putAll(other.properties);
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) {
this.contextName = contextName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@ public Object getState(String contextName, String property) {
}
}

/**
* 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<Context> getContext(String contextName) {
synchronized (store) {
return store.get(contextName).map(it -> (Context) it);
return store.get(contextName).map(it -> (Context) it).map(Context::new);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ private void createStatePostStub() {
"context", "{{request.pathSegments.[1]}}",
"state", Map.of(
"stateValueOne", "{{jsonPath request.body '$.contextValueOne'}}",
"stateValueTwo", "{{jsonPath request.body '$.contextValueTwo'}}"
"stateValueTwo", "{{jsonPath request.body '$.contextValueTwo'}}",
"previousStateValueTwo", "{{state context=request.pathSegments.[1] property='stateValueTwo' default='noPrevious'}}"
)
)
)
Expand Down Expand Up @@ -130,7 +131,7 @@ public void test_stateIsWritten_ok() {
.pollInterval(Duration.ofMillis(10))
.atMost(Duration.ofSeconds(5)).untilAsserted(() -> assertThat(contextManager.numUpdates(contextName)).isEqualTo(1));

assertContext(contextName, contextName, "one");
assertContext(contextName, contextName, "one", "noPrevious");
}

@Test
Expand All @@ -145,7 +146,7 @@ public void test_stateIsOverwritten_ok() {
.pollInterval(Duration.ofMillis(10))
.atMost(Duration.ofSeconds(5)).untilAsserted(() -> assertThat(contextManager.numUpdates(contextName)).isEqualTo(2));

assertContext(contextName, contextName, "two");
assertContext(contextName, contextName, "two", "one");
}

@Test
Expand All @@ -163,19 +164,20 @@ public void test_otherStateIsWritten_ok() {
.pollInterval(Duration.ofMillis(10))
.atMost(Duration.ofSeconds(5)).untilAsserted(() -> assertThat(contextManager.numUpdates(contextNameTwo)).isEqualTo(1));

assertContext(contextNameOne, contextNameOne, "one");
assertContext(contextNameTwo, contextNameTwo, "two");
assertContext(contextNameOne, contextNameOne, "one", "noPrevious");
assertContext(contextNameTwo, contextNameTwo, "two", "noPrevious");
}

private void assertContext(String contextNameTwo, String stateValueOne, String stateValueTwo) {
private void assertContext(String contextNameTwo, String stateValueOne, String stateValueTwo, String statePrevious) {
assertThat(contextManager.getContext(contextNameTwo))
.isPresent()
.hasValueSatisfying(it -> {
assertThat(it.getList()).isEmpty();
assertThat(it.getProperties())
.hasSize(2)
.hasSize(3)
.containsEntry("stateValueOne", stateValueOne)
.containsEntry("stateValueTwo", stateValueTwo);
.containsEntry("stateValueTwo", stateValueTwo)
.containsEntry("previousStateValueTwo", statePrevious);
}
);
}
Expand Down

0 comments on commit c442f31

Please sign in to comment.