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:
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user