Skip to content

Commit 46b6dd6

Browse files
authored
Adding state delete + migrate to new event system (#4)
* Adding state delete + migrate to new event system - bump wiremock version to 3.0.0-beta-11 - use new event system + new interfaces for everything - use ExtensionFactory for registering everything - adding state deleting event listener - use Store for caching + provide caffeine store - ensure that context interactions are synchronized to avoid race conditions - updated documentation
1 parent 8e63620 commit 46b6dd6

17 files changed

+1047
-159
lines changed

README.md

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -50,37 +50,33 @@ the `GET` won't have any knowledge of the previous post.
5050

5151
# Usage
5252

53-
## Register extensions
53+
## Register extension
5454

55-
Two extensions have to be registered:
56-
57-
- `StateRecordingAction` to record any state in `postServeActions`
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
55+
This extension makes use of Wiremock's `ExtensionFactory`, so only one extension has to be registered: `StateExtension`.
56+
In order to use them, templating has to be enabled as well:
6057

6158
```java
6259
public class MySandbox {
6360
private final WireMockServer server;
6461

6562
public MySandbox() {
6663
var stateRecordingAction = new StateRecordingAction();
64+
var store = new CaffeineStore();
6765
server = new WireMockServer(
6866
options()
6967
.dynamicPort()
70-
.extensions(
71-
stateRecordingAction,
72-
new StateRequestMatcher(stateRecordingAction),
73-
new ResponseTemplateTransformer(true, "state", new StateHelper(stateRecordingAction))
74-
)
68+
.templatingEnabled(true)
69+
.globalTemplating(true)
70+
.extensions(new StateExtension(store))
7571
);
7672
server.start();
7773
}
7874
}
7975
```
8076

81-
## Store a state
77+
## Record a state
8278

83-
The state is stored in `postServeActions` of a stub. The following parameters have to be provided:
79+
The state is recorded in `withServeEventListener` of a stub. The following parameters have to be provided:
8480

8581
<table>
8682
<tr>
@@ -134,7 +130,7 @@ Full example:
134130
{
135131
"request": {},
136132
"response": {},
137-
"postServeActions": [
133+
"withServeEventListener": [
138134
{
139135
"name": "recordState",
140136
"parameters": {
@@ -151,14 +147,63 @@ Full example:
151147

152148
```
153149

150+
## Deleting a state
151+
152+
Similar to recording a state, its deletion can be initiated in `withServeEventListener` of a stub. The following parameters have to be provided:
153+
154+
<table>
155+
<tr>
156+
<th>Parameter</th>
157+
<th>Type</th>
158+
<th>Example</th>
159+
</tr>
160+
<tr>
161+
<td>
162+
163+
`context`
164+
165+
</td>
166+
<td>String</td>
167+
<td>
168+
169+
- `"context": "{{jsonPath response.body '$.id'}}"`
170+
- `"context": "{{request.pathSegments.[3]}}"`
171+
172+
</td>
173+
</tr>
174+
</table>
175+
176+
Templating (as in [Response Templating](https://wiremock.org/docs/response-templating/)) is supported for these. The following models are exposed:
177+
178+
- `request`: All model elements of as in [Response Templating](https://wiremock.org/docs/response-templating/)
179+
- `response`: `body` and `headers`
180+
181+
Full example:
182+
183+
```json
184+
{
185+
"request": {},
186+
"response": {},
187+
"withServeEventListener": [
188+
{
189+
"name": "deleteState",
190+
"parameters": {
191+
"context": "{{jsonPath response.body '$.id'}}"
192+
}
193+
}
194+
]
195+
}
196+
197+
```
198+
154199
### state expiration
155200

156-
This extension uses [caffeine](https://github.com/ben-manes/caffeine) to store the current state and to achieve an expiration (to avoid memory leaks).
201+
This extension provides a `CaffeineStore` which uses [caffeine](https://github.com/ben-manes/caffeine) to store the current state and to achieve an expiration (to avoid memory leaks).
157202
The default expiration is 60 minutes. The default value can be overwritten (`0` = default = 60 minutes):
158203

159204
```java
160205
int expiration=1024;
161-
var stateRecordingAction=new StateRecordingAction(expiration);
206+
var store = new CaffeineStore(expiration);
162207
```
163208

164209
## Match a request against a context
@@ -236,15 +281,17 @@ Example response with error:
236281
## Java
237282

238283
```java
239-
class StateTest {
284+
class StateExtensionExampleTest {
285+
286+
private static final String TEST_URL = "/test";
287+
private static final Store<String, Object> store = new CaffeineStore();
288+
private static final ObjectMapper mapper = new ObjectMapper();
289+
240290
@RegisterExtension
241291
public static WireMockExtension wm = WireMockExtension.newInstance()
242292
.options(
243293
wireMockConfig().dynamicPort().dynamicHttpsPort()
244-
.extensions(
245-
stateRecordingAction,
246-
new ResponseTemplateTransformer(true, "state", new StateHelper(stateRecordingAction))
247-
)
294+
.extensions(new StateExtension(store))
248295
)
249296
.build();
250297

@@ -260,7 +307,7 @@ class StateTest {
260307
mapper.writeValueAsString(Map.of("id", "{{randomValue length=32 type='ALPHANUMERIC' uppercase=false}}")))
261308
)
262309
)
263-
.withPostServeAction(
310+
.withServeEventListener(
264311
"recordState",
265312
Parameters.from(
266313
Map.of(
@@ -285,14 +332,16 @@ class StateTest {
285332
.withJsonBody(
286333
mapper.readTree(
287334
mapper.writeValueAsString(Map.of(
288-
"id", "{{state context=request.pathSegments.[1] property='id'}}"),
289-
"firstName", "{{state context=request.pathSegments.[1] property='firstName'}}"),
290-
"lastName", "{{state context=request.pathSegments.[1] property='lastName'}}")
335+
"id", "{{state context=request.pathSegments.[1] property='id'}}",
336+
"firstName", "{{state context=request.pathSegments.[1] property='firstName'}}",
337+
"lastName", "{{state context=request.pathSegments.[1] property='lastName'}}"
338+
)
339+
)
340+
)
291341
)
292342
)
293343
);
294344
}
295-
296345
}
297346
```
298347

@@ -325,7 +374,7 @@ class StateTest {
325374
"lastName": "{{jsonPath request.body '$.lastName'}}"
326375
}
327376
},
328-
"postServeActions": [
377+
"withServeEventListener": [
329378
{
330379
"name": "recordState",
331380
"parameters": {

build.gradle

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@ wrapper {
2121
project.archivesBaseName = 'state'
2222
project.ext {
2323
versions = [
24-
wiremock : '3.0.0-beta-10',
24+
wiremock : '3.0.0-beta-11',
2525
caffeine : '3.1.6',
2626
handlebars: '4.3.1',
2727
junit : '5.9.3',
2828
assertj : '3.24.2',
29-
restAssured: '5.3.1'
29+
restAssured: '5.3.1',
30+
awaitility: '4.2.0'
3031
]
3132
}
3233

3334

3435
group 'com.github.dirkbolte.wiremock.extensions'
35-
version '0.0.1'
36+
version '0.0.2-3.0.0-beta-11'
3637

3738

3839
publishing {
@@ -80,7 +81,7 @@ repositories {
8081
}
8182

8283
dependencies {
83-
implementation("com.github.tomakehurst:wiremock:${versions.wiremock}")
84+
implementation("org.wiremock:wiremock:${versions.wiremock}")
8485
implementation("com.github.ben-manes.caffeine:caffeine:${versions.caffeine}")
8586
implementation("com.github.jknack:handlebars-helpers:${versions.handlebars}") {
8687
exclude group: 'org.mozilla', module: 'rhino'
@@ -91,6 +92,7 @@ dependencies {
9192
testImplementation("org.assertj:assertj-core:${versions.assertj}")
9293
testImplementation(platform("io.rest-assured:rest-assured-bom:${versions.restAssured}"))
9394
testImplementation("io.rest-assured:rest-assured")
95+
testImplementation("org.awaitility:awaitility:${versions.awaitility}")
9496
}
9597

9698

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (C) 2023 Dirk Bolte
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.wiremock.extensions.state;
17+
18+
import com.github.benmanes.caffeine.cache.Cache;
19+
import com.github.benmanes.caffeine.cache.Caffeine;
20+
import com.github.tomakehurst.wiremock.store.Store;
21+
22+
import java.time.Duration;
23+
import java.util.Optional;
24+
import java.util.stream.Stream;
25+
26+
public class CaffeineStore implements Store<String, Object> {
27+
28+
private static final int DEFAULT_EXPIRATION_SECONDS = 60 * 60;
29+
30+
private final Cache<String, Object> cache;
31+
32+
public CaffeineStore() {
33+
this(0);
34+
}
35+
36+
public CaffeineStore(int expirationSeconds) {
37+
var builder = Caffeine.newBuilder();
38+
if (expirationSeconds == 0) {
39+
builder.expireAfterWrite(Duration.ofSeconds(DEFAULT_EXPIRATION_SECONDS));
40+
} else {
41+
builder.expireAfterWrite(Duration.ofSeconds(expirationSeconds));
42+
}
43+
cache = builder.build();
44+
}
45+
46+
@Override
47+
public Stream<String> getAllKeys() {
48+
return cache.asMap().keySet().stream();
49+
}
50+
51+
@Override
52+
public Optional<Object> get(String key) {
53+
return Optional.ofNullable(cache.getIfPresent(key));
54+
}
55+
56+
@Override
57+
public void put(String key, Object content) {
58+
cache.put(key, content);
59+
}
60+
61+
@Override
62+
public void remove(String key) {
63+
cache.invalidate(key);
64+
}
65+
66+
@Override
67+
public void clear() {
68+
cache.invalidateAll();
69+
}
70+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (C) 2023 Dirk Bolte
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.wiremock.extensions.state;
17+
18+
import com.github.tomakehurst.wiremock.extension.Extension;
19+
import com.github.tomakehurst.wiremock.extension.ExtensionFactory;
20+
import com.github.tomakehurst.wiremock.extension.WireMockServices;
21+
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
22+
import com.github.tomakehurst.wiremock.store.Store;
23+
import org.wiremock.extensions.state.extensions.DeleteStateEventListener;
24+
import org.wiremock.extensions.state.extensions.RecordStateEventListener;
25+
import org.wiremock.extensions.state.extensions.StateRequestMatcher;
26+
import org.wiremock.extensions.state.extensions.StateTemplateHelperProviderExtension;
27+
import org.wiremock.extensions.state.internal.ContextManager;
28+
29+
import java.util.Collections;
30+
import java.util.List;
31+
32+
/**
33+
* Factory to register all extensions for handling state.
34+
* <p>
35+
* Register with:
36+
*
37+
* <pre>{@code
38+
* private static final Store<String, Object> store = new CaffeineStore();
39+
*
40+
* @RegisterExtension
41+
* public static WireMockExtension wm = WireMockExtension.newInstance()
42+
* .options(
43+
* wireMockConfig().dynamicPort().dynamicHttpsPort()
44+
* .extensions(new StateExtension(store))
45+
* )
46+
* .build();
47+
* }
48+
* </pre>
49+
*/
50+
public class StateExtension implements ExtensionFactory {
51+
52+
private final TemplateEngine templateEngine = new TemplateEngine(Collections.emptyMap(), null, Collections.emptySet(), false);
53+
54+
private final ContextManager contextManager;
55+
56+
public StateExtension(Store<String, Object> store) {
57+
this.contextManager = new ContextManager(store);
58+
}
59+
60+
@Override
61+
public List<Extension> create(WireMockServices services) {
62+
return List.of(
63+
new RecordStateEventListener(contextManager, templateEngine),
64+
new DeleteStateEventListener(contextManager, templateEngine),
65+
new StateRequestMatcher(contextManager, templateEngine),
66+
new StateTemplateHelperProviderExtension(contextManager)
67+
);
68+
}
69+
}

0 commit comments

Comments
 (0)