001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.test.web.client; 018 019import java.io.IOException; 020import java.net.URI; 021import java.util.Collection; 022import java.util.LinkedHashSet; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Set; 026 027import org.springframework.http.HttpMethod; 028import org.springframework.http.client.ClientHttpRequest; 029import org.springframework.http.client.ClientHttpResponse; 030import org.springframework.util.Assert; 031 032/** 033 * Base class for {@code RequestExpectationManager} implementations responsible 034 * for storing expectations and actual requests, and checking for unsatisfied 035 * expectations at the end. 036 * 037 * <p>Subclasses are responsible for validating each request by matching it to 038 * to expectations following the order of declaration or not. 039 * 040 * @author Rossen Stoyanchev 041 * @author Juergen Hoeller 042 * @since 4.3 043 */ 044public abstract class AbstractRequestExpectationManager implements RequestExpectationManager { 045 046 private final List<RequestExpectation> expectations = new LinkedList<RequestExpectation>(); 047 048 private final List<ClientHttpRequest> requests = new LinkedList<ClientHttpRequest>(); 049 050 051 protected List<RequestExpectation> getExpectations() { 052 return this.expectations; 053 } 054 055 protected List<ClientHttpRequest> getRequests() { 056 return this.requests; 057 } 058 059 060 @Override 061 public ResponseActions expectRequest(ExpectedCount count, RequestMatcher matcher) { 062 Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made"); 063 RequestExpectation expectation = new DefaultRequestExpectation(count, matcher); 064 getExpectations().add(expectation); 065 return expectation; 066 } 067 068 @Override 069 public ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException { 070 List<ClientHttpRequest> requests = getRequests(); 071 synchronized (requests) { 072 if (requests.isEmpty()) { 073 afterExpectationsDeclared(); 074 } 075 try { 076 return validateRequestInternal(request); 077 } 078 finally { 079 requests.add(request); 080 } 081 } 082 } 083 084 /** 085 * Invoked at the time of the first actual request, which effectively means 086 * the expectations declaration phase is over. 087 */ 088 protected void afterExpectationsDeclared() { 089 } 090 091 /** 092 * Subclasses must implement the actual validation of the request 093 * matching to declared expectations. 094 */ 095 protected abstract ClientHttpResponse validateRequestInternal(ClientHttpRequest request) 096 throws IOException; 097 098 @Override 099 public void verify() { 100 if (getExpectations().isEmpty()) { 101 return; 102 } 103 int count = 0; 104 for (RequestExpectation expectation : getExpectations()) { 105 if (!expectation.isSatisfied()) { 106 count++; 107 } 108 } 109 if (count > 0) { 110 String message = "Further request(s) expected leaving " + count + " unsatisfied expectation(s).\n"; 111 throw new AssertionError(message + getRequestDetails()); 112 } 113 } 114 115 /** 116 * Return details of executed requests. 117 */ 118 protected String getRequestDetails() { 119 StringBuilder sb = new StringBuilder(); 120 sb.append(getRequests().size()).append(" request(s) executed"); 121 if (!getRequests().isEmpty()) { 122 sb.append(":\n"); 123 for (ClientHttpRequest request : getRequests()) { 124 sb.append(request.toString()).append("\n"); 125 } 126 } 127 else { 128 sb.append(".\n"); 129 } 130 return sb.toString(); 131 } 132 133 /** 134 * Return an {@code AssertionError} that a sub-class can raise for an 135 * unexpected request. 136 */ 137 protected AssertionError createUnexpectedRequestError(ClientHttpRequest request) { 138 HttpMethod method = request.getMethod(); 139 URI uri = request.getURI(); 140 String message = "No further requests expected: HTTP " + method + " " + uri + "\n"; 141 return new AssertionError(message + getRequestDetails()); 142 } 143 144 @Override 145 public void reset() { 146 this.expectations.clear(); 147 this.requests.clear(); 148 } 149 150 151 /** 152 * Helper class to manage a group of remaining expectations. 153 */ 154 protected static class RequestExpectationGroup { 155 156 private final Set<RequestExpectation> expectations = new LinkedHashSet<RequestExpectation>(); 157 158 public Set<RequestExpectation> getExpectations() { 159 return this.expectations; 160 } 161 162 /** 163 * Return a matching expectation, or {@code null} if none match. 164 */ 165 public RequestExpectation findExpectation(ClientHttpRequest request) throws IOException { 166 for (RequestExpectation expectation : getExpectations()) { 167 try { 168 expectation.match(request); 169 return expectation; 170 } 171 catch (AssertionError error) { 172 // We're looking to find a match or return null.. 173 } 174 } 175 return null; 176 } 177 178 /** 179 * Invoke this for an expectation that has been matched. 180 * <p>The given expectation will either be stored if it has a remaining 181 * count or it will be removed otherwise. 182 */ 183 public void update(RequestExpectation expectation) { 184 if (expectation.hasRemainingCount()) { 185 getExpectations().add(expectation); 186 } 187 else { 188 getExpectations().remove(expectation); 189 } 190 } 191 192 /** 193 * Collection variant of {@link #update(RequestExpectation)} that can 194 * be used to insert expectations. 195 */ 196 public void updateAll(Collection<RequestExpectation> expectations) { 197 for (RequestExpectation expectation : expectations) { 198 update(expectation); 199 } 200 } 201 202 /** 203 * Reset all expectations for this group. 204 */ 205 public void reset() { 206 getExpectations().clear(); 207 } 208 } 209 210}