Skip to content

Commit

Permalink
Add Matcher Support for Headers (#72)
Browse files Browse the repository at this point in the history
* Add Matcher Support for Headers

* Apply PR Comments
  • Loading branch information
driverpt authored Jun 22, 2022
1 parent 530a62e commit 1b70bef
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@
import com.agorapulse.gru.minions.TextMinion;
import net.javacrumbs.jsonunit.core.Option;
import net.javacrumbs.jsonunit.fluent.JsonFluentAssert;
import org.hamcrest.Matcher;

import java.util.ArrayList;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

Expand Down Expand Up @@ -112,18 +111,11 @@ public DefaultResponseDefinitionBuilder json(Function<JsonFluentAssert.Configura
return command(JsonMinion.class, minion -> minion.setJsonUnitConfiguration(additionalConfiguration));
}

/**
* Adds HTTP headers which are expected to be returned after action execution.
*
* @param additionalHeaders additional HTTP headers which are expected to be returned after action execution
* @return self
*/
public DefaultResponseDefinitionBuilder headers(final Map<String, String> additionalHeaders) {
return command(HttpMinion.class, minion ->
additionalHeaders.forEach((key, value) ->
minion.getResponseHeaders().computeIfAbsent(key, k -> new ArrayList<>()).add(value)
)
);
@Override
public ResponseDefinitionBuilder header(String name, Matcher<String> matcher) {
return command(HttpMinion.class, minion -> {
minion.putHeaderMatcher(name, matcher);
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
import net.javacrumbs.jsonunit.core.Option;
import net.javacrumbs.jsonunit.core.internal.JsonUtils;
import net.javacrumbs.jsonunit.fluent.JsonFluentAssert;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.intellij.lang.annotations.Language;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
Expand Down Expand Up @@ -155,12 +156,19 @@ default ResponseDefinitionBuilder json(List<?> list) {
* @param additionalHeaders additional HTTP headers which are expected to be returned after action execution
* @return self
*/
ResponseDefinitionBuilder headers(Map<String, String> additionalHeaders);
default ResponseDefinitionBuilder headers(Map<String, String> additionalHeaders) {
additionalHeaders
.forEach((k, v) -> header(k, Matchers.equalTo(v)));

return this;
}

default ResponseDefinitionBuilder header(@Language("http-header-reference") String name, String value) {
return headers(Collections.singletonMap(name, value));
return header(name, Matchers.equalTo(value));
}

ResponseDefinitionBuilder header(@Language("http-header-reference") String name, Matcher<String> matcher);

/**
* Sets the expected redirection URI.
*
Expand Down
51 changes: 42 additions & 9 deletions libs/gru/src/main/java/com/agorapulse/gru/minions/HttpMinion.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,20 @@
import com.agorapulse.gru.Client;
import com.agorapulse.gru.GruContext;
import com.agorapulse.gru.Squad;

import java.util.*;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;

import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

/**
Expand All @@ -34,6 +46,7 @@ public class HttpMinion extends AbstractMinion<Client> {
private Set<Integer> statuses = Collections.singleton(DEFAULT_STATUS);
private final Map<String, Collection<String>> requestHeaders = new LinkedHashMap<>();
private final Map<String, Collection<String>> responseHeaders = new LinkedHashMap<>();
private final Map<String, Collection<Matcher<String>>> responseHeaderMatchers = new LinkedHashMap<>();
private String redirectUri;

public HttpMinion() {
Expand Down Expand Up @@ -66,14 +79,25 @@ public void doVerify(Client client, Squad squad, GruContext context) {
}
}

for (Map.Entry<String, Collection<String>> header : responseHeaders.entrySet()) {
Optional.ofNullable(header.getValue()).ifPresent(headers -> {
for (String value : headers) {
if (!(client.getResponse().getHeaders(header.getKey()).contains(value))) {
throw new AssertionError("Missing header " + header.getKey() + " with value " + value);
for (Map.Entry<String, Collection<Matcher<String>>> headerMatcher : responseHeaderMatchers.entrySet()) {
List<String> headerValues = client.getResponse().getHeaders(headerMatcher.getKey());
if (Objects.isNull(headerValues) || headerValues.isEmpty()) {
throw new AssertionError("Missing header " + headerMatcher.getKey());
}

headerMatcher.getValue()
.stream()
.allMatch(matcher -> {
boolean matches = headerValues.stream().anyMatch(matcher::matches);
if (!matches) {
StringDescription description = new StringDescription();
matcher.describeMismatch(headerValues, description);
throw new AssertionError(MessageFormat
.format("Header value mismatch {0}: {1}", headerMatcher.getKey(), description));
}
}
});

return true;
});
}

if (redirectUri != null) {
Expand Down Expand Up @@ -106,6 +130,15 @@ public Map<String, Collection<String>> getResponseHeaders() {
return responseHeaders;
}

public Map<String, Collection<Matcher<String>>> putHeaderMatcher(String name, Matcher<String> matcher) {
Collection<Matcher<String>> matchers = responseHeaderMatchers.getOrDefault(name, new LinkedList<>());
responseHeaderMatchers.put(name, matchers);

matchers.add(matcher);

return responseHeaderMatchers;
}

public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/
package com.agorapulse.gru

import com.agorapulse.gru.minions.HttpMinion
import org.hamcrest.Matchers
import spock.lang.Specification
import spock.lang.Unroll

Expand Down Expand Up @@ -65,4 +67,32 @@ class DefaultTestDefinitionBuilderSpec extends Specification {
} *.name
}

void "multi header response is matched"() {
given:
String expectedHeader = "X-Foo"

Client.Request request = Mock(Client.Request)
Client.Response response = Mock(Client.Response)

Client client = Mock(Client) {
getInitialSquad() >> []
getRequest() >> request
getResponse() >> response
}

response.getHeaders(expectedHeader) >> ["Bar", "Baz"]
Gru gru = Gru.create(client)

when:
gru.verify {
get '/foo/bar'
expect {
header(expectedHeader, Matchers.equalToIgnoringCase("Bar"))
header(expectedHeader, Matchers.equalToIgnoringCase("Baz"))
}
}
then:
noExceptionThrown()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2018-2022 Agorapulse.
*
* 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
*
* https://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.agorapulse.gru.minions

import com.agorapulse.gru.Client
import com.agorapulse.gru.Gru
import com.agorapulse.gru.Squad
import com.agorapulse.gru.TestClient
import org.hamcrest.Matchers
import spock.lang.Specification

import static com.agorapulse.gru.HttpStatusShortcuts.OK

/**
* Tests for http minion.
*/
class HttpMinionSpec extends Specification {

void 'multi header response is matched'() {
given:
Client.Request request = Mock(Client.Request)
Client.Response response = Mock(Client.Response)

Client client = new TestClient(this, request, response)
Squad squad = new Squad()
HttpMinion minion = new HttpMinion()
squad.add(minion)

String expectedHeader = "X-Foo"
response.getHeaders(expectedHeader) >> ["Bar", "Baz"]
response.getStatus() >> OK

when:
Gru gru = Gru.create(client)

gru.verify {
get '/foo/bar'
expect {
status(OK)
header(expectedHeader, Matchers.equalToIgnoringCase("Bar"))
header(expectedHeader, Matchers.equalToIgnoringCase("Baz"))
}
}
then:
noExceptionThrown()
}

void 'legacy header matching still works'() {
given:
Client.Request request = Mock(Client.Request)
Client.Response response = Mock(Client.Response)

Client client = new TestClient(this, request, response)
Squad squad = new Squad()
HttpMinion minion = new HttpMinion()
squad.add(minion)

String expectedHeader = "X-Foo"
response.getHeaders(expectedHeader) >> ["Bar", "Baz"]
response.getStatus() >> OK

when:
Gru gru = Gru.create(client)

gru.verify {
get '/foo/bar'
expect {
status(OK)
header(expectedHeader, "Bar")
header(expectedHeader, "Baz")
}
}
then:
noExceptionThrown()
}

}

0 comments on commit 1b70bef

Please sign in to comment.