Skip to content

Commit 07db161

Browse files
authored
Merge pull request #104 from thc202/test-api-tasks
2 parents d6cfc46 + e4f9ebc commit 07db161

6 files changed

Lines changed: 929 additions & 0 deletions

File tree

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ dependencies {
4545
testImplementation("org.assertj:assertj-core:3.27.7")
4646
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
4747
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
48+
functionalTestImplementation("org.nanohttpd:nanohttpd:2.3.1")
49+
functionalTestImplementation("commons-io:commons-io:2.22.0")
4850
}
4951

5052
val functionalTestTask =
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.gradle.addon;
21+
22+
import fi.iki.elonen.NanoHTTPD;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.Map;
26+
27+
public class HTTPDTestServer extends NanoHTTPD {
28+
29+
private List<NanoServerHandler> handlers = new ArrayList<>();
30+
private List<Request> requests = new ArrayList<>();
31+
32+
private NanoServerHandler handler404 =
33+
new NanoServerHandler("") {
34+
@Override
35+
protected Response serve(IHTTPSession session) {
36+
consumeBody(session);
37+
return newFixedLengthResponse(
38+
Response.Status.NOT_FOUND,
39+
MIME_HTML,
40+
"<html><head><title>404</title></head><body>404 Not Found</body></html>");
41+
}
42+
};
43+
44+
public HTTPDTestServer(int port) {
45+
super(port);
46+
}
47+
48+
public List<Request> getRequests() {
49+
return requests;
50+
}
51+
52+
@Override
53+
public Response serve(IHTTPSession session) {
54+
requests.add(
55+
new Request(
56+
session.getUri(),
57+
session.getMethod().toString(),
58+
session.getParameters(),
59+
session.getHeaders(),
60+
NanoServerHandler.getBody(session)));
61+
62+
for (NanoServerHandler handler : handlers) {
63+
if (handler.handles(session)) {
64+
return handler.serve(session);
65+
}
66+
}
67+
return handler404.serve(session);
68+
}
69+
70+
public void addHandler(NanoServerHandler handler) {
71+
this.handlers.add(handler);
72+
}
73+
74+
public void removeHandler(NanoServerHandler handler) {
75+
this.handlers.remove(handler);
76+
}
77+
78+
public void setHandler404(NanoServerHandler handler) {
79+
this.handler404 = handler;
80+
}
81+
82+
public static record Request(
83+
String uri,
84+
String method,
85+
Map<String, List<String>> parameters,
86+
Map<String, String> headers,
87+
String body) {}
88+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.gradle.addon;
21+
22+
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
23+
import fi.iki.elonen.NanoHTTPD.Response;
24+
import java.io.IOException;
25+
import org.apache.commons.io.IOUtils;
26+
27+
public abstract class NanoServerHandler {
28+
29+
private String name;
30+
31+
public NanoServerHandler(String name) {
32+
this.name = name;
33+
}
34+
35+
public String getName() {
36+
return name;
37+
}
38+
39+
protected boolean handles(IHTTPSession session) {
40+
return session.getUri().startsWith(getName());
41+
}
42+
43+
protected abstract Response serve(IHTTPSession session);
44+
45+
/**
46+
* Consumes the request body.
47+
*
48+
* @param session the session that has the request
49+
*/
50+
protected static void consumeBody(IHTTPSession session) {
51+
try {
52+
session.getInputStream().skip(getBodySize(session));
53+
} catch (IOException e) {
54+
System.err.println("Failed to consume body:");
55+
e.printStackTrace();
56+
}
57+
}
58+
59+
/**
60+
* Gets the size of the request body.
61+
*
62+
* @param session the session that has the request
63+
* @return the size of the body
64+
*/
65+
protected static int getBodySize(IHTTPSession session) {
66+
String contentLengthHeader = session.getHeaders().get("content-length");
67+
if (contentLengthHeader == null) {
68+
return 0;
69+
}
70+
71+
int contentLength = 0;
72+
try {
73+
contentLength = Integer.parseInt(contentLengthHeader);
74+
} catch (NumberFormatException e) {
75+
System.err.println("Failed to parse content-length value: " + contentLengthHeader);
76+
e.printStackTrace();
77+
return 0;
78+
}
79+
80+
if (contentLength <= 0) {
81+
return 0;
82+
}
83+
return contentLength;
84+
}
85+
86+
/**
87+
* Gets the request body.
88+
*
89+
* @param session the session that has the request
90+
* @return the body
91+
*/
92+
public static String getBody(IHTTPSession session) {
93+
int contentLength = getBodySize(session);
94+
if (contentLength == 0) {
95+
return "";
96+
}
97+
98+
byte[] bytes = new byte[contentLength];
99+
try {
100+
IOUtils.readFully(session.getInputStream(), bytes);
101+
} catch (IOException e) {
102+
System.err.println("Failed to read the body:");
103+
e.printStackTrace();
104+
return "";
105+
}
106+
return new String(bytes);
107+
}
108+
109+
/**
110+
* Gets the first parameter which is contained in the parameters list
111+
*
112+
* @param session the session that has the request
113+
* @param param the parameter name
114+
* @return the first value of the parameters
115+
*/
116+
protected static String getFirstParamValue(IHTTPSession session, String param) {
117+
return session.getParameters().get(param) != null
118+
? session.getParameters().get(param).get(0)
119+
: null;
120+
}
121+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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.gradle.addon.misc;
21+
22+
import static org.assertj.core.api.Assertions.as;
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.assertj.core.api.InstanceOfAssertFactories.map;
25+
26+
import fi.iki.elonen.NanoHTTPD;
27+
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
28+
import fi.iki.elonen.NanoHTTPD.Response;
29+
import java.util.List;
30+
import org.gradle.testkit.runner.BuildResult;
31+
import org.junit.jupiter.api.AfterEach;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.Test;
34+
import org.zaproxy.gradle.addon.FunctionalTest;
35+
import org.zaproxy.gradle.addon.HTTPDTestServer;
36+
import org.zaproxy.gradle.addon.NanoServerHandler;
37+
38+
class InstallAddOnFunctionalTest extends FunctionalTest {
39+
40+
private static final String INSTALL_ADD_ON_TASK = ":installZapAddOn";
41+
private static final String ADD_ON_ID = "testaddon";
42+
private static final String INSTALL_LOCAL_ADDON_URI =
43+
"http://zap/xml/autoupdate/action/installLocalAddon/";
44+
45+
private HTTPDTestServer zapServer;
46+
private int zapPort;
47+
48+
@BeforeEach
49+
void startZapServer() throws Exception {
50+
zapServer = new HTTPDTestServer(0);
51+
zapServer.start();
52+
zapPort = zapServer.getListeningPort();
53+
}
54+
55+
@AfterEach
56+
void stopZapServer() {
57+
zapServer.stop();
58+
}
59+
60+
@Override
61+
protected void buildFile(String content) throws Exception {
62+
super.buildFile(
63+
"""
64+
plugins {
65+
java
66+
id("org.zaproxy.add-on")
67+
}
68+
repositories {
69+
mavenCentral()
70+
}
71+
version = "1"
72+
zapAddOn {
73+
addOnId.set("%s")
74+
addOnName.set("Test Add-On")
75+
}
76+
"""
77+
.formatted(ADD_ON_ID)
78+
+ content);
79+
}
80+
81+
@Test
82+
void shouldInstallAddOn() throws Exception {
83+
// Given
84+
zapServer.addHandler(
85+
new NanoServerHandler(INSTALL_LOCAL_ADDON_URI) {
86+
@Override
87+
protected Response serve(IHTTPSession session) {
88+
consumeBody(session);
89+
return NanoHTTPD.newFixedLengthResponse(
90+
Response.Status.OK, "text/xml", "<Result>OK</Result>");
91+
}
92+
});
93+
buildFile("");
94+
95+
// When
96+
BuildResult result = build(INSTALL_ADD_ON_TASK, "--port", String.valueOf(zapPort));
97+
98+
// Then
99+
assertTaskSuccess(result, INSTALL_ADD_ON_TASK);
100+
assertApiRequest();
101+
}
102+
103+
@Test
104+
void shouldFailWhenZapResponseIsNotOk() throws Exception {
105+
// Given
106+
zapServer.addHandler(
107+
new NanoServerHandler(INSTALL_LOCAL_ADDON_URI) {
108+
@Override
109+
protected Response serve(IHTTPSession session) {
110+
consumeBody(session);
111+
return NanoHTTPD.newFixedLengthResponse(
112+
Response.Status.OK, "text/xml", "<Result>FAIL</Result>");
113+
}
114+
});
115+
buildFile("");
116+
117+
// When
118+
BuildResult result = buildAndFail(INSTALL_ADD_ON_TASK, "--port", String.valueOf(zapPort));
119+
120+
// Then
121+
assertTaskFailed(result, INSTALL_ADD_ON_TASK);
122+
assertThat(result.getOutput()).contains("Failed to install the add-on");
123+
assertApiRequest();
124+
}
125+
126+
@Test
127+
void shouldFailWhenZapIsNotAvailable() throws Exception {
128+
// Given
129+
zapServer.stop();
130+
buildFile("");
131+
132+
// When
133+
BuildResult result = buildAndFail(INSTALL_ADD_ON_TASK, "--port", String.valueOf(zapPort));
134+
135+
// Then
136+
assertTaskFailed(result, INSTALL_ADD_ON_TASK);
137+
assertThat(result.getOutput()).contains("An error occurred while installing the add-on");
138+
assertThat(zapServer.getRequests()).isEmpty();
139+
}
140+
141+
private void assertApiRequest() {
142+
assertThat(zapServer.getRequests())
143+
.singleElement()
144+
.extracting(HTTPDTestServer.Request::parameters, as(map(String.class, List.class)))
145+
.hasEntrySatisfying("file", e -> e.contains(ADD_ON_ID));
146+
}
147+
}

0 commit comments

Comments
 (0)