From a14d388321719a22ecbbad6e35eea1868e886470 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 12 Mar 2025 23:43:19 +0100 Subject: [PATCH] Improved upon the "listArchives(_: context: )" function for the ArchiveController controller in the library target. --- .../Controllers/ArchiveController.swift | 34 ++++--- .../Internal/Protocols/FileServicing.swift | 4 +- .../Controllers/ArchiveControllerTests.swift | 93 +++++++++++++++++-- .../Public/Builders/AppBuilderTests.swift | 2 +- 4 files changed, 108 insertions(+), 25 deletions(-) diff --git a/Library/Sources/Internal/Controllers/ArchiveController.swift b/Library/Sources/Internal/Controllers/ArchiveController.swift index 606ef99..b16e0ae 100644 --- a/Library/Sources/Internal/Controllers/ArchiveController.swift +++ b/Library/Sources/Internal/Controllers/ArchiveController.swift @@ -1,6 +1,6 @@ import Hummingbird -/// A controller that handles +/// A controller type that provides information about the *DocC* archives containers. struct ArchiveController { // MARK: Properties @@ -12,8 +12,8 @@ struct ArchiveController { /// Initialises this controller. /// - Parameters: - /// - folderArchives: <#folderArchives description#> - /// - fileService: <#fileService description#> + /// - folderArchives: A folder in the file system where the *DocC* archive contained are located. + /// - fileService: A service that interfaces with the local file system. init( _ folderArchives: String, fileService: any FileServicing = FileService() @@ -27,7 +27,7 @@ struct ArchiveController { /// Registers the controller to a given router. /// - Parameter router: A router to register this controller to. func register(to router: Router) { - router.get(.archives, use: listAllArchives) + router.get(.archives, use: listArchives) } } @@ -38,16 +38,26 @@ private extension ArchiveController { // MARK: Functions - @Sendable func listAllArchives( + @Sendable func listArchives( _ request: Request, context: Context - ) async throws -> ArchiveList { - let archives = try await fileService - .listItems(in: folderArchives) - .filter { $0.hasSuffix(.suffixArchive) } - .sorted { $0 < $1 } - - return .init(archives) + ) async throws (HTTPError) -> ArchiveList { + do { + let nameArchives = try await fileService + .listItems(in: folderArchives) + .filter { $0.hasSuffix(.suffixArchive) } + .map { $0.dropLast(String.suffixArchive.count) } + .map(String.init) + .sorted { $0 < $1 } + + return .init(nameArchives) + } catch .folderNotFound { + throw .init(.notFound) + } catch .folderPathEmpty, .folderNotDirectory { + throw .init(.unprocessableContent) + } catch { + throw .init(.badRequest) + } } } diff --git a/Library/Sources/Internal/Protocols/FileServicing.swift b/Library/Sources/Internal/Protocols/FileServicing.swift index 39bd4d8..22ef5bf 100644 --- a/Library/Sources/Internal/Protocols/FileServicing.swift +++ b/Library/Sources/Internal/Protocols/FileServicing.swift @@ -1,9 +1,9 @@ -/// A type that services access to information from the local file system. +/// A type that interfaces with the local file system. protocol FileServicing: Sendable { // MARK: Functions - /// Lists the names of items located at a given folder. path + /// Lists the names of items located at a given folder path. in the local file system. /// - Parameter folder: A path to a folder that could be found in the local file system. /// - Returns: A list of names related to the items retrieved from a given folder path. /// - Throws: A ``FileServiceError`` error type in case any error happens while retrieving the list. diff --git a/Test/Sources/Cases/Internal/Controllers/ArchiveControllerTests.swift b/Test/Sources/Cases/Internal/Controllers/ArchiveControllerTests.swift index 9c7d351..6901e7f 100644 --- a/Test/Sources/Cases/Internal/Controllers/ArchiveControllerTests.swift +++ b/Test/Sources/Cases/Internal/Controllers/ArchiveControllerTests.swift @@ -11,13 +11,20 @@ struct ArchiveControllerTests { // MARK: Properties private let decoder: JSONDecoder = .init() + + // MARK: Controller tests - @Test func xxx() async throws { + @Test(arguments: zip([[String].folderWithArchives, .folderWithNoArchives, .folderEmpty], + [[String].listWithArchives, .listWithNoArchives, .listWithNoArchives])) + func getArchives( + with archivesInFolder: [String], + expects archivesInList: [String] + ) async throws { // GIVEN - let fileService = FileServiceMock(items: ["SomeArchive.doccarchive", "some-file.txt"]) + let fileService = FileServiceMock(items: archivesInFolder) let router = Router() - ArchiveController("/path/to/archives/folder", fileService: fileService) + ArchiveController(.Path.archivesFolder, fileService: fileService) .register(to: router) let app = Application(router: router) @@ -25,21 +32,26 @@ struct ArchiveControllerTests { // WHEN // THEN try await app.test(.router) { client in - try await client.execute(uri: "/archives", method: .get) { response in + try await client.execute(uri: .Path.archivesResource, method: .get) { response in let archiveList = try decoder.decode(ArchiveList.self, from: response.body) #expect(response.status == .ok) - #expect(archiveList.archives == ["SomeArchive.doccarchive"]) + #expect(archiveList.archives == archivesInList) } } } - @Test func yyy() async throws { + @Test(arguments: zip([FileServiceError].errorFileService, + [HTTPResponse.Status].errorResponse)) + func getArchives( + with errorInFolder: FileServiceError, + expects errorResponse: HTTPResponse.Status + ) async throws { // GIVEN - let fileService = FileServiceMock(error: .folderNotFound) + let fileService = FileServiceMock(error: errorInFolder) let router = Router() - ArchiveController("/path/to/archives/folder", fileService: fileService) + ArchiveController(.Path.archivesFolder, fileService: fileService) .register(to: router) let app = Application(router: router) @@ -47,10 +59,71 @@ struct ArchiveControllerTests { // WHEN // THEN try await app.test(.router) { client in - try await client.execute(uri: "/archives", method: .get) { response in - #expect(response.status == .ok) + try await client.execute(uri: .Path.archivesResource, method: .get) { response in + #expect(response.status == errorResponse) } } } } + +// MARK: - Collection+String + +private extension Collection where Element == String { + static var folderEmpty: [Element] {[]} + static var folderWithArchives: [Element] {[ + ".DS_Store", + "SomeOtherArchive.doccarchive", + "some-text.txt", + "some-zipped-file.zip", + "SomeArchive.doccarchive", + "some-image.png", + "AnotherArchive.doccarchive", + "some-folder" + ]} + static var folderWithNoArchives: [Element] {[ + ".DS_Store", + "some-text.txt", + "some-zipped-file.zip", + "some-image.png", + "some-folder" + ]} + static var listWithArchives: [Element] {[ + "AnotherArchive", + "SomeArchive", + "SomeOtherArchive" + + ]} + static var listWithNoArchives: [Element] {[]} +} + +// MARK: - Collection+FileServiceError + +private extension Collection where Element == FileServiceError { + static var errorFileService: [Element] {[ + .folderPathEmpty, + .folderNotFound, + .folderNotDirectory, + .other(NSError(domain: "", code: 0)) + ]} +} + +// MARK: - Collection+HTTPResponseStatus + +private extension Collection where Element == HTTPResponse.Status { + static var errorResponse: [Element] {[ + .unprocessableContent, + .notFound, + .unprocessableContent, + .badRequest + ]} +} + +// MARK: - String+Constants + +private extension String { + enum Path { + static let archivesFolder: String = "/path/to/archives/folder" + static let archivesResource: String = "/archives" + } +} diff --git a/Test/Sources/Cases/Public/Builders/AppBuilderTests.swift b/Test/Sources/Cases/Public/Builders/AppBuilderTests.swift index 133dbfc..4009ad9 100644 --- a/Test/Sources/Cases/Public/Builders/AppBuilderTests.swift +++ b/Test/Sources/Cases/Public/Builders/AppBuilderTests.swift @@ -10,7 +10,7 @@ struct AppBuilderTests { private let appBuilder = AppBuilder( appName: "DoxyTest", - archivesFolder: "Resources/Archives/Test" + folderArchives: "Resources/Archives/Test" ) private let arguments = TestArguments()