1 | package com.renomad.minum.web; | |
2 | ||
3 | import com.renomad.minum.state.Constants; | |
4 | import com.renomad.minum.htmlparsing.HtmlParseNode; | |
5 | import com.renomad.minum.htmlparsing.HtmlParser; | |
6 | import com.renomad.minum.htmlparsing.TagName; | |
7 | import com.renomad.minum.logging.ILogger; | |
8 | import com.renomad.minum.state.Context; | |
9 | import com.renomad.minum.utils.InvariantException; | |
10 | import com.renomad.minum.utils.StacktraceUtils; | |
11 | import java.io.IOException; | |
12 | import java.io.InputStream; | |
13 | import java.net.Socket; | |
14 | import java.nio.charset.StandardCharsets; | |
15 | import java.util.ArrayList; | |
16 | import java.util.List; | |
17 | import java.util.Map; | |
18 | ||
19 | /** | |
20 | * Tools to enable system-wide integration testing | |
21 | */ | |
22 | public final class FunctionalTesting { | |
23 | ||
24 | private final String host; | |
25 | private final int port; | |
26 | private final IInputStreamUtils inputStreamUtils; | |
27 | private final ILogger logger; | |
28 | private final Constants constants; | |
29 | private final IBodyProcessor bodyProcessor; | |
30 | ||
31 | /** | |
32 | * Allows the user to set the host and port to target | |
33 | * for testing. | |
34 | */ | |
35 | public FunctionalTesting(Context context, String host, int port) { | |
36 | this.constants = context.getConstants(); | |
37 | this.host = host; | |
38 | this.port = port; | |
39 | ||
40 | this.inputStreamUtils = new InputStreamUtils(constants.maxReadLineSizeBytes); | |
41 | this.logger = context.getLogger(); | |
42 | bodyProcessor = new BodyProcessor(context); | |
43 | } | |
44 | ||
45 | /** | |
46 | * A {@link Response} designed to work with {@link FunctionalTesting} | |
47 | */ | |
48 | public record TestResponse(StatusLine statusLine, Headers headers, Body body) { | |
49 | ||
50 | public static final TestResponse EMPTY = new TestResponse(StatusLine.EMPTY, Headers.EMPTY, Body.EMPTY); | |
51 | ||
52 | /** | |
53 | * Presuming the response body is HTML, search for a single | |
54 | * HTML element with the given tag name and attributes. | |
55 | * | |
56 | * @return {@link HtmlParseNode#EMPTY} if none found, a particular node if found, | |
57 | * and an exception thrown if more than one found. | |
58 | */ | |
59 | public HtmlParseNode searchOne(TagName tagName, Map<String, String> attributes) { | |
60 | var htmlParser = new HtmlParser(); | |
61 | var nodes = htmlParser.parse(body.asString()); | |
62 | var searchResults = htmlParser.search(nodes, tagName, attributes); | |
63 |
2
1. searchOne : changed conditional boundary → KILLED 2. searchOne : negated conditional → KILLED |
if (searchResults.size() > 1) { |
64 | throw new InvariantException("More than 1 node found. Here they are:" + searchResults); | |
65 | } | |
66 |
1
1. searchOne : negated conditional → KILLED |
if (searchResults.isEmpty()) { |
67 |
1
1. searchOne : replaced return value with null for com/renomad/minum/web/FunctionalTesting$TestResponse::searchOne → KILLED |
return HtmlParseNode.EMPTY; |
68 | } else { | |
69 |
1
1. searchOne : replaced return value with null for com/renomad/minum/web/FunctionalTesting$TestResponse::searchOne → KILLED |
return searchResults.getFirst(); |
70 | } | |
71 | } | |
72 | ||
73 | /** | |
74 | * Presuming the response body is HTML, search for all | |
75 | * HTML elements with the given tag name and attributes. | |
76 | * | |
77 | * @return a list of however many elements matched | |
78 | */ | |
79 | public List<HtmlParseNode> search(TagName tagName, Map<String, String> attributes) { | |
80 | var htmlParser = new HtmlParser(); | |
81 | var nodes = htmlParser.parse(body.asString()); | |
82 |
1
1. search : replaced return value with Collections.emptyList for com/renomad/minum/web/FunctionalTesting$TestResponse::search → SURVIVED |
return htmlParser.search(nodes, tagName, attributes); |
83 | } | |
84 | } | |
85 | ||
86 | /** | |
87 | * Send a GET request (as a client to the server) | |
88 | * @param path the path to an endpoint, that is, the value for path | |
89 | * that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)} | |
90 | * for pathname | |
91 | */ | |
92 | public TestResponse get(String path) { | |
93 | ArrayList<String> headers = new ArrayList<>(); | |
94 |
1
1. get : replaced return value with null for com/renomad/minum/web/FunctionalTesting::get → KILLED |
return send(RequestLine.Method.GET, path, new byte[0], headers); |
95 | } | |
96 | ||
97 | /** | |
98 | * Send a GET request (as a client to the server) | |
99 | * @param path the path to an endpoint, that is, the value for path | |
100 | * that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)} | |
101 | * for pathname | |
102 | * @param extraHeaders a list containing extra headers you need the client to send, for | |
103 | * example, <pre>{@code List.of("cookie: id=foo")}</pre> | |
104 | */ | |
105 | public TestResponse get(String path, List<String> extraHeaders) { | |
106 |
1
1. get : replaced return value with null for com/renomad/minum/web/FunctionalTesting::get → KILLED |
return send(RequestLine.Method.GET, path, new byte[0], extraHeaders); |
107 | } | |
108 | ||
109 | /** | |
110 | * Send a POST request (as a client to the server) using url encoding | |
111 | * @param path the path to an endpoint, that is, the value for path | |
112 | * that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)} | |
113 | * for pathname | |
114 | * @param payload a body payload in string form | |
115 | */ | |
116 | public TestResponse post(String path, String payload) { | |
117 | ArrayList<String> headers = new ArrayList<>(); | |
118 |
1
1. post : replaced return value with null for com/renomad/minum/web/FunctionalTesting::post → KILLED |
return post(path, payload, headers); |
119 | } | |
120 | ||
121 | /** | |
122 | * Send a POST request (as a client to the server) using url encoding | |
123 | * @param path the path to an endpoint, that is, the value for path | |
124 | * that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)} | |
125 | * for pathname | |
126 | * @param payload a body payload in string form | |
127 | * @param extraHeaders a list containing extra headers you need the client to send, for | |
128 | * example, <pre>{@code List.of("cookie: id=foo")}</pre> | |
129 | */ | |
130 | public TestResponse post(String path, String payload, List<String> extraHeaders) { | |
131 | var headers = new ArrayList<String>(); | |
132 | headers.add("Content-Type: application/x-www-form-urlencoded"); | |
133 | headers.addAll(extraHeaders); | |
134 |
1
1. post : replaced return value with null for com/renomad/minum/web/FunctionalTesting::post → KILLED |
return send(RequestLine.Method.POST, path, payload.getBytes(StandardCharsets.UTF_8), headers); |
135 | } | |
136 | ||
137 | /** | |
138 | * Send a request as a client to the server | |
139 | * <p> | |
140 | * This helper method is the same as {@link #send(RequestLine.Method, String, byte[], List)} except | |
141 | * it will set the body as empty and does not require any extra headers to be set. In this | |
142 | * case, the headers sent are very minimal. See the source. | |
143 | * </p> | |
144 | * @param path the path to an endpoint, that is, the value for path | |
145 | * that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)} | |
146 | * for pathname | |
147 | */ | |
148 | public TestResponse send(RequestLine.Method method, String path) { | |
149 |
1
1. send : replaced return value with null for com/renomad/minum/web/FunctionalTesting::send → KILLED |
return send(method, path, new byte[0], List.of()); |
150 | } | |
151 | ||
152 | /** | |
153 | * Send a request as a client to the server | |
154 | * @param path the path to an endpoint, that is, the value for path | |
155 | * that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)} | |
156 | * for pathname | |
157 | * @param payload a body payload in byte array form | |
158 | * @param extraHeaders a list containing extra headers you need the client to send, for | |
159 | * example, <pre>{@code List.of("cookie: id=foo")}</pre> | |
160 | * @return a properly-built client, or null if exceptions occur | |
161 | */ | |
162 | public TestResponse send(RequestLine.Method method, String path, byte[] payload, List<String> extraHeaders) { | |
163 | try (Socket socket = new Socket(host, port)) { | |
164 | try (ISocketWrapper client = startClient(socket)) { | |
165 | return innerClientSend(client, method, path, payload, extraHeaders); | |
166 | } | |
167 | } catch (Exception e) { | |
168 | logger.logDebug(() -> "Error during client send: " + StacktraceUtils.stackTraceToString(e)); | |
169 |
1
1. send : replaced return value with null for com/renomad/minum/web/FunctionalTesting::send → KILLED |
return TestResponse.EMPTY; |
170 | } | |
171 | } | |
172 | ||
173 | /** | |
174 | * Create a client {@link ISocketWrapper} connected to the running host server | |
175 | */ | |
176 | ISocketWrapper startClient(Socket socket) throws IOException { | |
177 | logger.logDebug(() -> String.format("Just created new client socket: %s", socket)); | |
178 |
1
1. startClient : replaced return value with null for com/renomad/minum/web/FunctionalTesting::startClient → KILLED |
return new SocketWrapper(socket, null, logger, constants.socketTimeoutMillis, constants.hostName); |
179 | } | |
180 | ||
181 | public TestResponse innerClientSend( | |
182 | ISocketWrapper client, | |
183 | RequestLine.Method method, | |
184 | String path, | |
185 | byte[] payload, | |
186 | List<String> extraHeaders) throws IOException { | |
187 | Body body = Body.EMPTY; | |
188 | ||
189 | InputStream is = client.getInputStream(); | |
190 | ||
191 |
1
1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED |
client.sendHttpLine(method + " /" + path + " HTTP/1.1"); |
192 |
1
1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → SURVIVED |
client.sendHttpLine(String.format("Host: %s:%d", host, port)); |
193 |
1
1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED |
client.sendHttpLine("Content-Length: " + payload.length); |
194 | for (String header : extraHeaders) { | |
195 |
1
1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED |
client.sendHttpLine(header); |
196 | } | |
197 |
1
1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → TIMED_OUT |
client.sendHttpLine(""); |
198 |
1
1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::send → TIMED_OUT |
client.send(payload); |
199 | ||
200 | StatusLine statusLine = StatusLine.extractStatusLine(inputStreamUtils.readLine(is)); | |
201 | List<String> allHeaders = Headers.getAllHeaders(is, inputStreamUtils); | |
202 | Headers headers = new Headers(allHeaders); | |
203 | ||
204 | ||
205 |
2
1. innerClientSend : negated conditional → TIMED_OUT 2. innerClientSend : negated conditional → KILLED |
if (WebFramework.isThereIsABody(headers) && method != RequestLine.Method.HEAD) { |
206 | logger.logTrace(() -> "There is a body. Content-type is " + headers.contentType()); | |
207 | body = bodyProcessor.extractData(is, headers); | |
208 | } | |
209 |
1
1. innerClientSend : replaced return value with null for com/renomad/minum/web/FunctionalTesting::innerClientSend → KILLED |
return new TestResponse(statusLine, headers, body); |
210 | } | |
211 | ||
212 | ||
213 | } | |
Mutations | ||
63 |
1.1 2.2 |
|
66 |
1.1 |
|
67 |
1.1 |
|
69 |
1.1 |
|
82 |
1.1 |
|
94 |
1.1 |
|
106 |
1.1 |
|
118 |
1.1 |
|
134 |
1.1 |
|
149 |
1.1 |
|
169 |
1.1 |
|
178 |
1.1 |
|
191 |
1.1 |
|
192 |
1.1 |
|
193 |
1.1 |
|
195 |
1.1 |
|
197 |
1.1 |
|
198 |
1.1 |
|
205 |
1.1 2.2 |
|
209 |
1.1 |