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}