/*
* Copyright 2002-2017 the original author or authors.
*
* 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 org.springframework.test.web.client;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* Base class for {@code RequestExpectationManager} implementations responsible
* for storing expectations and actual requests, and checking for unsatisfied
* expectations at the end.
*
* <p>Subclasses are responsible for validating each request by matching it to
* to expectations following the order of declaration or not.
*
* @author Rossen Stoyanchev
* @since 4.3
*/
public abstract class AbstractRequestExpectationManager implements RequestExpectationManager {
private final List<RequestExpectation> expectations = new LinkedList<>();
private final List<ClientHttpRequest> requests = new LinkedList<>();
private final Object lock = new Object();
protected List<RequestExpectation> getExpectations() {
return this.expectations;
}
protected List<ClientHttpRequest> getRequests() {
return this.requests;
}
@Override
public ResponseActions expectRequest(ExpectedCount count, RequestMatcher matcher) {
Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made");
RequestExpectation expectation = new DefaultRequestExpectation(count, matcher);
getExpectations().add(expectation);
return expectation;
}
@Override
public ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException {
synchronized (this.lock) {
if (getRequests().isEmpty()) {
afterExpectationsDeclared();
}
ClientHttpResponse response = validateRequestInternal(request);
getRequests().add(request);
return response;
}
}
/**
* Invoked after the phase of declaring expected requests is over. This is
* detected from {@link #validateRequest} on the first actual request.
*/
protected void afterExpectationsDeclared() {
}
/**
* Subclasses must implement the actual validation of the request
* matching it to a declared expectation.
*/
protected abstract ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException;
@Override
public void verify() {
if (getExpectations().isEmpty()) {
return;
}
int count = 0;
for (RequestExpectation expectation : getExpectations()) {
if (!expectation.isSatisfied()) {
count++;
}
}
if (count > 0) {
String message = "Further request(s) expected leaving " + count + " unsatisfied expectation(s).\n";
throw new AssertionError(message + getRequestDetails());
}
}
/**
* Return details of executed requests.
*/
protected String getRequestDetails() {
StringBuilder sb = new StringBuilder();
sb.append(getRequests().size()).append(" request(s) executed");
if (!getRequests().isEmpty()) {
sb.append(":\n");
for (ClientHttpRequest request : getRequests()) {
sb.append(request.toString()).append("\n");
}
}
else {
sb.append(".\n");
}
return sb.toString();
}
/**
* Return an {@code AssertionError} that a sub-class can raise for an
* unexpected request.
*/
protected AssertionError createUnexpectedRequestError(ClientHttpRequest request) {
HttpMethod method = request.getMethod();
URI uri = request.getURI();
String message = "No further requests expected: HTTP " + method + " " + uri + "\n";
return new AssertionError(message + getRequestDetails());
}
@Override
public void reset() {
this.expectations.clear();
this.requests.clear();
}
/**
* Helper class to manage a group of request expectations. It helps with
* operations against the entire group such as finding a match and updating
* (add or remove) based on expected request count.
*/
protected static class RequestExpectationGroup {
private final Set<RequestExpectation> expectations = new LinkedHashSet<>();
public Set<RequestExpectation> getExpectations() {
return this.expectations;
}
public void update(RequestExpectation expectation) {
if (expectation.hasRemainingCount()) {
getExpectations().add(expectation);
}
else {
getExpectations().remove(expectation);
}
}
public void updateAll(Collection<RequestExpectation> expectations) {
for (RequestExpectation expectation : expectations) {
update(expectation);
}
}
public RequestExpectation findExpectation(ClientHttpRequest request) throws IOException {
for (RequestExpectation expectation : getExpectations()) {
try {
expectation.match(request);
return expectation;
}
catch (AssertionError error) {
// Ignore
}
}
return null;
}
public void reset() {
this.expectations.clear();
}
}
}