Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Matcher Support for Headers #72

Merged
merged 2 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit messy. Can someone take a look at this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you run into some concurrent issues that we need a ConcurrentHashMap and a lock object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at the moment. But I suspect we'll run into some issues when using JUnit's 5 Parallelized Tests.

I can remove it if you think it's best.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, please. if we face these issue we'll have to find out some other solutions how to handle concurrency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, i was on vacation. Just updated

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no worries 👍

.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()
}

}