1 /*
2 * Copyright (c) 2002-2025 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit;
16
17 import static java.nio.charset.StandardCharsets.ISO_8859_1;
18
19 import java.io.IOException;
20 import java.net.URL;
21 import java.nio.charset.Charset;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27
28 import org.apache.commons.logging.Log;
29 import org.apache.commons.logging.LogFactory;
30 import org.htmlunit.util.MimeType;
31 import org.htmlunit.util.NameValuePair;
32
33 /**
34 * A fake {@link WebConnection} designed to mock out the actual HTTP connections.
35 *
36 * @author Mike Bowler
37 * @author Noboru Sinohara
38 * @author Marc Guillemot
39 * @author Brad Clarke
40 * @author Ahmed Ashour
41 * @author Ronald Brill
42 */
43 public class MockWebConnection implements WebConnection {
44
45 private static final Log LOG = LogFactory.getLog(MockWebConnection.class);
46
47 private final Map<String, IOException> throwableMap_ = new HashMap<>();
48 private final Map<String, RawResponseData> responseMap_ = new HashMap<>();
49 private RawResponseData defaultResponse_;
50 private WebRequest lastRequest_;
51 private int requestCount_;
52 private final List<URL> requestedUrls_ = Collections.synchronizedList(new ArrayList<>());
53
54 /**
55 * Contains the raw data configured for a response.
56 */
57 public static class RawResponseData {
58 private final List<NameValuePair> headers_;
59 private final byte[] byteContent_;
60 private final String stringContent_;
61 private final int statusCode_;
62 private final String statusMessage_;
63 private Charset charset_;
64
65 RawResponseData(final byte[] byteContent, final int statusCode, final String statusMessage,
66 final String contentType, final List<NameValuePair> headers) {
67 byteContent_ = byteContent;
68 stringContent_ = null;
69 statusCode_ = statusCode;
70 statusMessage_ = statusMessage;
71 headers_ = compileHeaders(headers, contentType);
72 }
73
74 RawResponseData(final String stringContent, final Charset charset, final int statusCode,
75 final String statusMessage, final String contentType, final List<NameValuePair> headers) {
76 byteContent_ = null;
77 charset_ = charset;
78 stringContent_ = stringContent;
79 statusCode_ = statusCode;
80 statusMessage_ = statusMessage;
81 headers_ = compileHeaders(headers, contentType);
82 }
83
84 private static List<NameValuePair> compileHeaders(final List<NameValuePair> headers, final String contentType) {
85 final List<NameValuePair> compiledHeaders = new ArrayList<>();
86 if (headers != null) {
87 compiledHeaders.addAll(headers);
88 }
89 if (contentType != null) {
90 compiledHeaders.add(new NameValuePair(HttpHeader.CONTENT_TYPE, contentType));
91 }
92 return compiledHeaders;
93 }
94
95 WebResponseData asWebResponseData() {
96 final byte[] content;
97 if (byteContent_ != null) {
98 content = byteContent_;
99 }
100 else if (stringContent_ == null) {
101 content = new byte[] {};
102 }
103 else {
104 content = stringContent_.getBytes(charset_);
105 }
106 return new WebResponseData(content, statusCode_, statusMessage_, headers_);
107 }
108
109 /**
110 * Gets the configured headers.
111 * @return the headers
112 */
113 public List<NameValuePair> getHeaders() {
114 return headers_;
115 }
116
117 /**
118 * Gets the configured content bytes.
119 * @return {@code null} if a String content has been configured
120 */
121 public byte[] getByteContent() {
122 return byteContent_;
123 }
124
125 /**
126 * Gets the configured content String.
127 * @return {@code null} if a byte content has been configured
128 */
129 public String getStringContent() {
130 return stringContent_;
131 }
132
133 /**
134 * Gets the configured status code.
135 * @return the status code
136 */
137 public int getStatusCode() {
138 return statusCode_;
139 }
140
141 /**
142 * Gets the configured status message.
143 * @return the message
144 */
145 public String getStatusMessage() {
146 return statusMessage_;
147 }
148
149 /**
150 * Gets the configured charset.
151 * @return {@code null} for byte content
152 */
153 public Charset getCharset() {
154 return charset_;
155 }
156 }
157
158 /**
159 * {@inheritDoc}
160 */
161 @Override
162 public WebResponse getResponse(final WebRequest request) throws IOException {
163 final RawResponseData rawResponse = getRawResponse(request);
164 return new WebResponse(rawResponse.asWebResponseData(), request, 0);
165 }
166
167 /**
168 * Gets the raw response configured for the request.
169 * @param request the request
170 * @return the raw response
171 * @throws IOException if defined
172 */
173 public RawResponseData getRawResponse(final WebRequest request) throws IOException {
174 final URL url = request.getUrl();
175
176 if (LOG.isDebugEnabled()) {
177 LOG.debug("Getting response for " + url.toExternalForm());
178 }
179
180 lastRequest_ = request;
181 requestCount_++;
182 requestedUrls_.add(url);
183
184 String urlString = url.toExternalForm();
185 final IOException throwable = throwableMap_.get(urlString);
186 if (throwable != null) {
187 throw throwable;
188 }
189
190 RawResponseData rawResponse = responseMap_.get(urlString);
191 if (rawResponse == null) {
192 // try to find without query params
193 final int queryStart = urlString.lastIndexOf('?');
194 if (queryStart > -1) {
195 urlString = urlString.substring(0, queryStart);
196 rawResponse = responseMap_.get(urlString);
197 }
198
199 // fall back to default
200 if (rawResponse == null) {
201 rawResponse = defaultResponse_;
202 if (rawResponse == null) {
203 throw new IllegalStateException("No response specified that can handle URL "
204 + request.getHttpMethod()
205 + " [" + urlString + "]");
206 }
207 }
208 }
209
210 return rawResponse;
211 }
212
213 /**
214 * Gets the list of requested URLs.
215 * @return the list of relative URLs
216 */
217 public List<URL> getRequestedUrls() {
218 return Collections.unmodifiableList(requestedUrls_);
219 }
220
221 /**
222 * Gets the list of requested URLs relative to the provided URL.
223 * @param relativeTo what should be removed from the requested URLs.
224 * @return the list of relative URLs
225 */
226 public List<String> getRequestedUrls(final URL relativeTo) {
227 final String baseUrl = relativeTo.toString();
228 final List<String> response = new ArrayList<>();
229 for (final URL url : requestedUrls_) {
230 String s = url.toString();
231 if (s.startsWith(baseUrl)) {
232 s = s.substring(baseUrl.length());
233 }
234 response.add(s);
235 }
236
237 return Collections.unmodifiableList(response);
238 }
239
240 /**
241 * Returns the method that was used in the last call to submitRequest().
242 *
243 * @return the method that was used in the last call to submitRequest()
244 */
245 public HttpMethod getLastMethod() {
246 return lastRequest_.getHttpMethod();
247 }
248
249 /**
250 * Returns the parameters that were used in the last call to submitRequest().
251 *
252 * @return the parameters that were used in the last call to submitRequest()
253 */
254 public List<NameValuePair> getLastParameters() {
255 return lastRequest_.getRequestParameters();
256 }
257
258 /**
259 * Sets the response that will be returned when the specified URL is requested.
260 * @param url the URL that will return the given response
261 * @param content the content to return
262 * @param statusCode the status code to return
263 * @param statusMessage the status message to return
264 * @param contentType the content type to return
265 * @param headers the response headers to return
266 */
267 public void setResponse(final URL url, final String content, final int statusCode,
268 final String statusMessage, final String contentType,
269 final List<NameValuePair> headers) {
270
271 setResponse(
272 url,
273 content,
274 statusCode,
275 statusMessage,
276 contentType,
277 ISO_8859_1,
278 headers);
279 }
280
281 /**
282 * Sets the response that will be returned when the specified URL is requested.
283 * @param url the URL that will return the given response
284 * @param content the content to return
285 * @param statusCode the status code to return
286 * @param statusMessage the status message to return
287 * @param contentType the content type to return
288 * @param charset the charset
289 * @param headers the response headers to return
290 */
291 public void setResponse(final URL url, final String content, final int statusCode,
292 final String statusMessage, final String contentType, final Charset charset,
293 final List<NameValuePair> headers) {
294
295 final RawResponseData responseEntry = buildRawResponseData(content, charset, statusCode, statusMessage,
296 contentType, headers);
297 responseMap_.put(url.toExternalForm(), responseEntry);
298 }
299
300 /**
301 * Sets the exception that will be thrown when the specified URL is requested.
302 * @param url the URL that will force the exception
303 * @param throwable the Throwable
304 */
305 public void setThrowable(final URL url, final IOException throwable) {
306 throwableMap_.put(url.toExternalForm(), throwable);
307 }
308
309 /**
310 * Sets the response that will be returned when the specified URL is requested.
311 * @param url the URL that will return the given response
312 * @param content the content to return
313 * @param statusCode the status code to return
314 * @param statusMessage the status message to return
315 * @param contentType the content type to return
316 * @param headers the response headers to return
317 */
318 public void setResponse(final URL url, final byte[] content, final int statusCode,
319 final String statusMessage, final String contentType,
320 final List<NameValuePair> headers) {
321
322 final RawResponseData responseEntry = buildRawResponseData(content, statusCode, statusMessage, contentType,
323 headers);
324 responseMap_.put(url.toExternalForm(), responseEntry);
325 }
326
327 private static RawResponseData buildRawResponseData(final byte[] content, final int statusCode,
328 final String statusMessage, final String contentType, final List<NameValuePair> headers) {
329 return new RawResponseData(content, statusCode, statusMessage, contentType, headers);
330 }
331
332 private static RawResponseData buildRawResponseData(final String content, Charset charset, final int statusCode,
333 final String statusMessage, final String contentType, final List<NameValuePair> headers) {
334
335 if (charset == null) {
336 charset = ISO_8859_1;
337 }
338 return new RawResponseData(content, charset, statusCode, statusMessage, contentType, headers);
339 }
340
341 /**
342 * Convenient method that is the same as calling
343 * {@link #setResponse(URL,String,int,String,String,List)} with a status
344 * of "200 OK", a content type of "text/html" and no additional headers.
345 *
346 * @param url the URL that will return the given response
347 * @param content the content to return
348 */
349 public void setResponse(final URL url, final String content) {
350 setResponse(url, content, 200, "OK", MimeType.TEXT_HTML, null);
351 }
352
353 /**
354 * Convenient method that is the same as calling
355 * {@link #setResponse(URL,String,int,String,String,List)} with a status
356 * of "200 OK" and no additional headers.
357 *
358 * @param url the URL that will return the given response
359 * @param content the content to return
360 * @param contentType the content type to return
361 */
362 public void setResponse(final URL url, final String content, final String contentType) {
363 setResponse(url, content, 200, "OK", contentType, null);
364 }
365
366 /**
367 * Convenient method that is the same as calling
368 * {@link #setResponse(URL, String, int, String, String, Charset, List)} with a status
369 * of "200 OK" and no additional headers.
370 *
371 * @param url the URL that will return the given response
372 * @param content the content to return
373 * @param contentType the content type to return
374 * @param charset the charset
375 */
376 public void setResponse(final URL url, final String content, final String contentType, final Charset charset) {
377 setResponse(url, content, 200, "OK", contentType, charset, null);
378 }
379
380 /**
381 * Specify a generic HTML page that will be returned when the given URL is specified.
382 * The page will contain only minimal HTML to satisfy the HTML parser but will contain
383 * the specified title so that tests can check for titleText.
384 *
385 * @param url the URL that will return the given response
386 * @param title the title of the page
387 */
388 public void setResponseAsGenericHtml(final URL url, final String title) {
389 final String content = "<!DOCTYPE html><html><head><title>" + title + "</title></head><body></body></html>";
390 setResponse(url, content);
391 }
392
393 /**
394 * Sets the response that will be returned when a URL is requested that does
395 * not have a specific content set for it.
396 *
397 * @param content the content to return
398 * @param statusCode the status code to return
399 * @param statusMessage the status message to return
400 * @param contentType the content type to return
401 */
402 public void setDefaultResponse(final String content, final int statusCode,
403 final String statusMessage, final String contentType) {
404
405 defaultResponse_ = buildRawResponseData(content, null, statusCode, statusMessage, contentType, null);
406 }
407
408 /**
409 * Sets the response that will be returned when a URL is requested that does
410 * not have a specific content set for it.
411 *
412 * @param content the content to return
413 * @param statusCode the status code to return
414 * @param statusMessage the status message to return
415 * @param contentType the content type to return
416 */
417 public void setDefaultResponse(final byte[] content, final int statusCode,
418 final String statusMessage, final String contentType) {
419
420 defaultResponse_ = buildRawResponseData(content, statusCode, statusMessage, contentType, null);
421 }
422
423 /**
424 * Sets the response that will be returned when a URL is requested that does
425 * not have a specific content set for it.
426 *
427 * @param content the content to return
428 */
429 public void setDefaultResponse(final String content) {
430 setDefaultResponse(content, 200, "OK", MimeType.TEXT_HTML);
431 }
432
433 /**
434 * Sets the response that will be returned when a URL is requested that does
435 * not have a specific content set for it.
436 *
437 * @param content the content to return
438 * @param contentType the content type to return
439 */
440 public void setDefaultResponse(final String content, final String contentType) {
441 setDefaultResponse(content, 200, "OK", contentType, null);
442 }
443
444 /**
445 * Sets the response that will be returned when a URL is requested that does
446 * not have a specific content set for it.
447 *
448 * @param content the content to return
449 * @param contentType the content type to return
450 * @param charset the charset
451 */
452 public void setDefaultResponse(final String content, final String contentType, final Charset charset) {
453 setDefaultResponse(content, 200, "OK", contentType, charset, null);
454 }
455
456 /**
457 * Sets the response that will be returned when the specified URL is requested.
458 * @param content the content to return
459 * @param statusCode the status code to return
460 * @param statusMessage the status message to return
461 * @param contentType the content type to return
462 * @param headers the response headers to return
463 */
464 public void setDefaultResponse(final String content, final int statusCode,
465 final String statusMessage, final String contentType,
466 final List<NameValuePair> headers) {
467
468 defaultResponse_ = buildRawResponseData(content, null, statusCode, statusMessage, contentType, headers);
469 }
470
471 /**
472 * Sets the response that will be returned when the specified URL is requested.
473 * @param content the content to return
474 * @param statusCode the status code to return
475 * @param statusMessage the status message to return
476 * @param contentType the content type to return
477 * @param charset the charset
478 * @param headers the response headers to return
479 */
480 public void setDefaultResponse(final String content, final int statusCode,
481 final String statusMessage, final String contentType, final Charset charset,
482 final List<NameValuePair> headers) {
483
484 defaultResponse_ = buildRawResponseData(content, charset, statusCode, statusMessage, contentType, headers);
485 }
486
487 /**
488 * Returns the additional headers that were used in the last call
489 * to {@link #getResponse(WebRequest)}.
490 * @return the additional headers that were used in the last call
491 * to {@link #getResponse(WebRequest)}
492 */
493 public Map<String, String> getLastAdditionalHeaders() {
494 return lastRequest_.getAdditionalHeaders();
495 }
496
497 /**
498 * Returns the {@link WebRequest} that was used in the last call
499 * to {@link #getResponse(WebRequest)}.
500 * @return the {@link WebRequest} that was used in the last call
501 * to {@link #getResponse(WebRequest)}
502 */
503 public WebRequest getLastWebRequest() {
504 return lastRequest_;
505 }
506
507 /**
508 * Returns the number of requests made to this mock web connection.
509 * @return the number of requests made to this mock web connection
510 */
511 public int getRequestCount() {
512 return requestCount_;
513 }
514
515 /**
516 * Indicates if a response has already been configured for this URL.
517 * @param url the url
518 * @return {@code false} if no response has been configured
519 */
520 public boolean hasResponse(final URL url) {
521 return responseMap_.containsKey(url.toExternalForm());
522 }
523
524 /**
525 * {@inheritDoc}
526 */
527 @Override
528 public void close() {
529 clear();
530 }
531
532 /**
533 * Resets this.
534 */
535 public void clear() {
536 throwableMap_.clear();
537 responseMap_.clear();
538 defaultResponse_ = null;
539 lastRequest_ = null;
540 requestCount_ = 0;
541 requestedUrls_.clear();
542 }
543 }