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.mock.http.server.reactive;
018
019import java.net.InetSocketAddress;
020import java.net.URI;
021import java.nio.charset.Charset;
022import java.nio.charset.StandardCharsets;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.List;
026import java.util.Locale;
027import java.util.Objects;
028import java.util.Optional;
029
030import org.reactivestreams.Publisher;
031import reactor.core.publisher.Flux;
032
033import org.springframework.core.io.buffer.DataBuffer;
034import org.springframework.core.io.buffer.DataBufferFactory;
035import org.springframework.core.io.buffer.DefaultDataBufferFactory;
036import org.springframework.http.HttpCookie;
037import org.springframework.http.HttpHeaders;
038import org.springframework.http.HttpMethod;
039import org.springframework.http.HttpRange;
040import org.springframework.http.MediaType;
041import org.springframework.http.server.reactive.AbstractServerHttpRequest;
042import org.springframework.http.server.reactive.SslInfo;
043import org.springframework.lang.Nullable;
044import org.springframework.util.Assert;
045import org.springframework.util.LinkedMultiValueMap;
046import org.springframework.util.MimeType;
047import org.springframework.util.MultiValueMap;
048import org.springframework.web.util.UriComponentsBuilder;
049
050/**
051 * Mock extension of {@link AbstractServerHttpRequest} for use in tests without
052 * an actual server. Use the static methods to obtain a builder.
053 *
054 * @author Rossen Stoyanchev
055 * @since 5.0
056 */
057public final class MockServerHttpRequest extends AbstractServerHttpRequest {
058
059        @Nullable
060        private final HttpMethod httpMethod;
061
062        @Nullable
063        private final String customHttpMethod;
064
065        private final MultiValueMap<String, HttpCookie> cookies;
066
067        @Nullable
068        private final InetSocketAddress localAddress;
069
070        @Nullable
071        private final InetSocketAddress remoteAddress;
072
073        @Nullable
074        private final SslInfo sslInfo;
075
076        private final Flux<DataBuffer> body;
077
078
079        private MockServerHttpRequest(@Nullable HttpMethod httpMethod, @Nullable String customHttpMethod,
080                        URI uri, @Nullable String contextPath, HttpHeaders headers, MultiValueMap<String, HttpCookie> cookies,
081                        @Nullable InetSocketAddress localAddress, @Nullable InetSocketAddress remoteAddress,
082                        @Nullable SslInfo sslInfo, Publisher<? extends DataBuffer> body) {
083
084                super(uri, contextPath, headers);
085                Assert.isTrue(httpMethod != null || customHttpMethod != null, "HTTP method must not be null");
086                this.httpMethod = httpMethod;
087                this.customHttpMethod = customHttpMethod;
088                this.cookies = cookies;
089                this.localAddress = localAddress;
090                this.remoteAddress = remoteAddress;
091                this.sslInfo = sslInfo;
092                this.body = Flux.from(body);
093        }
094
095
096        @Override
097        public HttpMethod getMethod() {
098                return this.httpMethod;
099        }
100
101        @Override
102        public String getMethodValue() {
103                return (this.httpMethod != null ? this.httpMethod.name() : Objects.requireNonNull(this.customHttpMethod));
104        }
105
106        @Override
107        @Nullable
108        public InetSocketAddress getLocalAddress() {
109                return this.localAddress;
110        }
111
112        @Override
113        @Nullable
114        public InetSocketAddress getRemoteAddress() {
115                return this.remoteAddress;
116        }
117
118        @Override
119        @Nullable
120        protected SslInfo initSslInfo() {
121                return this.sslInfo;
122        }
123
124        @Override
125        public Flux<DataBuffer> getBody() {
126                return this.body;
127        }
128
129        @Override
130        protected MultiValueMap<String, HttpCookie> initCookies() {
131                return this.cookies;
132        }
133
134        @Override
135        public <T> T getNativeRequest() {
136                throw new IllegalStateException("This is a mock. No running server, no native request.");
137        }
138
139
140        // Static builder methods
141
142        /**
143         * Create an HTTP GET builder with the given URI template. The given URI may
144         * contain query parameters, or those may be added later via
145         * {@link BaseBuilder#queryParam queryParam} builder methods.
146         * @param urlTemplate a URL template; the resulting URL will be encoded
147         * @param uriVars zero or more URI variables
148         * @return the created builder
149         */
150        public static BaseBuilder<?> get(String urlTemplate, Object... uriVars) {
151                return method(HttpMethod.GET, urlTemplate, uriVars);
152        }
153
154        /**
155         * HTTP HEAD variant. See {@link #get(String, Object...)} for general info.
156         * @param urlTemplate a URL template; the resulting URL will be encoded
157         * @param uriVars zero or more URI variables
158         * @return the created builder
159         */
160        public static BaseBuilder<?> head(String urlTemplate, Object... uriVars) {
161                return method(HttpMethod.HEAD, urlTemplate, uriVars);
162        }
163
164        /**
165         * HTTP POST variant. See {@link #get(String, Object...)} for general info.
166         * @param urlTemplate a URL template; the resulting URL will be encoded
167         * @param uriVars zero or more URI variables
168         * @return the created builder
169         */
170        public static BodyBuilder post(String urlTemplate, Object... uriVars) {
171                return method(HttpMethod.POST, urlTemplate, uriVars);
172        }
173
174        /**
175         * HTTP PUT variant. See {@link #get(String, Object...)} for general info.
176         * {@link BaseBuilder#queryParam queryParam} builder methods.
177         * @param urlTemplate a URL template; the resulting URL will be encoded
178         * @param uriVars zero or more URI variables
179         * @return the created builder
180         */
181        public static BodyBuilder put(String urlTemplate, Object... uriVars) {
182                return method(HttpMethod.PUT, urlTemplate, uriVars);
183        }
184
185        /**
186         * HTTP PATCH variant. See {@link #get(String, Object...)} for general info.
187         * @param urlTemplate a URL template; the resulting URL will be encoded
188         * @param uriVars zero or more URI variables
189         * @return the created builder
190         */
191        public static BodyBuilder patch(String urlTemplate, Object... uriVars) {
192                return method(HttpMethod.PATCH, urlTemplate, uriVars);
193        }
194
195        /**
196         * HTTP DELETE variant. See {@link #get(String, Object...)} for general info.
197         * @param urlTemplate a URL template; the resulting URL will be encoded
198         * @param uriVars zero or more URI variables
199         * @return the created builder
200         */
201        public static BaseBuilder<?> delete(String urlTemplate, Object... uriVars) {
202                return method(HttpMethod.DELETE, urlTemplate, uriVars);
203        }
204
205        /**
206         * HTTP OPTIONS variant. See {@link #get(String, Object...)} for general info.
207         * @param urlTemplate a URL template; the resulting URL will be encoded
208         * @param uriVars zero or more URI variables
209         * @return the created builder
210         */
211        public static BaseBuilder<?> options(String urlTemplate, Object... uriVars) {
212                return method(HttpMethod.OPTIONS, urlTemplate, uriVars);
213        }
214
215        /**
216         * Create a builder with the given HTTP method and a {@link URI}.
217         * @param method the HTTP method (GET, POST, etc)
218         * @param url the URL
219         * @return the created builder
220         */
221        public static BodyBuilder method(HttpMethod method, URI url) {
222                return new DefaultBodyBuilder(method, url);
223        }
224
225        /**
226         * Alternative to {@link #method(HttpMethod, URI)} that accepts a URI template.
227         * The given URI may contain query parameters, or those may be added later via
228         * {@link BaseBuilder#queryParam queryParam} builder methods.
229         * @param method the HTTP method (GET, POST, etc)
230         * @param urlTemplate the URL template
231         * @param vars variables to expand into the template
232         * @return the created builder
233         */
234        public static BodyBuilder method(HttpMethod method, String urlTemplate, Object... vars) {
235                Assert.notNull(method, "HttpMethod is required. If testing a custom HTTP method, " +
236                                "please use the variant that accepts a String based HTTP method.");
237                URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri();
238                return new DefaultBodyBuilder(method, url);
239        }
240
241        /**
242         * Create a builder with a raw HTTP method value that is outside the range
243         * of {@link HttpMethod} enum values.
244         * @param method the HTTP method value
245         * @param urlTemplate the URL template
246         * @param vars variables to expand into the template
247         * @return the created builder
248         * @since 5.2.7
249         */
250        public static BodyBuilder method(String method, String urlTemplate, Object... vars) {
251                URI url = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(vars).encode().toUri();
252                return new DefaultBodyBuilder(method, url);
253        }
254
255
256        /**
257         * Request builder exposing properties not related to the body.
258         * @param <B> the builder sub-class
259         */
260        public interface BaseBuilder<B extends BaseBuilder<B>> {
261
262                /**
263                 * Set the contextPath to return.
264                 */
265                B contextPath(String contextPath);
266
267                /**
268                 * Append the given query parameter to the existing query parameters.
269                 * If no values are given, the resulting URI will contain the query
270                 * parameter name only (i.e. {@code ?foo} instead of {@code ?foo=bar}).
271                 * <p>The provided query name and values will be encoded.
272                 * @param name the query parameter name
273                 * @param values the query parameter values
274                 * @return this UriComponentsBuilder
275                 */
276                B queryParam(String name, Object... values);
277
278                /**
279                 * Add the given query parameters and values. The provided query name
280                 * and corresponding values will be encoded.
281                 * @param params the params
282                 * @return this UriComponentsBuilder
283                 */
284                B queryParams(MultiValueMap<String, String> params);
285
286                /**
287                 * Set the remote address to return.
288                 */
289                B remoteAddress(InetSocketAddress remoteAddress);
290
291                /**
292                 * Set the local address to return.
293                 * @since 5.2.3
294                 */
295                B localAddress(InetSocketAddress localAddress);
296
297                /**
298                 * Set SSL session information and certificates.
299                 */
300                void sslInfo(SslInfo sslInfo);
301
302                /**
303                 * Add one or more cookies.
304                 */
305                B cookie(HttpCookie... cookie);
306
307                /**
308                 * Add the given cookies.
309                 * @param cookies the cookies.
310                 */
311                B cookies(MultiValueMap<String, HttpCookie> cookies);
312
313                /**
314                 * Add the given, single header value under the given name.
315                 * @param headerName  the header name
316                 * @param headerValues the header value(s)
317                 * @see HttpHeaders#add(String, String)
318                 */
319                B header(String headerName, String... headerValues);
320
321                /**
322                 * Add the given header values.
323                 * @param headers the header values
324                 */
325                B headers(MultiValueMap<String, String> headers);
326
327                /**
328                 * Set the list of acceptable {@linkplain MediaType media types}, as
329                 * specified by the {@code Accept} header.
330                 * @param acceptableMediaTypes the acceptable media types
331                 */
332                B accept(MediaType... acceptableMediaTypes);
333
334                /**
335                 * Set the list of acceptable {@linkplain Charset charsets}, as specified
336                 * by the {@code Accept-Charset} header.
337                 * @param acceptableCharsets the acceptable charsets
338                 */
339                B acceptCharset(Charset... acceptableCharsets);
340
341                /**
342                 * Set the list of acceptable {@linkplain Locale locales}, as specified
343                 * by the {@code Accept-Languages} header.
344                 * @param acceptableLocales the acceptable locales
345                 */
346                B acceptLanguageAsLocales(Locale... acceptableLocales);
347
348                /**
349                 * Set the value of the {@code If-Modified-Since} header.
350                 * <p>The date should be specified as the number of milliseconds since
351                 * January 1, 1970 GMT.
352                 * @param ifModifiedSince the new value of the header
353                 */
354                B ifModifiedSince(long ifModifiedSince);
355
356                /**
357                 * Set the (new) value of the {@code If-Unmodified-Since} header.
358                 * <p>The date should be specified as the number of milliseconds since
359                 * January 1, 1970 GMT.
360                 * @param ifUnmodifiedSince the new value of the header
361                 * @see HttpHeaders#setIfUnmodifiedSince(long)
362                 */
363                B ifUnmodifiedSince(long ifUnmodifiedSince);
364
365                /**
366                 * Set the values of the {@code If-None-Match} header.
367                 * @param ifNoneMatches the new value of the header
368                 */
369                B ifNoneMatch(String... ifNoneMatches);
370
371                /**
372                 * Set the (new) value of the Range header.
373                 * @param ranges the HTTP ranges
374                 * @see HttpHeaders#setRange(List)
375                 */
376                B range(HttpRange... ranges);
377
378                /**
379                 * Builds the request with no body.
380                 * @return the request
381                 * @see BodyBuilder#body(Publisher)
382                 * @see BodyBuilder#body(String)
383                 */
384                MockServerHttpRequest build();
385        }
386
387
388        /**
389         * A builder that adds a body to the request.
390         */
391        public interface BodyBuilder extends BaseBuilder<BodyBuilder> {
392
393                /**
394                 * Set the length of the body in bytes, as specified by the
395                 * {@code Content-Length} header.
396                 * @param contentLength the content length
397                 * @return this builder
398                 * @see HttpHeaders#setContentLength(long)
399                 */
400                BodyBuilder contentLength(long contentLength);
401
402                /**
403                 * Set the {@linkplain MediaType media type} of the body, as specified
404                 * by the {@code Content-Type} header.
405                 * @param contentType the content type
406                 * @return this builder
407                 * @see HttpHeaders#setContentType(MediaType)
408                 */
409                BodyBuilder contentType(MediaType contentType);
410
411                /**
412                 * Set the body of the request and build it.
413                 * @param body the body
414                 * @return the built request entity
415                 */
416                MockServerHttpRequest body(Publisher<? extends DataBuffer> body);
417
418                /**
419                 * Set the body of the request and build it.
420                 * <p>The String is assumed to be UTF-8 encoded unless the request has a
421                 * "content-type" header with a charset attribute.
422                 * @param body the body as text
423                 * @return the built request entity
424                 */
425                MockServerHttpRequest body(String body);
426        }
427
428
429        private static class DefaultBodyBuilder implements BodyBuilder {
430
431                private static final DataBufferFactory BUFFER_FACTORY = new DefaultDataBufferFactory();
432
433                @Nullable
434                private final HttpMethod method;
435
436                @Nullable
437                private final String customMethod;
438
439                private final URI url;
440
441                @Nullable
442                private String contextPath;
443
444                private final UriComponentsBuilder queryParamsBuilder = UriComponentsBuilder.newInstance();
445
446                private final HttpHeaders headers = new HttpHeaders();
447
448                private final MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
449
450                @Nullable
451                private InetSocketAddress remoteAddress;
452
453                @Nullable
454                private InetSocketAddress localAddress;
455
456                @Nullable
457                private SslInfo sslInfo;
458
459
460                DefaultBodyBuilder(HttpMethod method, URI url) {
461                        this.method = method;
462                        this.customMethod = null;
463                        this.url = url;
464                }
465
466                DefaultBodyBuilder(String method, URI url) {
467                        HttpMethod resolved = HttpMethod.resolve(method);
468                        if (resolved != null) {
469                                this.method = resolved;
470                                this.customMethod = null;
471                        }
472                        else {
473                                this.method = null;
474                                this.customMethod = method;
475                        }
476                        this.url = url;
477                }
478
479                @Override
480                public BodyBuilder contextPath(String contextPath) {
481                        this.contextPath = contextPath;
482                        return this;
483                }
484
485                @Override
486                public BodyBuilder queryParam(String name, Object... values) {
487                        this.queryParamsBuilder.queryParam(name, values);
488                        return this;
489                }
490
491                @Override
492                public BodyBuilder queryParams(MultiValueMap<String, String> params) {
493                        this.queryParamsBuilder.queryParams(params);
494                        return this;
495                }
496
497                @Override
498                public BodyBuilder remoteAddress(InetSocketAddress remoteAddress) {
499                        this.remoteAddress = remoteAddress;
500                        return this;
501                }
502
503                @Override
504                public BodyBuilder localAddress(InetSocketAddress localAddress) {
505                        this.localAddress = localAddress;
506                        return this;
507                }
508
509                @Override
510                public void sslInfo(SslInfo sslInfo) {
511                        this.sslInfo = sslInfo;
512                }
513
514                @Override
515                public BodyBuilder cookie(HttpCookie... cookies) {
516                        Arrays.stream(cookies).forEach(cookie -> this.cookies.add(cookie.getName(), cookie));
517                        return this;
518                }
519
520                @Override
521                public BodyBuilder cookies(MultiValueMap<String, HttpCookie> cookies) {
522                        this.cookies.putAll(cookies);
523                        return this;
524                }
525
526                @Override
527                public BodyBuilder header(String headerName, String... headerValues) {
528                        for (String headerValue : headerValues) {
529                                this.headers.add(headerName, headerValue);
530                        }
531                        return this;
532                }
533
534                @Override
535                public BodyBuilder headers(MultiValueMap<String, String> headers) {
536                        this.headers.putAll(headers);
537                        return this;
538                }
539
540                @Override
541                public BodyBuilder accept(MediaType... acceptableMediaTypes) {
542                        this.headers.setAccept(Arrays.asList(acceptableMediaTypes));
543                        return this;
544                }
545
546                @Override
547                public BodyBuilder acceptCharset(Charset... acceptableCharsets) {
548                        this.headers.setAcceptCharset(Arrays.asList(acceptableCharsets));
549                        return this;
550                }
551
552                @Override
553                public BodyBuilder acceptLanguageAsLocales(Locale... acceptableLocales) {
554                        this.headers.setAcceptLanguageAsLocales(Arrays.asList(acceptableLocales));
555                        return this;
556                }
557
558                @Override
559                public BodyBuilder contentLength(long contentLength) {
560                        this.headers.setContentLength(contentLength);
561                        return this;
562                }
563
564                @Override
565                public BodyBuilder contentType(MediaType contentType) {
566                        this.headers.setContentType(contentType);
567                        return this;
568                }
569
570                @Override
571                public BodyBuilder ifModifiedSince(long ifModifiedSince) {
572                        this.headers.setIfModifiedSince(ifModifiedSince);
573                        return this;
574                }
575
576                @Override
577                public BodyBuilder ifUnmodifiedSince(long ifUnmodifiedSince) {
578                        this.headers.setIfUnmodifiedSince(ifUnmodifiedSince);
579                        return this;
580                }
581
582                @Override
583                public BodyBuilder ifNoneMatch(String... ifNoneMatches) {
584                        this.headers.setIfNoneMatch(Arrays.asList(ifNoneMatches));
585                        return this;
586                }
587
588                @Override
589                public BodyBuilder range(HttpRange... ranges) {
590                        this.headers.setRange(Arrays.asList(ranges));
591                        return this;
592                }
593
594                @Override
595                public MockServerHttpRequest build() {
596                        return body(Flux.empty());
597                }
598
599                @Override
600                public MockServerHttpRequest body(String body) {
601                        return body(Flux.just(BUFFER_FACTORY.wrap(body.getBytes(getCharset()))));
602                }
603
604                private Charset getCharset() {
605                        return Optional.ofNullable(this.headers.getContentType())
606                                        .map(MimeType::getCharset).orElse(StandardCharsets.UTF_8);
607                }
608
609                @Override
610                public MockServerHttpRequest body(Publisher<? extends DataBuffer> body) {
611                        applyCookiesIfNecessary();
612                        return new MockServerHttpRequest(this.method, this.customMethod, getUrlToUse(), this.contextPath,
613                                        this.headers, this.cookies, this.localAddress, this.remoteAddress, this.sslInfo, body);
614                }
615
616                private void applyCookiesIfNecessary() {
617                        if (this.headers.get(HttpHeaders.COOKIE) == null) {
618                                this.cookies.values().stream().flatMap(Collection::stream)
619                                                .forEach(cookie -> this.headers.add(HttpHeaders.COOKIE, cookie.toString()));
620                        }
621                }
622
623                private URI getUrlToUse() {
624                        MultiValueMap<String, String> params =
625                                        this.queryParamsBuilder.buildAndExpand().encode().getQueryParams();
626                        if (!params.isEmpty()) {
627                                return UriComponentsBuilder.fromUri(this.url).queryParams(params).build(true).toUri();
628                        }
629                        return this.url;
630                }
631        }
632
633}