Implemented the ServeResourceUseCase use case in the library target.

This commit is contained in:
2025-09-25 01:38:48 +02:00
parent 04d1ca6a26
commit ff56875a24
6 changed files with 385 additions and 9 deletions
@@ -14,13 +14,13 @@ extension String {
/// An empty string.
static let empty = ""
/// A namespace that defines logging representations.
/// A namespace that defines logging values.
enum Logging {
/// A name of the middleware that triggered a logging event.
static let source = "DocCMiddleware"
}
/// A namespace that defines relative path representations.
/// A namespace that defines relative path values.
enum Path {
/// A forwarding slash.
static let forwardSlash = "/"
@@ -0,0 +1,100 @@
// ===----------------------------------------------------------------------===
//
// 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 struct Hummingbird.Response
import struct Logging.Logger
/// A use case that serves a resource, defined by its URI path, from a physical location.
struct ServeResourceUseCase<FileSystemProvider: FileProvider> {
// MARK: Properties
/// A type that conforms to a protocol that defines file system interactions.
let fileProvider: FileSystemProvider
/// A type that interacts with the logging system.
private let logger: Logger
// MARK: Initializers
/// Initializes this use case.
/// - Parameters:
/// - fileProvider: A type that conforms to a protocol that defines file system interactions.
/// - logger: A type that interacts with the logging system.
init(
fileProvider: FileSystemProvider,
logger: Logger
) {
self.fileProvider = fileProvider
self.logger = logger
}
// MARK: Functions
/// Serves a certain resource based on a given URI path from a physical location.
/// - Parameters:
/// - uriPath: A URI path that represents a resource to be served.
/// - folderPath: A URI path to a physical folder that contains the resource.
/// - contextualInfo: A pseudo-type that contains data about a request and its related context.
/// - Returns: A response that either contains the data of the resource in its body in case the resource is found, or a not found otherwise.
/// - Throws: An error in case an issue is encountered while serving the resource.
func callAsFunction(
_ uriPath: String,
at folderPath: String,
with contextualInfo: ContextualInfo
) async throws -> Response {
let filePath = folderPath + uriPath
guard let fileIdentifier = fileProvider.getFileIdentifier(filePath) else {
defer {
logger.log(
level: .error,
"The resource \(filePath) has not been found.",
metadata: .metadata(
context: contextualInfo.context,
request: contextualInfo.request,
statusCode: .notFound
),
source: .Logging.source
)
}
return .init(status: .notFound)
}
let body = try await fileProvider.loadFile(
id: fileIdentifier,
context: contextualInfo.context
)
defer {
logger.log(
level: .debug,
"The body of the resource \(filePath) has \(body.contentLength ?? 0) bytes.",
metadata: .metadata(
context: contextualInfo.context,
request: contextualInfo.request,
statusCode: .ok
),
source: .Logging.source
)
}
return .init(
status: .ok,
body: body
)
}
}
@@ -38,7 +38,7 @@ struct LoggerMetadata_HelpersTests {
assertMetadata(
method: try randomMethod,
statusCode: try randomStatusCode,
redirect: .uriRedirection
redirect: .Sample.uriRedirection
)
}
#else
@@ -27,7 +27,7 @@ struct RedirectURIUseCaseTests {
func `response when logger expects an event`() async throws {
try await assertResponse(
logLevel: try randomLogLevelWithEvent,
uriRedirection: .uriRedirection,
uriRedirection: .Sample.uriRedirection,
expects: .movedPermanently
)
}
@@ -36,7 +36,7 @@ struct RedirectURIUseCaseTests {
func `response when logger does not expects an event`() async throws {
try await assertResponse(
logLevel: try randomLogLevelWithNoEvent,
uriRedirection: .uriRedirection,
uriRedirection: .Sample.uriRedirection,
expects: .movedPermanently
)
}
@@ -123,7 +123,7 @@ private extension RedirectURIUseCaseTests {
"hb.request.redirect": "\(uriRedirection)"
],
message: "The URI path is redirected to this path: \(uriRedirection)",
source: "DocCMiddleware"
source: .Logging.source
))
} else {
#expect(events.isEmpty)
@@ -0,0 +1,268 @@
// ===----------------------------------------------------------------------===
//
// 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 DocCMiddleware.ServeResourceUseCase
@Suite("Serve Resource Use Case", .tags(.useCase))
struct ServeResourceUseCaseTests {
// MARK: Use case tests
#if swift(>=6.2)
@Test
func `response when resource served and logging event triggered`() async throws {
try await assertResponse(
logLevel: .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: .info,
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: .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: .critical,
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder,
expects: .notFound
)
}
@Test
func `response throws error when loading resource`() async throws {
try await assertResponse(
logLevel: .warning,
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: .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: .info,
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: .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: .critical,
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: .warning,
uriPath: .Sample.uriResource,
folderPath: .Sample.uriFolder
)
}
#endif
}
// MARK: - Assertions
private extension ServeResourceUseCaseTests {
// MARK: Functions
/// Asserts a response returned by the ``ServeResourceUseCase`` 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 = ServeResourceUseCase(
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 = await 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 ServeResourceUseCaseTests {
// MARK: Functions
// 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)
}
}
@@ -14,7 +14,15 @@ extension String {
// MARK: Constants
/// A URI path to use as a redirection sample.
static let uriRedirection = "/some/redirect/path"
/// A namespace that defines sample values.
enum Sample {
/// 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"
}
}