001/* 002 * Copyright 2002-2020 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.Collections; 023import java.util.LinkedHashMap; 024import java.util.LinkedHashSet; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.stream.Collectors; 030 031import org.springframework.http.HttpMethod; 032import org.springframework.http.client.ClientHttpRequest; 033import org.springframework.http.client.ClientHttpResponse; 034import org.springframework.lang.Nullable; 035import org.springframework.util.Assert; 036 037/** 038 * Base class for {@code RequestExpectationManager} implementations responsible 039 * for storing expectations and actual requests, and checking for unsatisfied 040 * expectations at the end. 041 * 042 * <p>Subclasses are responsible for validating each request by matching it to 043 * to expectations following the order of declaration or not. 044 * 045 * @author Rossen Stoyanchev 046 * @author Juergen Hoeller 047 * @since 4.3 048 */ 049public abstract class AbstractRequestExpectationManager implements RequestExpectationManager { 050 051 private final List<RequestExpectation> expectations = new LinkedList<>(); 052 053 private final List<ClientHttpRequest> requests = new LinkedList<>(); 054 055 private final Map<ClientHttpRequest, Throwable> requestFailures = new LinkedHashMap<>(); 056 057 058 /** 059 * Return a read-only list of the expectations. 060 */ 061 protected List<RequestExpectation> getExpectations() { 062 return Collections.unmodifiableList(this.expectations); 063 } 064 065 /** 066 * Return a read-only list of requests executed so far. 067 */ 068 protected List<ClientHttpRequest> getRequests() { 069 return Collections.unmodifiableList(this.requests); 070 } 071 072 073 @Override 074 public ResponseActions expectRequest(ExpectedCount count, RequestMatcher matcher) { 075 Assert.state(this.requests.isEmpty(), "Cannot add more expectations after actual requests are made"); 076 RequestExpectation expectation = new DefaultRequestExpectation(count, matcher); 077 this.expectations.add(expectation); 078 return expectation; 079 } 080 081 @SuppressWarnings("deprecation") 082 @Override 083 public ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException { 084 RequestExpectation expectation = null; 085 synchronized (this.requests) { 086 if (this.requests.isEmpty()) { 087 afterExpectationsDeclared(); 088 } 089 try { 090 // Try this first for backwards compatibility 091 ClientHttpResponse response = validateRequestInternal(request); 092 if (response != null) { 093 return response; 094 } 095 else { 096 expectation = matchRequest(request); 097 } 098 } 099 catch (Throwable ex) { 100 this.requestFailures.put(request, ex); 101 throw ex; 102 } 103 finally { 104 this.requests.add(request); 105 } 106 } 107 return expectation.createResponse(request); 108 } 109 110 /** 111 * Invoked at the time of the first actual request, which effectively means 112 * the expectations declaration phase is over. 113 */ 114 protected void afterExpectationsDeclared() { 115 } 116 117 /** 118 * Subclasses must implement the actual validation of the request 119 * matching to declared expectations. 120 * @deprecated as of 5.0.3, subclasses should implement {@link #matchRequest(ClientHttpRequest)} 121 * instead and return only the matched expectation, leaving the call to create the response 122 * as a separate step (to be invoked by this class). 123 */ 124 @Deprecated 125 @Nullable 126 protected ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { 127 return null; 128 } 129 130 /** 131 * As of 5.0.3 subclasses should implement this method instead of 132 * {@link #validateRequestInternal(ClientHttpRequest)} in order to match the 133 * request to an expectation, leaving the call to create the response as a separate step 134 * (to be invoked by this class). 135 * @param request the current request 136 * @return the matched expectation with its request count updated via 137 * {@link RequestExpectation#incrementAndValidate()}. 138 * @since 5.0.3 139 */ 140 protected RequestExpectation matchRequest(ClientHttpRequest request) throws IOException { 141 throw new UnsupportedOperationException( 142 "It looks like neither the deprecated \"validateRequestInternal\"" + 143 "nor its replacement (this method) are implemented."); 144 } 145 146 @Override 147 public void verify() { 148 if (this.expectations.isEmpty()) { 149 return; 150 } 151 int count = 0; 152 for (RequestExpectation expectation : this.expectations) { 153 if (!expectation.isSatisfied()) { 154 count++; 155 } 156 } 157 if (count > 0) { 158 String message = "Further request(s) expected leaving " + count + " unsatisfied expectation(s).\n"; 159 throw new AssertionError(message + getRequestDetails()); 160 } 161 if (!this.requestFailures.isEmpty()) { 162 throw new AssertionError("Some requests did not execute successfully.\n" + 163 this.requestFailures.entrySet().stream() 164 .map(entry -> "Failed request:\n" + entry.getKey() + "\n" + entry.getValue()) 165 .collect(Collectors.joining("\n", "\n", ""))); 166 } 167 } 168 169 /** 170 * Return details of executed requests. 171 */ 172 protected String getRequestDetails() { 173 StringBuilder sb = new StringBuilder(); 174 sb.append(this.requests.size()).append(" request(s) executed"); 175 if (!this.requests.isEmpty()) { 176 sb.append(":\n"); 177 for (ClientHttpRequest request : this.requests) { 178 sb.append(request.toString()).append("\n"); 179 } 180 } 181 else { 182 sb.append(".\n"); 183 } 184 return sb.toString(); 185 } 186 187 /** 188 * Return an {@code AssertionError} that a sub-class can raise for an 189 * unexpected request. 190 */ 191 protected AssertionError createUnexpectedRequestError(ClientHttpRequest request) { 192 HttpMethod method = request.getMethod(); 193 URI uri = request.getURI(); 194 String message = "No further requests expected: HTTP " + method + " " + uri + "\n"; 195 return new AssertionError(message + getRequestDetails()); 196 } 197 198 @Override 199 public void reset() { 200 this.expectations.clear(); 201 this.requests.clear(); 202 this.requestFailures.clear(); 203 } 204 205 206 /** 207 * Helper class to manage a group of remaining expectations. 208 */ 209 protected static class RequestExpectationGroup { 210 211 private final Set<RequestExpectation> expectations = new LinkedHashSet<>(); 212 213 public void addAllExpectations(Collection<RequestExpectation> expectations) { 214 this.expectations.addAll(expectations); 215 } 216 217 public Set<RequestExpectation> getExpectations() { 218 return this.expectations; 219 } 220 221 /** 222 * Return a matching expectation, or {@code null} if none match. 223 */ 224 @Nullable 225 public RequestExpectation findExpectation(ClientHttpRequest request) throws IOException { 226 for (RequestExpectation expectation : this.expectations) { 227 try { 228 expectation.match(request); 229 return expectation; 230 } 231 catch (AssertionError error) { 232 // We're looking to find a match or return null.. 233 } 234 } 235 return null; 236 } 237 238 /** 239 * Invoke this for an expectation that has been matched. 240 * <p>The count of the given expectation is incremented, then it is 241 * either stored if remainingCount > 0 or removed otherwise. 242 */ 243 public void update(RequestExpectation expectation) { 244 expectation.incrementAndValidate(); 245 updateInternal(expectation); 246 } 247 248 private void updateInternal(RequestExpectation expectation) { 249 if (expectation.hasRemainingCount()) { 250 this.expectations.add(expectation); 251 } 252 else { 253 this.expectations.remove(expectation); 254 } 255 } 256 257 /** 258 * Add expectations to this group. 259 * @deprecated as of 5.0.3, if favor of {@link #addAllExpectations} 260 */ 261 @Deprecated 262 public void updateAll(Collection<RequestExpectation> expectations) { 263 expectations.forEach(this::updateInternal); 264 } 265 266 /** 267 * Reset all expectations for this group. 268 */ 269 public void reset() { 270 this.expectations.clear(); 271 } 272 } 273 274}