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}