Skip to content

Commit 3681c17

Browse files
committed
selenium: Headless browser unit tests
Signed-off-by: Simon Bennetts <psiinon@gmail.com>
1 parent 8591c11 commit 3681c17

4 files changed

Lines changed: 313 additions & 0 deletions

File tree

addOns/selenium/selenium.gradle.kts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,25 @@ dependencies {
6262

6363
testImplementation(project(":testutils"))
6464
}
65+
66+
val webdriverProjectPath =
67+
when {
68+
org.gradle.internal.os.OperatingSystem.current().isMacOsX -> ":addOns:webdrivers:webdrivermacos"
69+
org.gradle.internal.os.OperatingSystem.current().isLinux -> ":addOns:webdrivers:webdriverlinux"
70+
else -> ":addOns:webdrivers:webdriverwindows"
71+
}
72+
73+
tasks.register<Sync>("prepareTestWebdrivers") {
74+
val wdProject = project(webdriverProjectPath)
75+
dependsOn(wdProject.tasks.named("generateZapAddOnManifest"))
76+
from(wdProject.layout.buildDirectory.dir("webdrivers"))
77+
into(layout.buildDirectory.dir("test-zap-webdrivers"))
78+
}
79+
80+
tasks.withType<Test>().configureEach {
81+
if (name == "test") {
82+
dependsOn("prepareTestWebdrivers")
83+
systemProperties["zap.test.webdrivers.home"] =
84+
layout.buildDirectory.dir("test-zap-webdrivers").get().asFile.absolutePath
85+
}
86+
}

addOns/selenium/src/main/java/org/zaproxy/zap/extension/selenium/ExtensionSelenium.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,25 @@ public WebDriver getWebDriver(String browserId, DriverConfiguration driverConf)
922922
"Unknown ProvidedBrowser: " + provided.getClass().getCanonicalName());
923923
}
924924

925+
if (driverConf.getPreferences() != null && !driverConf.getPreferences().isEmpty()) {
926+
Map<String, String> merged = new HashMap<>(config.getPreferences());
927+
merged.putAll(driverConf.getPreferences());
928+
config =
929+
DriverConfiguration.builder()
930+
.requester(config.getRequester())
931+
.proxyAddress(config.getProxyAddress())
932+
.proxyPort(config.getProxyPort())
933+
.consumer(config.getConsumer())
934+
.type(config.getType())
935+
.headless(config.isHeadless())
936+
.binaryPath(config.getBinaryPath())
937+
.driverPath(config.getDriverPath())
938+
.arguments(config.getArguments())
939+
.preferences(merged)
940+
.enableExtensions(config.isEnableExtensions())
941+
.build();
942+
}
943+
925944
try {
926945
wd = createWebDriver(config);
927946
Stats.incCounter(statsKey);
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* Zed Attack Proxy (ZAP) and its related class files.
3+
*
4+
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
5+
*
6+
* Copyright 2026 The ZAP Development Team
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
package org.zaproxy.zap.extension.selenium;
21+
22+
import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse;
23+
import static org.hamcrest.MatcherAssert.assertThat;
24+
import static org.hamcrest.Matchers.containsString;
25+
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
26+
import static org.hamcrest.Matchers.is;
27+
import static org.mockito.BDDMockito.given;
28+
import static org.mockito.Mockito.mock;
29+
import static org.mockito.Mockito.withSettings;
30+
31+
import fi.iki.elonen.NanoHTTPD;
32+
import java.io.ByteArrayInputStream;
33+
import java.io.IOException;
34+
import java.nio.file.Files;
35+
import java.nio.file.Paths;
36+
import java.util.Collections;
37+
import java.util.Map;
38+
import java.util.stream.Stream;
39+
import org.junit.jupiter.api.AfterEach;
40+
import org.junit.jupiter.api.BeforeAll;
41+
import org.junit.jupiter.api.BeforeEach;
42+
import org.junit.jupiter.params.ParameterizedTest;
43+
import org.junit.jupiter.params.provider.MethodSource;
44+
import org.mockito.quality.Strictness;
45+
import org.openqa.selenium.WebDriver;
46+
import org.parosproxy.paros.control.Control;
47+
import org.parosproxy.paros.extension.ExtensionHook;
48+
import org.parosproxy.paros.extension.ExtensionLoader;
49+
import org.parosproxy.paros.model.Model;
50+
import org.parosproxy.paros.model.OptionsParam;
51+
import org.parosproxy.paros.model.Session;
52+
import org.zaproxy.addon.network.ExtensionNetwork;
53+
import org.zaproxy.zap.testutils.NanoServerHandler;
54+
import org.zaproxy.zap.testutils.TestUtils;
55+
import org.zaproxy.zap.utils.ZapXmlConfiguration;
56+
57+
/**
58+
* Unit tests that verify Chrome, Edge, and Firefox headless browsers can be launched and access a
59+
* simple web page.
60+
*/
61+
class HeadlessBrowsersUnitTest extends TestUtils {
62+
63+
private static final String PAGE_TITLE = "Test Page";
64+
private static final String PAGE_BODY_MARKER = "Headless browser test content";
65+
66+
private static final String IMAGE_PATH = "/test.png";
67+
private static final String PAGE_WITH_IMAGE_PATH = "/page-with-image.html";
68+
69+
// These env vars should be set up when running in GitHub CI
70+
private static final String CHROME_WEB_DRIVER = System.getenv("CHROMEWEBDRIVER");
71+
private static final String EDGE_WEB_DRIVER = System.getenv("EDGEWEBDRIVER");
72+
private static final String GECKO_WEB_DRIVER = System.getenv("GECKOWEBDRIVER");
73+
74+
private byte[] imageBytes;
75+
76+
private static byte[] loadImageFromResource() throws IOException {
77+
return Files.readAllBytes(
78+
TestUtils.getResourcePath(HeadlessBrowsersUnitTest.class, "test.png"));
79+
}
80+
81+
private static ExtensionNetwork extensionNetwork;
82+
private static Model model;
83+
private static Session session;
84+
85+
@BeforeAll
86+
static void setupAll() {
87+
if (CHROME_WEB_DRIVER == null) {
88+
// Assume we are running locally
89+
String webdriversHome = System.getProperty("zap.test.webdrivers.home");
90+
if (webdriversHome != null && !webdriversHome.isEmpty()) {
91+
Browser.setZapHomeDir(Paths.get(webdriversHome));
92+
}
93+
}
94+
}
95+
96+
@BeforeEach
97+
void setUp() throws Exception {
98+
mockMessages(new ExtensionSelenium());
99+
setUpZap();
100+
model = mock(Model.class, withSettings().strictness(Strictness.LENIENT));
101+
Model.setSingletonForTesting(model);
102+
given(model.getOptionsParam()).willReturn(new OptionsParam());
103+
extensionNetwork = new ExtensionNetwork();
104+
ExtensionSelenium extensionSelenium = new ExtensionSelenium();
105+
extensionNetwork.initModel(model);
106+
ExtensionLoader extensionLoader =
107+
mock(ExtensionLoader.class, withSettings().strictness(Strictness.LENIENT));
108+
Control.initSingletonForTesting(model, extensionLoader);
109+
110+
given(extensionLoader.getExtension(ExtensionSelenium.class)).willReturn(extensionSelenium);
111+
extensionSelenium.init();
112+
113+
extensionNetwork.init();
114+
extensionNetwork.hook(new ExtensionHook(model, null));
115+
116+
model.getOptionsParam().load(new ZapXmlConfiguration());
117+
SeleniumOptions options = new SeleniumOptions();
118+
if (CHROME_WEB_DRIVER != null) {
119+
options.setChromeDriverPath(CHROME_WEB_DRIVER);
120+
}
121+
if (EDGE_WEB_DRIVER != null) {
122+
options.setEdgeDriverPath(EDGE_WEB_DRIVER);
123+
}
124+
if (GECKO_WEB_DRIVER != null) {
125+
options.setFirefoxDriverPath(GECKO_WEB_DRIVER);
126+
}
127+
model.getOptionsParam().addParamSet(options);
128+
129+
session = new Session(model);
130+
given(model.getSession()).willReturn(session);
131+
132+
startServer();
133+
imageBytes = loadImageFromResource();
134+
// More specific paths first (server matches by uri.startsWith(handler.getName()))
135+
nano.addHandler(
136+
new NanoServerHandler(IMAGE_PATH) {
137+
@Override
138+
protected NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession session) {
139+
return newFixedLengthResponse(
140+
NanoHTTPD.Response.Status.OK,
141+
"image/png",
142+
new ByteArrayInputStream(imageBytes),
143+
imageBytes.length);
144+
}
145+
});
146+
nano.addHandler(
147+
new NanoServerHandler(PAGE_WITH_IMAGE_PATH) {
148+
@Override
149+
protected NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession session) {
150+
String html =
151+
"<html><head><title>Page With Image</title></head><body>"
152+
+ "<img src=\"http://localhost:"
153+
+ nano.getListeningPort()
154+
+ IMAGE_PATH
155+
+ "\" alt=\"test\"></body></html>";
156+
return newFixedLengthResponse(html);
157+
}
158+
});
159+
nano.addHandler(
160+
new NanoServerHandler("/") {
161+
@Override
162+
protected NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession session) {
163+
return newFixedLengthResponse(
164+
"<html><head><title>"
165+
+ PAGE_TITLE
166+
+ "</title></head><body>"
167+
+ PAGE_BODY_MARKER
168+
+ "</body></html>");
169+
}
170+
});
171+
}
172+
173+
@AfterEach
174+
void tearDown() throws Exception {
175+
stopServer();
176+
Browser.setZapHomeDir(null);
177+
extensionNetwork.stop();
178+
}
179+
180+
static Stream<String> headlessBrowsers() {
181+
// TODO temp testing
182+
// return Stream.of("chrome-headless", "firefox-headless", "edge-headless");
183+
return Stream.of("chrome-headless", "firefox-headless");
184+
}
185+
186+
@ParameterizedTest
187+
@MethodSource("headlessBrowsers")
188+
void shouldAccessSimpleWebPageWithHeadlessBrowser(String browserId) throws IOException {
189+
ExtensionSelenium extensionSelenium =
190+
Control.getSingleton().getExtensionLoader().getExtension(ExtensionSelenium.class);
191+
String url = "http://localhost:" + nano.getListeningPort() + "/";
192+
193+
WebDriver driver = null;
194+
try {
195+
driver =
196+
extensionSelenium.getWebDriver(
197+
browserId, DriverConfiguration.builder().build());
198+
driver.get(url);
199+
200+
assertThat(driver.getTitle(), is(PAGE_TITLE));
201+
assertThat(driver.getPageSource(), containsString(PAGE_BODY_MARKER));
202+
} finally {
203+
if (driver != null) {
204+
driver.quit();
205+
}
206+
}
207+
}
208+
209+
@ParameterizedTest
210+
@MethodSource("headlessBrowsers")
211+
void shouldRequestImageWhenPageContainsImage(String browserId) throws IOException {
212+
ExtensionSelenium extensionSelenium =
213+
Control.getSingleton().getExtensionLoader().getExtension(ExtensionSelenium.class);
214+
String url = "http://localhost:" + nano.getListeningPort() + PAGE_WITH_IMAGE_PATH;
215+
216+
WebDriver driver = null;
217+
try {
218+
driver =
219+
extensionSelenium.getWebDriver(
220+
browserId, DriverConfiguration.builder().build());
221+
driver.get(url);
222+
223+
assertThat(
224+
"Browser should request the image when loading the page",
225+
Collections.frequency(nano.getRequestedUris(), IMAGE_PATH),
226+
greaterThanOrEqualTo(1));
227+
} finally {
228+
if (driver != null) {
229+
driver.quit();
230+
}
231+
}
232+
}
233+
234+
/**
235+
* Returns browser preferences that disable image loading. Chrome/Edge use the same Chromium
236+
* pref ({@code profile.default_content_setting_values.images} = 2). Firefox uses {@code
237+
* permissions.default.image} = 2 (may not work in all Firefox versions).
238+
*/
239+
private static Map<String, String> getPreferencesToBlockImages(String browserId) {
240+
if (browserId.startsWith("firefox")) {
241+
return Map.of("permissions.default.image", "2");
242+
}
243+
return Map.of("profile.default_content_setting_values.images", "2");
244+
}
245+
246+
@ParameterizedTest
247+
@MethodSource("headlessBrowsers")
248+
void shouldNotRequestImageWhenPreferenceBlocksImages(String browserId) throws IOException {
249+
ExtensionSelenium extensionSelenium =
250+
Control.getSingleton().getExtensionLoader().getExtension(ExtensionSelenium.class);
251+
String url = "http://localhost:" + nano.getListeningPort() + PAGE_WITH_IMAGE_PATH;
252+
Map<String, String> blockImagesPrefs = getPreferencesToBlockImages(browserId);
253+
254+
WebDriver driver = null;
255+
try {
256+
driver =
257+
extensionSelenium.getWebDriver(
258+
browserId,
259+
DriverConfiguration.builder().preferences(blockImagesPrefs).build());
260+
driver.get(url);
261+
262+
assertThat(
263+
"Browser should not request the image when prefs block images",
264+
Collections.frequency(nano.getRequestedUris(), IMAGE_PATH),
265+
is(0));
266+
} finally {
267+
if (driver != null) {
268+
driver.quit();
269+
}
270+
}
271+
}
272+
}
70 Bytes
Loading

0 commit comments

Comments
 (0)