From bb1d04cfa366411c73660a8430d511ca71b8b011 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Tue, 1 Jul 2025 22:08:44 -0400 Subject: [PATCH] Added testing module, more docs, and response writing methods. --- source/handy_http_primitives/handler.d | 4 ++ source/handy_http_primitives/package.d | 6 ++ source/handy_http_primitives/request.d | 4 ++ source/handy_http_primitives/response.d | 49 ++++++++++++- source/handy_http_primitives/testing.d | 95 +++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 source/handy_http_primitives/testing.d diff --git a/source/handy_http_primitives/handler.d b/source/handy_http_primitives/handler.d index 50cd5f3..98ae5d9 100644 --- a/source/handy_http_primitives/handler.d +++ b/source/handy_http_primitives/handler.d @@ -1,3 +1,7 @@ +/** + * Defines the core request handler interface that's the starting point for + * all HTTP request processing. + */ module handy_http_primitives.handler; import handy_http_primitives.request; diff --git a/source/handy_http_primitives/package.d b/source/handy_http_primitives/package.d index 10b28fe..3bba66d 100644 --- a/source/handy_http_primitives/package.d +++ b/source/handy_http_primitives/package.d @@ -1,3 +1,8 @@ +/** + * The handy_http_primitives module defines a set of primitive types and + * interfaces that are shared among all Handy-Http libraries, and form the + * basis of how requests are handled. + */ module handy_http_primitives; public import handy_http_primitives.request; @@ -6,3 +11,4 @@ public import handy_http_primitives.handler; public import handy_http_primitives.optional; public import handy_http_primitives.multivalue_map; public import handy_http_primitives.builder; +public import handy_http_primitives.testing; diff --git a/source/handy_http_primitives/request.d b/source/handy_http_primitives/request.d index 498e1c9..4306dc4 100644 --- a/source/handy_http_primitives/request.d +++ b/source/handy_http_primitives/request.d @@ -1,3 +1,7 @@ +/** + * Defines the request structure and associated types that are generally used + * when dealing with a client's HTTP request. + */ module handy_http_primitives.request; import streams; diff --git a/source/handy_http_primitives/response.d b/source/handy_http_primitives/response.d index 7396c17..540c018 100644 --- a/source/handy_http_primitives/response.d +++ b/source/handy_http_primitives/response.d @@ -1,3 +1,7 @@ +/** + * Defines the HTTP response structure and associated types that are generally + * used when formulating a response to a client's request. + */ module handy_http_primitives.response; import streams : OutputStream; @@ -15,6 +19,40 @@ struct ServerHttpResponse { StringMultiValueMap headers; /// The stream to which the response body is written. OutputStream!ubyte outputStream; + + /** + * Writes an array of bytes to the response's output stream. + * Params: + * bytes = The bytes to write. + * contentType = The declared content type of the data, which is written + * as the "Content-Type" header. + */ + void writeBodyBytes(ubyte[] bytes, string contentType = ContentTypes.APPLICATION_OCTET_STREAM) { + import std.conv : to; + headers.add("Content-Type", contentType); + headers.add("Content-Length", to!string(bytes.length)); + // We trust that when we write to the output stream, the transport + // implementation will handle properly formatting the headers and other + // HTTP boilerplate response content prior to actually writing the body. + auto result = outputStream.writeToStream(bytes); + if (result.hasError) { + throw new Exception( + "Failed to write bytes to the response's output stream: " ~ + cast(string) result.error.message + ); + } + } + + /** + * Writes a string of content to the response's output stream. + * Params: + * content = The content to write. + * contentType = The declared content type of the data, which is written + * as the "Content-Type" header. + */ + void writeBodyString(string content, string contentType = ContentTypes.TEXT_PLAIN) { + writeBodyBytes(cast(ubyte[]) content, contentType); + } } /** @@ -106,10 +144,19 @@ enum HttpStatus : StatusInfo { enum ContentTypes : string { APPLICATION_JSON = "application/json", APPLICATION_XML = "application/xml", + APPLICATION_OCTET_STREAM = "application/octet-stream", + APPLICATION_PDF = "application/pdf", TEXT_PLAIN = "text/plain", TEXT_HTML = "text/html", - TEXT_CSS = "text/css" + TEXT_CSS = "text/css", + TEXT_CSV = "text/csv", + TEXT_JAVASCRIPT = "text/javascript", + TEXT_MARKDOWN = "text/markdown", + + IMAGE_JPEG = "image/jpeg", + IMAGE_PNG = "image/png", + IMAGE_SVG = "image/svg+xml" } /** diff --git a/source/handy_http_primitives/testing.d b/source/handy_http_primitives/testing.d new file mode 100644 index 0000000..671434b --- /dev/null +++ b/source/handy_http_primitives/testing.d @@ -0,0 +1,95 @@ +/** + * The testing module defines helper methods for testing your HTTP handling + * code. + */ +module handy_http_primitives.testing; + +import handy_http_primitives.response; + +/** + * Asserts that the given response's status matches an expected status. + * Params: + * response = The response to check. + * expectedStatus = The expected status that the response should have. + */ +void assertStatus(in ServerHttpResponse response, in StatusInfo expectedStatus) { + import std.format : format; + assert( + expectedStatus == response.status, + format!"The HTTP response's status of %d (%s) didn't match the expected status %d (%s)."( + response.status.code, + response.status.text, + expectedStatus.code, + expectedStatus.text + ) + ); +} + +unittest { + import handy_http_primitives.builder; + ServerHttpResponseBuilder() + .withStatus(HttpStatus.OK) + .build() + .assertStatus(HttpStatus.OK); +} + +// Some common status assertions: + +void assertStatusOk(in ServerHttpResponse response) { + assertStatus(response, HttpStatus.OK); +} + +void assertStatusNotFound(in ServerHttpResponse response) { + assertStatus(response, HttpStatus.NOT_FOUND); +} + +void assertStatusBadRequest(in ServerHttpResponse response) { + assertStatus(response, HttpStatus.BAD_REQUEST); +} + +void assertStatusUnauthorized(in ServerHttpResponse response) { + assertStatus(response, HttpStatus.UNAUTHORIZED); +} + +void assertStatusForbidden(in ServerHttpResponse response) { + assertStatus(response, HttpStatus.FORBIDDEN); +} + +void assertStatusInternalServerError(in ServerHttpResponse response) { + assertStatus(response, HttpStatus.FORBIDDEN); +} + +/** + * Asserts that the given response has a header with a given value. + * Params: + * response = The response to check. + * header = The name of the header to check the value of. + * expectedValue = The expected value of the header. + */ +void assertHasHeader(in ServerHttpResponse response, string header, string expectedValue) { + import std.format : format; + assert( + response.headers.contains(header), + format!"The HTTP response doesn't have a header named \"%s\"."(header) + ); + string value = response.headers.getFirst(header).orElseThrow(); + assert( + value == expectedValue, + format!"The HTTP response's header \"%s\" with value \"%s\" didn't match the expected value \"%s\"."( + header, + value, + expectedValue + ) + ); +} + +unittest { + import streams; + import handy_http_primitives.builder; + ArrayOutputStream!ubyte bufferOut = byteArrayOutputStream(); + ServerHttpResponse r1 = ServerHttpResponseBuilder() + .withOutputStream(&bufferOut) + .build(); + r1.writeBodyString("Hello, world!"); + r1.assertHasHeader("Content-Type", ContentTypes.TEXT_PLAIN); +}