diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 5093254430..0f95f7a906 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -31,7 +31,7 @@ jobs: PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7 steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Checkout sources uses: actions/checkout@v4 diff --git a/build.gradle b/build.gradle index c568c02cf3..5b5d47084e 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ plugins { id "org.sonarqube" version "5.0.0.4638" id 'jacoco' id "me.champeau.jmh" version "0.7.2" - id 'com.dorongold.task-tree' version '3.0.0' + id 'com.dorongold.task-tree' version '4.0.0' id 'com.github.node-gradle.node' version '7.0.2' } @@ -33,13 +33,13 @@ group = 'org.wiremock' project.ext { versions = [ - handlebars : '4.3.1', - jetty : '11.0.20', - guava : '33.2.1-jre', - jackson : '2.17.1', - xmlUnit : '2.10.0', - jsonUnit : '2.38.0', - junitJupiter: '5.10.3' + handlebars : '4.3.1', + jetty : '11.0.20', + guava : '33.2.1-jre', + jackson : '2.17.2', + xmlUnit : '2.10.0', + jsonUnit : '2.40.0', + junitJupiter : '5.10.3' ] } @@ -108,7 +108,7 @@ dependencies { api 'commons-fileupload:commons-fileupload:1.5' - api 'com.networknt:json-schema-validator:1.4.3' + api 'com.networknt:json-schema-validator:1.5.0' testFixturesApi("org.junit.jupiter:junit-jupiter:$versions.junitJupiter") testFixturesApi("org.junit.platform:junit-platform-testkit") diff --git a/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java b/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java index 264ceb758e..9ba2aeb64b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java +++ b/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java @@ -20,6 +20,7 @@ import static com.github.tomakehurst.wiremock.common.Strings.isNotBlank; import static com.github.tomakehurst.wiremock.security.NoClientAuthenticator.noClientAuthenticator; import static java.util.Objects.requireNonNull; +import static org.apache.hc.core5.http.HttpHeaders.CONTENT_TYPE; import static org.apache.hc.core5.http.HttpHeaders.HOST; import com.github.tomakehurst.wiremock.admin.*; @@ -462,12 +463,14 @@ private ProxySettings createProxySettings(String proxyHost, int proxyPort) { private String postJsonAssertOkAndReturnBody(String url, String json) { HttpPost post = new HttpPost(url); + post.addHeader(CONTENT_TYPE, "application/json"); post.setEntity(jsonStringEntity(Optional.ofNullable(json).orElse(""))); return safelyExecuteRequest(url, post); } private String putJsonAssertOkAndReturnBody(String url, String json) { HttpPut put = new HttpPut(url); + put.addHeader(CONTENT_TYPE, "application/json"); put.setEntity(jsonStringEntity(Optional.ofNullable(json).orElse(""))); return safelyExecuteRequest(url, put); } @@ -518,6 +521,7 @@ private R executeRequest( if (requestSpec.method().hasEntity()) { requestBuilder.setEntity( jsonStringEntity(Optional.ofNullable(requestBody).map(Json::write).orElse(""))); + requestBuilder.addHeader(CONTENT_TYPE, "application/json"); } String responseBodyString = safelyExecuteRequest(url, requestBuilder.build()); diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/DateTimeParser.java b/src/main/java/com/github/tomakehurst/wiremock/common/DateTimeParser.java index b01d805893..d41c4250de 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/DateTimeParser.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/DateTimeParser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,6 +116,22 @@ public LocalDate parseLocalDate(String dateTimeString) { return null; } + public YearMonth parseYearMonth(String dateTimeString) { + if (dateTimeFormatter != null) { + return YearMonth.parse(dateTimeString, dateTimeFormatter); + } + + return null; + } + + public Year parseYear(String dateTimeString) { + if (dateTimeFormatter != null) { + return Year.parse(dateTimeString, dateTimeFormatter); + } + + return null; + } + public RenderableDate parseDate(String dateTimeString) { if (isUnix || isEpoch) { return new RenderableDate( diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/PickRandomHelper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/PickRandomHelper.java index d5c7900b07..52c36e3e1b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/PickRandomHelper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/PickRandomHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Thomas Akehurst + * Copyright (C) 2020-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,7 @@ import com.github.jknack.handlebars.Options; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; import java.util.concurrent.ThreadLocalRandom; public class PickRandomHelper extends HandlebarsHelper { @@ -40,7 +38,16 @@ public Object apply(Object context, Options options) throws IOException { valueList.addAll(Arrays.asList(options.params)); } + Integer count = (Integer) options.hash.get("count"); + if (count != null && count > 0) { + int desiredLength = Math.min(valueList.size(), count); + for (int i = 0; i < desiredLength; i++) { + Collections.swap(valueList, i, ThreadLocalRandom.current().nextInt(i, valueList.size())); + } + return valueList.subList(0, desiredLength); + } + int index = ThreadLocalRandom.current().nextInt(valueList.size()); - return valueList.get(index).toString(); + return valueList.get(index); } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/matching/AbstractDateTimePattern.java b/src/main/java/com/github/tomakehurst/wiremock/matching/AbstractDateTimePattern.java index 82ba1cbaef..1e4f160a0f 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/matching/AbstractDateTimePattern.java +++ b/src/main/java/com/github/tomakehurst/wiremock/matching/AbstractDateTimePattern.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,7 @@ import com.github.tomakehurst.wiremock.common.DateTimeParser; import com.github.tomakehurst.wiremock.common.DateTimeTruncation; import com.github.tomakehurst.wiremock.common.DateTimeUnit; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; +import java.time.*; import java.time.format.DateTimeParseException; import java.util.List; @@ -271,7 +269,21 @@ private static LocalDateTime parseLocalOrNull(String dateTimeString, DateTimePar : LocalDate.parse(dateTimeString)) .atStartOfDay(); } catch (DateTimeParseException ignored2) { - return null; + try { + return (parser != null + ? parser.parseYearMonth(dateTimeString) + : YearMonth.parse(dateTimeString)) + .atDay(1) + .atStartOfDay(); + } catch (DateTimeParseException ignored3) { + try { + return (parser != null ? parser.parseYear(dateTimeString) : Year.parse(dateTimeString)) + .atDay(1) + .atStartOfDay(); + } catch (DateTimeParseException ignored4) { + return null; + } + } } } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPattern.java b/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPattern.java index 714f44b5ff..a5204d704f 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPattern.java +++ b/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPattern.java @@ -19,8 +19,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.ClientError; +import com.github.tomakehurst.wiremock.common.Errors; import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.common.JsonException; +import com.github.tomakehurst.wiremock.stubbing.SubEvent; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.SchemaValidatorsConfig; @@ -32,6 +35,7 @@ public class MatchesJsonSchemaPattern extends StringValuePattern { private final JsonSchema schema; private final WireMock.JsonSchemaVersion schemaVersion; private final int schemaPropertyCount; + private final Errors invalidSchemaErrors; public MatchesJsonSchemaPattern(String schemaJson) { this(schemaJson, WireMock.JsonSchemaVersion.V202012); @@ -48,10 +52,24 @@ public MatchesJsonSchemaPattern( final JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(schemaVersion.toVersionFlag()); - schema = schemaFactory.getSchema(schemaJson, config); + JsonSchema schema; + JsonNode schemaAsJson = Json.read(schemaJson, JsonNode.class); + int schemaPropertyCount; + Errors invalidSchemaErrors; + try { + schema = schemaFactory.getSchema(schemaAsJson, config); + schemaPropertyCount = Json.schemaPropertyCount(schemaAsJson); + invalidSchemaErrors = null; + } catch (Exception e) { + schema = null; + schemaPropertyCount = 0; + invalidSchemaErrors = getInvalidSchemaErrors(e); + } + this.schema = schema; this.schemaVersion = schemaVersion; - schemaPropertyCount = Json.schemaPropertyCount(Json.read(schemaJson, JsonNode.class)); + this.schemaPropertyCount = schemaPropertyCount; + this.invalidSchemaErrors = invalidSchemaErrors; } public MatchesJsonSchemaPattern( @@ -74,6 +92,9 @@ public String getExpected() { @Override public MatchResult match(String json) { + if (schema == null) { + return MatchResult.noMatch(new SubEvent(SubEvent.ERROR, invalidSchemaErrors)); + } if (json == null) { return MatchResult.noMatch(); } @@ -85,7 +106,13 @@ public MatchResult match(String json) { jsonNode = new TextNode(json); } - final Set validationMessages = validate(jsonNode, json); + final Set validationMessages; + try { + validationMessages = validate(jsonNode, json); + } catch (Exception e) { + return MatchResult.noMatch(new SubEvent(SubEvent.ERROR, getInvalidSchemaErrors(e))); + } + if (validationMessages.isEmpty()) { return MatchResult.exactMatch(); } @@ -107,6 +134,30 @@ public double getDistance() { }; } + private static Errors getInvalidSchemaErrors(Exception e) { + Errors invalidSchemaErrors; + if (e instanceof ClientError) { + Errors.Error error = ((ClientError) e).getErrors().first(); + invalidSchemaErrors = + Errors.single( + error.getCode(), + error.getSource().getPointer(), + "Invalid JSON Schema", + error.getDetail()); + } else { + invalidSchemaErrors = + Errors.singleWithDetail(10, "Invalid JSON Schema", getRootCause(e).getMessage()); + } + return invalidSchemaErrors; + } + + private static Throwable getRootCause(Throwable e) { + if (e.getCause() != null) { + return getRootCause(e.getCause()); + } + return e; + } + private Set validate(JsonNode jsonNode, String originalJson) { final Set validationMessages = schema.validate(jsonNode); if (validationMessages.isEmpty() || jsonNode.isTextual() || jsonNode.isContainerNode()) { diff --git a/src/main/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStore.java b/src/main/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStore.java index fb1f8ac091..04296a9682 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStore.java +++ b/src/main/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStore.java @@ -15,18 +15,25 @@ */ package com.github.tomakehurst.wiremock.store; +import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; + +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; -public class InMemoryObjectStore implements ObjectStore { +public class InMemoryObjectStore implements ObjectStore, StoreEventEmitter { private final ConcurrentHashMap cache; private final Queue keyUseOrder = new ConcurrentLinkedQueue<>(); private final int maxItems; + private final List>> listeners = new ArrayList<>(); public InMemoryObjectStore(int maxItems) { this.cache = new ConcurrentHashMap<>(); @@ -54,23 +61,39 @@ public Stream getAllKeys() { @Override public void put(String key, Object content) { - cache.put(key, content); + Object previousValue = cache.put(key, content); touchAndResize(key); + handleEvent(StoreEvent.set(key, previousValue, content)); } @Override @SuppressWarnings("unchecked") public T compute(String key, Function valueFunction) { + final AtomicReference previousValue = new AtomicReference<>(); final T result = - (T) cache.compute(key, (k, currentValue) -> valueFunction.apply((T) currentValue)); - touchAndResize(key); + (T) + cache.compute( + key, + (k, currentValue) -> { + previousValue.set((T) currentValue); + return valueFunction.apply((T) currentValue); + }); + if (result != null) { + touchAndResize(key); + } else { + keyUseOrder.remove(key); + } + handleEvent(StoreEvent.set(key, previousValue.get(), result)); return result; } @Override public void remove(String key) { - cache.remove(key); + Object previousValue = cache.remove(key); keyUseOrder.remove(key); + if (previousValue != null) { + handleEvent(StoreEvent.remove(key, previousValue)); + } } @Override @@ -79,6 +102,21 @@ public void clear() { keyUseOrder.clear(); } + @Override + public void registerEventListener(Consumer> handler) { + listeners.add(handler); + } + + private void handleEvent(StoreEvent event) { + for (Consumer> listener : listeners) { + try { + listener.accept(event); + } catch (Exception e) { + notifier().error("Error handling store event", e); + } + } + } + private void touchAndResize(String key) { touch(key); resize(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/store/Store.java b/src/main/java/com/github/tomakehurst/wiremock/store/Store.java index 67d431dc18..e14ca3fb8e 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/store/Store.java +++ b/src/main/java/com/github/tomakehurst/wiremock/store/Store.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 Thomas Akehurst + * Copyright (C) 2022-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/github/tomakehurst/wiremock/store/StoreEvent.java b/src/main/java/com/github/tomakehurst/wiremock/store/StoreEvent.java new file mode 100644 index 0000000000..892d046047 --- /dev/null +++ b/src/main/java/com/github/tomakehurst/wiremock/store/StoreEvent.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 com.github.tomakehurst.wiremock.store; + +import java.util.Objects; +import org.wiremock.annotations.Beta; + +@Beta(justification = "Externalized State API: https://github.com/wiremock/wiremock/issues/2144") +public class StoreEvent { + + public static StoreEvent set(K key, V previousValue, V newValue) { + return new StoreEvent<>(key, previousValue, newValue); + } + + public static StoreEvent remove(K key, V previousValue) { + return new StoreEvent<>(key, previousValue, null); + } + + private final K key; + private final V oldValue; + private final V newValue; + + public StoreEvent(K key, V oldValue, V newValue) { + this.key = key; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public K getKey() { + return key; + } + + public V getOldValue() { + return oldValue; + } + + public V getNewValue() { + return newValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StoreEvent that = (StoreEvent) o; + return Objects.equals(key, that.key) + && Objects.equals(oldValue, that.oldValue) + && Objects.equals(newValue, that.newValue); + } + + @Override + public int hashCode() { + return Objects.hash(key, oldValue, newValue); + } + + @Override + public String toString() { + return "StoreEvent{" + "key=" + key + ", oldValue=" + oldValue + ", newValue=" + newValue + '}'; + } +} diff --git a/src/main/java/com/github/tomakehurst/wiremock/store/StoreEventEmitter.java b/src/main/java/com/github/tomakehurst/wiremock/store/StoreEventEmitter.java new file mode 100644 index 0000000000..6c5e5087f9 --- /dev/null +++ b/src/main/java/com/github/tomakehurst/wiremock/store/StoreEventEmitter.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 com.github.tomakehurst.wiremock.store; + +import java.util.function.Consumer; +import org.wiremock.annotations.Beta; + +@Beta(justification = "Externalized State API: https://github.com/wiremock/wiremock/issues/2144") +public interface StoreEventEmitter { + + void registerEventListener(Consumer> handler); +} diff --git a/src/main/resources/swagger/examples/health.yaml b/src/main/resources/swagger/examples/health.yaml new file mode 100644 index 0000000000..4c74d40e58 --- /dev/null +++ b/src/main/resources/swagger/examples/health.yaml @@ -0,0 +1,5 @@ +status: "healthy" +message: "Wiremock is ok" +version: "3.8.0" +uptimeInSeconds: 14355 +timestamp: "2024-07-03T13:16:06.172362Z" \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/absent-pattern.yaml b/src/main/resources/swagger/schemas/absent-pattern.yaml new file mode 100644 index 0000000000..17fe4cefdc --- /dev/null +++ b/src/main/resources/swagger/schemas/absent-pattern.yaml @@ -0,0 +1,8 @@ +title: Absent matcher +type: object +properties: + absent: + type: boolean + +required: + - absent \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/after-pattern.yaml b/src/main/resources/swagger/schemas/after-pattern.yaml new file mode 100644 index 0000000000..82419e82a0 --- /dev/null +++ b/src/main/resources/swagger/schemas/after-pattern.yaml @@ -0,0 +1,14 @@ +title: Before datetime +type: object +properties: + after: + $ref: "date-time-elements.yaml#/dateTimeExpression" + actualFormat: + $ref: "date-time-elements.yaml#/format" + truncateExpected: + $ref: "date-time-elements.yaml#/truncation" + truncateActual: + $ref: "date-time-elements.yaml#/truncation" + +required: + - after \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/and-pattern.yaml b/src/main/resources/swagger/schemas/and-pattern.yaml new file mode 100644 index 0000000000..a79b77a89f --- /dev/null +++ b/src/main/resources/swagger/schemas/and-pattern.yaml @@ -0,0 +1,10 @@ +title: Logical AND matcher +type: object +properties: + and: + type: array + items: + $ref: "content-pattern.yaml" + +required: + - and \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/bad-request-entity.yaml b/src/main/resources/swagger/schemas/bad-request-entity.yaml new file mode 100644 index 0000000000..931752a77e --- /dev/null +++ b/src/main/resources/swagger/schemas/bad-request-entity.yaml @@ -0,0 +1,16 @@ +title: Bad request entity +type: object +properties: + errors: + type: array + items: + type: object + properties: + code: + type: integer + source: + type: string + title: + type: string + detail: + type: string \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/base64-string.yaml b/src/main/resources/swagger/schemas/base64-string.yaml new file mode 100644 index 0000000000..add4c47240 --- /dev/null +++ b/src/main/resources/swagger/schemas/base64-string.yaml @@ -0,0 +1,4 @@ +title: Base64 string +type: string +pattern: ^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$ +description: A base64 encoded string used to describe binary data. \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/before-pattern.yaml b/src/main/resources/swagger/schemas/before-pattern.yaml new file mode 100644 index 0000000000..79f4242571 --- /dev/null +++ b/src/main/resources/swagger/schemas/before-pattern.yaml @@ -0,0 +1,14 @@ +title: Before datetime +type: object +properties: + before: + $ref: "date-time-elements.yaml#/dateTimeExpression" + actualFormat: + $ref: "date-time-elements.yaml#/format" + truncateExpected: + $ref: "date-time-elements.yaml#/truncation" + truncateActual: + $ref: "date-time-elements.yaml#/truncation" + +required: + - before \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/binary-equal-to-pattern.yaml b/src/main/resources/swagger/schemas/binary-equal-to-pattern.yaml new file mode 100644 index 0000000000..40b553c2c0 --- /dev/null +++ b/src/main/resources/swagger/schemas/binary-equal-to-pattern.yaml @@ -0,0 +1,7 @@ +title: Binary equals +type: object +required: + - binaryEqualTo +properties: + binaryEqualTo: + $ref: "base64-string.yaml" \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/contains-pattern.yaml b/src/main/resources/swagger/schemas/contains-pattern.yaml new file mode 100644 index 0000000000..1974dd02d7 --- /dev/null +++ b/src/main/resources/swagger/schemas/contains-pattern.yaml @@ -0,0 +1,7 @@ +title: String contains +type: object +properties: + contains: + type: string +required: + - contains \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/content-pattern.yaml b/src/main/resources/swagger/schemas/content-pattern.yaml index 7453b1d2fc..8205d3d0e6 100644 --- a/src/main/resources/swagger/schemas/content-pattern.yaml +++ b/src/main/resources/swagger/schemas/content-pattern.yaml @@ -1,67 +1,24 @@ +type: object +title: Content pattern oneOf: - - title: String equals - type: object - properties: - equalTo: - type: boolean - caseInsensitive: - type: boolean - required: - - equalTo - - title: String contains - type: object - properties: - contains: - type: string - required: - - contains - - title: Regular expression match - type: object - properties: - matches: - type: string - required: - - matches - - title: Negative regular expression match - type: object - properties: - doesNotMatch: - type: string - required: - - doesNotMatch - - title: JSON equals - type: object - properties: - equalToJson: - type: string - required: - - equalToJson - - title: JSONPath match - type: object - properties: - matchesJsonPath: - type: string - ignoreArrayOrder: - type: boolean - ignoreExtraElements: - type: boolean - required: - - matchesJsonPath - - title: XML equality - type: object - properties: - equalToXml: - type: string - required: - - equalToXml - - title: XPath match - type: object - properties: - matchesXpath: - type: string - namespaces: - type: object - valuePattern: - type: object - required: - - matchesXpath + - $ref: "equal-to-pattern.yaml" + - $ref: "binary-equal-to-pattern.yaml" + - $ref: "contains-pattern.yaml" + - $ref: "does-not-contain-pattern.yaml" + - $ref: "matches-pattern.yaml" + - $ref: "does-not-match-pattern.yaml" + - $ref: "not-pattern.yaml" + - $ref: "before-pattern.yaml" + - $ref: "after-pattern.yaml" + - $ref: "equal-to-date-time-pattern.yaml" + - $ref: "equal-to-json-pattern.yaml" + - $ref: "matches-json-path-pattern.yaml" + - $ref: "equal-to-xml-pattern.yaml" + - $ref: "matches-xpath-pattern.yaml" + - $ref: "matches-json-schema-pattern.yaml" + - $ref: "absent-pattern.yaml" + - $ref: "and-pattern.yaml" + - $ref: "or-pattern.yaml" + - $ref: "has-exactly-multivalue-pattern.yaml" + - $ref: "includes-multivalue-pattern.yaml" + diff --git a/src/main/resources/swagger/schemas/date-time-elements.yaml b/src/main/resources/swagger/schemas/date-time-elements.yaml new file mode 100644 index 0000000000..2c879352cf --- /dev/null +++ b/src/main/resources/swagger/schemas/date-time-elements.yaml @@ -0,0 +1,21 @@ +dateTimeExpression: + type: string + example: now +3 days + +format: + type: string + example: yyyy-MM-dd + +truncation: + type: string + enum: + - first second of minute + - first minute of hour + - first hour of day + - first day of month + - first day of next month + - last day of month + - first day of year + - first day of next year + - last day of year + example: first day of month \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/delay-distribution.yaml b/src/main/resources/swagger/schemas/delay-distribution.yaml index 9cd8bb52fa..cd8086cf51 100644 --- a/src/main/resources/swagger/schemas/delay-distribution.yaml +++ b/src/main/resources/swagger/schemas/delay-distribution.yaml @@ -1,3 +1,5 @@ +type: object +description: The delay distribution. Valid property configuration is either median/sigma/type or lower/type/upper. oneOf: - title: Log normal description: Log normal randomly distributed response delay. @@ -8,18 +10,38 @@ oneOf: sigma: type: number type: + type: string enum: - lognormal - type: string + required: + - median + - sigma + - title: Uniform description: Uniformly distributed random response delay. type: object properties: lower: type: integer + upper: + type: integer type: + type: string enum: - uniform - type: string - upper: + required: + - lower + - upper + + - title: Fixed + description: Fixed response delay. + type: object + properties: + milliseconds: type: integer + type: + type: string + enum: + - fixed + required: + - milliseconds diff --git a/src/main/resources/swagger/schemas/does-not-contain-pattern.yaml b/src/main/resources/swagger/schemas/does-not-contain-pattern.yaml new file mode 100644 index 0000000000..8bc6922db5 --- /dev/null +++ b/src/main/resources/swagger/schemas/does-not-contain-pattern.yaml @@ -0,0 +1,7 @@ +title: String does not contain +type: object +properties: + doesNotContain: + type: string +required: + - doesNotContain \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/does-not-match-pattern.yaml b/src/main/resources/swagger/schemas/does-not-match-pattern.yaml new file mode 100644 index 0000000000..25a89df0ae --- /dev/null +++ b/src/main/resources/swagger/schemas/does-not-match-pattern.yaml @@ -0,0 +1,7 @@ +title: Negative regular expression match +type: object +properties: + doesNotMatch: + type: string +required: + - doesNotMatch \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/equal-to-date-time-pattern.yaml b/src/main/resources/swagger/schemas/equal-to-date-time-pattern.yaml new file mode 100644 index 0000000000..b3c5505053 --- /dev/null +++ b/src/main/resources/swagger/schemas/equal-to-date-time-pattern.yaml @@ -0,0 +1,14 @@ +title: Before datetime +type: object +properties: + equalToDateTime: + $ref: "date-time-elements.yaml#/dateTimeExpression" + actualFormat: + $ref: "date-time-elements.yaml#/format" + truncateExpected: + $ref: "date-time-elements.yaml#/truncation" + truncateActual: + $ref: "date-time-elements.yaml#/truncation" + +required: + - equalToDateTime \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/equal-to-json-pattern.yaml b/src/main/resources/swagger/schemas/equal-to-json-pattern.yaml new file mode 100644 index 0000000000..69c08ae7cf --- /dev/null +++ b/src/main/resources/swagger/schemas/equal-to-json-pattern.yaml @@ -0,0 +1,14 @@ +title: JSON equals +type: object +properties: + equalToJson: + type: string + example: | + { "message": "hello" } + ignoreExtraElements: + type: boolean + ignoreArrayOrder: + type: boolean + +required: + - equalToJson \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/equal-to-pattern.yaml b/src/main/resources/swagger/schemas/equal-to-pattern.yaml new file mode 100644 index 0000000000..a6f62d9e5c --- /dev/null +++ b/src/main/resources/swagger/schemas/equal-to-pattern.yaml @@ -0,0 +1,9 @@ +title: String equals +type: object +required: + - equalTo +properties: + equalTo: + type: string + caseInsensitive: + type: boolean \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/equal-to-xml-pattern.yaml b/src/main/resources/swagger/schemas/equal-to-xml-pattern.yaml new file mode 100644 index 0000000000..1319a9e109 --- /dev/null +++ b/src/main/resources/swagger/schemas/equal-to-xml-pattern.yaml @@ -0,0 +1,17 @@ +title: XML equality +type: object +properties: + equalToXml: + type: string + example: "123" + enablePlaceholders: + type: boolean + placeholderOpeningDelimiterRegex: + type: string + example: "[" + placeholderClosingDelimiterRegex: + type: string + example: "]" + +required: + - equalToXml \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/has-exactly-multivalue-pattern.yaml b/src/main/resources/swagger/schemas/has-exactly-multivalue-pattern.yaml new file mode 100644 index 0000000000..e130d2ef3a --- /dev/null +++ b/src/main/resources/swagger/schemas/has-exactly-multivalue-pattern.yaml @@ -0,0 +1,10 @@ +title: Has exactly multi value matcher +type: object +properties: + hasExactly: + type: array + items: + $ref: "content-pattern.yaml" + +required: + - hasExactly \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/health.yaml b/src/main/resources/swagger/schemas/health.yaml new file mode 100644 index 0000000000..cd0eb57f28 --- /dev/null +++ b/src/main/resources/swagger/schemas/health.yaml @@ -0,0 +1,25 @@ +type: object +properties: + status: + type: string + example: "healthy" + description: "The status of the server" + enum: + - healthy + - unhealthy + message: + type: string + description: "Longer message regarding the status of the server" + example: "Wiremock is ok" + version: + type: string + description: "The WireMock version" + example: "3.8.0" + uptimeInSeconds: + type: integer + description: "How long the server has been running" + example: 14355 + timestamp: + type: string + description: "The current timestamp" + example: "2024-07-03T13:16:06.172362Z" diff --git a/src/main/resources/swagger/schemas/includes-multivalue-pattern.yaml b/src/main/resources/swagger/schemas/includes-multivalue-pattern.yaml new file mode 100644 index 0000000000..e9ef0b5cb8 --- /dev/null +++ b/src/main/resources/swagger/schemas/includes-multivalue-pattern.yaml @@ -0,0 +1,10 @@ +title: Has exactly multi value matcher +type: object +properties: + includes: + type: array + items: + $ref: "content-pattern.yaml" + +required: + - includes \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/matches-json-path-pattern.yaml b/src/main/resources/swagger/schemas/matches-json-path-pattern.yaml new file mode 100644 index 0000000000..f691804d97 --- /dev/null +++ b/src/main/resources/swagger/schemas/matches-json-path-pattern.yaml @@ -0,0 +1,20 @@ +title: JSONPath match +type: object +properties: + matchesJsonPath: + oneOf: + - type: string + example: "$.name" + - type: object + allOf: + - properties: + expression: + type: string + example: "$.name" + - $ref: "content-pattern.yaml" + + required: + - expression + +required: + - matchesJsonPath \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/matches-json-schema-pattern.yaml b/src/main/resources/swagger/schemas/matches-json-schema-pattern.yaml new file mode 100644 index 0000000000..2766d56d6c --- /dev/null +++ b/src/main/resources/swagger/schemas/matches-json-schema-pattern.yaml @@ -0,0 +1,25 @@ +title: JSON Schema match +type: object +properties: + matchesJsonSchema: + oneOf: + - type: string + example: "//Order/Amount" + - type: object + allOf: + - properties: + expression: + type: string + example: "//Order/Amount" + - $ref: "content-pattern.yaml" + + required: + - expression + + xPathNamespaces: + type: object + additionalProperties: + type: string + +required: + - matchesJsonSchema \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/matches-pattern.yaml b/src/main/resources/swagger/schemas/matches-pattern.yaml new file mode 100644 index 0000000000..6744a51671 --- /dev/null +++ b/src/main/resources/swagger/schemas/matches-pattern.yaml @@ -0,0 +1,7 @@ +title: Regular expression match +type: object +properties: + matches: + type: string +required: + - matches \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/matches-xpath-pattern.yaml b/src/main/resources/swagger/schemas/matches-xpath-pattern.yaml new file mode 100644 index 0000000000..11ca54a682 --- /dev/null +++ b/src/main/resources/swagger/schemas/matches-xpath-pattern.yaml @@ -0,0 +1,25 @@ +title: XPath match +type: object +properties: + matchesXPath: + oneOf: + - type: string + example: "//Order/Amount" + - type: object + allOf: + - properties: + expression: + type: string + example: "//Order/Amount" + - $ref: "content-pattern.yaml" + + required: + - expression + + xPathNamespaces: + type: object + additionalProperties: + type: string + +required: + - matchesXPath \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/not-pattern.yaml b/src/main/resources/swagger/schemas/not-pattern.yaml new file mode 100644 index 0000000000..a9509666a0 --- /dev/null +++ b/src/main/resources/swagger/schemas/not-pattern.yaml @@ -0,0 +1,7 @@ +title: NOT modifier +type: object +properties: + not: + $ref: "content-pattern.yaml" +required: + - not \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/or-pattern.yaml b/src/main/resources/swagger/schemas/or-pattern.yaml new file mode 100644 index 0000000000..1b451a172c --- /dev/null +++ b/src/main/resources/swagger/schemas/or-pattern.yaml @@ -0,0 +1,10 @@ +title: Logical AND matcher +type: object +properties: + or: + type: array + items: + $ref: "content-pattern.yaml" + +required: + - or \ No newline at end of file diff --git a/src/main/resources/swagger/schemas/request-pattern.yaml b/src/main/resources/swagger/schemas/request-pattern.yaml index 4035ff8e67..b806d34aba 100644 --- a/src/main/resources/swagger/schemas/request-pattern.yaml +++ b/src/main/resources/swagger/schemas/request-pattern.yaml @@ -1,24 +1,31 @@ type: object -example: - bodyPatterns: - - equalToJson: '{ "numbers": [1, 2, 3] }' - headers: - Content-Type: - equalTo: application/json - method: POST - url: /some/thing +example: | + { + "urlPath" : "/charges", + "method" : "POST", + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } properties: scheme: type: string - description: The URI scheme + enum: + - http + - https + description: The scheme (protocol) part of the request URL host: - type: object - description: 'URI host pattern to match against in the "": "" form' + type: string + description: The hostname part of the request URL port: type: integer - description: The HTTP port number + minimum: 1 + maximum: 65535 + description: The HTTP port number of the request URL method: type: string + pattern: ^[A-Z]+$ description: The HTTP request method e.g. GET url: type: string @@ -32,12 +39,34 @@ properties: urlPattern: type: string description: The path and query regex to match against. Only one of url, urlPattern, urlPath or urlPathPattern may be specified. + + pathParameters: + type: object + description: | + Path parameter patterns to match against in the : { "": "" } form. Can only + be used when the urlPathPattern URL match type is in use and all keys must be present as variables + in the path template. + additionalProperties: + $ref: "content-pattern.yaml" + queryParameters: type: object description: 'Query parameter patterns to match against in the : { "": "" } form' + additionalProperties: + $ref: "content-pattern.yaml" + + formParameters: + type: object + description: 'application/x-www-form-urlencoded form parameter patterns to match against in the : { "": "" } form' + additionalProperties: + $ref: "content-pattern.yaml" + headers: type: object description: 'Header patterns to match against in the : { "": "" } form' + additionalProperties: + $ref: "content-pattern.yaml" + basicAuthCredentials: type: object description: Pre-emptive basic auth credentials to match against @@ -52,11 +81,14 @@ properties: cookies: type: object description: 'Cookie patterns to match against in the : { "": "" } form' + additionalProperties: + $ref: "content-pattern.yaml" bodyPatterns: type: array description: 'Request body patterns to match against in the : { "": "" } form' items: - type: object + $ref: "content-pattern.yaml" + customMatcher: type: object description: Custom request matcher to match against @@ -66,9 +98,10 @@ properties: description: The matcher's name specified in the implementation of the matcher. parameters: type: object + multipartPatterns: type: array - description: Multipart patterns to match against headers and body + description: Multipart patterns to match against headers and body. items: type: object properties: @@ -76,13 +109,19 @@ properties: type: string matchingType: type: string + description: Determines whether all or any of the parts must match the criteria for an overall match. default: ANY enum: - ALL - ANY headers: type: object + description: 'Header patterns to match against in the : { "": "" } form' + additionalProperties: + $ref: "content-pattern.yaml" + bodyPatterns: type: array + description: 'Body patterns to match against in the : { "": "" } form' items: - type: object + $ref: "content-pattern.yaml" diff --git a/src/main/resources/swagger/schemas/response-definition.yaml b/src/main/resources/swagger/schemas/response-definition.yaml index 43bf4c5802..670f7f4610 100644 --- a/src/main/resources/swagger/schemas/response-definition.yaml +++ b/src/main/resources/swagger/schemas/response-definition.yaml @@ -7,29 +7,38 @@ allOf: statusMessage: type: string description: The HTTP status message to be returned + headers: type: object description: Map of response headers to send + additionalProperties: + type: string additionalProxyRequestHeaders: type: object description: Extra request headers to send when proxying to another host. + additionalProperties: + type: string removeProxyRequestHeaders: type: array description: Request headers to remove when proxying to another host. items: type: string + body: type: string description: The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified. base64Body: - type: string - description: The response body as a base64 encoded string (useful for binary content). Only one of body, base64Body, jsonBody or bodyFileName may be specified. + $ref: "base64-string.yaml" jsonBody: - type: object description: The response body as a JSON object. Only one of body, base64Body, jsonBody or bodyFileName may be specified. + oneOf: + - type: object + - type: array bodyFileName: type: string description: The path to the file containing the response body, relative to the configured file root. Only one of body, base64Body, jsonBody or bodyFileName may be specified. + example: user-profile-responses/user1.json + fault: type: string description: The fault to apply (instead of a full, valid response). @@ -38,18 +47,35 @@ allOf: - EMPTY_RESPONSE - MALFORMED_RESPONSE_CHUNK - RANDOM_DATA_THEN_CLOSE + fixedDelayMilliseconds: type: integer description: Number of milliseconds to delay be before sending the response. delayDistribution: - description: The delay distribution. Valid property configuration is either median/sigma/type or lower/type/upper. $ref: "delay-distribution.yaml" + chunkedDribbleDelay: + type: object + description: The parameters for chunked dribble delay - chopping the response into pieces and sending them at delayed intervals + properties: + numberOfChunks: + type: integer + totalDuration: + type: integer + required: + - numberOfChunks + - totalDuration + fromConfiguredStub: type: boolean description: Read-only flag indicating false if this was the default, unmatched response. Not present otherwise. + proxyBaseUrl: type: string description: The base URL of the target to proxy matching requests to. + proxyUrlPrefixToRemove: + type: string + description: A path segment to remove from the beginning in incoming request URL paths before proxying to the target. + transformerParameters: type: object description: Parameters to apply to response transformers. diff --git a/src/main/resources/swagger/wiremock-admin-api.json b/src/main/resources/swagger/wiremock-admin-api.json index 14406c606e..9cf6a50b50 100644 --- a/src/main/resources/swagger/wiremock-admin-api.json +++ b/src/main/resources/swagger/wiremock-admin-api.json @@ -2,11 +2,12 @@ "openapi": "3.0.0", "info": { "title": "WireMock", - "version": "3.8.0" + "version": "3.9.1", + "description": "WireMock offers a REST API for administration, troubleshooting and analysis purposes" }, "externalDocs": { "description": "WireMock user documentation", - "url": "http://wiremock.org/docs/" + "url": "https://wiremock.org/docs/" }, "servers": [ { @@ -19,7 +20,7 @@ "description": "Operations on stub mappings", "externalDocs": { "description": "User documentation", - "url": "http://wiremock.org/docs/stubbing/" + "url": "https://wiremock.org/docs/stubbing/" } }, { @@ -27,7 +28,7 @@ "description": "Logged requests and responses received by the mock service", "externalDocs": { "description": "User documentation", - "url": "http://wiremock.org/docs/verifying/" + "url": "https://wiremock.org/docs/verifying/" } }, { @@ -35,7 +36,7 @@ "description": "Near misses allow querying of received requests or request patterns according to similarity", "externalDocs": { "description": "User documentation", - "url": "http://wiremock.org/docs/verifying/#near-misses" + "url": "https://wiremock.org/docs/verifying/#near-misses" } }, { @@ -43,7 +44,7 @@ "description": "Stub mapping record and snapshot functions", "externalDocs": { "description": "User documentation", - "url": "http://wiremock.org/docs/record-playback/" + "url": "https://wiremock.org/docs/record-playback/" } }, { @@ -51,9 +52,13 @@ "description": "Scenarios support modelling of stateful behaviour", "externalDocs": { "description": "User documentation", - "url": "http://wiremock.org/docs/stateful-behaviour/" + "url": "https://wiremock.org/docs/stateful-behaviour/" } }, + { + "name": "Files", + "description": "Manage the files used to support WireMock stubs" + }, { "name": "System", "description": "Global operations" @@ -4021,14 +4026,14 @@ "targetBaseUrl": { "type": "string", "description": "Target URL when using the record and playback API", - "example": "http://example.mocklab.io" + "example": "https://example.wiremock.org" } } } ] }, "example": { - "targetBaseUrl": "http://example.mocklab.io", + "targetBaseUrl": "https://example.wiremock.org", "filters": { "urlPathPattern": "/api/.*", "method": "GET" @@ -5077,6 +5082,98 @@ } } }, + "/__admin/files": { + "get": { + "operationId": "getAllFileNames", + "summary": "Get all file names", + "tags": [ + "Files" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "file1.json", + "file2.json", + "file3.txt" + ] + } + } + }, + "description": "All scenarios" + } + } + } + }, + "/__admin/files/{fileId}": { + "parameters": [ + { + "description": "The name of the file", + "in": "path", + "name": "fileId", + "required": true, + "example": "file1.json", + "schema": { + "type": "string" + } + } + ], + "get": { + "operationId": "getFileById", + "summary": "Get file by ID", + "tags": [ + "Files" + ], + "responses": { + "404": { + "description": "File not found" + }, + "200": { + "description": "The contents of the file" + } + } + }, + "put": { + "operationId": "updateFileById", + "summary": "Update or create a file", + "tags": [ + "Files" + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "byte" + } + } + } + }, + "responses": { + "200": { + "description": "OK - contents of the request body as a string" + } + } + }, + "delete": { + "operationId": "deleteFileById", + "summary": "Delete a file if it exists", + "tags": [ + "Files" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/__admin/settings": { "post": { "operationId": "updateGlobalSettings", @@ -5170,16 +5267,106 @@ } } }, - "/__admin/shutdown": { - "post": { - "operationId": "shutdownServer", - "description": "Shutdown the WireMock server", + "/__admin/shutdown" : { + "post" : { + "operationId" : "shutdownServer", + "summary" : "Shutdown the WireMock server", + "description" : "Shutdown the WireMock server", + "tags" : [ "System" ], + "responses" : { + "200" : { + "description" : "Server will be shut down" + } + } + } + }, + "/__admin/version": { + "get": { + "operationId": "getVersion", + "summary": "Return the version of the WireMock server", + "description": "Returns the version of the WireMock server", "tags": [ "System" ], "responses": { "200": { - "description": "Server will be shut down" + "description": "Successfully returned the version of the WireMock server", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "string", + "example": "3.8.0" + } + } + }, + "example": { + "version": "3.9.1" + } + } + } + } + } + } + }, + "/__admin/health": { + "get": { + "operationId": "getHealth", + "summary": "Return the health of the WireMock server", + "description": "Returns the health of the WireMock server", + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "Successful health and uptime data", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The status of the server", + "example": "healthy", + "enum": [ + "healthy", + "unhealthy" + ] + }, + "message": { + "type": "string", + "description": "Longer message regarding the status of the server", + "example": "Wiremock is ok" + }, + "version": { + "type": "string", + "description": "The WireMock version", + "example": "3.8.0" + }, + "upTimeInSeconds": { + "type": "integer", + "description": "How long the server has been running", + "example": 14355 + }, + "timestamp": { + "type": "string", + "description": "The current timestamp", + "example": "2024-07-03T13:16:06.172362Z" + } + } + }, + "example": { + "status": "healthy", + "message": "Wiremock is ok", + "version": "3.9.1", + "uptimeInSeconds": 14355, + "timestamp": "2024-07-03T13:16:06.172362Z" + } + } + } } } } @@ -5786,14 +5973,14 @@ "targetBaseUrl": { "type": "string", "description": "Target URL when using the record and playback API", - "example": "http://example.mocklab.io" + "example": "https://example.wiremock.org" } } } ] }, "example": { - "targetBaseUrl": "http://example.mocklab.io", + "targetBaseUrl": "https://example.wiremock.org", "filters": { "urlPathPattern": "/api/.*", "method": "GET" diff --git a/src/main/resources/swagger/wiremock-admin-api.yaml b/src/main/resources/swagger/wiremock-admin-api.yaml index 9c161407a0..af9d1d3fc1 100644 --- a/src/main/resources/swagger/wiremock-admin-api.yaml +++ b/src/main/resources/swagger/wiremock-admin-api.yaml @@ -2,11 +2,12 @@ openapi: 3.0.0 info: title: WireMock - version: 3.8.0 + version: 3.9.1 + description: "WireMock offers a REST API for administration, troubleshooting and analysis purposes" externalDocs: description: WireMock user documentation - url: http://wiremock.org/docs/ + url: https://wiremock.org/docs/ servers: - url: / @@ -16,27 +17,29 @@ tags: description: Operations on stub mappings externalDocs: description: User documentation - url: http://wiremock.org/docs/stubbing/ + url: https://wiremock.org/docs/stubbing/ - name: Requests description: Logged requests and responses received by the mock service externalDocs: description: User documentation - url: http://wiremock.org/docs/verifying/ + url: https://wiremock.org/docs/verifying/ - name: Near Misses description: Near misses allow querying of received requests or request patterns according to similarity externalDocs: description: User documentation - url: http://wiremock.org/docs/verifying/#near-misses + url: https://wiremock.org/docs/verifying/#near-misses - name: Recordings description: Stub mapping record and snapshot functions externalDocs: description: User documentation - url: http://wiremock.org/docs/record-playback/ + url: https://wiremock.org/docs/record-playback/ - name: Scenarios description: Scenarios support modelling of stateful behaviour externalDocs: description: User documentation - url: http://wiremock.org/docs/stateful-behaviour/ + url: https://wiremock.org/docs/stateful-behaviour/ + - name: Files + description: Manage the files used to support WireMock stubs - name: System description: Global operations @@ -81,6 +84,8 @@ paths: responses: '201': $ref: "#/components/responses/stubMapping" + '422': + $ref: "#/components/responses/badRequestEntity" delete: operationId: deleteAllStubMappings summary: Delete all stub mappings @@ -330,6 +335,8 @@ paths: example: $ref: "examples/requests.yaml" + + /__admin/requests/remove-by-metadata: post: operationId: removeRequestsByMetadata @@ -524,6 +531,66 @@ paths: '200': description: Successfully reset + + /__admin/files: + get: + operationId: getAllFileNames + summary: Get all file names + tags: + - Files + responses: + '200': + content: + application/json: + schema: + type: array + items: + type: string + example: ["file1.json", "file2.json", "file3.txt"] + description: All scenarios + + /__admin/files/{fileId}: + parameters: + - description: The name of the file + in: path + name: fileId + required: true + example: file1.json + schema: + type: string + get: + operationId: getFileById + summary: Get file by ID + tags: + - Files + responses: + '404': + description: File not found + '200': + description: The contents of the file + put: + operationId: updateFileById + summary: Update or create a file + tags: + - Files + requestBody: + content: + application/octet-stream: + schema: + type: string + format: byte + responses: + '200': + description: OK - contents of the request body as a string + delete: + operationId: deleteFileById + summary: Delete a file if it exists + tags: + - Files + responses: + '200': + description: OK + /__admin/settings: post: operationId: updateGlobalSettings @@ -561,12 +628,50 @@ paths: /__admin/shutdown: post: operationId: shutdownServer + summary: Shutdown the WireMock server description: Shutdown the WireMock server tags: - System responses: '200': description: Server will be shut down + + + /__admin/version: + get: + operationId: getVersion + summary: Return the version of the WireMock server + description: Returns the version of the WireMock server + tags: + - System + responses: + '200': + description: Successfully returned the version of the WireMock server + content: + application/json: + schema: + type: object + properties: + version: + type: string + example: "3.8.0" + + /__admin/health: + get: + operationId: getHealth + summary: Return the health of the WireMock server + description: Returns the health of the WireMock server + tags: + - System + responses: + '200': + description: Successful health and uptime data + content: + application/json: + schema: + $ref: 'schemas/health.yaml' + example: + $ref: 'examples/health.yaml' components: requestBodies: @@ -602,7 +707,7 @@ components: targetBaseUrl: type: string description: Target URL when using the record and playback API - example: http://example.mocklab.io + example: https://example.wiremock.org example: $ref: "examples/record-spec.yaml" @@ -651,4 +756,11 @@ components: items: $ref: "schemas/logged-request.yaml" example: - $ref: 'examples/near-misses.yaml' \ No newline at end of file + $ref: 'examples/near-misses.yaml' + + badRequestEntity: + description: Bad request body + content: + application/json: + schema: + $ref: "schemas/bad-request-entity.yaml" \ No newline at end of file diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index 0e04abf911..9e4100c117 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -1 +1 @@ -version=3.8.0 \ No newline at end of file +version=3.9.1 diff --git a/src/test/java/com/github/tomakehurst/wiremock/AcceptanceTestBase.java b/src/test/java/com/github/tomakehurst/wiremock/AcceptanceTestBase.java index e222aca8a3..84a1d8ef29 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/AcceptanceTestBase.java +++ b/src/test/java/com/github/tomakehurst/wiremock/AcceptanceTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.io.IOException; import java.nio.file.Files; import java.util.Locale; +import java.util.Optional; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -83,14 +84,27 @@ public static void setupServerWithMappingsInFileRoot() { public static void setupServer(WireMockConfiguration options) { System.out.println( "Configuring WireMockServer with root directory: " + options.filesRoot().getPath()); - if (options.portNumber() == Options.DEFAULT_PORT) { + + // SERVER_PORT + CLIENT PORT are here to support routing through an external proxy for e.g. + // validation + final String serverPort = System.getenv("SERVER_PORT"); + if (serverPort != null) { + options.port(Integer.parseInt(serverPort)); + } else if (options.portNumber() == Options.DEFAULT_PORT) { options.dynamicPort(); } wireMockServer = new WireMockServer(options); wireMockServer.start(); testClient = new WireMockTestClient(wireMockServer.port()); - WireMock.configureFor(wireMockServer.port()); + + int clientPort = + Optional.ofNullable(System.getenv("CLIENT_PORT")) + .map(Integer::parseInt) + .orElse(wireMockServer.port()); + + WireMock wireMockClient = new WireMock(clientPort); + WireMock.configureFor(wireMockClient); wm = wireMockServer; } diff --git a/src/test/java/com/github/tomakehurst/wiremock/ContentPatternsJsonValidityTest.java b/src/test/java/com/github/tomakehurst/wiremock/ContentPatternsJsonValidityTest.java new file mode 100644 index 0000000000..24948a54cd --- /dev/null +++ b/src/test/java/com/github/tomakehurst/wiremock/ContentPatternsJsonValidityTest.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 com.github.tomakehurst.wiremock; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.havingExactly; +import static com.github.tomakehurst.wiremock.common.DateTimeTruncation.FIRST_DAY_OF_MONTH; +import static com.github.tomakehurst.wiremock.common.DateTimeTruncation.LAST_DAY_OF_MONTH; +import static com.github.tomakehurst.wiremock.common.DateTimeUnit.DAYS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.Json; +import com.github.tomakehurst.wiremock.testsupport.TestFiles; +import com.networknt.schema.*; +import java.util.Map; +import java.util.Set; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ContentPatternsJsonValidityTest { + + static JsonSchemaFactory schemaFactory; + static SchemaValidatorsConfig config; + static JsonSchema schema; + + @BeforeAll + static void init() { + config = SchemaValidatorsConfig.builder().build(); + + schemaFactory = + JsonSchemaFactory.getInstance(WireMock.JsonSchemaVersion.V202012.toVersionFlag()); + + schema = + schemaFactory.getSchema( + SchemaLocation.of(TestFiles.fileUri("swagger/schemas/content-pattern.yaml").toString()), + config); + } + + @Test + void equalToValidates() { + assertThat(validate(equalTo("abc")), empty()); + assertThat( + validate("{ \"equalTo\": \"thing\", \"caseInsensitive\": 3 }"), Matchers.not(empty())); + } + + @Test + void binaryEqualToValidates() { + assertThat(validate(binaryEqualTo("abc".getBytes())), empty()); + assertThat(validate("{ \"binaryEqualTo\": \"not base 64\" }"), Matchers.not(empty())); + } + + @Test + void equalToJsonWithMinimalParametersValidates() { + assertThat(validate(equalToJson("{}")), empty()); + assertThat(validate("{ \"equalToJson\": 5 }"), Matchers.not(empty())); + } + + @Test + void equalToJsonWithAllParametersValidates() { + assertThat(validate(equalToJson("{}", false, true)), empty()); + assertThat( + validate( + "{ \"equalToJson\": \"{}\", \"ignoreExtraElements\": false, \"ignoreArrayOrder\": {} }"), + Matchers.not(empty())); + } + + @Test + void simpleMatchesJsonPathValidates() { + assertThat(validate(matchingJsonPath("$.id")), empty()); + assertThat(validate("{ \"matchesJsonPath\": 5 }"), Matchers.not(empty())); + } + + @Test + void matchesJsonPathWithSubMatcherValidates() { + assertThat(validate(matchingJsonPath("$.id", equalTo("123"))), empty()); + assertThat(validate("{ \"matchesJsonPath\": 5 }"), Matchers.not(empty())); + } + + @Test + void equalToXmlWithMinimalParametersValidates() { + assertThat(validate(equalToXml("")), empty()); + assertThat(validate("{ \"equalToXml\": 5 }"), Matchers.not(empty())); + } + + @Test + void equalToXmlWithAllParametersValidates() { + assertThat(validate(equalToXml("", true, "[", "]", true)), empty()); + assertThat( + validate( + "{\n" + + " \"equalToXml\" : \"\",\n" + + " \"enablePlaceholders\" : true,\n" + + " \"placeholderOpeningDelimiterRegex\" : 3,\n" + + " \"placeholderClosingDelimiterRegex\" : \"]\"\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void simpleMatchesXPathValidates() { + assertThat(validate(matchingXPath("//Order/Quantity")), empty()); + assertThat(validate("{ \"matchesXPath\": 5 }"), Matchers.not(empty())); + } + + @Test + void matchesXPathWithSubMatcherValidates() { + assertThat(validate(matchingXPath("//Order/Quantity", equalTo("123"))), empty()); + assertThat( + validate( + "{\n" + + " \"matchesXPath\": {\n" + + " \"expression\": true,\n" + + " \"equalTo\": \"123\"\n" + + " }\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void matchesXPathWithNamespacesValidates() { + assertThat( + validate( + matchingXPath( + "//Order/Quantity", + Map.of("one", "https://example.com/one", "two", "https://example.com/two"))), + empty()); + + assertThat( + validate( + "{\n" + + " \"matchesXPath\" : \"//Order/Quantity\",\n" + + " \"xPathNamespaces\" : {\n" + + " \"one\": \"https://example.com/one\",\n" + + " \"two\": 543 \n" + + " }\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void matchesJsonSchemaValidates() { + assertThat(validate(matchingJsonSchema("{ \"type\": \"string\" }")), empty()); + assertThat(validate("{ \"matchesJsonSchema\": true }"), Matchers.not(empty())); + } + + @Test + void matchesJsonSchemaWithVersionValidates() { + assertThat(validate(matchingJsonSchema("{ \"type\": \"string\" }")), empty()); + assertThat(validate("{ \"matchesJsonSchema\": true }"), Matchers.not(empty())); + } + + @Test + void containsValidates() { + assertThat(validate(containing("abc")), empty()); + assertThat(validate("{ \"contains\": true }"), Matchers.not(empty())); + } + + @Test + void doesNotContainValidates() { + assertThat(validate(notContaining("abc")), empty()); + assertThat(validate("{ \"doesNotContain\": true }"), Matchers.not(empty())); + } + + @Test + void matchesValidates() { + assertThat(validate(matching("abc")), empty()); + assertThat(validate("{ \"matches\": true }"), Matchers.not(empty())); + } + + @Test + void not_equalToValidates() { + assertThat(validate(not(equalTo("abc"))), empty()); + + assertThat(validate("{\n" + " \"not\": true\n" + "}"), Matchers.not(empty())); + } + + @Test + void doesNotMatchValidates() { + assertThat(validate(notMatching("abc")), empty()); + assertThat(validate("{ \"doesNotMatch\": true }"), Matchers.not(empty())); + } + + @Test + void beforeWithMinimalParametersValidates() { + assertThat(validate(before("2018-05-05T00:11:22Z")), empty()); + assertThat(validate("{ \"before\": 55 }"), Matchers.not(empty())); + } + + @Test + void beforeWithAllParametersValidates() { + assertThat( + validate( + before("2018-05-05T00:11:22Z") + .actualFormat("yyyy-MM-dd") + .expectedOffset(3, DAYS) + .truncateExpected(FIRST_DAY_OF_MONTH) + .truncateActual(LAST_DAY_OF_MONTH)), + empty()); + + assertThat( + validate( + "{\n" + + " \"before\" : \"now +3 days\",\n" + + " \"actualFormat\" : \"yyyy-MM-dd\",\n" + + " \"truncateExpected\" : true,\n" + + " \"truncateActual\" : \"last day of month\"\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void afterWithMinimalParametersValidates() { + assertThat(validate(after("2018-05-05T00:11:22Z")), empty()); + assertThat(validate("{ \"after\": 55 }"), Matchers.not(empty())); + } + + @Test + void afterWithAllParametersValidates() { + assertThat( + validate( + after("2018-05-05T00:11:22Z") + .actualFormat("yyyy-MM-dd") + .expectedOffset(3, DAYS) + .truncateExpected(FIRST_DAY_OF_MONTH) + .truncateActual(LAST_DAY_OF_MONTH)), + empty()); + + assertThat( + validate( + "{\n" + + " \"after\" : \"now +3 days\",\n" + + " \"actualFormat\" : \"yyyy-MM-dd\",\n" + + " \"truncateExpected\" : true,\n" + + " \"truncateActual\" : \"last day of month\"\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void equalToDateTimeWithMinimalParametersValidates() { + assertThat(validate(equalToDateTime("2018-05-05T00:11:22Z")), empty()); + assertThat(validate("{ \"equalToDateTime\": 55 }"), Matchers.not(empty())); + } + + @Test + void equalToDateTimeWithAllParametersValidates() { + assertThat( + validate( + equalToDateTime("2018-05-05T00:11:22Z") + .actualFormat("yyyy-MM-dd") + .expectedOffset(3, DAYS) + .truncateExpected(FIRST_DAY_OF_MONTH) + .truncateActual(LAST_DAY_OF_MONTH)), + empty()); + + assertThat( + validate( + "{\n" + + " \"equalToDateTime\" : \"now +3 days\",\n" + + " \"actualFormat\" : \"yyyy-MM-dd\",\n" + + " \"truncateExpected\" : true,\n" + + " \"truncateActual\" : \"last day of month\"\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void tmp() { + System.out.println( + Json.write( + matchingXPath( + "//Order/Quantity", + Map.of("one", "https://example.com/one", "two", "https://example.com/two")))); + } + + @Test + void afterValidates() { + assertThat(validate(after("2018-05-05T00:11:22Z")), empty()); + assertThat(validate("{ \"after\": 55 }"), Matchers.not(empty())); + } + + @Test + void absentValidates() { + assertThat(validate(absent()), empty()); + assertThat(validate("{ \"absent\": 11 }"), Matchers.not(empty())); + } + + @Test + void and_containsValidates() { + assertThat(validate(containing("abc").and(containing("123"))), empty()); + + assertThat( + validate( + "{\n" + + " \"and\": [\n" + + " {\n" + + " \"contains\": \"abc\"\n" + + " },\n" + + " \"wrong\"\n" + + " ]\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void or_containsValidates() { + assertThat(validate(containing("abc").or(containing("123"))), empty()); + + assertThat( + validate( + "{\n" + + " \"or\": [\n" + + " {\n" + + " \"contains\": \"abc\"\n" + + " },\n" + + " \"wrong\"\n" + + " ]\n" + + "}"), + Matchers.not(empty())); + } + + @Test + void hasExactlyValidates() { + assertThat(validate(havingExactly(equalTo("1"), containing("2"))), empty()); + assertThat(validate("{ \"hasExactly\": \"blah\" }"), Matchers.not(empty())); + } + + @Test + void includesExactlyValidates() { + assertThat(validate(including(equalTo("1"), containing("2"))), empty()); + assertThat(validate("{ \"includes\": \"blah\" }"), Matchers.not(empty())); + } + + private static Set validate(Object obj) { + return schema.validate(Json.write(obj), InputFormat.JSON); + } + + private static Set validate(String json) { + return schema.validate(json, InputFormat.JSON); + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/JsonSchemaMatchingAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/JsonSchemaMatchingAcceptanceTest.java index af2419507f..a4fdf85be5 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/JsonSchemaMatchingAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/JsonSchemaMatchingAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,15 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.testsupport.TestFiles.file; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import com.github.tomakehurst.wiremock.common.InvalidInputException; import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class JsonSchemaMatchingAcceptanceTest extends AcceptanceTestBase { @@ -70,5 +75,44 @@ void doesNotMatchStubWhenRequestBodyIsNotValidJson() { assertThat(response.statusCode(), is(404)); } + @ParameterizedTest + @ValueSource( + strings = { + "", + "not json", + "{\"type\": \"string\"", + }) + void doesNotAcceptStubWhenSchemaIsNotValidJson(String schema) { + InvalidInputException e = + assertThrows( + InvalidInputException.class, + () -> + stubFor( + post(urlPathEqualTo("/schema-match")) + .withRequestBody(matchingJsonSchema(schema)) + .willReturn(ok()))); + + assertThat(wireMockServer.getStubMappings(), is(empty())); + } + + @ParameterizedTest + @ValueSource( + strings = { + "{\"id\": 1, \"name\": \"alice\"}", + "{\"type\": \"array\", \"items\": {\"$ref\": \"#/does/not/exist\"}}", + }) + void doesNotMatchStubWhenSchemaIsValidJsonButNotValidSchema(String schema) { + String json = "{\"id\": 1, \"name\": \"alice\"}"; + + stubFor( + post(urlPathEqualTo("/schema-match")) + .withRequestBody(matchingJsonSchema(schema)) + .willReturn(ok())); + + WireMockResponse response = testClient.postJson("/schema-match", json); + + assertThat(response.statusCode(), is(404)); + } + // TODO: Diffs } diff --git a/src/test/java/com/github/tomakehurst/wiremock/common/DateTimeParserTest.java b/src/test/java/com/github/tomakehurst/wiremock/common/DateTimeParserTest.java index 5ff47d06bc..6933f4b6fa 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/common/DateTimeParserTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/common/DateTimeParserTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; +import java.time.*; import java.time.format.DateTimeFormatter; import org.junit.jupiter.api.Test; @@ -60,6 +58,18 @@ public void parsesLocalDateFromFormatString() { assertThat(parser.parseLocalDate("23/06/2021"), is(LocalDate.parse("2021-06-23"))); } + @Test + public void parsesYearMonthFromFormatString() { + DateTimeParser parser = DateTimeParser.forFormat("MM/yyyy"); + assertThat(parser.parseYearMonth("06/2021"), is(YearMonth.parse("2021-06"))); + } + + @Test + public void parsesYearFromFormatString() { + DateTimeParser parser = DateTimeParser.forFormat("yy"); + assertThat(parser.parseYear("21"), is(Year.parse("2021"))); + } + @Test public void parsesUnix() { DateTimeParser parser = DateTimeParser.forFormat("unix"); diff --git a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformerTest.java b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformerTest.java index 682e8525b7..9d7bae8f4e 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformerTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformerTest.java @@ -44,6 +44,7 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; public class ResponseTemplateTransformerTest { @@ -743,6 +744,36 @@ public void picksRandomElementFromListVariable() { assertThat(body, anyOf(is("Gus"), is("Tom"), is("Rob"))); } + @Test + void picksRandomObjectFromListVariable() { + String body = + transform( + "{{val (parseJson '{\"level\":1}') assign='one'}}\n" + + "{{val (parseJson '{\"level\":2}') assign='two'}}\n" + + "{{val (parseJson '{\"level\":3}') assign='three'}}\n" + + "{{lookup (pickRandom (array one two three)) 'level'}}"); + + assertThat(body.trim(), anyOf(is("1"), is("2"), is("3"))); + } + + @RepeatedTest(10) + void picksMultipleRandomItemsFromListVariableWhenCountSpecified() { + String body = + transform( + "{{val (pickRandom (array 1 2 3 4 5) count=3) assign='result'}}{{result.0}} {{result.1}} {{result.2}} size={{size result}}"); + + assertThat(body, matchesRegex("\\d \\d \\d size=3")); + assertThat(body.split(" ")[0], not(body.split(" ")[1])); + } + + @Test + void picksAsManyRandomItemsAsPossibleFromListVariableWhenCountSpecifiedHigherThanItemCount() { + String body = + transform("{{val (pickRandom (array 1 2 3 4 5) count=8) assign='result'}}{{size result}}"); + + assertThat(body, matchesRegex("5")); + } + @Test public void squareBracketedRequestParameters1() { String body = diff --git a/src/test/java/com/github/tomakehurst/wiremock/matching/EqualToDateTimePatternTest.java b/src/test/java/com/github/tomakehurst/wiremock/matching/EqualToDateTimePatternTest.java index a001606ab1..5e789ae352 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/matching/EqualToDateTimePatternTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/matching/EqualToDateTimePatternTest.java @@ -31,10 +31,8 @@ import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.http.MultiValue; import com.google.common.collect.Lists; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; +import java.time.format.DateTimeFormatter; import org.junit.jupiter.api.Test; public class EqualToDateTimePatternTest { @@ -85,6 +83,62 @@ public void matchesZonedToLocal() { assertFalse(matcher.match(bad).isExactMatch()); } + @Test + public void matchesNowToYearMonth() { + YearMonth currentYearMonth = YearMonth.now(); + YearMonth previousYearMonth = currentYearMonth.minusMonths(1); + StringValuePattern matcher = + WireMock.isNow().truncateExpected(DateTimeTruncation.FIRST_DAY_OF_MONTH); + + String good = currentYearMonth.toString(); + String bad = previousYearMonth.toString(); + + assertTrue(matcher.match(good).isExactMatch()); + assertFalse(matcher.match(bad).isExactMatch()); + } + + @Test + public void matchesNowToYearMonthInCustomFormat() { + YearMonth currentYearMonth = YearMonth.now(); + StringValuePattern matcher = + WireMock.isNow() + .truncateExpected(DateTimeTruncation.FIRST_DAY_OF_MONTH) + .actualFormat("MM/yyyy"); + + String good = currentYearMonth.format(DateTimeFormatter.ofPattern("MM/yyyy")); + String bad = currentYearMonth.toString(); + + assertTrue(matcher.match(good).isExactMatch()); + assertFalse(matcher.match(bad).isExactMatch()); + } + + @Test + public void matchesNowToYear() { + Year currentYear = Year.now(); + Year previousYear = currentYear.minusYears(1); + StringValuePattern matcher = + WireMock.isNow().truncateExpected(DateTimeTruncation.FIRST_DAY_OF_YEAR); + + String good = currentYear.toString(); + String bad = previousYear.toString(); + + assertTrue(matcher.match(good).isExactMatch()); + assertFalse(matcher.match(bad).isExactMatch()); + } + + @Test + public void matchesNowToYearInCustomFormat() { + Year currentYear = Year.now(); + StringValuePattern matcher = + WireMock.isNow().truncateExpected(DateTimeTruncation.FIRST_DAY_OF_YEAR).actualFormat("yy"); + + String good = currentYear.format(DateTimeFormatter.ofPattern("yy")); + String bad = currentYear.toString(); + + assertTrue(matcher.match(good).isExactMatch()); + assertFalse(matcher.match(bad).isExactMatch()); + } + @Test public void matchesActualInUnixTimeFormat() { String dateTime = "2021-06-14T12:13:14Z"; diff --git a/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPatternTest.java b/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPatternTest.java index e5c65fdd07..27f7456017 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPatternTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonSchemaPatternTest.java @@ -24,16 +24,22 @@ import static org.hamcrest.Matchers.*; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.Errors; import com.github.tomakehurst.wiremock.common.Json; +import com.github.tomakehurst.wiremock.stubbing.SubEvent; import com.jayway.jsonpath.JsonPath; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Stream; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; public class MatchesJsonSchemaPatternTest { @@ -358,6 +364,82 @@ private static Stream recursiveSchemaNonMatchingExamples() { "{ \"name\": \"invalid_grandchild\", \"children\": [{ \"name\": \"invalid_child\", \"children\": [{}] }] }")); } + @ParameterizedTest + @ValueSource( + strings = { + "{\"id\": 1, \"name\": \"alice\"}", + "{\"type\": \"array\", \"items\": {\"$ref\": \"#/does/not/exist\"}}", + }) + void invalidJsonSchemaNeverMatches(String schema) { + MatchesJsonSchemaPattern pattern = new MatchesJsonSchemaPattern(schema); + + assertThat(pattern.match("{\"field\":\"value\"}").isExactMatch(), is(false)); + assertThat(pattern.match("\"json string\"").isExactMatch(), is(false)); + assertThat(pattern.match("{\"id\":1,\"name\":\"alice\"}").isExactMatch(), is(false)); + assertThat(pattern.match("[{\"id\":1,\"name\":\"alice\"}]").isExactMatch(), is(false)); + } + + @Test + void invalidJsonSchemaMatchResultsContainExplanatorySubEvent() { + class SubEventMatcher extends TypeSafeMatcher { + + private final Map expectedData; + + SubEventMatcher(Errors expectedData) { + this.expectedData = Json.objectToMap(expectedData); + } + + @Override + protected boolean matchesSafely(SubEvent item) { + return item.getType().equals(SubEvent.ERROR) && item.getData().equals(expectedData); + } + + @Override + public void describeTo(Description description) { + description.appendText( + "a sub event of type " + SubEvent.ERROR + " with data " + expectedData); + } + } + + MatchResult matchResult1 = + new MatchesJsonSchemaPattern("{\"id\":1,\"name\":\"alice\"}") + .match("{\"field\":\"value\"}"); + assertThat(matchResult1.isExactMatch(), is(false)); + Errors expectedErrors1 = + Errors.singleWithDetail(10, "Invalid JSON Schema", "No suitable validator for id"); + assertThat(matchResult1.getSubEvents(), contains(new SubEventMatcher(expectedErrors1))); + + MatchResult matchResult2 = + new MatchesJsonSchemaPattern( + "{\"type\": \"array\", \"items\": {\"$ref\": \"#/does/not/exist\"}}") + .match("[{\"id\":1,\"name\":\"alice\"}]"); + assertThat(matchResult2.isExactMatch(), is(false)); + Errors expectedErrors2 = + Errors.singleWithDetail( + 10, "Invalid JSON Schema", ": Reference /does/not/exist cannot be resolved"); + assertThat(matchResult2.getSubEvents(), contains(new SubEventMatcher(expectedErrors2))); + + // Check for false positives. + MatchResult matchResult3 = + new MatchesJsonSchemaPattern("{\"type\": \"string\"}").match("\"my value\""); + assertThat(matchResult3.isExactMatch(), is(true)); + assertThat( + matchResult3.getSubEvents(), + not( + contains( + new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(SubEvent item) { + return item.getType().equals(SubEvent.ERROR); + } + + @Override + public void describeTo(Description description) { + description.appendText("a sub event of type " + SubEvent.ERROR); + } + }))); + } + private static String stringify(String json) { return "\"" + json.replace("\n", "").replace("\"", "\\\"") + "\""; } diff --git a/src/test/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStoreTest.java b/src/test/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStoreTest.java index 7b4514721e..2e66d4f223 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStoreTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/store/InMemoryObjectStoreTest.java @@ -19,6 +19,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; public class InMemoryObjectStoreTest { @@ -114,4 +118,171 @@ void tryingToRetrieveMissingKeyDoesNotEjectOtherKeys() { assertThat(store.getAllKeys().count(), is(3L)); } + + @Test + void computingNullValueIsEquivalentToRemoval() { + InMemoryObjectStore store = new InMemoryObjectStore(3); + + store.put("one", "1"); + store.put("two", "2"); + store.put("three", "3"); + + store.compute("three", current -> null); + + assertThat(store.get("three"), is(Optional.empty())); + assertThat(store.getAllKeys().collect(toList()), containsInAnyOrder("one", "two")); + + store.put("four", "4"); + + assertThat(store.getAllKeys().collect(toList()), containsInAnyOrder("one", "two", "four")); + + AtomicReference previousValue = new AtomicReference<>(""); + store.compute("three", previousValue::getAndSet); + assertThat(previousValue.get(), is(nullValue())); + } + + @Test + void eventEmittedOnPut() { + InMemoryObjectStore store = new InMemoryObjectStore(3); + + List> events1 = new ArrayList<>(); + store.registerEventListener(events1::add); + assertThat(events1, is(empty())); + + store.put("one", "1"); + + assertThat(events1, containsInAnyOrder(new StoreEvent<>("one", null, "1"))); + + List> events2 = new ArrayList<>(); + store.registerEventListener(events2::add); + assertThat(events2, is(empty())); + events1.clear(); + + store.put("two", "2"); + + assertThat(events1, containsInAnyOrder(new StoreEvent<>("two", null, "2"))); + assertThat(events2, containsInAnyOrder(new StoreEvent<>("two", null, "2"))); + + events1.clear(); + events2.clear(); + store.put("one", "3"); + + assertThat(events1, containsInAnyOrder(new StoreEvent<>("one", "1", "3"))); + assertThat(events2, containsInAnyOrder(new StoreEvent<>("one", "1", "3"))); + } + + @Test + void eventEmittedOnRemoval() { + InMemoryObjectStore store = new InMemoryObjectStore(3); + + store.put("one", "1"); + store.put("two", "2"); + store.put("three", "3"); + + List> events1 = new ArrayList<>(); + store.registerEventListener(events1::add); + assertThat(events1, is(empty())); + + store.remove("two"); + + assertThat(events1, containsInAnyOrder(new StoreEvent<>("two", "2", null))); + + List> events2 = new ArrayList<>(); + store.registerEventListener(events2::add); + events1.clear(); + + store.remove("three"); + + assertThat(events1, containsInAnyOrder(new StoreEvent<>("three", "3", null))); + assertThat(events2, containsInAnyOrder(new StoreEvent<>("three", "3", null))); + + events1.clear(); + events2.clear(); + // No event is emitted when nothing is removed. + store.remove("does not exist"); + + assertThat(events1, is(empty())); + assertThat(events2, is(empty())); + } + + @Test + void eventEmittedOnCompute() { + InMemoryObjectStore store = new InMemoryObjectStore(3); + + List> events = new ArrayList<>(); + store.registerEventListener(events::add); + + store.compute("one", o -> "1"); + + assertThat(events, containsInAnyOrder(new StoreEvent<>("one", null, "1"))); + + events.clear(); + store.compute("two", o -> "2"); + + assertThat(events, containsInAnyOrder(new StoreEvent<>("two", null, "2"))); + + events.clear(); + store.compute("one", o -> "3"); + + assertThat(events, containsInAnyOrder(new StoreEvent<>("one", "1", "3"))); + } + + @Test + void eventEmittedOnRemovalByMaxItemLimit() { + InMemoryObjectStore store = new InMemoryObjectStore(3); + + store.put("one", "1"); + store.put("two", "2"); + store.put("three", "3"); + + List> events = new ArrayList<>(); + store.registerEventListener(events::add); + + store.put("four", "4"); + + assertThat( + events, + containsInAnyOrder( + new StoreEvent<>("four", null, "4"), new StoreEvent<>("one", "1", null))); + + events.clear(); + store.compute("five", o -> "5"); + + assertThat( + events, + containsInAnyOrder( + new StoreEvent<>("five", null, "5"), new StoreEvent<>("two", "2", null))); + } + + @Test + void exceptionsThrownInAnEventHandlerAreCaught() { + InMemoryObjectStore store = new InMemoryObjectStore(3); + + List> events1 = new ArrayList<>(); + store.registerEventListener(events1::add); + store.registerEventListener( + event -> { + throw new RuntimeException(); + }); + List> events2 = new ArrayList<>(); + store.registerEventListener(events2::add); + + store.put("one", "1"); + + assertThat(events1, containsInAnyOrder(new StoreEvent<>("one", null, "1"))); + assertThat(events2, containsInAnyOrder(new StoreEvent<>("one", null, "1"))); + } + + @Test + void eventsCanBeEmittedToAMoreGeneralEventListener() { + InMemoryObjectStore store = new InMemoryObjectStore(3); + + List> events1 = new ArrayList<>(); + + // Do not remove the event type, it then compiles without the fix + store.registerEventListener((StoreEvent e) -> events1.add(e)); + store.put("1", 1); + + assertThat(events1, contains(new StoreEvent<>("1", null, 1))); + } } diff --git a/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java index 2a93378095..adffc2d180 100644 --- a/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; public class TestFiles { @@ -56,7 +57,11 @@ public static String file(String path) { } public static String filePath(String path) { - return new File(getResourceURI(TestFiles.class, path)).getAbsolutePath(); + return new File(fileUri(path)).getAbsolutePath(); + } + + public static URI fileUri(String path) { + return getResourceURI(TestFiles.class, path); } public static String sampleWarRootDir() { diff --git a/ui/package.json b/ui/package.json index b5ad888b20..b69fe5dfb7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "wiremock-ui-resources", - "version": "3.8.0", + "version": "3.9.1", "description": "WireMock UI resources processor", "engines": { "node": ">= 0.10.0"