diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/JiraConfig.java b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/JiraConfig.java index 5c39ba4f4..dbf525b0e 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/JiraConfig.java +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/JiraConfig.java @@ -18,6 +18,7 @@ import static retrofit.Endpoints.newFixedEndpoint; +import com.netflix.spinnaker.echo.jackson.EchoObjectMapper; import com.netflix.spinnaker.echo.jira.JiraProperties; import com.netflix.spinnaker.echo.jira.JiraService; import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; @@ -54,7 +55,7 @@ JiraService jiraService( RestAdapter.Builder builder = new RestAdapter.Builder() .setEndpoint(newFixedEndpoint(jiraProperties.getBaseUrl())) - .setConverter(new JacksonConverter()) + .setConverter(new JacksonConverter(EchoObjectMapper.getInstance())) .setClient(retrofitClient) .setLogLevel(retrofitLogLevel) .setLog(new Slf4jRetrofitLogger(JiraService.class)); diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationService.java b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationService.java index 8a057238d..bc519a94c 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationService.java +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationService.java @@ -18,17 +18,23 @@ import static net.logstash.logback.argument.StructuredArguments.kv; +import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.netflix.spinnaker.echo.api.Notification; import com.netflix.spinnaker.echo.controller.EchoResponse; -import com.netflix.spinnaker.echo.jira.JiraService.CreateJiraIssueRequest; -import com.netflix.spinnaker.echo.jira.JiraService.CreateJiraIssueResponse; +import com.netflix.spinnaker.echo.jira.JiraService.CommentIssueRequest; +import com.netflix.spinnaker.echo.jira.JiraService.CreateIssueRequest; +import com.netflix.spinnaker.echo.jira.JiraService.CreateIssueResponse; +import com.netflix.spinnaker.echo.jira.JiraService.IssueTransitions; +import com.netflix.spinnaker.echo.jira.JiraService.TransitionIssueRequest; import com.netflix.spinnaker.echo.notification.NotificationService; import com.netflix.spinnaker.kork.core.RetrySupport; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -37,36 +43,102 @@ import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.ResponseStatus; import retrofit.RetrofitError; +import retrofit.client.Response; @Component @ConditionalOnProperty("jira.enabled") public class JiraNotificationService implements NotificationService { private static final Logger LOGGER = LoggerFactory.getLogger(JiraNotificationService.class); private static final int MAX_RETRY = 3; - private static final long RETRY_BACKOFF = 3; + private static final long RETRY_BACKOFF = 100; private final JiraService jiraService; private final RetrySupport retrySupport; private final ObjectMapper mapper; @Autowired - public JiraNotificationService(JiraService jiraService, ObjectMapper objectMapper) { + public JiraNotificationService( + JiraService jiraService, RetrySupport retrySupport, ObjectMapper objectMapper) { this.jiraService = jiraService; - this.retrySupport = new RetrySupport(); + this.retrySupport = retrySupport; this.mapper = objectMapper; } @Override public boolean supportsType(String type) { - return "JIRA".equals(type.toUpperCase()); + return "JIRA".equalsIgnoreCase(type); } @Override - public EchoResponse handle(Notification notification) { + public EchoResponse handle(Notification notification) { + return isTransition(notification) ? transitionIssue(notification) : create(notification); + } + + private boolean isTransition(Notification notification) { + return notification.getAdditionalContext().get("transitionContext") != null; + } + + private EchoResponse.Void transitionIssue(Notification notification) { + TransitionJiraNotification transitionNotification = + mapper.convertValue(notification.getAdditionalContext(), TransitionJiraNotification.class); + String jiraIssue = transitionNotification.getJiraIssue(); + + try { + // transitionContext is the full Jira transition API payload (which is stored in + // transitionDetails) - except the transition ID is probably unknown. So, we get the + // transition ID from the transition name. + Map transition = + transitionNotification.getTransitionContext().getTransition(); + Map transitionDetails = + transitionNotification.getTransitionContext().getTransitionDetails(); + String transitionName = transition.get("name"); + + IssueTransitions issueTransitions = + retrySupport.retry(getIssueTransitions(jiraIssue), MAX_RETRY, RETRY_BACKOFF, false); + + issueTransitions.getTransitions().stream() + .filter(it -> it.getName().equals(transitionName)) + .findFirst() + .ifPresentOrElse( + t -> { + transition.put("id", t.getId()); + transitionDetails.put("transition", transition); + }, + () -> { + throw new IllegalArgumentException( + ImmutableMap.of( + "issue", jiraIssue, + "transitionName", transitionName, + "validTransitionNames", + issueTransitions.getTransitions().stream() + .map(IssueTransitions.Transition::getName) + .collect(Collectors.toList())) + .toString()); + }); + + retrySupport.retry( + transitionIssue(jiraIssue, transitionDetails), MAX_RETRY, RETRY_BACKOFF, false); + + if (transitionNotification.getComment() != null) { + retrySupport.retry( + addComment(jiraIssue, transitionNotification.getComment()), + MAX_RETRY, + RETRY_BACKOFF, + false); + } + + return new EchoResponse.Void(); + } catch (Exception e) { + throw new TransitionJiraIssueException( + String.format("Failed to transition Jira issue %s: %s", jiraIssue, errors(e)), e); + } + } + + private EchoResponse create(Notification notification) { Map issueRequestBody = issueRequestBody(notification); try { - CreateJiraIssueResponse response = - retrySupport.retry(createJiraIssue(issueRequestBody), MAX_RETRY, RETRY_BACKOFF, false); + CreateIssueResponse response = + retrySupport.retry(createIssue(issueRequestBody), MAX_RETRY, RETRY_BACKOFF, false); return new EchoResponse<>(response); } catch (Exception e) { @@ -78,8 +150,22 @@ public EchoResponse handle(Notification notification) { } } - private Supplier createJiraIssue(Map issueRequestBody) { - return () -> jiraService.createJiraIssue(new CreateJiraIssueRequest(issueRequestBody)); + private Supplier getIssueTransitions(String issueIdOrKey) { + return () -> jiraService.getIssueTransitions(issueIdOrKey); + } + + private Supplier transitionIssue( + String issueIdOrKey, Map transitionDetails) { + return () -> + jiraService.transitionIssue(issueIdOrKey, new TransitionIssueRequest(transitionDetails)); + } + + private Supplier addComment(String issueIdOrKey, String comment) { + return () -> jiraService.addComment(issueIdOrKey, new CommentIssueRequest(comment)); + } + + private Supplier createIssue(Map issueRequestBody) { + return () -> jiraService.createIssue(new CreateIssueRequest(issueRequestBody)); } private Map issueRequestBody(Notification notification) { @@ -113,4 +199,65 @@ public CreateJiraIssueException(String message, Throwable cause) { super(message, cause); } } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST) + static class TransitionJiraIssueException extends RuntimeException { + public TransitionJiraIssueException(String message, Throwable cause) { + super(message, cause); + } + } + + static class TransitionJiraNotification { + private String jiraIssue; + private String comment; + private TransitionContext transitionContext; + + public String getJiraIssue() { + return jiraIssue; + } + + public void setJiraIssue(String jiraIssue) { + this.jiraIssue = jiraIssue; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public TransitionContext getTransitionContext() { + return transitionContext; + } + + public void setTransitionContext(TransitionContext transitionContext) { + this.transitionContext = transitionContext; + } + + static class TransitionContext { + private Map transition; + + // placeholder for all the other remaining transition context payload + private Map transitionDetails = new HashMap<>(); + + public Map getTransition() { + return transition; + } + + public void setTransition(Map transition) { + this.transition = transition; + } + + public Map getTransitionDetails() { + return transitionDetails; + } + + @JsonAnySetter + public void setTransitionDetails(String name, Object value) { + this.transitionDetails.put(name, value); + } + } + } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraService.java b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraService.java index e945a7f42..dd0799e61 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraService.java +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraService.java @@ -18,21 +18,92 @@ import com.netflix.spinnaker.echo.controller.EchoResponse; import java.util.HashMap; +import java.util.List; import java.util.Map; +import retrofit.client.Response; import retrofit.http.Body; +import retrofit.http.GET; import retrofit.http.POST; +import retrofit.http.Path; public interface JiraService { @POST("/rest/api/2/issue/") - CreateJiraIssueResponse createJiraIssue(@Body CreateJiraIssueRequest createJiraIssueRequest); + CreateIssueResponse createIssue(@Body CreateIssueRequest createIssueRequest); - class CreateJiraIssueRequest extends HashMap { - CreateJiraIssueRequest(Map body) { + @GET("/rest/api/2/issue/{issueIdOrKey}/transitions") + IssueTransitions getIssueTransitions(@Path("issueIdOrKey") String issueIdOrKey); + + @POST("/rest/api/2/issue/{issueIdOrKey}/transitions") + Response transitionIssue( + @Path("issueIdOrKey") String issueIdOrKey, + @Body TransitionIssueRequest transitionIssueRequest); + + @POST("/rest/api/2/issue/{issueIdOrKey}/comment") + Response addComment( + @Path("issueIdOrKey") String issueIdOrKey, @Body CommentIssueRequest commentIssueRequest); + + class CreateIssueRequest extends HashMap { + CreateIssueRequest(Map body) { + super(body); + } + } + + class TransitionIssueRequest extends HashMap { + TransitionIssueRequest(Map body) { super(body); } } - class CreateJiraIssueResponse implements EchoResponse.EchoResult { + class CommentIssueRequest { + private String body; + + CommentIssueRequest(String body) { + this.body = body; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + } + + class IssueTransitions { + private List transitions; + + public List getTransitions() { + return transitions; + } + + public void setTransitions(List transitions) { + this.transitions = transitions; + } + + public static class Transition { + private String id; + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + } + + class CreateIssueResponse implements EchoResponse.EchoResult { private String id; private String key; private String self; diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationServiceSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationServiceSpec.groovy new file mode 100644 index 000000000..cf940d788 --- /dev/null +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationServiceSpec.groovy @@ -0,0 +1,48 @@ +package com.netflix.spinnaker.echo.jira + +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.echo.api.Notification +import com.netflix.spinnaker.echo.jackson.EchoObjectMapper +import com.netflix.spinnaker.kork.core.RetrySupport +import spock.lang.Specification +import spock.lang.Unroll + + +class JiraNotificationServiceSpec extends Specification { + + def jiraService = Mock(JiraService) + def retrySupport = new RetrySupport() + def objectMapper = EchoObjectMapper.getInstance() + JiraNotificationService service = new JiraNotificationService(jiraService, retrySupport, objectMapper) + + @Unroll + void 'Handles Jira transition, calls comment if comment is set'() { + given: + def notification = new Notification( + notificationType: "JIRA", + source: new Notification.Source(user: "foo@example.com"), + additionalContext: [ + jiraIssue: "EXMPL-0000", + comment: comment, + transitionContext: [ + transition: [ + name: "Done" + ] + ] + ] + ) + + when: + service.handle(notification) + + then: + 1 * jiraService.getIssueTransitions(_) >> new JiraService.IssueTransitions(transitions: [new JiraService.IssueTransitions.Transition(name: "Done", id: "4")]) + 1 * jiraService.transitionIssue(_, _) + addCommentCall * jiraService.addComment(_, _) + + where: + comment || addCommentCall + null || 0 + "testing 1234" || 1 + } +} diff --git a/echo-pipelinetriggers/echo-pipelinetriggers.gradle b/echo-pipelinetriggers/echo-pipelinetriggers.gradle index 502c90399..91a67d691 100644 --- a/echo-pipelinetriggers/echo-pipelinetriggers.gradle +++ b/echo-pipelinetriggers/echo-pipelinetriggers.gradle @@ -47,6 +47,7 @@ dependencies { implementation "commons-codec:commons-codec" implementation 'com.vdurmont:semver4j' + implementation "com.jayway.jsonpath:json-path" testImplementation project(':echo-test') testImplementation "org.springframework.boot:spring-boot-starter-test" diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcher.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcher.java index 07b0cab08..1a9a1e566 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcher.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcher.java @@ -16,6 +16,8 @@ package com.netflix.spinnaker.echo.pipelinetriggers.artifacts; +import com.google.gson.Gson; +import com.jayway.jsonpath.*; import com.netflix.spinnaker.echo.model.Trigger; import com.netflix.spinnaker.kork.artifacts.model.Artifact; import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact; @@ -27,6 +29,11 @@ @Slf4j public class ArtifactMatcher { + + private static final Gson gson = new Gson(); + private static final Configuration conf = + Configuration.defaultConfiguration().setOptions(Option.SUPPRESS_EXCEPTIONS); + public static boolean anyArtifactsMatchExpected( List messageArtifacts, Trigger trigger, @@ -86,7 +93,64 @@ public static boolean isConstraintInPayload(final Map constraints, final Map pay return true; } + /** + * Check that there is a key in the payload for each constraint declared in a Trigger. Also check + * that if there is a value for a given key, that the value matches the value in the payload. + * + *

The constraint key may accept a JsonPath expression for deeper json search, the evaluation + * value should be a String or a List. + * + * @param constraints A map of constraints configured in the Trigger (eg, created in Deck). A + * constraint is a [key, java regex value] pair or a [JsonPathExp, java regex value]. + * @param payload A map of the payload contents POST'd in the triggering event. + * @return Whether every key or expression (and value if applicable) in the constraints map is + * represented in the payload. + */ + public static boolean isJsonPathConstraintInPayload(final Map constraints, final Map payload) { + String json = gson.toJson(payload); + DocumentContext documentContext = JsonPath.using(conf).parse(json); + + for (Object key : constraints.keySet()) { + if (!payload.containsKey(key) || payload.get(key) == null) { + log.debug("key not present in payload, needs to check with jsonpath"); + List values = getValueUsingJsonPath(documentContext, key.toString()); + if (values != null && anyMatch(constraints.get(key).toString(), values)) { + continue; + } + return false; + } else { + if (constraints.get(key) != null + && !matches(constraints.get(key).toString(), payload.get(key).toString())) { + return false; + } + } + } + return true; + } + private static boolean matches(String us, String other) { return Pattern.compile(us).asPredicate().test(other); } + + private static boolean anyMatch(String us, List values) { + return values.stream().anyMatch(v -> matches(us, v)); + } + + private static List getValueUsingJsonPath(DocumentContext ctx, String query) { + try { + String value = ctx.read(query, String.class); + if (value != null) { + return Collections.singletonList(value); + } + // lets try to cast to List + return ctx.read(query, List.class); + } catch (InvalidPathException e) { + log.error("Invalid JsonPath query in constrains, with query: " + query); + } catch (ClassCastException e) { + log.error( + "Payload value cannot be cast, only String or List are valid types, with query: " + + query); + } + return null; + } } diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/WebhookEventHandler.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/WebhookEventHandler.java index 10441b08b..f9cb5b41a 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/WebhookEventHandler.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/WebhookEventHandler.java @@ -14,7 +14,7 @@ package com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers; -import static com.netflix.spinnaker.echo.pipelinetriggers.artifacts.ArtifactMatcher.isConstraintInPayload; +import static com.netflix.spinnaker.echo.pipelinetriggers.artifacts.ArtifactMatcher.isJsonPathConstraintInPayload; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.spectator.api.Registry; @@ -104,7 +104,7 @@ protected Predicate matchTriggerFor(WebhookEvent webhookEvent) { // If the Constraints are present, check that there are equivalents in the webhook // payload. (trigger.getPayloadConstraints() != null - && isConstraintInPayload( + && isJsonPathConstraintInPayload( trigger.getPayloadConstraints(), webhookEvent.getPayload()))); } diff --git a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcherSpec.groovy b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcherSpec.groovy index 4e12f0d3d..0a16965b9 100644 --- a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcherSpec.groovy +++ b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/artifacts/ArtifactMatcherSpec.groovy @@ -27,8 +27,8 @@ class ArtifactMatcherSpec extends Specification { ] def noMatchPayload = [ - "four": "four", - "five": "five" + "four": "four", + "five": "five" ] def contstraints = [ @@ -40,17 +40,102 @@ class ArtifactMatcherSpec extends Specification { ] def constraintsOR = [ - "one": ["uno", "one"] + "one": ["uno", "one"] ] def payloadWithList = [ - "one": ["one"] + "one": ["one"] ] def stringifiedListConstraints = [ "one": "['uno', 'one']" ] + def jsonPathConstraintsToString = [ + "\$.one.test1.title": "st" + ] + + def jsonPathConstraintsToStringNOT = [ + "\$.one.test2.title": "no match" + ] + + def jsonPathConstraintsToBool = [ + "\$.one.test1.isValid": "true" + ] + + def jsonPathConstraintsToList = [ + "\$.one.test1.modified": ".yml" + ] + + def jsonPathConstraintsToListNOT = [ + "\$.one.test1.modified": ".xml" + ] + + def jsonPathConstraintsToObject = [ + "\$.one.test1.author": "edgar" + ] + + def jsonPathConstraintsToNumber = [ + "\$.two.test3.count": "3" + ] + + def jsonPathConstraintsToListOfObjects = [ + "\$.two.test2.changes": "bar" + ] + + def jsonPathConstraintsToUnknownField = [ + "\$.two.test4.changes": "bar" + ] + + def jsonPathConstraintsBadJsonPath = [ + "\$.one.test2.changes[a].value": "bar" + ] + + def multipleJsonPathConstraints = [ + "\$.one.test1.title": "st", + "\$.one.test2.title": "no match" + ] + + def complexPayload = [ + "one": [ + "test1": [ + "title": "test", + "modified": ["folder1/file1.txt", "folder1/file2.txt", "folder2/file1.yml"], + "isValid": true, + "author": [ + "name": "edgar", + "username": "edgarulg" + ] + ], + "test2": [ + "title": "another test", + "created": ["folder3/new.txt"], + "isValid": false, + "author": [ + "name": "jorge", + "username": "jorge123" + ], + "changes": [ + [ + "id": 1, + "value": "foo" + ], + [ + "id": 2, + "value": "bar" + ] + ] + ] + ], + "two": [ + "test3": [ + "count": 3, + "ref": null, + "isActive": false + ] + ] + ] + def "matches when constraint is partial word"() { when: boolean result = ArtifactMatcher.isConstraintInPayload(shortConstraint, matchPayload) @@ -114,4 +199,158 @@ class ArtifactMatcherSpec extends Specification { then: result } + + def "matches when constraint is partial word using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(shortConstraint, matchPayload) + + then: + result + } + + def "matches exact string using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(contstraints, matchPayload) + + then: + result + } + + def "no match when constraint word not present using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(contstraints, noMatchPayload) + + then: + !result + } + + def "matches when payload value is in a list of constraint strings using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(constraintsOR, matchPayload) + + then: + result + } + + def "no match when val not present in list of constraint strings using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(constraintsOR, noMatchPayload) + + then: + !result + } + + def "matches when val is in stringified list of constraints using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(stringifiedListConstraints, matchPayload) + + then: + result + } + + def "matches when payload contains list and constraint is a stringified list using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(stringifiedListConstraints, payloadWithList) + + then: + result + } + + def "matches when payload is a list list and constraints are a list using Jsonpath"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(constraintsOR, payloadWithList) + + then: + result + } + + def "matches when val is a string using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToString, complexPayload) + + then: + result + } + + def "no match when val is a string using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToStringNOT, complexPayload) + + then: + !result + } + + def "matches when val is a boolean using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToBool, complexPayload) + + then: + result + } + + def "matches when val is a number using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToNumber, complexPayload) + + then: + result + } + + def "matches when val is a List using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToList, complexPayload) + + then: + result + } + + def "no matches when val is a List using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToListNOT, complexPayload) + + then: + !result + } + + def "no match when val is an Object using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToObject, complexPayload) + + then: + !result + } + + def "no match when val is a List using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToListOfObjects, complexPayload) + + then: + !result + } + + def "no match when field not found using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsToUnknownField, complexPayload) + + then: + !result + } + + def "no match when bad expression using JSONPath constraint with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(jsonPathConstraintsBadJsonPath, complexPayload) + + then: + !result + } + + def "no match when multiple JSONPath constraints with multi-level json"() { + when: + boolean result = ArtifactMatcher.isJsonPathConstraintInPayload(multipleJsonPathConstraints, complexPayload) + + then: + !result + } + + } diff --git a/gradle.properties b/gradle.properties index a054e0aa9..a13de1681 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ fiatVersion=1.26.0 -korkVersion=7.98.0 +korkVersion=7.99.0 kotlinVersion=1.4.0 org.gradle.parallel=true spinnakerGradleVersion=8.10.1