FunctionalTesting.java

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
12
import java.io.IOException;
13
import java.io.InputStream;
14
import java.net.Socket;
15
import java.nio.charset.StandardCharsets;
16
import java.util.ArrayList;
17
import java.util.List;
18
import java.util.Map;
19
import java.util.regex.Matcher;
20
import java.util.regex.Pattern;
21
22
import static com.renomad.minum.utils.Invariants.mustBeTrue;
23
24
/**
25
 * Tools to enable system-wide integration testing
26
 */
27
public final class FunctionalTesting {
28
29
    private final String host;
30
    private final int port;
31
    private final IInputStreamUtils inputStreamUtils;
32
    private final ILogger logger;
33
    private final Constants constants;
34
    private final IBodyProcessor bodyProcessor;
35
36
    /**
37
     * Allows the user to set the host and port to target
38
     * for testing.
39
     */
40
    public FunctionalTesting(Context context, String host, int port) {
41
        this.constants = context.getConstants();
42
        this.host = host;
43
        this.port = port;
44
45
        this.inputStreamUtils = new InputStreamUtils(constants.maxReadLineSizeBytes);
46
        this.logger = context.getLogger();
47
        bodyProcessor = new BodyProcessor(context);
48
    }
49
50
    /**
51
     * A {@link Response} designed to work with {@link FunctionalTesting}
52
     */
53
    public record TestResponse(StatusLine statusLine, Headers headers, Body body) {
54
55
        public static final TestResponse EMPTY = new TestResponse(StatusLine.EMPTY, Headers.EMPTY, Body.EMPTY);
56
57
        /**
58
         * Presuming the response body is HTML, search for a single
59
         * HTML element with the given tag name and attributes.
60
         *
61
         * @return {@link HtmlParseNode#EMPTY} if none found, a particular node if found,
62
         * and an exception thrown if more than one found.
63
         */
64
        public HtmlParseNode searchOne(TagName tagName, Map<String, String> attributes) {
65
            var htmlParser = new HtmlParser();
66
            var nodes = htmlParser.parse(body.asString());
67
            var searchResults = htmlParser.search(nodes, tagName, attributes);
68 2 1. searchOne : negated conditional → KILLED
2. searchOne : changed conditional boundary → KILLED
            if (searchResults.size() > 1) {
69
                throw new InvariantException("More than 1 node found.  Here they are:" + searchResults);
70
            }
71 1 1. searchOne : negated conditional → KILLED
            if (searchResults.isEmpty()) {
72 1 1. searchOne : replaced return value with null for com/renomad/minum/web/FunctionalTesting$TestResponse::searchOne → KILLED
                return HtmlParseNode.EMPTY;
73
            } else {
74 1 1. searchOne : replaced return value with null for com/renomad/minum/web/FunctionalTesting$TestResponse::searchOne → KILLED
                return searchResults.getFirst();
75
            }
76
        }
77
78
        /**
79
         * Presuming the response body is HTML, search for all
80
         * HTML elements with the given tag name and attributes.
81
         *
82
         * @return a list of however many elements matched
83
         */
84
        public List<HtmlParseNode> search(TagName tagName, Map<String, String> attributes) {
85
            var htmlParser = new HtmlParser();
86
            var nodes = htmlParser.parse(body.asString());
87 1 1. search : replaced return value with Collections.emptyList for com/renomad/minum/web/FunctionalTesting$TestResponse::search → SURVIVED
            return htmlParser.search(nodes, tagName, attributes);
88
        }
89
    }
90
91
    /**
92
     * Send a GET request (as a client to the server)
93
     * @param path the path to an endpoint, that is, the value for path
94
     *            that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)}
95
     *             for pathname
96
     */
97
    public TestResponse get(String path) {
98
        ArrayList<String> headers = new ArrayList<>();
99 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);
100
    }
101
102
    /**
103
     * Send a GET request (as a client to the server)
104
     * @param path the path to an endpoint, that is, the value for path
105
     *            that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)}
106
     *             for pathname
107
     * @param extraHeaders a list containing extra headers you need the client to send, for
108
     *                     example, <pre>{@code List.of("cookie: id=foo")}</pre>
109
     */
110
    public TestResponse get(String path, List<String> extraHeaders) {
111 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);
112
    }
113
114
    /**
115
     * Send a POST request (as a client to the server) using url encoding
116
     * @param path the path to an endpoint, that is, the value for path
117
     *            that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)}
118
     *             for pathname
119
     * @param payload a body payload in string form
120
     */
121
    public TestResponse post(String path, String payload) {
122
        ArrayList<String> headers = new ArrayList<>();
123 1 1. post : replaced return value with null for com/renomad/minum/web/FunctionalTesting::post → KILLED
        return post(path, payload, headers);
124
    }
125
126
    /**
127
     * Send a POST request (as a client to the server) using url encoding
128
     * @param path the path to an endpoint, that is, the value for path
129
     *            that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)}
130
     *             for pathname
131
     * @param payload a body payload in string form
132
     * @param extraHeaders a list containing extra headers you need the client to send, for
133
     *                     example, <pre>{@code List.of("cookie: id=foo")}</pre>
134
     */
135
    public TestResponse post(String path, String payload, List<String> extraHeaders) {
136
        var headers = new ArrayList<String>();
137
        headers.add("Content-Type: application/x-www-form-urlencoded");
138
        headers.addAll(extraHeaders);
139 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);
140
    }
141
142
    /**
143
     * Send a request as a client to the server
144
     * <p>
145
     *     This helper method is the same as {@link #send(RequestLine.Method, String, byte[], List)} except
146
     *     it will set the body as empty and does not require any extra headers to be set. In this
147
     *     case, the headers sent are very minimal.  See the source.
148
     * </p>
149
     * @param path the path to an endpoint, that is, the value for path
150
     *            that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)}
151
     *             for pathname
152
     */
153
    public TestResponse send(RequestLine.Method method, String path) {
154 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());
155
    }
156
157
    /**
158
     * Send a request as a client to the server
159
     * @param path the path to an endpoint, that is, the value for path
160
     *            that is entered in {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)}
161
     *             for pathname
162
     * @param payload a body payload in byte array form
163
     * @param extraHeaders a list containing extra headers you need the client to send, for
164
     *                     example, <pre>{@code List.of("cookie: id=foo")}</pre>
165
     * @return a properly-built client, or null if exceptions occur
166
     */
167
    public TestResponse send(RequestLine.Method method, String path, byte[] payload, List<String> extraHeaders) {
168
        try (Socket socket = new Socket(host, port)) {
169
            logger.logDebug(() -> String.format("Just created new client socket: %s", socket));
170
            try (ISocketWrapper client = new SocketWrapper(socket, null, logger, constants.socketTimeoutMillis, constants.hostName)) {
171
                return innerClientSend(client, method, path, payload, extraHeaders);
172
            }
173
        } catch (Exception e) {
174
            logger.logDebug(() -> "Error during client send: " + StacktraceUtils.stackTraceToString(e));
175 1 1. send : replaced return value with null for com/renomad/minum/web/FunctionalTesting::send → KILLED
            return TestResponse.EMPTY;
176
        }
177
    }
178
179
    public TestResponse innerClientSend(
180
            ISocketWrapper client,
181
            RequestLine.Method method,
182
            String path,
183
            byte[] payload,
184
            List<String> extraHeaders) throws IOException {
185
        Body body = Body.EMPTY;
186
187
        InputStream is = client.getInputStream();
188
        List<String> allHeaders;
189
        StatusLine statusLine;
190 1 1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED
        client.sendHttpLine(method + " /" + path + " HTTP/1.1");
191 1 1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → TIMED_OUT
        client.sendHttpLine(String.format("Host: %s:%d", host, port));
192 1 1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED
        client.sendHttpLine("Content-Length: " + payload.length);
193
        for (String header : extraHeaders) {
194 1 1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED
            client.sendHttpLine(header);
195
        }
196 1 1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → TIMED_OUT
        client.sendHttpLine("");
197 1 1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::send → TIMED_OUT
        client.send(payload);
198 1 1. innerClientSend : removed call to com/renomad/minum/web/ISocketWrapper::flush → KILLED
        client.flush();
199
        statusLine = extractStatusLine(inputStreamUtils.readLine(is));
200
        allHeaders = Headers.getAllHeaders(is, inputStreamUtils);
201
202
        Headers headers = new Headers(allHeaders);
203 1 1. innerClientSend : negated conditional → TIMED_OUT
        boolean hasBody = !headers.contentType().isBlank();
204
205 2 1. innerClientSend : negated conditional → TIMED_OUT
2. innerClientSend : negated conditional → TIMED_OUT
        if (hasBody && !method.equals(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
     * This is the regex used to analyze a status line sent by the server and
214
     * read by the client.  Servers will send messages like: "HTTP/1.1 200 OK" or "HTTP/1.1 500 Internal Server Error"
215
     */
216
    static final String statusLinePattern = "^HTTP/(...) (\\d{3}) (.*)$";
217
    static final Pattern statusLineRegex = Pattern.compile(statusLinePattern);
218
219
    /**
220
     * Parses a string value of a status line from an HTTP
221
     * server.  If the input value is null or empty, we'll
222
     * return a {@link StatusLine} with null-object values
223
     */
224
    public static StatusLine extractStatusLine(String value) {
225 2 1. extractStatusLine : negated conditional → KILLED
2. extractStatusLine : negated conditional → KILLED
        if (value == null || value.isBlank()) {
226 1 1. extractStatusLine : replaced return value with null for com/renomad/minum/web/FunctionalTesting::extractStatusLine → TIMED_OUT
            return StatusLine.EMPTY;
227
        }
228
        Matcher mr = statusLineRegex.matcher(value);
229
        mustBeTrue(mr.matches(), String.format("%s must match the statusLinePattern: %s", value, statusLinePattern));
230
        String version = mr.group(1);
231
        HttpVersion httpVersion = switch (version) {
232
            case "1.1" -> HttpVersion.ONE_DOT_ONE;
233
            case "1.0" -> HttpVersion.ONE_DOT_ZERO;
234
            default -> throw new WebServerException(String.format("HTTP version was not an acceptable value. Given: %s", version));
235
        };
236
        StatusLine.StatusCode status = StatusLine.StatusCode.findByCode(Integer.parseInt(mr.group(2)));
237
238 1 1. extractStatusLine : replaced return value with null for com/renomad/minum/web/FunctionalTesting::extractStatusLine → KILLED
        return new StatusLine(status, httpVersion, value);
239
    }
240
241
}

Mutations

68

1.1
Location : searchOne
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

2.2
Location : searchOne
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

71

1.1
Location : searchOne
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

72

1.1
Location : searchOne
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting$TestResponse::searchOne → KILLED

74

1.1
Location : searchOne
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting$TestResponse::searchOne → KILLED

87

1.1
Location : search
Killed by : none
replaced return value with Collections.emptyList for com/renomad/minum/web/FunctionalTesting$TestResponse::search → SURVIVED
Covering tests

99

1.1
Location : get
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::get → KILLED

111

1.1
Location : get
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::get → KILLED

123

1.1
Location : post
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::post → KILLED

139

1.1
Location : post
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::post → KILLED

154

1.1
Location : send
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::send → KILLED

175

1.1
Location : send
Killed by : com.renomad.minum.web.FunctionalTestingTests.test_sendDealsWithException(com.renomad.minum.web.FunctionalTestingTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::send → KILLED

190

1.1
Location : innerClientSend
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED

191

1.1
Location : innerClientSend
Killed by : none
removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → TIMED_OUT

192

1.1
Location : innerClientSend
Killed by : com.renomad.minum.web.WebPerformanceTests.test2(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED

194

1.1
Location : innerClientSend
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → KILLED

196

1.1
Location : innerClientSend
Killed by : none
removed call to com/renomad/minum/web/ISocketWrapper::sendHttpLine → TIMED_OUT

197

1.1
Location : innerClientSend
Killed by : none
removed call to com/renomad/minum/web/ISocketWrapper::send → TIMED_OUT

198

1.1
Location : innerClientSend
Killed by : com.renomad.minum.web.WebPerformanceTests.test2(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/ISocketWrapper::flush → KILLED

203

1.1
Location : innerClientSend
Killed by : none
negated conditional → TIMED_OUT

205

1.1
Location : innerClientSend
Killed by : none
negated conditional → TIMED_OUT

2.2
Location : innerClientSend
Killed by : none
negated conditional → TIMED_OUT

209

1.1
Location : innerClientSend
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::innerClientSend → KILLED

225

1.1
Location : extractStatusLine
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

2.2
Location : extractStatusLine
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

226

1.1
Location : extractStatusLine
Killed by : none
replaced return value with null for com/renomad/minum/web/FunctionalTesting::extractStatusLine → TIMED_OUT

238

1.1
Location : extractStatusLine
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/FunctionalTesting::extractStatusLine → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0