Added (first version of) sample Hummingbird app. (#4)

This PR contains the work done to:
* Implemented a basic `Hummingbird` application in which to integrate the `HummingbirdDocC` library.
* Added the *ArgumentParser* package dependency to the `Package.swift` file;
* Added a new *sample* target to the `Package.swift` file;
* Added library and documentation tasks to the `Makefile` file.

Reviewed-on: #4
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #4.
This commit is contained in:
2025-09-30 15:38:12 +00:00
committed by Javier Cicchelli
parent 3a9e3d176f
commit 1382f33ae6
49 changed files with 1095 additions and 488 deletions
@@ -0,0 +1,77 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
@testable import enum HummingbirdDocC.AssetFile
@Suite("Asset File", .tags(.enumeration))
struct AssetFileTests {
// MARK: Properties tests
#if swift(>=6.2)
@Test(arguments: zip(
AssetFile.allCases,
Output.assetFilePaths
))
func `path`(
`case`: AssetFile,
expects result: String
) {
assertPath(`case`, expects: result)
}
#else
@Test("path", arguments: zip(
AssetFile.allCases,
Output.assetFilePaths
))
func path(
`case`: AssetFile,
expects result: String
) {
assertPath(`case`, expects: result)
}
#endif
}
// MARK: - Assertions
private extension AssetFileTests {
// MARK: Functions
/// Asserts the path property based on a given ``AssetFile`` enumeration case and an expected result.
/// - Parameters:
/// - case: A representation of the ``AssetFile`` enumeration
/// - result: An expected result coming out of the property.
func assertPath(
_ case: AssetFile,
expects result: String
) {
// GIVEN
// WHEN
let output = `case`.path
// THEN
#expect(output == result)
}
}
// MARK: - Constants
private extension Output {
/// A list of expected outputs for the paths of the ``AssetFile`` enumeration cases.
static let assetFilePaths: [String] = ["/data/documentation.json", "/favicon.ico", "/favicon.svg", "/theme-settings.json"]
}
@@ -0,0 +1,77 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
@testable import enum HummingbirdDocC.AssetFolder
@Suite("Asset Folder", .tags(.enumeration))
struct AssetFolderTests {
// MARK: Properties tests
#if swift(>=6.2)
@Test(arguments: zip(
AssetFolder.allCases,
Output.assetFolderPaths
))
func `path`(
`case`: AssetFolder,
expects result: String
) {
assertPath(`case`, expects: result)
}
#else
@Test("path", arguments: zip(
AssetFolder.allCases,
Output.assetFolderPaths
))
func path(
`case`: AssetFolder,
expects result: String
) {
assertPath(`case`, expects: result)
}
#endif
}
// MARK: - Assertions
private extension AssetFolderTests {
// MARK: Functions
/// Asserts the path property based on a given ``AssetFolder`` enumeration case and an expected result.
/// - Parameters:
/// - case: A representation of the ``AssetFolder`` enumeration
/// - result: An expected result coming out of the property.
func assertPath(
_ case: AssetFolder,
expects result: String
) {
// GIVEN
// WHEN
let output = `case`.path
// THEN
#expect(output == result)
}
}
// MARK: - Constants
private extension Output {
/// A list of expected outputs for the paths of the ``AssetFolder`` enumeration cases.
static let assetFolderPaths: [String] = ["/css/", "/data/", "/downloads/", "/images/", "/img/", "/index/", "/js/", "/videos/"]
}
@@ -0,0 +1,77 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
@testable import enum HummingbirdDocC.DocumentationFolder
@Suite("Documentation Type", .tags(.enumeration))
struct DocumentationTypeTests {
// MARK: Properties tests
#if swift(>=6.2)
@Test(arguments: zip(
DocumentationFolder.allCases,
Output.documentationFolderPaths
))
func `path`(
`case`: DocumentationFolder,
expects result: String
) {
assertPath(`case`, expects: result)
}
#else
@Test("path", arguments: zip(
DocumentationType.allCases,
Output.documentationTypePaths
))
func path(
`case`: DocumentationType,
expects result: String
) {
assertPath(`case`, expects: result)
}
#endif
}
// MARK: - Assertions
private extension DocumentationTypeTests {
// MARK: Functions
/// Asserts the path property based on a given ``DocumentationFolder`` enumeration case and an expected result.
/// - Parameters:
/// - case: A representation of the ``DocumentationFolder`` enumeration
/// - result: An expected result coming out of the property.
func assertPath(
_ case: DocumentationFolder,
expects result: String
) {
// GIVEN
// WHEN
let output = `case`.path
// THEN
#expect(output == result)
}
}
// MARK: - Constants
private extension Output {
/// A list of expected outputs for the paths of the ``DocumentationFolder`` enumeration cases.
static let documentationFolderPaths: [String] = ["/documentation", "/tutorials"]
}
@@ -0,0 +1,128 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
import struct Hummingbird.HTTPRequest
import struct Hummingbird.HTTPResponse
import struct Hummingbird.Request
import struct Logging.Logger
@testable import HummingbirdDocC
@Suite("Logger Metadata Helpers", .tags(.extension))
struct LoggerMetadata_HelpersTests {
// MARK: Functions tests
#if swift(>=6.2)
@Test
func `metadata with HTTP method and status code`() throws {
assertMetadata(
method: try randomMethod,
statusCode: try randomStatusCode
)
}
@Test
func `metadata with HTTP method, status code and redirection URI path`() throws {
assertMetadata(
method: try randomMethod,
statusCode: try randomStatusCode,
redirect: .Sample.uriRedirection
)
}
#else
@Test("metadata with HTTP method and status code")
func metadata_withMethod_andStatusCode() throws {
assertMetadata(
method: try randomMethod,
statusCode: try randomStatusCode
)
}
@Test("metadata with HTTP method, status code and redirection URI path")
func metadata_withMethod_statusCode_andRedirection() throws {
assertMetadata(
method: try randomMethod,
statusCode: try randomStatusCode,
redirect: .uriRedirection
)
}
#endif
}
// MARK: - Assertions
private extension LoggerMetadata_HelpersTests {
// MARK: Functions
/// Asserts the generated metadata dictionary based on provided parameters.
/// - Parameters:
/// - method: A HTTP method of the request.
/// - statusCode: A status code of the response.
/// - redirect: A redirection URI path, if any.
func assertMetadata(
method: HTTPRequest.Method,
statusCode: HTTPResponse.Status,
redirect: String? = nil
) {
// GIVEN
let logger: Logger = .test()
let context: RequestContextMock = .init(logger: logger)
let request: Request = .test(method: method)
// WHEN
let metadata: Logger.Metadata = .metadata(
context: context,
request: request,
statusCode: statusCode,
redirect: redirect
)
// THEN
#expect(metadata.keys.count == (redirect == nil ? 4 : 5))
#expect(metadata["hb.request.id"] == logger[metadataKey: "hb.request.id"])
#expect(metadata["hb.request.method"] == "\(method.rawValue)")
#expect(metadata["hb.request.path"] == "/")
#expect(metadata["hb.request.status"] == "\(statusCode.code)")
if let redirect {
#expect(metadata["hb.request.redirect"] == "\(redirect)")
}
}
}
// MARK: - Helpers
private extension LoggerMetadata_HelpersTests {
// MARK: Computed
/// Extracts a random HTTP method of the request from a list of pre-defined values.
var randomMethod: HTTPRequest.Method {
get throws {
try #require([.connect, .delete, .get, .head, .options, .patch, .post, .put, .trace].randomElement())
}
}
/// Extracts a random status code of the response from a list of pre-defined values.
var randomStatusCode: HTTPResponse.Status {
get throws {
try #require([.`continue`, .earlyHints, .ok, .accepted, .multipleChoices, .seeOther, .badRequest, .notFound, .internalServerError, .serviceUnavailable].randomElement())
}
}
}
@@ -0,0 +1,97 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
@testable import HummingbirdDocC
@Suite("String Helpers", .tags(.extension))
struct String_HelpersTests {
// MARK: Functions tests
#if swift(>=6.2)
@Test(arguments: zip(
Input.prefixesToSubtract,
Output.prefixesToSubtract
))
func `subtract`(
prefix: String,
expects newString: String?
) {
assertSubtract(
string: .sample,
prefix: prefix,
expects: newString
)
}
#else
@Test("subtract", arguments: zip(
Input.prefixesToSubtract,
Output.prefixesToSubtract
))
func subtract(
prefix: String,
expects newString: String?
) {
assertSubtract(
string: .sample,
prefix: prefix,
expects: newString
)
}
#endif
}
// MARK: - Assertions
private extension String_HelpersTests {
// MARK: Functions
/// Asserts a string subtraction.
/// - Parameters:
/// - string: A string from where the subtraction will occur.
/// - prefix: A prefix to subtract from a string.
/// - newString: An expected new string created out of the subtraction, if any.
func assertSubtract(
string: String,
prefix: String,
expects newString: String?
) {
// GIVEN
// WHEN
let result = string.subtract(prefix)
// THEN
#expect(result == newString)
}
}
// MARK: - Constants
private extension Input {
/// A list of prefix strings.
static let prefixesToSubtract: [String] = ["Some", .sample, "some", "Else"]
}
private extension Output {
/// A list of outcomes that are expected from subtracting the prefix substrings out of the sample string.
static let prefixesToSubtract: [String?] = ["thing", .empty, nil, nil]
}
private extension String {
/// A sample string.
static let sample = "Something"
}
@@ -0,0 +1,106 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
@testable import struct HummingbirdDocC.Resource
@Suite("Resource", .tags(.model))
struct ResourceTests {
// MARK: Properties tests
#if swift(>=6.2)
@Test
func `archive path`() {
assertArchivePath(
archiveName: "SomeDocument",
expects: "/SomeDocument.doccarchive"
)
}
@Test
func `archive reference`() {
assertArchiveReference(
archiveName: "SomeDocument",
expects: "somedocument"
)
}
#else
@Test("archive path")
func archivePath() {
assertArchivePath(
archiveName: "SomeDocument",
expects: "/SomeDocument.doccarchive"
)
}
@Test("archive reference")
func archiveReference() {
assertArchiveReference(
archiveName: "SomeDocument",
expects: "somedocument"
)
}
#endif
}
// MARK: - Assertions
private extension ResourceTests {
// MARK: Functions
/// Asserts the `archivePath` computed property of a resource.
/// - Parameters:
/// - archiveName: A name of the archive the resource belongs to.
/// - archivePath: An expected path to a documentation archive related to a given archive name.
func assertArchivePath(
archiveName: String,
expects archivePath: String
) {
// GIVEN
var resource = Resource(
archiveName: archiveName,
relativePath: .empty
)
// WHEN
let result = resource.archivePath
// THEN
#expect(result == archivePath)
}
/// Asserts the `archiveReference` computed property of a resource.
/// - Parameters:
/// - archiveName: A name of the archive the resource belongs to.
/// - archiveReference: An expected reference related to a given archive name.
func assertArchiveReference(
archiveName: String,
expects archiveReference: String
) {
// GIVEN
var resource = Resource(
archiveName: archiveName,
relativePath: .empty
)
// WHEN
let result = resource.archiveReference
// THEN
#expect(result == archiveReference)
}
}
@@ -0,0 +1,147 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
import struct HummingbirdCore.URI
@testable import struct HummingbirdDocC.CheckURIUseCase
@Suite("Check URI use case", .tags(.useCase))
struct CheckURIUseCaseTests {
// MARK: Use case tests
#if swift(>=6.2)
@Test(
arguments: zip(
Input.nonEncodedURIs,
Output.nonEncodedURIs
)
)
func `check non encoded URIs`(
uri uriPath: String,
expects result: String?
) {
assertURI(uriPath, expects: result)
}
@Test(
arguments: zip(
Input.percentEncodedURIs,
Output.percentEncodedURIs
)
)
func `check percent-encoded URIs`(
uri uriPath: String,
expects result: String?
) {
assertURI(uriPath, expects: result)
}
#else
@Test(
"check non-encoded URIs",
arguments: zip(
Input.nonEncodedURIs,
Output.nonEncodedURIs
)
)
func check_nonEncodedURIs(
uri uriPath: String,
expects result: String?
) {
assertURI(uriPath, expects: result)
}
@Test(
"check percent-encoded URIs",
arguments: zip(
Input.percentEncodedURIs,
Output.percentEncodedURIs
)
)
func check_percentEncodedURIs(
uri uriPath: String,
expects result: String?
) {
assertURI(uriPath, expects: result)
}
#endif
}
// MARK: - Assertions
extension CheckURIUseCaseTests {
// MARK: Functions
/// Asserts a URI path provided by the ``CheckURIPathUseCase`` use case based on a given path and an expected result.
/// - Parameters:
/// - uriPath: A URI path to use with a URI type.
/// - uriRoot: A URI path that prefixes the `DocC` documentation resources.
/// - result: An expected result coming out of the use case.
fileprivate func assertURI(
_ uriPath: String,
uriRoot: String = .Sample.uriRoot,
expects result: String?
) {
// GIVEN
let useCase = CheckURIUseCase(uriRoot: uriRoot)
let uri = URI(uriPath)
// WHEN
let output = useCase(uri)
// THEN
#expect(output == result)
}
}
// MARK: - Constants
extension Input {
/// A list of non-encoded URI samples.
fileprivate static let nonEncodedURIs: [String] = [
.Sample.uriRoot + .empty,
.Sample.uriRoot + .Path.forwardSlash,
.Sample.uriRoot + "/some/known/path",
.Sample.uriRoot + "/some/../path",
"some/other/root/some/known/path",
]
/// A list of percent-encoded URI samples.
fileprivate static let percentEncodedURIs: [String] = [
.Sample.uriRoot + "%2F",
.Sample.uriRoot + "/some%2Fknown%3Fpath",
.Sample.uriRoot + "/some/%2E%2E/path",
"some/other%2Froot/some%2Fknown%3Fpath",
]
}
extension Output {
/// A list of expected outputs for the non-encoded URI samples.
fileprivate static let nonEncodedURIs: [String?] = [
.Sample.uriRoot,
.Sample.uriRoot + .Path.forwardSlash,
.Sample.uriRoot + "/some/known/path",
nil,
nil,
]
/// A list of expected outputs for the percent-encoded URI samples.
fileprivate static let percentEncodedURIs: [String?] = [
.Sample.uriRoot + .Path.forwardSlash,
.Sample.uriRoot + "/some/known?path",
nil,
nil,
]
}
@@ -0,0 +1,146 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
import struct HummingbirdDocC.Resource
@testable import struct HummingbirdDocC.PrepareURIPathUseCase
@Suite("Prepare URI Path Use Case", .tags(.useCase))
struct PrepareURIPathUseCaseTests {
// MARK: Use case tests
#if swift(>=6.2)
@Test(arguments: zip(
Input.prepareURIPaths,
Output.prepareURIPaths
))
func `extract data with URI root not suffixed with forward slash`(
uri uriPath: String,
expects result: Resource?
) throws {
try assertData(
uriRoot: .uriRoot,
uriPath: uriPath,
expects: result
)
}
@Test(arguments: zip(
Input.prepareURIPathsSlashed,
Output.prepareURIPaths
))
func `extract data with URI root suffixed with forward slash`(
uri uriPath: String,
expects result: Resource?
) throws {
try assertData(
uriRoot: .uriRootSlashed,
uriPath: uriPath,
expects: result
)
}
#else
@Test("extract data with URI root not suffixed with forward slash", arguments: zip(
Input.prepareURIPaths,
Output.prepareURIPaths
))
func data_withURIRoot_notSuffixed_withForwardSlash(
uri uriPath: String,
expects result: Resource?
) throws {
try assertData(
uriRoot: .uriRoot,
uriPath: uriPath,
expects: result
)
}
@Test("extract data with URI root suffixed with forward slash", arguments: zip(
Input.prepareURIPathsSlashed,
Output.prepareURIPaths
))
func data_withURIRoot_suffixed_withForwardSlash(
uri uriPath: String,
expects result: Resource?
) throws {
try assertData(
uriRoot: .uriRootSlashed,
uriPath: uriPath,
expects: result
)
}
#endif
}
// MARK: - Assertions
private extension PrepareURIPathUseCaseTests {
// MARK: Functions
/// Asserts the data returned by the ``PrepareURIPathUseCase`` use case based on the given `uriRoot` and `uriPath` URI paths plus
/// an expected result.
/// - Parameters:
/// - uriRoot: A URI path to initialize the use case with.
/// - uriPath: A URI path to use with the use case.
/// - resource: An expected resource coming out of the use case, if any.
func assertData(
uriRoot: String,
uriPath: String,
expects resource: Resource?
) throws {
// GIVEN
let useCase = PrepareURIPathUseCase(uriRoot: uriRoot)
// WHEN
let result = useCase(uriPath)
// THEN
#expect(result == resource)
}
}
// MARK: - Constants
private extension Input {
/// A list of URI paths to match against the root URI path not suffixed with a forward slash.
static let prepareURIPaths: [String] = [.uriRoot, .uriOffset, .uriOther]
/// A list of URI paths to match against the root URI path suffixed with a forward slash.
static let prepareURIPathsSlashed: [String] = [.uriRootSlashed, .uriOffsetSlashed, .uriOther]
}
private extension Output {
/// A list of expected outputs for the URI path samples, regardless their match against suffixed or not suffixed root URI paths.
static let prepareURIPaths: [Resource?] = [
.init(archiveName: .empty, relativePath: .Path.forwardSlash),
.init(archiveName: "SomeArchive", relativePath: "/some/content/path"),
nil
]
}
private extension String {
/// A root URI path to initialize the use case with.
static let uriRoot = "/some/path"
/// A root URI path suffixed with a forward slash to initialize the use case with.
static let uriRootSlashed = "/some/path/"
/// A URI path prefixed with a root URI path not suffixed with a forward slash.
static let uriOffset = .uriRoot + "/SomeArchive/some/content/path"
/// A URI path prefixed with a root URI path suffixed with a forward slash.
static let uriOffsetSlashed = .uriRootSlashed + "SomeArchive/some/content/path"
/// A URI path not related to any root URI path.
static let uriOther = "/some/other/path"
}
@@ -0,0 +1,150 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
import protocol Hummingbird.RequestContext
import struct Hummingbird.HTTPResponse
import struct Hummingbird.Request
import struct Logging.Logger
@testable import struct HummingbirdDocC.RedirectURIUseCase
@Suite("Redirect URI Use Case", .tags(.useCase))
struct RedirectURIUseCaseTests {
// MARK: Use case tests
#if swift(>=6.2)
@Test
func `response when logging event triggered`() async throws {
try await assertResponse(
logLevel: try .random(upTo: .debug),
uriRedirection: .Sample.uriRedirection,
expects: .movedPermanently
)
}
@Test
func `response when logging event not triggered`() async throws {
try await assertResponse(
logLevel: try .random(fromExclusive: .debug),
uriRedirection: .Sample.uriRedirection,
expects: .movedPermanently
)
}
#else
@Test("response when logging event triggered")
func response_whenEventTriggered() async throws {
try await assertResponse(
logLevel: try .random(upTo: .debug),
uriRedirection: .uriRedirection,
expects: .movedPermanently
)
}
@Test("response when logging event not triggered")
func response_whenEventNotTriggered() async throws {
try await assertResponse(
logLevel: try randomLogLevelWithNoEvent,
uriRedirection: .uriRedirection,
expects: .movedPermanently
)
}
#endif
}
// MARK: - Assertions
private extension RedirectURIUseCaseTests {
// MARK: Functions
/// Asserts a response returned by the ``RedirectURIUseCase`` use case.
/// - Parameters:
/// - logLevel: A representation of the logging level to set in the `Logger` instance.
/// - uriRedirection: A URI path to use in the redirection.
/// - statusCode: An expected status code from the response coming out of the use case.
/// - Throws: An error in case an issue is encountered while asserting the use case.
func assertResponse(
logLevel: Logger.Level,
uriRedirection: String,
expects statusCode: HTTPResponse.Status
) async throws {
let logHandler = LogHandlerMock()
let logger = Logger.test(
level: logLevel,
handler: logHandler
)
let context: any RequestContext = RequestContextMock(logger: logger)
let request: Request = .test(method: .get)
let useCase = RedirectURIUseCase(logger: logger)
// WHEN
let result = useCase(
uriRedirection,
with: (request, context)
)
// THEN
#expect(result.status == .movedPermanently)
#expect(result.body.contentLength == 0)
#expect(result.headers == [
.location: uriRedirection,
.contentLength: "0"
])
let events = logHandler.entries
if shouldEventBeLogged(logLevel) {
#expect(!events.isEmpty)
#expect(events.count == 1)
let loggedEvent = try #require(events.first)
#expect(loggedEvent == .init(
level: .debug,
metadata: [
"hb.request.id": "\(context.id)",
"hb.request.method": "\(request.method.rawValue)",
"hb.request.path": "\(request.uri.path)",
"hb.request.status": "\(statusCode.code)",
"hb.request.redirect": "\(uriRedirection)"
],
message: "The URI path is redirected to this path: \(uriRedirection)",
source: .Logging.source
))
} else {
#expect(events.isEmpty)
}
}
}
// MARK: - Helpers
private extension RedirectURIUseCaseTests {
// MARK: Functions
/// Checks whether a logging event should be logged or not.
/// - Parameter logLevel: A representation of a logging level defined in the logger.
/// - Returns: A boolean value that indicates whether a logging event should have been logged or not.
func shouldEventBeLogged(_ logLevel: Logger.Level) -> Bool {
[Logger.Level.debug, .trace].contains(logLevel)
}
}
@@ -0,0 +1,266 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
import protocol Hummingbird.RequestContext
import struct Hummingbird.HTTPResponse
import struct Hummingbird.Request
import struct Logging.Logger
@testable import struct HummingbirdDocC.ServeURIUseCase
@Suite("Serve URI Use Case", .tags(.useCase))
struct ServeURIUseCaseTests {
// MARK: Use case tests
#if swift(>=6.2)
@Test
func `response when resource served and logging event triggered`() async throws {
try await assertResponse(
logLevel: try .random(upTo: .debug),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .ok
)
}
@Test
func `response when resource served and logging event not triggered`() async throws {
try await assertResponse(
logLevel: try .random(fromExclusive: .debug),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .ok
)
}
@Test
func `response when resource not found and logging event triggered`() async throws {
try await assertResponse(
logLevel: try .random(upTo: .error),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .notFound
)
}
@Test
func `response when resource not found and logging event not triggered`() async throws {
try await assertResponse(
logLevel: try .random(fromExclusive: .error),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .notFound
)
}
@Test
func `response throws error when loading resource`() async throws {
try await assertResponse(
logLevel: try .random(),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder
)
}
#else
@Test("response when resource served and logging event triggered")
func response_whenResourceServed_andEventTriggered() async throws {
try await assertResponse(
logLevel: try .random(upTo: .debug),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .ok
)
}
@Test("response when resource served and logging event not triggered")
func response_whenResourceServed_andEventNotTriggered() async throws {
try await assertResponse(
logLevel: try .random(fromExclusive: .debug),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .ok
)
}
@Test("response when resource not found and logging event triggered")
func resource_whenResourceNotFound_andEventTriggered() async throws {
try await assertResponse(
logLevel: try .random(upTo: .error),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .notFound
)
}
@Test("response when resource not found and logging event not triggered")
func resource_whenResourceNotFound_andEventNotTriggered() async throws {
try await assertResponse(
logLevel: try .random(fromExclusive: .error),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .notFound
)
}
@Test("response throws error when loading resource")
func resource_throwsError_whenLoadingResource() async throws {
try await assertResponse(
logLevel: try .random(),
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder
)
}
#endif
}
// MARK: - Assertions
private extension ServeURIUseCaseTests {
// MARK: Functions
/// Asserts a response returned by the ``ServeURIUseCase`` use case.
///
/// > important: In case no `statusCode` value is given, the function then assumes that the loading of a file will throw an error.
///
/// - Parameters:
/// - logLevel: A representation of the logging level to set in the `Logger` instance.
/// - uriPath: A URI path to a resource.
/// - folderPath: A URI path to a folder that contains the resource.
/// - statusCode: An expected status code from the response coming out of the use case, if any.
/// - Throws: An error in case an issue is encountered while asserting the use case.
func assertResponse(
logLevel: Logger.Level,
uriPath: String,
folderPath: String,
expects statusCode: HTTPResponse.Status? = nil
) async throws {
// GIVEN
let logHandler = LogHandlerMock()
let logger = Logger.test(
level: logLevel,
handler: logHandler
)
let fileProvider: FileProviderMock = switch statusCode {
case .ok: .init(fileIdentifier: .init())
case .notFound: .init()
default: .init(fileIdentifier: .init(), shouldLoadFile: false)
}
let context: any RequestContext = RequestContextMock(logger: logger)
let request: Request = .test(method: .get)
let useCase = ServeURIUseCase(
fileProvider: fileProvider,
logger: logger
)
// WHEN
// THEN
if let statusCode {
let result = try await useCase(
uriPath,
at: folderPath,
with: (request, context)
)
#expect(result.headers[.contentLength] == (statusCode == .ok ? "36" : "0"))
#expect(result.status == statusCode)
let contentLength = try #require(result.body.contentLength)
if statusCode == .ok {
#expect(contentLength > 0)
} else {
#expect(contentLength == 0)
}
let events = logHandler.entries
if shouldEventBeLogged(
logLevel: logLevel,
statusCode: statusCode
) {
#expect(!events.isEmpty)
#expect(events.count == 1)
let loggedEvent = try #require(events.first)
let filePath: String = .Sample.uriFile
#expect(loggedEvent == .init(
level: statusCode == .ok ? .debug : .error,
metadata: [
"hb.request.id": "\(context.id)",
"hb.request.method": "\(request.method.rawValue)",
"hb.request.path": "\(request.uri.path)",
"hb.request.status": "\(statusCode.code)"
],
message: {
if statusCode == .ok {
"The body of the resource \(filePath) has \(contentLength) bytes."
} else {
"The resource \(filePath) has not been found."
}
}(),
source: .Logging.source
))
} else {
#expect(events.isEmpty)
}
} else {
do {
_ = try await useCase(
uriPath,
at: folderPath,
with: (request, context)
)
} catch is FileProviderMockError {
#expect(true)
} catch {
#expect(true == false)
}
}
}
}
// MARK: - Helpers
private extension ServeURIUseCaseTests {
// MARK: Functions
/// Checks whether a logging event should be logged or not, based on a given logging level.
/// - Parameters:
/// - logLevel: A representation of a logging level defined in in the logger.
/// - statusCode: A representation of a status code from the response.
/// - Returns: A boolean value that indicates whether a logging event should have been logged or not.
func shouldEventBeLogged(
logLevel: Logger.Level,
statusCode: HTTPResponse.Status
) -> Bool {
let levels: [Logger.Level] = switch statusCode {
case .ok: [.debug, .trace]
case .notFound: [.debug, .error, .info, .notice, .trace, .warning]
default: []
}
return levels.contains(logLevel)
}
}
@@ -0,0 +1,598 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
import protocol Hummingbird.FileProvider
import protocol Hummingbird.RequestContext
import struct Hummingbird.HTTPResponse
import struct Hummingbird.LocalFileSystem
import struct Hummingbird.Request
import struct Logging.Logger
@testable import struct HummingbirdDocC.DocCConfiguration
@testable import struct HummingbirdDocC.DocCMiddleware
@Suite("DocC Middleware", .tags(.middleware))
struct DocCMiddlewareTests {
// MARK: Initializers tests
#if swift(>=6.2)
@Test
func `initialize with URI and folder paths`() {
assertInit(configuration: .init(
uriRoot: .Sample.uriResource,
folderRoot: .Sample.uriFolder
))
}
@Test
func `initialize with URI path and type that conforms to the FileProvider protocol`() {
assertInit(
configuration: .init(
uriRoot: .Sample.uriResource,
folderRoot: .empty
),
fileProvider: FileProviderStub()
)
}
#else
@Test("initialize with URI and folder paths")
func init_withURI_andFolderPaths() {
assertInit(configuration: .init(
uriRoot: .Sample.uriResource,
folderRoot: .Sample.uriFolder
))
}
@Test("initialize with type that conforms to the FileProvider protocol")
func init_withURI_path_andFileProviderType() {
assertInit(
configuration: .init(
uriRoot: .Sample.uriResource,
folderRoot: .empty
),
fileProvider: FileProviderStub()
)
}
#endif
// MARK: RouterMiddleware tests
#if swift(>=6.2)
@Test(arguments: zip(
Input.redirectURIPaths,
Output.redirectURIPaths
))
func `redirect a URI path while triggering logging event`(
uriPath: String,
expects uriRedirect: String
) async throws {
try await assertRedirect(
logLevel: try .random(upTo: .debug),
uriPath: .Sample.uriDocument + uriPath,
to: .Sample.uriDocument + uriRedirect
)
}
@Test(arguments: zip(
Input.redirectURIPaths,
Output.redirectURIPaths
))
func `redirect a URI path without triggering logging event`(
uriPath: String,
expects uriRedirect: String
) async throws {
try await assertRedirect(
logLevel: try .random(fromExclusive: .debug),
uriPath: .Sample.uriDocument + uriPath,
to: .Sample.uriDocument + uriRedirect
)
}
@Test(arguments: Input.redirectURIPaths)
func `redirect a URI path not prefixed with root URI path`(uriPath: String) async throws {
try await assertRedirect(
logLevel: try .random(),
uriPath: uriPath,
expects: .ok
)
}
@Test(arguments: zip(
Input.serveURIPaths,
Output.serveURIFilePaths
))
func `serve an existing URI resource while triggering logging event`(
uriPath: String,
uriFile: String
) async throws {
try await assertServe(
logLevel: try .random(upTo: .debug),
uriPath: .Sample.uriDocument + uriPath,
uriFile: uriFile,
statusCode: .ok
)
}
@Test(arguments: Input.serveURIPaths)
func `serve an existing URI resource without triggering logging event`(
uriPath: String
) async throws {
try await assertServe(
logLevel: try .random(fromExclusive: .debug),
uriPath: .Sample.uriDocument + uriPath,
statusCode: .ok
)
}
@Test(arguments: zip(
Input.serveURIPaths,
Output.serveURIFilePaths
))
func `serve a non existing URI resource while triggering logging event`(
uriPath: String,
uriFile: String
) async throws {
try await assertServe(
logLevel: try .random(upTo: .error),
uriPath: .Sample.uriDocument + uriPath,
uriFile: uriFile,
statusCode: .notFound
)
}
@Test(arguments: Input.serveURIPaths)
func `serve a non existing URI resource without triggering logging event`(
uriPath: String
) async throws {
try await assertServe(
logLevel: try .random(fromExclusive: .error),
uriPath: .Sample.uriDocument + uriPath,
statusCode: .notFound
)
}
@Test(arguments: Input.serveURIPaths)
func `serve a URI resource not prefixed with root URI path`(
uriPath: String
) async throws {
try await assertServe(
logLevel: try .random(),
uriPath: uriPath
)
}
#else
@Test("redirect a URI path while triggering logging event", arguments: zip(
Input.redirectURIPaths,
Output.redirectURIPaths
))
func redirect_aURIPath_triggeringLoggingEvent(
uriPath: String,
expects uriRedirect: String
) async throws {
try await assertRedirect(
logLevel: try .random(upTo: .debug),
uriPath: .Sample.uriRoot + uriPath,
to: .Sample.uriRoot + uriRedirect
)
}
@Test("redirect a URI path without triggering logging event", arguments: zip(
Input.redirectURIPaths,
Output.redirectURIPaths
))
func redirect_aURIPath_notTriggeringLoggingEvent(
uriPath: String,
expects uriRedirect: String
) async throws {
try await assertRedirect(
logLevel: try .random(fromExclusive: .debug),
uriPath: .Sample.uriRoot + uriPath,
to: .Sample.uriRoot + uriRedirect
)
}
@Test("redirect a URI path not prefixed with root URI path", arguments: Input.redirectURIPaths)
func redirect_aURIPath_notPrefixedURIRoot(uriPath: String) async throws {
try await assertRedirect(
logLevel: try .random(),
uriPath: .Sample.uriResource + uriPath,
expects: .ok
)
}
@Test("serve an existing URI resource while triggering logging event", arguments: zip(
Input.serveURIPaths,
Output.serveURIFilePaths
))
func serve_exitingURIResource_triggeringLoggingEvent(
uriPath: String,
uriFile: String
) async throws {
try await assertServe(
logLevel: try .random(upTo: .debug),
uriPath: .Sample.uriDocument + uriPath,
uriFile: uriFile,
statusCode: .ok
)
}
@Test("serve an existing URI resource without triggering logging event", arguments: Input.serveURIPaths)
func server_existingURIResource_notTriggeringLoggingEvent(
uriPath: String
) async throws {
try await assertServe(
logLevel: try .random(fromExclusive: .debug),
uriPath: .Sample.uriDocument + uriPath,
statusCode: .ok
)
}
@Test("serve a non existing URI resource while triggering logging event", arguments: zip(
Input.serveURIPaths,
Output.serveURIFilePaths
))
func serve_notExistingURIResource_triggeringLoggingEvent(
uriPath: String,
uriFile: String
) async throws {
try await assertServe(
logLevel: try .random(upTo: .error),
uriPath: .Sample.uriDocument + uriPath,
uriFile: uriFile,
statusCode: .notFound
)
}
@Test("serve a non existing URI resource without triggering logging event", arguments: Input.serveURIPaths)
func serve_notExistingURIResource_triggeringLoggingEvent(
uriPath: String
) async throws {
try await assertServe(
logLevel: try .random(fromExclusive: .error),
uriPath: .Sample.uriDocument + uriPath,
statusCode: .notFound
)
}
@Test("serve a URI resource not prefixed with root URI path", arguments: Input.serveURIPaths)
func server_aURIResource_notPrefixed_withURIRoot(
uriPath: String
) async throws {
try await assertServe(
logLevel: try .random(),
uriPath: uriPath
)
}
#endif
}
// MARK: - Assertions
private extension DocCMiddlewareTests {
// MARK: Functions
/// Asserts the public initializer.
/// - Parameters:
/// - configuration: A type that contains the parameters to configure the middleware.
/// - logger: A type that interacts with the logging system.
func assertInit(
configuration: DocCConfiguration,
logger: Logger = .test()
) {
// GIVEN
// WHEN
let middleware = DocCMiddleware<RequestContextMock, LocalFileSystem>(
configuration: configuration,
logger: logger
)
// THEN
#expect(middleware.logger.label == logger.label)
#expect(middleware.logger.logLevel == logger.logLevel)
#expect(middleware.logger.metadataProvider == nil)
#expect(type(of:middleware.fileProvider) == LocalFileSystem.self)
}
/// Asserts the internal initializer with a concrete file provider type.
/// - Parameters:
/// - configuration: A type that contains the parameters to configure the middleware.
/// - logger: A type that interacts with the logging system.
/// - fileProvider: A type that conforms to the protocol that defines file system interactions, if any.
func assertInit<FileSystemProvider: FileProvider>(
configuration: DocCConfiguration,
logger: Logger = .test(),
fileProvider: FileSystemProvider
) {
// GIVEN
// WHEN
let middleware = DocCMiddleware<RequestContextMock, FileSystemProvider>(
configuration: configuration,
fileProvider: fileProvider,
logger: logger
)
// THEN
#expect(middleware.logger.label == logger.label)
#expect(middleware.logger.logLevel == logger.logLevel)
#expect(middleware.logger.metadataProvider == nil)
#expect(type(of:middleware.fileProvider) == FileSystemProvider.self)
}
/// Asserts a URI path redirection done by the middleware.
/// - Parameters:
/// - logLevel: A representation of the logging level to set in the `Logger` instance.
/// - uriPath: A URI path to a resource.
/// - uriRedirect: A redirected URI path, if any.
/// - statusCode: An expected status code from the response coming out of the use case.
/// - Throws: An error in case an issue is encountered while asserting URI path redirections by the middleware.
func assertRedirect(
logLevel: Logger.Level,
uriPath: String,
to uriRedirect: String? = nil,
expects statusCode: HTTPResponse.Status = .movedPermanently
) async throws {
// GIVEN
let logHandler: LogHandlerMock = .init()
let logger: Logger = .test(
level: logLevel,
handler: logHandler
)
let context: RequestContextMock = .init(logger: logger)
let request: Request = .test(
method: .get,
path: uriPath
)
let middleware = DocCMiddleware<RequestContextMock, FileProviderMock>(
configuration: .init(
uriRoot: .Sample.uriRoot,
folderRoot: .Sample.uriFolder
),
fileProvider: FileProviderMock(),
logger: logger
)
// WHEN
let result = try await middleware.handle(request, context: context) { _, _ in
.init(status: .ok)
}
// THEN
#expect(result.status == statusCode)
let events = logHandler.entries
if statusCode == .movedPermanently, let uriRedirect {
#expect(result.body.contentLength == 0)
#expect(result.headers == [
.location: uriRedirect,
.contentLength: "0"
])
if shouldEventBeLogged(
logLevel: logLevel,
statusCode: statusCode
) {
#expect(!events.isEmpty)
#expect(events.count == 1)
let loggedEvent = try #require(events.first)
#expect(loggedEvent == .init(
level: .debug,
metadata: [
"hb.request.id": "\(context.id)",
"hb.request.method": "\(request.method.rawValue)",
"hb.request.path": "\(request.uri.path)",
"hb.request.status": "\(statusCode.code)",
"hb.request.redirect": "\(uriRedirect)"
],
message: "The URI path is redirected to this path: \(uriRedirect)",
source: .Logging.source
))
} else {
#expect(events.isEmpty)
}
} else {
#expect(events.isEmpty)
}
}
/// Asserts a URI resource serving done by the middleware.
/// - Parameters:
/// - logLevel: A representation of the logging level to set in the `Logger` instance.
/// - uriPath: A URI path for a resource.
/// - uriFile: A URI path for a file in the local file system.
/// - statusCode: An expected status code from the response coming out of the use case, if any.
/// - Throws: An error in case an issue is encountered while asserting URI path servings by the middleware.
func assertServe(
logLevel: Logger.Level,
uriPath: String,
uriFile: String? = nil,
statusCode: HTTPResponse.Status? = nil
) async throws {
// GIVEN
let logHandler: LogHandlerMock = .init()
let logger: Logger = .test(
level: logLevel,
handler: logHandler
)
let fileProvider: FileProviderMock = switch statusCode {
case .ok: .init(fileIdentifier: .init())
case .notFound: .init()
default: .init(fileIdentifier: .init(), shouldLoadFile: false)
}
let context: RequestContextMock = .init(logger: logger)
let request: Request = .test(
method: .get,
path: uriPath
)
let middleware = DocCMiddleware<RequestContextMock, FileProviderMock>(
configuration: .init(
uriRoot: .Sample.uriRoot,
folderRoot: .Sample.uriFolder
),
fileProvider: fileProvider,
logger: logger
)
// WHEN
let result = try await middleware.handle(request, context: context) { _, _ in
.init(status: .ok)
}
// THEN
if let statusCode {
#expect(result.status == statusCode)
#expect(result.headers == [
.contentLength: (statusCode == .ok ? "36" : "0")
])
let contentLength = try #require(result.body.contentLength)
if statusCode == .ok {
#expect(contentLength > 0)
} else {
#expect(contentLength == 0)
}
let events = logHandler.entries
if shouldEventBeLogged(
logLevel: logLevel,
statusCode: statusCode
) {
#expect(!events.isEmpty)
#expect(events.count == 1)
let loggedEvent = try #require(events.first)
let uriFile = try #require(uriFile)
#expect(loggedEvent == .init(
level: statusCode == .ok ? .debug : .error,
metadata: [
"hb.request.id": "\(context.id)",
"hb.request.method": "\(request.method.rawValue)",
"hb.request.path": "\(request.uri.path)",
"hb.request.status": "\(statusCode.code)"
],
message: {
if statusCode == .ok {
"The body of the resource \(uriFile) has \(contentLength) bytes."
} else {
"The resource \(uriFile) has not been found."
}
}(),
source: .Logging.source
))
} else {
#expect(events.isEmpty)
}
} else {
#expect(result.status == .ok)
}
}
}
// MARK: - Helpers
private extension DocCMiddlewareTests {
// MARK: Functions
/// Checks whether a logging event should be logged or not, based on a given logging level.
/// - Parameters:
/// - logLevel: A representation of a logging level defined in in the logger.
/// - statusCode: A representation of a status code from the response.
/// - Returns: A boolean value that indicates whether a logging event should have been logged or not.
func shouldEventBeLogged(
logLevel: Logger.Level,
statusCode: HTTPResponse.Status
) -> Bool {
let levels: [Logger.Level] = switch statusCode {
case .movedPermanently, .ok: [.debug, .trace]
case .notFound: [.debug, .error, .info, .notice, .trace, .warning]
default: []
}
return levels.contains(logLevel)
}
}
// MARK: - Constants
private extension Input {
/// A list of relative URI paths to match against the URI path redirections done by the middleware.
static let redirectURIPaths: [String] = [
.empty,
.Path.forwardSlash,
"/documentation",
"/tutorials"
]
/// A list of relative URI paths to match against the URI path servings done by the middleware.
static let serveURIPaths: [String] = [
"/documentation/",
"/tutorials/",
"/data/documentation.json",
"/favicon.ico",
"/favicon.svg",
"/theme-settings.json",
"/css/file.css",
"/data/data.bin",
"/downloads/file.txt",
"/images/image.png",
"/img/image.jpg",
"/index/file",
"/js/file.js",
"/videos/video.mp4"
]
}
private extension Output {
/// A list of expected relative URI path redirections outputs coming out of the URI path redirections done by the middleware.
static let redirectURIPaths: [String] = [
"/documentation",
"/documentation",
"/documentation/",
"/tutorials/"
]
/// A list of expected relative file URI paths of the logged messages coming out of the URI path servings done by the middleware.
static let serveURIFilePaths: [String] = [
"/SomeDocument.doccarchive/documentation/somedocument/index.html",
"/SomeDocument.doccarchive/tutorials/somedocument/index.html",
"/SomeDocument.doccarchive/data/documentation/somedocument.json",
"/SomeDocument.doccarchive/favicon.ico",
"/SomeDocument.doccarchive/favicon.svg",
"/SomeDocument.doccarchive/theme-settings.json",
"/SomeDocument.doccarchive/css/file.css",
"/SomeDocument.doccarchive/data/data.bin",
"/SomeDocument.doccarchive/downloads/file.txt",
"/SomeDocument.doccarchive/images/image.png",
"/SomeDocument.doccarchive/img/image.jpg",
"/SomeDocument.doccarchive/index/file",
"/SomeDocument.doccarchive/js/file.js",
"/SomeDocument.doccarchive/videos/video.mp4"
]
}
@@ -0,0 +1,57 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Foundation
import Testing
import protocol Logging.LogHandler
import struct Logging.Logger
extension Logger {
// MARK: Functions
/// Generates a logger instance that is ready to use in test cases.
/// - Parameters:
/// - level: A logger level, if any.
/// - handler: A custom log handler, if any.
/// - Returns: A generated logger instance ready to use in test cases.
static func test(
level: Logger.Level? = nil,
handler: (any LogHandler)? = nil
) -> Self {
var logger: Logger = if let handler {
.init(label: .loggerLabel) { _ in handler }
} else {
.init(label: .loggerLabel)
}
logger.logLevel = if let level {
level
} else {
try! #require(Logger.Level.allCases.randomElement())
}
logger[metadataKey: "hb.request.id"] = "\(UUID().uuidString)"
return logger
}
}
// MARK: - Constants
private extension String {
/// A label to assign to a test logger instance.
static let loggerLabel = "test.hummingbird-docc.logger"
}
@@ -0,0 +1,61 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
import struct Logging.Logger
extension Logger.Level {
// MARK: Functions
/// Extracts a random logging level value out of an inclusive subset of logging levels, arranged by severity.
/// - Parameter level: A representation of a logging level that defines a subset of values to choose from, if any.
/// - Returns: A randomized logging value.
/// - Throws: An error thrown in case an issue is encountered when deciding for a random value.
static func random(upTo level: Self? = nil) throws -> Self {
guard let level else {
return try #require(Self.allCases.randomElement())
}
let levels: [Self] = switch level {
case .trace: [.trace]
case .debug: [.debug, .trace]
case .info: [.debug, .info, .trace]
case .notice: [.debug, .info, .notice, .trace]
case .warning: [.debug, .info, .notice, .trace, .warning]
case .error: [.debug, .error, .info, .notice, .trace, .warning]
case .critical: Self.allCases
}
return try #require(levels.randomElement())
}
/// /// Extracts a random logging level value out of an exclusive subset of logging levels, arranged by severity.
/// - Parameter level: A representation of a logging level that defines a subset of values to choose from.
/// - Returns: A randomized logging value.
/// - Throws: An error thrown in case an issue is encountered when deciding for a random value.
static func random(fromExclusive level: Self) throws -> Self {
let levels: [Self] = switch level {
case .trace: [.critical, .debug, .error, .info, .notice, .warning]
case .debug: [.critical, .error, .info, .notice, .warning]
case .info: [.critical, .error, .notice, .warning]
case .notice: [.critical, .error, .warning]
case .warning: [.critical, .error]
case .error: [.critical]
case .critical: []
}
return try #require(levels.randomElement())
}
}
@@ -0,0 +1,41 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import struct Hummingbird.HTTPRequest
import struct Hummingbird.Request
import struct Hummingbird.RequestBody
extension Request {
// MARK: Functions
/// Generates a request that is ready to use in test case.
/// - Parameters:
/// - method: A HTTP method.
/// - path: A URI path, if any.
/// - Returns: A generated request instance to use in test cases.
static func test(
method: HTTPRequest.Method,
path: String? = nil
) -> Self {
.init(
head: .init(
method: method,
scheme: nil,
authority: nil,
path: path
),
body: .init(buffer: .init())
)
}
}
@@ -0,0 +1,32 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
extension String {
// MARK: Constants
/// A namespace that defines sample values.
enum Sample {
/// A URI path to use as a documentation root sample.
static let uriDocument = uriRoot + "/SomeDocument"
/// A URI path to use as a file sample.
static let uriFile = uriFolder + uriResource
/// A URI path to use as a folder sample.
static let uriFolder = "/some/folder/path"
/// A URI path to use as a redirection sample.
static let uriRedirection = "/some/redirect/path"
/// A URI path to use as a resource sample.
static let uriResource = "/some/path/to/resource"
/// A URI path to use as a root sample.
static let uriRoot = "/some/root/path"
}
}
@@ -0,0 +1,30 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Testing
extension Tag {
// MARK: Constants
/// Tag that indicate a test case for an enumeration type.
@Tag static var enumeration: Self
/// Tag that indicate a test case for an extended type.
@Tag static var `extension`: Self
/// Tag that indicate a test case for a middleware type.
@Tag static var middleware: Self
/// Tag that indicate a test case for a model type.
@Tag static var model: Self
/// Tag that indicate a test case for a use case type.
@Tag static var useCase: Self
}
@@ -0,0 +1,102 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import protocol Hummingbird.FileProvider
import protocol Hummingbird.RequestContext
import struct Foundation.Data
import struct Foundation.UUID
import struct Hummingbird.ResponseBody
/// A mock that conforms to the `FileProvider` protocol.
struct FileProviderMock {
// MARK: Properties
/// A type that identifies a sample file.
private let fileIdentifier: UUID?
/// A flag that indicates whether a file should be loaded or not.
private let shouldLoadFile: Bool
// MARK: Initializers
/// Initializes this mock.
/// - Parameters:
/// - fileIdentifier: A type that identifies a sample file, if any.
/// - shouldLoadFile: A flag that indicates whether a file should be loaded or not.
init(
fileIdentifier: UUID? = nil,
shouldLoadFile: Bool = true
) {
self.fileIdentifier = fileIdentifier
self.shouldLoadFile = shouldLoadFile
}
}
// MARK: - FileProvider
extension FileProviderMock: FileProvider {
// MARK: Type aliases
typealias FileAttributes = String
typealias FileIdentifier = String
// MARK: Functions
func getFileIdentifier(_ path: String) -> String? {
fileIdentifier?.uuidString
}
func getAttributes(id: String) async throws -> String? {
nil
}
func loadFile(
id: String,
context: some RequestContext
) async throws -> ResponseBody {
guard shouldLoadFile else {
throw FileProviderMockError.fileNotLoaded
}
guard let content = fileIdentifier?.uuidString else {
return .init()
}
return .init(byteBuffer: .init(
data: .init(content.utf8)
))
}
func loadFile(
id: String,
range: ClosedRange<Int>,
context: some RequestContext
) async throws -> ResponseBody {
try await loadFile(
id: id,
context: context
)
}
}
// MARK: - FileProviderMockError
/// An error type that can only be thrown by the ``FileProviderMock`` mock.
enum FileProviderMockError: Error {
/// An error encountered while mocking the loading of a file.
case fileNotLoaded
}
@@ -0,0 +1,153 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Foundation
import protocol Logging.LogHandler
import struct Logging.Logger
/// A mock that conforms to the `LogHandler` protocol.
struct LogHandlerMock {
// MARK: Properties
/// A representation of the logging level assigned to this mock.
private var _logLevel: Logger.Level = .debug
/// A dictionary that contains all the metadata assigned to this mock.
private var _metadata: Logger.Metadata = [:]
/// A logging event recorder attached to this mock.
private let recorder: LogRecorder = .init()
// MARK: Computed
/// A list of all the logged events that are being persisted in the recorder.
var entries: [LogEntry] { recorder.entries }
}
// MARK: - LogEntry
/// A type that contains the information logged in a logging event.
struct LogEntry: Equatable {
// MARK: Properties
/// A representation of the level attached to a logged event.
let level: Logger.Level
/// A metadata dictionary that contains additional information attached to a logged event.
let metadata: Logger.Metadata?
/// A message attached to a logged event.
let message: Logger.Message
/// A source from where a logged event was triggered.
let source: String
}
// MARK: - LogRecorder
extension LogHandlerMock {
/// A class that records all the events logged by the ``LogHandlerMock`` mock handler.
///
/// This class conforms to the `Sendable` protocol by using the `@unchecked` modifier because a `NSLock`type is used to handle the access to the logged events in a thread-safe way.
final class LogRecorder: @unchecked Sendable {
// MARK: Properties
/// A list of all the logged events persisted in a thread-safe way.
private(set) var _entries: [LogEntry] = []
/// A type that coordinates the access to the persisted logged events in a thread-safe way.
private let lock: NSLock = .init()
// MARK: Computed
/// A list of all the logged events.
var entries: [LogEntry] {
lock.withLock { _entries }
}
// MARK: Functions
/// Records data related to a logged event.
/// - Parameters:
/// - level: A representation of the level attached to a logged event.
/// - metadata: A metadata dictionary that contains additional information attached to a logged event.
/// - message: A message attached to a logged event.
/// - source: A source from where a logged event was triggered.
func record(
level: Logger.Level,
metadata: Logger.Metadata?,
message: Logger.Message,
source: String
) {
lock.withLock {
_entries += [.init(
level: level,
metadata: metadata,
message: message,
source: source
)]
}
}
}
}
// MARK: - LogHandler
extension LogHandlerMock: LogHandler {
// MARK: Properties
var metadata: Logger.Metadata {
get { _metadata }
set(newValue) { _metadata = newValue }
}
var logLevel: Logger.Level {
get { _logLevel }
set(newValue) { _logLevel = newValue }
}
// MARK: Subscripts
subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
get { _metadata[metadataKey] }
set(newValue) { _metadata[metadataKey] = newValue }
}
// MARK: Functions
func log(
level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt
) {
recorder.record(
level: level,
metadata: metadata,
message: message,
source: source
)
}
}
@@ -0,0 +1,51 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import class NIOEmbedded.NIOAsyncTestingChannel
import protocol Hummingbird.RequestContext
import struct Hummingbird.ApplicationRequestContextSource
import struct Hummingbird.CoreRequestContextStorage
import struct Logging.Logger
/// A mock that conforms to the `RequestContext` protocol.
struct RequestContextMock {
// MARK: Properties
var coreContext: CoreRequestContextStorage
// MARK: Initializers
/// Initializes this mock.
/// - Parameter logger: A type that interacts with the logging system.
init(logger: Logger) {
self.coreContext = .init(source: ApplicationRequestContextSource(
channel: NIOAsyncTestingChannel(),
logger: logger
))
}
}
// MARK: - RequestContext
extension RequestContextMock: RequestContext {
// MARK: Initializers
init(source: ApplicationRequestContextSource) {
self.coreContext = .init(source: source)
}
}
@@ -0,0 +1,14 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
/// A namespace assigned for test arguments
enum Input {}
@@ -0,0 +1,14 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
/// A namespace assigned for test arguments that would be expected outputs coming from results of test cases.
enum Output {}
@@ -0,0 +1,53 @@
// ===----------------------------------------------------------------------===
//
// This source file is part of the Hummingbird DocC Middleware open source project
//
// Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors
// Licensed under the EUPL 1.2 or later.
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors
//
// ===----------------------------------------------------------------------===
import Hummingbird
/// A stub that conforms to the `FileProvider` protocol.
struct FileProviderStub {}
// MARK: - FileProvider
extension FileProviderStub: FileProvider {
// MARK: Type aliases
typealias FileAttributes = String
typealias FileIdentifier = String
// MARK: Functions
func getFileIdentifier(_ path: String) -> String? {
nil
}
func getAttributes(id: String) async throws -> String? {
nil
}
func loadFile(
id: String,
context: some RequestContext
) async throws -> ResponseBody {
.init()
}
func loadFile(
id: String,
range: ClosedRange<Int>,
context: some RequestContext
) async throws -> ResponseBody {
.init()
}
}