Skip to content

Commit c909edc

Browse files
authored
Adding state request matcher (#1)
- also add dependabot
1 parent 64e4f3a commit c909edc

File tree

7 files changed

+359
-12
lines changed

7 files changed

+359
-12
lines changed

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: gradle
4+
directory: "/"
5+
schedule:
6+
interval: daily
7+
time: "01:00"
8+
open-pull-requests-limit: 10
9+
assignees:
10+
- "dirkbolte"

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
name: Validate Gradle wrapper
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v2
16+
- uses: actions/checkout@v3
1717
- name: Validate Gradle wrapper
1818
uses: gradle/wrapper-validation-action@v1
1919
build:

README.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ the `GET` won't have any knowledge of the previous post.
5555
Two extensions have to be registered:
5656

5757
- `StateRecordingAction` to record any state in `postServeActions`
58-
- `ResponseTemplateTransformer` with `StateHelper` to retrieve a previously recorded state
58+
- `StateRequestMatcher` to match incoming request against a context using custom master `state-matcher`
59+
- `ResponseTemplateTransformer` with `StateHelper` to retrieve a previously recorded state for a context
5960

6061
```java
6162
public class MySandbox {
@@ -68,6 +69,7 @@ public class MySandbox {
6869
.dynamicPort()
6970
.extensions(
7071
stateRecordingAction,
72+
new StateRequestMatcher(stateRecordingAction),
7173
new ResponseTemplateTransformer(true, "state", new StateHelper(stateRecordingAction))
7274
)
7375
);
@@ -156,7 +158,52 @@ The default expiration is 60 minutes. The default value can be overwritten (`0`
156158

157159
```java
158160
int expiration=1024;
159-
var stateRecordingAction=new StateRecordingAction(expiration);
161+
var stateRecordingAction=new StateRecordingAction(expiration);
162+
```
163+
164+
## Match a request against a context
165+
166+
To have a WireMock stub only apply when there's actually a matching context, you can use the `StateRequestMatcher` . This helps to model different
167+
behavior for requests with and without a matching context. The parameter supports templates.
168+
169+
### Positive match
170+
171+
```json
172+
{
173+
"request": {
174+
"method": "GET",
175+
"urlPattern": "/test/[^\/]+",
176+
"customMatcher": {
177+
"name": "state-matcher",
178+
"parameters": {
179+
"hasContext": "{{request.pathSegments.[1]}}"
180+
}
181+
}
182+
},
183+
"response": {
184+
"status": 200
185+
}
186+
}
187+
```
188+
189+
### Negative match
190+
191+
```json
192+
{
193+
"request": {
194+
"method": "GET",
195+
"urlPattern": "/test/[^\/]+",
196+
"customMatcher": {
197+
"name": "state-matcher",
198+
"parameters": {
199+
"hasNotContext": "{{request.pathSegments.[1]}}"
200+
}
201+
}
202+
},
203+
"response": {
204+
"status": 400
205+
}
206+
}
160207
```
161208

162209
## Retrieve a state

src/main/java/com/github/dirkbolte/wiremock/state/StateRecordingAction.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
2828
import org.apache.commons.lang3.StringUtils;
2929

30+
import javax.swing.text.html.Option;
3031
import java.time.Duration;
3132
import java.util.Collections;
3233
import java.util.Map;
@@ -63,23 +64,28 @@ public void doAction(ServeEvent serveEvent, Admin admin, Parameters parameters)
6364
"response", ResponseTemplateModel.from(serveEvent.getResponse())
6465
);
6566
var context = createContext(model, parameters);
66-
storeState(context, model, parameters);
67+
storeContextAndState(context, model, parameters);
6768
}
6869

6970
@Override
7071
public String getName() {
7172
return "recordState";
7273
}
7374

74-
public Object getState(String context, String property) {
75+
Object getState(String context, String property) {
7576
return cache.getIfPresent(calculateKey(context, property));
7677
}
7778

78-
private void storeState(String context, Map<String, Object> model, Parameters parameters) {
79+
boolean hasContext(String context) {
80+
return cache.getIfPresent(context) != null;
81+
}
82+
83+
private void storeContextAndState(String context, Map<String, Object> model, Parameters parameters) {
7984
@SuppressWarnings("unchecked") Map<String, Object> state = Optional.ofNullable(parameters.get("state"))
8085
.filter(it -> it instanceof Map)
8186
.map(Map.class::cast)
8287
.orElseThrow(() -> new ConfigurationException("no state specified"));
88+
cache.put(context, context);
8389
state.entrySet()
8490
.stream()
8591
.map(entry -> Map.entry(entry.getKey(), renderTemplate(model, entry.getValue().toString())))
@@ -95,7 +101,7 @@ private String createContext(Map<String, Object> model, Parameters parameters) {
95101
return context;
96102
}
97103

98-
private String renderTemplate(Object context, String value) {
104+
String renderTemplate(Object context, String value) {
99105
return templateEngine.getUncachedTemplate(value).apply(context);
100106
}
101107

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.github.dirkbolte.wiremock.state;
2+
3+
import com.github.tomakehurst.wiremock.core.ConfigurationException;
4+
import com.github.tomakehurst.wiremock.extension.Parameters;
5+
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
6+
import com.github.tomakehurst.wiremock.http.Request;
7+
import com.github.tomakehurst.wiremock.matching.MatchResult;
8+
import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension;
9+
10+
import java.util.Map;
11+
import java.util.Optional;
12+
13+
public class StateRequestMatcher extends RequestMatcherExtension {
14+
15+
private final StateRecordingAction recordingAction;
16+
17+
public StateRequestMatcher(StateRecordingAction recordingAction) {
18+
this.recordingAction = recordingAction;
19+
}
20+
21+
@Override
22+
public String getName() {
23+
return "state-matcher";
24+
}
25+
26+
@Override
27+
public MatchResult match(Request request, Parameters parameters) {
28+
if (parameters.size() != 1) {
29+
throw new ConfigurationException("Parameters should only contain one entry ('hasContext' or 'hasNotContext'");
30+
}
31+
var model = Map.of("request", RequestTemplateModel.from(request));
32+
return Optional
33+
.ofNullable(parameters.getString("hasContext", null))
34+
.map(template -> hasContext(model, template))
35+
.or(() -> Optional.ofNullable(parameters.getString("hasNotContext", null)).map(template -> hasNotContext(model, template)))
36+
.orElseThrow(() -> new ConfigurationException("Parameters should only contain 'hasContext' or 'hasNotContext'"));
37+
38+
}
39+
40+
private MatchResult hasContext(Map<String, RequestTemplateModel> model, String template) {
41+
var context = recordingAction.renderTemplate(model, template);
42+
if (recordingAction.hasContext(context)) {
43+
return MatchResult.exactMatch();
44+
} else {
45+
return MatchResult.noMatch();
46+
}
47+
}
48+
49+
private MatchResult hasNotContext(Map<String, RequestTemplateModel> model, String template) {
50+
var context = recordingAction.renderTemplate(model, template);
51+
if (!recordingAction.hasContext(context)) {
52+
return MatchResult.exactMatch();
53+
} else {
54+
return MatchResult.noMatch();
55+
}
56+
}
57+
}

src/test/java/com/github/dirkbolte/wiremock/state/StateTest.java renamed to src/test/java/com/github/dirkbolte/wiremock/state/StateHelperTest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@
4444
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
4545
import static io.restassured.RestAssured.given;
4646
import static org.assertj.core.api.Assertions.assertThat;
47+
import static org.hamcrest.Matchers.equalTo;
4748
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
4849

4950
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
5051
@Execution(SAME_THREAD)
51-
class StateTest {
52+
class StateHelperTest {
5253

5354
private static final String TEST_URL = "/test";
5455
private static final StateRecordingAction stateRecordingAction = new StateRecordingAction();
@@ -94,7 +95,7 @@ void test_noExtensionUsage_ok() throws JsonProcessingException, URISyntaxExcepti
9495
.post(new URI(runtimeInfo.getHttpBaseUrl() + TEST_URL))
9596
.then()
9697
.statusCode(HttpStatus.SC_OK)
97-
.body("testKey", Matchers.equalTo("testValue"));
98+
.body("testKey", equalTo("testValue"));
9899
}
99100

100101
@Test
@@ -197,12 +198,10 @@ private void getAndAssertContextValue(String context, String contextValue) throw
197198
.get(new URI(String.format("%s%s/%s", wm.getRuntimeInfo().getHttpBaseUrl(), TEST_URL, context)))
198199
.then()
199200
.statusCode(HttpStatus.SC_OK)
200-
.body("value", Matchers.notNullValue())
201+
.body("value", equalTo(contextValue))
201202
.extract()
202203
.body()
203204
.jsonPath().get("value");
204-
205-
assertThat(responseValue).describedAs("context value is returned").isEqualTo(contextValue);
206205
}
207206

208207
private String postAndAssertContextValue(String contextValue) throws URISyntaxException {

0 commit comments

Comments
 (0)