Implemented the "handle(_: context: next: )" function for the DocCMiddleware type in the library target.
This commit is contained in:
@@ -11,8 +11,12 @@
|
|||||||
// ===----------------------------------------------------------------------===
|
// ===----------------------------------------------------------------------===
|
||||||
|
|
||||||
import protocol Hummingbird.FileProvider
|
import protocol Hummingbird.FileProvider
|
||||||
|
import protocol Hummingbird.RequestContext
|
||||||
|
import protocol Hummingbird.RouterMiddleware
|
||||||
|
|
||||||
import struct Hummingbird.LocalFileSystem
|
import struct Hummingbird.LocalFileSystem
|
||||||
|
import struct Hummingbird.Request
|
||||||
|
import struct Hummingbird.Response
|
||||||
import struct Logging.Logger
|
import struct Logging.Logger
|
||||||
|
|
||||||
/// A middleware that proxies requests to `DocC` documentation containers within a hosting app.
|
/// A middleware that proxies requests to `DocC` documentation containers within a hosting app.
|
||||||
@@ -32,6 +36,15 @@ public struct DocCMiddleware<FileSystemProvider: FileProvider> {
|
|||||||
/// A use case that checks whether a received URI could be processed or not.
|
/// A use case that checks whether a received URI could be processed or not.
|
||||||
private let checkURI: CheckURIUseCase = .init()
|
private let checkURI: CheckURIUseCase = .init()
|
||||||
|
|
||||||
|
/// A use case that extracts data from a given URI path, essential for routing the documentation contents.
|
||||||
|
private let prepareURIPath: PrepareURIPathUseCase
|
||||||
|
|
||||||
|
/// A use case that produces a redirect response based on a given URI path.
|
||||||
|
private let redirectURI: RedirectURIUseCase
|
||||||
|
|
||||||
|
/// A use case that serves a resource, defined by its URI path, from a physical location.
|
||||||
|
private let serveURI: ServeURIUseCase<FileSystemProvider>
|
||||||
|
|
||||||
// MARK: Initializers
|
// MARK: Initializers
|
||||||
|
|
||||||
/// Initializes this middleware.
|
/// Initializes this middleware.
|
||||||
@@ -93,11 +106,74 @@ extension DocCMiddleware: RouterMiddleware {
|
|||||||
context: any Context,
|
context: any Context,
|
||||||
next: (Input, any Context) async throws -> Output
|
next: (Input, any Context) async throws -> Output
|
||||||
) async throws -> Output {
|
) async throws -> Output {
|
||||||
guard let uri = checkURI(input.uri) else {
|
guard
|
||||||
|
let uriPath = checkURI(input.uri),
|
||||||
|
let uriData = prepareURIPath(uriPath)
|
||||||
|
else {
|
||||||
return try await next(input, context)
|
return try await next(input, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if uriData.resourcePath == .Path.forwardSlash {
|
||||||
|
// rule #1: Redirects URI root to `/`.
|
||||||
|
// rule #2: Redirects URI resources with `/` to `/documentation`.
|
||||||
|
return redirectURI(
|
||||||
|
uriPath.hasSuffix(.Path.forwardSlash)
|
||||||
|
? String(format: .Format.Path.documentation, uriPath)
|
||||||
|
: String(format: .Format.Path.forwardSlash, uriPath),
|
||||||
|
with: (input, context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for assetFile in AssetFile.allCases {
|
||||||
|
if uriData.resourcePath.contains(assetFile.path) {
|
||||||
|
return try await serveURI(
|
||||||
|
assetFile == .documentation
|
||||||
|
// Rule #6: Redirects URI resources with `/data/documentation.json` to the file in the `data/documentation/`
|
||||||
|
// folder that has the name of the module and ends with the `.json` extension in the *DocC* archive container.
|
||||||
|
? String(format: .Format.Path.documentationJSON, uriData.archiveName)
|
||||||
|
// Rule #7: Redirect URI resources for static files (`favicon.ico`, `favicon.svg`, and `theme-settings.json`)
|
||||||
|
// to their respective files in the *DocC* archive container.
|
||||||
|
: uriData.resourcePath,
|
||||||
|
at: uriData.archivePath,
|
||||||
|
with: (input, context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for assetFolder in AssetFolder.allCases {
|
||||||
|
if uriData.resourcePath.contains(assetFolder.path) {
|
||||||
|
// Rule #8: Redirect URI resources for asset files (`/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`,
|
||||||
|
// `/index/`, `/js/`, or `/videos/`) to their respective files in the *DocC* archive container.
|
||||||
|
return try await serveURI(
|
||||||
|
uriData.resourcePath,
|
||||||
|
at: uriData.archivePath,
|
||||||
|
with: (input, context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for documentationFolder in DocumentationFolder.allCases {
|
||||||
|
if uriData.resourcePath.contains(documentationFolder.path) {
|
||||||
|
if uriData.resourcePath.hasSuffix(.Path.forwardSlash) {
|
||||||
|
// Rule #5: Redirect URI resources for `/documentation/` and `/tutorials/` folders to their respective `index.html` file.
|
||||||
|
return try await serveURI(
|
||||||
|
String(format: .Format.Path.index, documentationFolder.path, uriData.archiveName),
|
||||||
|
at: uriData.archivePath,
|
||||||
|
with: (input, context)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// rule #3: Redirects URI resources with `/documentation` to `/documentation/`.
|
||||||
|
// rule #4: Redirects URI resources with `/tutorials` to `/tutorials/`.
|
||||||
|
return redirectURI(
|
||||||
|
String(format: .Format.Path.forwardSlash, uriPath),
|
||||||
|
with: (input, context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return try await next(input, context)
|
return try await next(input, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,8 +245,6 @@ private extension ServeURIUseCaseTests {
|
|||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
// MARK: Functions
|
|
||||||
|
|
||||||
/// Checks whether a logging event should be logged or not, based on a given logging level.
|
/// Checks whether a logging event should be logged or not, based on a given logging level.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - logLevel: A representation of a logging level defined in in the logger.
|
/// - logLevel: A representation of a logging level defined in in the logger.
|
||||||
|
|||||||
@@ -13,8 +13,11 @@
|
|||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
import protocol Hummingbird.FileProvider
|
import protocol Hummingbird.FileProvider
|
||||||
|
import protocol Hummingbird.RequestContext
|
||||||
|
|
||||||
|
import struct Hummingbird.HTTPResponse
|
||||||
import struct Hummingbird.LocalFileSystem
|
import struct Hummingbird.LocalFileSystem
|
||||||
|
import struct Hummingbird.Request
|
||||||
import struct Logging.Logger
|
import struct Logging.Logger
|
||||||
|
|
||||||
@testable import struct DocCMiddleware.DocCMiddleware
|
@testable import struct DocCMiddleware.DocCMiddleware
|
||||||
@@ -28,8 +31,8 @@ struct DocCMiddlewareTests {
|
|||||||
@Test
|
@Test
|
||||||
func `initialize with URI and folder paths`() {
|
func `initialize with URI and folder paths`() {
|
||||||
assertInit(configuration: .init(
|
assertInit(configuration: .init(
|
||||||
uriRoot: "/path/to/documentation",
|
uriRoot: .Sample.uriResource,
|
||||||
folderRoot: "/location/docc/documentation"
|
folderRoot: .Sample.uriFolder
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ struct DocCMiddlewareTests {
|
|||||||
func `initialize with URI path and type that conforms to the FileProvider protocol`() {
|
func `initialize with URI path and type that conforms to the FileProvider protocol`() {
|
||||||
assertInit(
|
assertInit(
|
||||||
configuration: .init(
|
configuration: .init(
|
||||||
uriRoot: "/path/to/documentation",
|
uriRoot: .Sample.uriResource,
|
||||||
folderRoot: .empty
|
folderRoot: .empty
|
||||||
),
|
),
|
||||||
fileProvider: FileProviderStub()
|
fileProvider: FileProviderStub()
|
||||||
@@ -47,8 +50,8 @@ struct DocCMiddlewareTests {
|
|||||||
@Test("initialize with URI and folder paths")
|
@Test("initialize with URI and folder paths")
|
||||||
func init_withURI_andFolderPaths() {
|
func init_withURI_andFolderPaths() {
|
||||||
assertInit(configuration: .init(
|
assertInit(configuration: .init(
|
||||||
uriRoot: "/path/to/documentation",
|
uriRoot: .Sample.uriResource,
|
||||||
folderRoot: "/location/docc/documentation"
|
folderRoot: .Sample.uriFolder
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +59,7 @@ struct DocCMiddlewareTests {
|
|||||||
func init_withURI_path_andFileProviderType() {
|
func init_withURI_path_andFileProviderType() {
|
||||||
assertInit(
|
assertInit(
|
||||||
configuration: .init(
|
configuration: .init(
|
||||||
uriRoot: "/path/to/documentation",
|
uriRoot: .Sample.uriResource,
|
||||||
folderRoot: .empty
|
folderRoot: .empty
|
||||||
),
|
),
|
||||||
fileProvider: FileProviderStub()
|
fileProvider: FileProviderStub()
|
||||||
@@ -64,6 +67,216 @@ struct DocCMiddlewareTests {
|
|||||||
}
|
}
|
||||||
#endif
|
#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 randomLogLevelForRedirectWithEvent,
|
||||||
|
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 randomLogLevelForRedirectWithNoEvent,
|
||||||
|
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 randomLogLevel,
|
||||||
|
uriPath: .Sample.uriDocument + 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 randomLogLevelForServeOKWithEvent,
|
||||||
|
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 randomLogLevelForServeOKWithNoEvent,
|
||||||
|
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 randomLogLevelForServeNotFoundWithEvent,
|
||||||
|
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 randomLogLevelForServeNotFoundWithNoEvent,
|
||||||
|
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 randomLogLevel,
|
||||||
|
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 randomLogLevelForRedirectWithEvent,
|
||||||
|
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 randomLogLevelForRedirectWithNoEvent,
|
||||||
|
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 randomLogLevel,
|
||||||
|
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 randomLogLevelForServeOKWithEvent,
|
||||||
|
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 randomLogLevelForServeOKWithNoEvent,
|
||||||
|
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 randomLogLevelForServeNotFoundWithEvent,
|
||||||
|
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 randomLogLevelForServeNotFoundWithNoEvent,
|
||||||
|
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 randomLogLevel,
|
||||||
|
uriPath: uriPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Assertions
|
// MARK: - Assertions
|
||||||
@@ -94,6 +307,7 @@ private extension DocCMiddlewareTests {
|
|||||||
|
|
||||||
#expect(middleware.logger.label == logger.label)
|
#expect(middleware.logger.label == logger.label)
|
||||||
#expect(middleware.logger.logLevel == logger.logLevel)
|
#expect(middleware.logger.logLevel == logger.logLevel)
|
||||||
|
#expect(middleware.logger.metadataProvider == nil)
|
||||||
|
|
||||||
#expect(type(of:middleware.fileProvider) == LocalFileSystem.self)
|
#expect(type(of:middleware.fileProvider) == LocalFileSystem.self)
|
||||||
}
|
}
|
||||||
@@ -123,8 +337,312 @@ private extension DocCMiddlewareTests {
|
|||||||
|
|
||||||
#expect(middleware.logger.label == logger.label)
|
#expect(middleware.logger.label == logger.label)
|
||||||
#expect(middleware.logger.logLevel == logger.logLevel)
|
#expect(middleware.logger.logLevel == logger.logLevel)
|
||||||
|
#expect(middleware.logger.metadataProvider == nil)
|
||||||
|
|
||||||
#expect(type(of:middleware.fileProvider) == FileSystemProvider.self)
|
#expect(type(of:middleware.fileProvider) == FileSystemProvider.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Asserts an 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: any RequestContext = RequestContextMock(logger: logger)
|
||||||
|
let request: Request = .test(
|
||||||
|
method: .get,
|
||||||
|
path: uriPath
|
||||||
|
)
|
||||||
|
|
||||||
|
let middleware = DocCMiddleware(
|
||||||
|
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 = await 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <#Description#>
|
||||||
|
/// - Parameters:
|
||||||
|
/// - logLevel: <#logLevel description#>
|
||||||
|
/// - uriPath: <#uriPath description#>
|
||||||
|
/// - uriFile: <#uriFile description#>
|
||||||
|
/// - folderPath: <#folderPath description#>
|
||||||
|
/// - statusCode: <#statusCode description#>
|
||||||
|
/// - 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,
|
||||||
|
folderPath: String = .Sample.uriFolder,
|
||||||
|
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: any RequestContext = RequestContextMock(logger: logger)
|
||||||
|
let request: Request = .test(
|
||||||
|
method: .get,
|
||||||
|
path: uriPath
|
||||||
|
)
|
||||||
|
|
||||||
|
let middleware = DocCMiddleware(
|
||||||
|
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 = await 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: Computed
|
||||||
|
|
||||||
|
/// Extracts a random logging level.
|
||||||
|
var randomLogLevel: Logger.Level {
|
||||||
|
get throws {
|
||||||
|
try #require(Logger.Level.allCases.randomElement())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a random logging level for redirection assertions that support event logging for the use case.
|
||||||
|
var randomLogLevelForRedirectWithEvent: Logger.Level {
|
||||||
|
get throws {
|
||||||
|
try #require([.debug, .trace].randomElement())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a random logging level for redirection assertions that does not support event logging for the use case.
|
||||||
|
var randomLogLevelForRedirectWithNoEvent: Logger.Level {
|
||||||
|
get throws {
|
||||||
|
try #require([.critical, .error, .info, .notice, .warning].randomElement())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a random logging level for OK serve assertions that support event logging for the use case.
|
||||||
|
var randomLogLevelForServeOKWithEvent: Logger.Level {
|
||||||
|
get throws {
|
||||||
|
try #require([.debug, .trace].randomElement())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a random logging level for OK serve assertions that does not support event logging for the use case.
|
||||||
|
var randomLogLevelForServeOKWithNoEvent: Logger.Level {
|
||||||
|
get throws {
|
||||||
|
try #require([.critical, .error, .info, .notice, .warning].randomElement())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a random logging level for Not Found serve assertions that support event logging for the use case.
|
||||||
|
var randomLogLevelForServeNotFoundWithEvent: Logger.Level {
|
||||||
|
get throws {
|
||||||
|
try #require([.debug, .error, .info, .notice, .trace, .warning].randomElement())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a random logging level for Not Found serve assertions that does not support event logging for the use case.
|
||||||
|
var randomLogLevelForServeNotFoundWithNoEvent: Logger.Level {
|
||||||
|
get throws {
|
||||||
|
try #require([.critical].randomElement())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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] = [.Path.forwardSlash, "/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/SomeDocument/favicon.ico",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/favicon.svg",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/theme-settings.json",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/css/file.css",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/data/data.bin",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/downloads/file.txt",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/images/image.png",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/img/image.jpg",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/index/file",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/js/file.js",
|
||||||
|
"/SomeDocument.doccarchive/SomeDocument/videos/video.mp4"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ extension String {
|
|||||||
|
|
||||||
/// A namespace that defines sample values.
|
/// A namespace that defines sample values.
|
||||||
enum Sample {
|
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.
|
/// A URI path to use as a file sample.
|
||||||
static let uriFile = uriFolder + uriResource
|
static let uriFile = uriFolder + uriResource
|
||||||
/// A URI path to use as a folder sample.
|
/// A URI path to use as a folder sample.
|
||||||
@@ -24,5 +26,7 @@ extension String {
|
|||||||
static let uriRedirection = "/some/redirect/path"
|
static let uriRedirection = "/some/redirect/path"
|
||||||
/// A URI path to use as a resource sample.
|
/// A URI path to use as a resource sample.
|
||||||
static let uriResource = "/some/path/to/resource"
|
static let uriResource = "/some/path/to/resource"
|
||||||
|
/// A URI path to use as a root sample.
|
||||||
|
static let uriRoot = "/some/root/path"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user