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}