From b06cb7547fe238a7b5385197aae9bcf51d3890e7 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 6 Mar 2025 21:55:04 -0500 Subject: [PATCH] Added more tests. --- source/handy_http_transport/helpers.d | 13 +- source/handy_http_transport/http1/transport.d | 117 ++++++++++++++++-- 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/source/handy_http_transport/helpers.d b/source/handy_http_transport/helpers.d index a671489..6ea3f2e 100644 --- a/source/handy_http_transport/helpers.d +++ b/source/handy_http_transport/helpers.d @@ -11,7 +11,7 @@ import streams; * Returns: The string that was read, or a stream error. */ Either!(string, "value", StreamError, "error") consumeUntil(S)( - S inputStream, + ref S inputStream, string target ) if (isByteInputStream!S) { ubyte[1024] buffer; @@ -61,13 +61,22 @@ ptrdiff_t indexOf(string s, char c, size_t offset = 0) { string stripSpaces(string s) { if (s.length == 0) return s; ptrdiff_t startIdx = 0; - while (s[startIdx] == ' ' && startIdx < s.length) startIdx++; + while (startIdx < s.length && s[startIdx] == ' ') startIdx++; s = s[startIdx .. $]; + if (s.length == 0) return ""; ptrdiff_t endIdx = s.length - 1; while (s[endIdx] == ' ' && endIdx >= 0) endIdx--; return s[0 .. endIdx + 1]; } +unittest { + assert(stripSpaces("") == ""); + assert(stripSpaces(" ") == ""); + assert(stripSpaces("test") == "test"); + assert(stripSpaces(" test") == "test"); + assert(stripSpaces(" test string ") == "test string"); +} + /** * Helper function to append an unsigned integer value to a char buffer. It is * assumed that there's enough space to write the value. diff --git a/source/handy_http_transport/http1/transport.d b/source/handy_http_transport/http1/transport.d index 1e8d1d7..b6afd43 100644 --- a/source/handy_http_transport/http1/transport.d +++ b/source/handy_http_transport/http1/transport.d @@ -92,10 +92,45 @@ void handleClient(Socket clientSocket, HttpRequestHandler requestHandler) { } catch (Exception e) { import std.stdio; stderr.writeln("Exception thrown while handling request: " ~ e.msg); + } catch (Throwable t) { + import std.stdio; + stderr.writeln("Throwable error while handling request: " ~ t.msg); + throw t; } inputStream.closeStream(); } +// Test case where we use a local socket pair to test the full handleClient +// workflow from the HttpRequestHandler's point of view. +unittest { + Socket[2] sockets = socketPair(); + Socket clientSocket = sockets[0]; + Socket serverSocket = sockets[1]; + const requestContent = + "POST /data HTTP/1.1\r\n" ~ + "Content-Type: application/json\r\n" ~ + "Content-Length: 22\r\n" ~ + "\r\n" ~ + "{\"x\": 5, \"flag\": true}"; + clientSocket.send(cast(ubyte[]) requestContent); + + class TestHandler : HttpRequestHandler { + import std.conv; + + void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) { + assert(request.headers["Content-Type"] == ["application/json"]); + assert("Content-Length" in request.headers && request.headers["Content-Length"].length > 0); + ulong contentLength = request.headers["Content-Length"][0].to!ulong; + assert(contentLength == 22); + ubyte[22] bodyBuffer; + auto readResult = request.inputStream.readFromStream(bodyBuffer); + assert(readResult.hasCount && readResult.count == 22); + assert(cast(string) bodyBuffer == "{\"x\": 5, \"flag\": true}"); + } + } + handleClient(serverSocket, new TestHandler()); +} + /** * Gets a ClientAddress value from a socket's address information. * Params: @@ -171,6 +206,45 @@ HttpRequestParseResult readHttpRequest(S)(S inputStream, in ClientAddress addr) )); } +unittest { + import streams; + + auto makeStream(string text) { + return arrayInputStreamFor(cast(ubyte[]) text); + } + + // Basic HTTP request. + ArrayInputStream!ubyte s1 = makeStream( + "GET /test?x=5 HTTP/1.1\r\n" ~ + "Accept: text/plain\r\n" ~ + "\r\n" + ); + auto r1 = readHttpRequest(&s1, ClientAddress.unknown()); + assert(r1.hasRequest); + assert(r1.request.httpVersion == HttpVersion.V1); + assert(r1.request.method == HttpMethod.GET); + assert(r1.request.url == "/test?x=5"); + const r1ExpectedHeaders = ["Accept": ["text/plain"]]; + assert(r1.request.headers == r1ExpectedHeaders); + assert(r1.request.clientAddress == ClientAddress.unknown()); + + // POST request with body. Test that the body is read correctly. + ArrayInputStream!ubyte s2 = makeStream( + "POST /data HTTP/1.1\r\n" ~ + "Content-Type: text/plain\r\n" ~ + "Content-Length: 12\r\n" ~ + "\r\n" ~ + "Hello world!" + ); + auto r2 = readHttpRequest(&s2, ClientAddress.unknown()); + assert(r2.hasRequest); + assert(r2.request.method == HttpMethod.POST); + ubyte[12] r2BodyBuffer; + StreamResult r2BodyReadResult = s2.readFromStream(r2BodyBuffer); + assert(r2BodyReadResult.count == 12); + assert(cast(string) r2BodyBuffer == "Hello world!"); +} + /** * Parses HTTP headers from an input stream, and returns them as an associative * array mapping header names to their list of values. @@ -201,15 +275,36 @@ Either!(string[][string], "headers", StreamError, "error") parseHeaders(S)(S inp return Either!(string[][string], "headers", StreamError, "error")(headers); } -// unittest { -// class TestHandler : HttpRequestHandler { -// void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) { -// response.status = HttpStatus.OK; -// response.headers.add("Content-Type", "application/json"); -// response.outputStream.writeToStream(cast(ubyte[]) "{\"a\": 1}"); -// } -// } +unittest { + import streams; -// HttpTransport tp = new Http1Transport(new TestHandler(), 8080); -// tp.start(); -// } + auto makeStream(string text) { + return arrayInputStreamFor(cast(ubyte[]) text); + } + + // Basic valid headers. + auto r1 = parseHeaders(makeStream("Content-Type: application/json\r\n\r\n")); + assert(r1.hasHeaders); + assert("Content-Type" in r1.headers); + assert(r1.headers["Content-Type"] == ["application/json"]); + + // Multiple headers. + auto r2 = parseHeaders(makeStream("Accept: text, json, image\r\nContent-Length: 1234\r\n\r\n")); + assert(r2.hasHeaders); + assert("Accept" in r2.headers); + assert(r2.headers["Accept"] == ["text, json, image"]); + assert(r2.headers["Content-Length"] == ["1234"]); + + // Basic invalid header string. + auto r3 = parseHeaders(makeStream("Invalid headers")); + assert(r3.hasError); + + // No trailing \r\n + auto r4 = parseHeaders(makeStream("Content-Type: application/json")); + assert(r4.hasError); + + // Empty headers. + auto r5 = parseHeaders(makeStream("\r\n")); + assert(r5.hasHeaders); + assert(r5.headers.length == 0); +}