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 }