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
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
Location : searchOne
Killed by : com.renomad.minum.FunctionalTests
changed conditional boundary → KILLED

2.2
Location : searchOne
Killed by : com.renomad.minum.FunctionalTests
negated conditional → KILLED

66

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

67

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

69

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

82

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

94

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

106

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

118

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

134

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

149

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

169

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

178

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

191

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

192

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

193

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

195

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

197

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

198

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

205

1.1
Location : innerClientSend
Killed by : com.renomad.minum.FunctionalTests
negated conditional → KILLED

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

209

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

Active mutators

Tests examined


Report generated by PIT 1.17.0