View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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.net.URL;
20  import java.nio.charset.Charset;
21  import java.time.Duration;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.eclipse.jetty.server.Server;
27  import org.htmlunit.WebDriverTestCase.MockWebConnectionServlet;
28  import org.htmlunit.html.HtmlPage;
29  import org.htmlunit.util.JettyServerUtils;
30  import org.htmlunit.util.MimeType;
31  import org.junit.jupiter.api.AfterEach;
32  
33  import jakarta.servlet.Servlet;
34  
35  /**
36   * A WebTestCase which starts a local server, and doesn't use WebDriver.
37   * <p>
38   * <b>Note that {@link WebDriverTestCase} should be used unless HtmlUnit-specific feature
39   * is needed and Selenium does not support it.</b>
40   *
41   * @author Ahmed Ashour
42   * @author Marc Guillemot
43   * @author Ronald Brill
44   */
45  public abstract class WebServerTestCase extends WebTestCase {
46  
47      public enum SSLVariant {
48          NONE,
49          INSECURE,
50          SELF_SIGNED
51      }
52  
53      /** Timeout used when waiting for successful bind. */
54      public static final int BIND_TIMEOUT = 1000;
55  
56      private Server server_;
57      private static Server STATIC_SERVER_;
58      private WebClient webClient_;
59      private CollectingAlertHandler alertHandler_ = new CollectingAlertHandler();
60  
61      /**
62       * Starts the web server on the default {@link #PORT}.
63       * The given resourceBase is used to be the ROOT directory that serves the default context.
64       * <p><b>Don't forget to stop the returned HttpServer after the test</b>
65       *
66       * @param resourceBase the base of resources for the default context
67       * @throws Exception if the test fails
68       */
69      protected void startWebServer(final String resourceBase) throws Exception {
70          if (server_ != null) {
71              throw new IllegalStateException("startWebServer() can not be called twice");
72          }
73  
74          server_ =  JettyServerUtils.startWebServer(PORT, resourceBase, null, null, isBasicAuthentication(), getSSLVariant());
75      }
76  
77      /**
78       * Starts the web server on the default {@link #PORT}.
79       * The given resourceBase is used to be the ROOT directory that serves the default context.
80       * <p><b>Don't forget to stop the returned HttpServer after the test</b>
81       *
82       * @param resourceBase the base of resources for the default context
83       * @param servlets map of {String, Class} pairs: String is the path spec, while class is the class
84       * @throws Exception if the test fails
85       */
86      protected void startWebServer(final String resourceBase,
87              final Map<String, Class<? extends Servlet>> servlets) throws Exception {
88          if (server_ != null) {
89              throw new IllegalStateException("startWebServer() can not be called twice");
90          }
91  
92          server_ = JettyServerUtils.startWebServer(PORT, resourceBase, servlets, null, false, SSLVariant.NONE);
93      }
94  
95      /**
96       * Performs post-test deconstruction.
97       * @throws Exception if an error occurs
98       */
99      @AfterEach
100     public void tearDown() throws Exception {
101         JettyServerUtils.stopServer(server_);
102         server_ = null;
103 
104         stopWebServer();
105     }
106 
107     /**
108      * Defines the provided string as response for the provided URL and loads it using the currently
109      * configured browser version. Finally it extracts the captured alerts and verifies them.
110      * @param html the HTML to use
111      * @param url the URL to use to load the page
112      * @return the page
113      * @throws Exception if something goes wrong
114      */
115     protected final HtmlPage loadPageWithAlerts(final String html, final URL url)
116         throws Exception {
117         return loadPageWithAlerts(html, url, Duration.ofSeconds(0));
118     }
119 
120     /**
121      * Same as {@link #loadPageWithAlerts(String, URL)}, but configuring the max wait time.
122      * @param html the HTML to use
123      * @param url the URL to use to load the page
124      * @param maxWaitTime to wait to get the alerts (in ms)
125      * @return the page
126      * @throws Exception if something goes wrong
127      */
128     protected final HtmlPage loadPageWithAlerts(final String html, final URL url, final Duration maxWaitTime)
129         throws Exception {
130         alertHandler_.clear();
131         expandExpectedAlertsVariables(URL_FIRST);
132 
133         final String[] expectedAlerts = getExpectedAlerts();
134         final HtmlPage page = loadPage(html, url);
135 
136         List<String> actualAlerts = getCollectedAlerts(page);
137         final long maxWait = System.currentTimeMillis() + maxWaitTime.toMillis();
138         while (actualAlerts.size() < expectedAlerts.length && System.currentTimeMillis() < maxWait) {
139             Thread.sleep(30);
140             actualAlerts = getCollectedAlerts(page);
141         }
142 
143         assertEquals(expectedAlerts, getCollectedAlerts(page));
144         return page;
145     }
146 
147     /**
148      * Defines the provided string as response for the default URL and loads it using the currently
149      * configured browser version.
150      * @param html the HTML to use
151      * @return the page
152      * @throws Exception if something goes wrong
153      */
154     protected final HtmlPage loadPage(final String html) throws Exception {
155         return loadPage(html, URL_FIRST);
156     }
157 
158     /**
159      * Same as {@link #loadPage(String)}... but defining the default URL.
160      * @param html the HTML to use
161      * @param url the url to use to load the page
162      * @return the page
163      * @throws Exception if something goes wrong
164      */
165     protected final HtmlPage loadPage(final String html, final URL url) throws Exception {
166         return loadPage(html, url, MimeType.TEXT_HTML, ISO_8859_1);
167     }
168 
169     /**
170      * Same as {@link #loadPage(String, URL)}... but defining content type and charset as well.
171      * @param html the HTML to use
172      * @param url the url to use to load the page
173      * @param contentType the content type to return
174      * @param charset the charset
175      * @return the page
176      * @throws Exception if something goes wrong
177      */
178     private HtmlPage loadPage(final String html, final URL url,
179             final String contentType, final Charset charset) throws Exception {
180         final MockWebConnection mockWebConnection = getMockWebConnection();
181         mockWebConnection.setResponse(url, html, contentType, charset);
182         startWebServer(mockWebConnection);
183 
184         return getWebClient().getPage(url);
185     }
186 
187     /**
188      * Starts the web server delivering response from the provided connection.
189      * @param mockConnection the sources for responses
190      * @throws Exception if a problem occurs
191      */
192     protected void startWebServer(final MockWebConnection mockConnection) throws Exception {
193         if (STATIC_SERVER_ == null) {
194             final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
195             servlets.put("/*", MockWebConnectionServlet.class);
196 
197             STATIC_SERVER_ = JettyServerUtils.startWebServer(PORT, "./", servlets, null, isBasicAuthentication(), getSSLVariant());
198         }
199         MockWebConnectionServlet.setMockconnection(mockConnection);
200     }
201 
202     /**
203      * Stops the WebServer.
204      * @throws Exception if it fails
205      */
206     protected static void stopWebServer() throws Exception {
207         JettyServerUtils.stopServer(STATIC_SERVER_);
208         STATIC_SERVER_ = null;
209     }
210 
211     /**
212      * Loads the provided URL serving responses from {@link #getMockWebConnection()}
213      * and verifies that the captured alerts are correct.
214      * @param url the URL to use to load the page
215      * @return the web driver
216      * @throws Exception if something goes wrong
217      */
218     protected final HtmlPage loadPageWithAlerts(final URL url) throws Exception {
219         alertHandler_.clear();
220         expandExpectedAlertsVariables(url);
221         final String[] expectedAlerts = getExpectedAlerts();
222 
223         startWebServer(getMockWebConnection());
224 
225         final HtmlPage page = getWebClient().getPage(url);
226 
227         assertEquals(expectedAlerts, getCollectedAlerts(page));
228         return page;
229     }
230 
231     /**
232      * Returns the collected alerts.
233      * @param page the page
234      * @return the alerts
235      */
236     protected List<String> getCollectedAlerts(final HtmlPage page) {
237         return alertHandler_.getCollectedAlerts();
238     }
239 
240     /**
241      * Returns whether to use basic authentication for all resources or not.
242      * The default implementation returns false.
243      * @return whether to use basic authentication or not
244      */
245     protected boolean isBasicAuthentication() {
246         return false;
247     }
248 
249     /**
250      * @return the {@link SSLVariant} to be used
251      */
252     public SSLVariant getSSLVariant() {
253         return SSLVariant.NONE;
254     }
255 
256     /**
257      * Returns the WebClient instance for the current test with the current {@link BrowserVersion}.
258      * @return a WebClient with the current {@link BrowserVersion}
259      */
260     protected WebClient getWebClient() {
261         if (webClient_ == null) {
262             webClient_ = new WebClient(getBrowserVersion());
263             webClient_.setAlertHandler(alertHandler_);
264         }
265         return webClient_;
266     }
267 
268     /**
269      * Cleanup after a test.
270      */
271     @Override
272     @AfterEach
273     public void releaseResources() {
274         super.releaseResources();
275         if (webClient_ != null) {
276             webClient_.close();
277             webClient_.getCookieManager().clearCookies();
278         }
279         webClient_ = null;
280         alertHandler_ = null;
281     }
282 }