From db11dfa31df196731bc74b422735f1a7105e7248 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 3 Dec 2022 12:02:38 +0100 Subject: [PATCH 01/21] Created the Libraries SPM package. --- BeReal.xcodeproj/project.pbxproj | 29 ++++++------------- BeReal/Extensions/String+Constants.swift | 11 ------- Libraries/.gitignore | 9 ++++++ Libraries/Package.swift | 22 ++++++++++++++ Libraries/Sources/Libraries/Libraries.swift | 6 ++++ .../Tests/LibrariesTests/LibrariesTests.swift | 11 +++++++ 6 files changed, 57 insertions(+), 31 deletions(-) delete mode 100644 BeReal/Extensions/String+Constants.swift create mode 100644 Libraries/.gitignore create mode 100644 Libraries/Package.swift create mode 100644 Libraries/Sources/Libraries/Libraries.swift create mode 100644 Libraries/Tests/LibrariesTests/LibrariesTests.swift diff --git a/BeReal.xcodeproj/project.pbxproj b/BeReal.xcodeproj/project.pbxproj index 03762d3..212a117 100644 --- a/BeReal.xcodeproj/project.pbxproj +++ b/BeReal.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 026D9825293B6374009FE888 /* Libraries in Frameworks */ = {isa = PBXBuildFile; productRef = 026D9824293B6374009FE888 /* Libraries */; }; 02AE64EF29363DBF005A4AF3 /* BeRealApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */; }; 02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64F029363DBF005A4AF3 /* ContentView.swift */; }; 02AE64F329363DC1005A4AF3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 02AE64F229363DC1005A4AF3 /* Assets.xcassets */; }; @@ -14,7 +15,6 @@ 02AE650029363DC1005A4AF3 /* BeRealTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE64FF29363DC1005A4AF3 /* BeRealTests.swift */; }; 02AE650A29363DC1005A4AF3 /* BeRealUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE650929363DC1005A4AF3 /* BeRealUITests.swift */; }; 02AE650C29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */; }; - 02FFFD7B29395DD200306533 /* String+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFFD7A29395DD200306533 /* String+Constants.swift */; }; 4694AAA0293A7C8800D54903 /* Modules in Frameworks */ = {isa = PBXBuildFile; productRef = 4694AA9F293A7C8800D54903 /* Modules */; }; /* End PBXBuildFile section */ @@ -36,6 +36,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 026D9823293B6365009FE888 /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = ""; }; 02784F03293A8331005F839D /* Modules */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Modules; sourceTree = ""; }; 02AE64EB29363DBF005A4AF3 /* BeReal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BeReal.app; sourceTree = BUILT_PRODUCTS_DIR; }; 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealApp.swift; sourceTree = ""; }; @@ -47,7 +48,6 @@ 02AE650529363DC1005A4AF3 /* BeRealUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BeRealUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02AE650929363DC1005A4AF3 /* BeRealUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITests.swift; sourceTree = ""; }; 02AE650B29363DC1005A4AF3 /* BeRealUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeRealUITestsLaunchTests.swift; sourceTree = ""; }; - 02FFFD7A29395DD200306533 /* String+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Constants.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,6 +55,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 026D9825293B6374009FE888 /* Libraries in Frameworks */, 4694AAA0293A7C8800D54903 /* Modules in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -79,6 +80,7 @@ 02AE64E229363DBF005A4AF3 = { isa = PBXGroup; children = ( + 026D9823293B6365009FE888 /* Libraries */, 02784F03293A8331005F839D /* Modules */, 02AE64ED29363DBF005A4AF3 /* BeReal */, 02AE64FE29363DC1005A4AF3 /* BeRealTests */, @@ -101,8 +103,6 @@ 02AE64ED29363DBF005A4AF3 /* BeReal */ = { isa = PBXGroup; children = ( - 02CE555F293B44C900730DC9 /* Profile */, - 02FFFD7929395DBF00306533 /* Extensions */, 02AE64EE29363DBF005A4AF3 /* BeRealApp.swift */, 02AE64F029363DBF005A4AF3 /* ContentView.swift */, 02AE64F229363DC1005A4AF3 /* Assets.xcassets */, @@ -136,21 +136,6 @@ path = BeRealUITests; sourceTree = ""; }; - 02CE555F293B44C900730DC9 /* Profile */ = { - isa = PBXGroup; - children = ( - ); - path = Profile; - sourceTree = ""; - }; - 02FFFD7929395DBF00306533 /* Extensions */ = { - isa = PBXGroup; - children = ( - 02FFFD7A29395DD200306533 /* String+Constants.swift */, - ); - path = Extensions; - sourceTree = ""; - }; 4694AA9E293A7C8800D54903 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -176,6 +161,7 @@ name = BeReal; packageProductDependencies = ( 4694AA9F293A7C8800D54903 /* Modules */, + 026D9824293B6374009FE888 /* Libraries */, ); productName = BeReal; productReference = 02AE64EB29363DBF005A4AF3 /* BeReal.app */; @@ -294,7 +280,6 @@ files = ( 02AE64F129363DBF005A4AF3 /* ContentView.swift in Sources */, 02AE64EF29363DBF005A4AF3 /* BeRealApp.swift in Sources */, - 02FFFD7B29395DD200306533 /* String+Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -645,6 +630,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 026D9824293B6374009FE888 /* Libraries */ = { + isa = XCSwiftPackageProductDependency; + productName = Libraries; + }; 4694AA9F293A7C8800D54903 /* Modules */ = { isa = XCSwiftPackageProductDependency; productName = Modules; diff --git a/BeReal/Extensions/String+Constants.swift b/BeReal/Extensions/String+Constants.swift deleted file mode 100644 index b4df93d..0000000 --- a/BeReal/Extensions/String+Constants.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// String+Constants.swift -// BeReal -// -// Created by Javier Cicchelli on 01/12/2022. -// Copyright © 2022 Röck+Cöde. All rights reserved. -// - -extension String { - static let empty = "" -} diff --git a/Libraries/.gitignore b/Libraries/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Libraries/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Libraries/Package.swift b/Libraries/Package.swift new file mode 100644 index 0000000..3f4bfd1 --- /dev/null +++ b/Libraries/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 5.7 + +import PackageDescription + +let package = Package( + name: "Libraries", + products: [ + .library( + name: "Libraries", + targets: ["Libraries"]), + ], + dependencies: [ + ], + targets: [ + .target( + name: "Libraries", + dependencies: []), + .testTarget( + name: "LibrariesTests", + dependencies: ["Libraries"]), + ] +) diff --git a/Libraries/Sources/Libraries/Libraries.swift b/Libraries/Sources/Libraries/Libraries.swift new file mode 100644 index 0000000..c1536b6 --- /dev/null +++ b/Libraries/Sources/Libraries/Libraries.swift @@ -0,0 +1,6 @@ +public struct Libraries { + public private(set) var text = "Hello, World!" + + public init() { + } +} diff --git a/Libraries/Tests/LibrariesTests/LibrariesTests.swift b/Libraries/Tests/LibrariesTests/LibrariesTests.swift new file mode 100644 index 0000000..dd47066 --- /dev/null +++ b/Libraries/Tests/LibrariesTests/LibrariesTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import Libraries + +final class LibrariesTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Libraries().text, "Hello, World!") + } +} From eb5d25dc9008e1547bfd4f19cae8248d3c101773 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 3 Dec 2022 23:20:49 +0100 Subject: [PATCH 02/21] Implemented the Me and Item models. --- Libraries/Package.swift | 14 ++++---- .../Sources/APIService/Models/Item.swift | 33 +++++++++++++++++++ Libraries/Sources/APIService/Models/Me.swift | 17 ++++++++++ Libraries/Sources/Libraries/Libraries.swift | 6 ---- .../Tests/LibrariesTests/LibrariesTests.swift | 11 ------- 5 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 Libraries/Sources/APIService/Models/Item.swift create mode 100644 Libraries/Sources/APIService/Models/Me.swift delete mode 100644 Libraries/Sources/Libraries/Libraries.swift delete mode 100644 Libraries/Tests/LibrariesTests/LibrariesTests.swift diff --git a/Libraries/Package.swift b/Libraries/Package.swift index 3f4bfd1..902fb07 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -7,16 +7,14 @@ let package = Package( products: [ .library( name: "Libraries", - targets: ["Libraries"]), - ], - dependencies: [ + targets: ["APIService"]), ], + dependencies: [], targets: [ - .target( - name: "Libraries", - dependencies: []), + .target(name: "APIService"), .testTarget( - name: "LibrariesTests", - dependencies: ["Libraries"]), + name: "APIServiceTests", + dependencies: ["APIService"] + ), ] ) diff --git a/Libraries/Sources/APIService/Models/Item.swift b/Libraries/Sources/APIService/Models/Item.swift new file mode 100644 index 0000000..b061629 --- /dev/null +++ b/Libraries/Sources/APIService/Models/Item.swift @@ -0,0 +1,33 @@ +// +// Item.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +struct Item { + let idParent: String? + let id: String + let name: String + let isDirectory: Bool + let lastModifiedAt: Date + let size: Int? + let contentType: String? +} + +// MARK: - Decodable + +extension Item: Decodable { + enum CodingKeys: String, CodingKey { + case id + case idParent = "parentId" + case name + case isDirectory = "isDir" + case lastModifiedAt = "modificationDate" + case size + case contentType + } +} diff --git a/Libraries/Sources/APIService/Models/Me.swift b/Libraries/Sources/APIService/Models/Me.swift new file mode 100644 index 0000000..43b9bcf --- /dev/null +++ b/Libraries/Sources/APIService/Models/Me.swift @@ -0,0 +1,17 @@ +// +// Me.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +public struct Me { + let firstName: String + let lastName: String + let rootItem: Item +} + +// MARK: - Decodable + +extension Me: Decodable {} diff --git a/Libraries/Sources/Libraries/Libraries.swift b/Libraries/Sources/Libraries/Libraries.swift deleted file mode 100644 index c1536b6..0000000 --- a/Libraries/Sources/Libraries/Libraries.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct Libraries { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Libraries/Tests/LibrariesTests/LibrariesTests.swift b/Libraries/Tests/LibrariesTests/LibrariesTests.swift deleted file mode 100644 index dd47066..0000000 --- a/Libraries/Tests/LibrariesTests/LibrariesTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest -@testable import Libraries - -final class LibrariesTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Libraries().text, "Hello, World!") - } -} From c6c544750035715ed5b3e59992803e3355c8525e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 01:13:35 +0100 Subject: [PATCH 03/21] Implemented the MakeAuthorizationHeaderUseCase use case. --- Libraries/Package.swift | 1 + .../MakeAuthorizationHeaderUseCase.swift | 46 ++++++++++++ .../MakeAuthorizationHeaderUseCaseTests.swift | 73 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift diff --git a/Libraries/Package.swift b/Libraries/Package.swift index 902fb07..99713d6 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -4,6 +4,7 @@ import PackageDescription let package = Package( name: "Libraries", + platforms: [.iOS(.v15)], products: [ .library( name: "Libraries", diff --git a/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift b/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift new file mode 100644 index 0000000..20b3080 --- /dev/null +++ b/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift @@ -0,0 +1,46 @@ +// +// MakeAuthorizationHeaderUseCase.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +struct MakeAuthorizationHeaderUseCase { + func callAsFunction( + username: String, + password: String + ) throws -> [String: String] { + guard !username.isEmpty else { throw MakeAuthorizationHeaderError.usernameIsEmpty } + guard !password.isEmpty else { throw MakeAuthorizationHeaderError.passwordIsEmpty } + + let loginString = String(format: .Formats.usernameAndPassword, username, password) + + guard let loginData = loginString.data(using: .utf8) else { + throw MakeAuthorizationHeaderError.loginDataNotCreated + } + + let loginBase64 = loginData.base64EncodedString() + + return [.Header.Keys.authorization: String(format: .Formats.authorizationValue, loginBase64)] + } +} + +// MARK: - Errors + +enum MakeAuthorizationHeaderError: Error { + case usernameIsEmpty + case passwordIsEmpty + case loginDataNotCreated +} + +// MARK: - String+Formats + +private extension String { + enum Formats { + static let usernameAndPassword = "%@:%@" + static let authorizationValue = "Basic %@" + } +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift b/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift new file mode 100644 index 0000000..f4d7b33 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift @@ -0,0 +1,73 @@ +// +// MakeAuthorizationHeaderUseCaseTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import XCTest + +@testable import APIService + +final class MakeAuthorizationHeaderUseCaseTests: XCTestCase { + + // MARK: Properties + + let makeAuthHeader = MakeAuthorizationHeaderUseCase() + + var username: String! + var password: String! + + var result: [String: String]! + + // MARK: Test cases + + func test_withCorrectUsernameAndPassword() throws { + // GIVEN + username = "username" + password = "password" + + // WHEN + + result = try makeAuthHeader(username: username, password: password) + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result[.Header.Keys.authorization], "Basic dXNlcm5hbWU6cGFzc3dvcmQ=") + } + + func test_withEmptyUsername() throws { + // GIVEN + username = "" + password = "password" + + // WHEN & THEN + XCTAssertThrowsError(try makeAuthHeader(username: username, password: password)) { error in + XCTAssertEqual(error as? MakeAuthorizationHeaderError, .usernameIsEmpty) + } + } + + func test_withEmptyPassword() throws { + // GIVEN + username = "username" + password = "" + + // WHEN & THEN + XCTAssertThrowsError(try makeAuthHeader(username: username, password: password)) { error in + XCTAssertEqual(error as? MakeAuthorizationHeaderError, .passwordIsEmpty) + } + } + + func test_withEmptyUsernameAndPassword() throws { + // GIVEN + username = "" + password = "" + + // WHEN & THEN + XCTAssertThrowsError(try makeAuthHeader(username: username, password: password)) { error in + XCTAssertEqual(error as? MakeAuthorizationHeaderError, .usernameIsEmpty) + } + } + +} From 8a6940ee8d9409e336f1f9e7c20350ecda31be69 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 01:15:02 +0100 Subject: [PATCH 04/21] Defined the Endpoint protocol. --- .../Enumerations/RequestMethod.swift | 15 +++++++ .../APIService/Protocols/Endpoint.swift | 43 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 Libraries/Sources/APIService/Enumerations/RequestMethod.swift create mode 100644 Libraries/Sources/APIService/Protocols/Endpoint.swift diff --git a/Libraries/Sources/APIService/Enumerations/RequestMethod.swift b/Libraries/Sources/APIService/Enumerations/RequestMethod.swift new file mode 100644 index 0000000..49deed7 --- /dev/null +++ b/Libraries/Sources/APIService/Enumerations/RequestMethod.swift @@ -0,0 +1,15 @@ +// +// RequestMethod.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +enum RequestMethod: String { + case delete = "DELETE" + case get = "GET" + case patch = "PATCH" + case post = "POST" + case put = "PUT" +} diff --git a/Libraries/Sources/APIService/Protocols/Endpoint.swift b/Libraries/Sources/APIService/Protocols/Endpoint.swift new file mode 100644 index 0000000..8834991 --- /dev/null +++ b/Libraries/Sources/APIService/Protocols/Endpoint.swift @@ -0,0 +1,43 @@ +// +// Endpoint.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +protocol Endpoint { + var scheme: String { get } + var host: String { get } + var path: String { get } + var method: RequestMethod { get } + var credentials: BasicCredentials { get } + var headers: [String: String] { get } + var body: [String: String]? { get } +} + +// MARK: - Defaults + +extension Endpoint { + var scheme: String { "http" } + var host: String { "163.172.147.216:8080" } + var authorizationHeader: [String: String] { + let makeAuthHeader = MakeAuthorizationHeaderUseCase() + + do { + return try makeAuthHeader( + username: credentials.username, + password: credentials.password + ) + } catch { + return [:] + } + } +} + +// MARK: - Structs + +struct BasicCredentials { + let username: String + let password: String +} From bfb47ea0f88971d67adc33d95d3199dfe63bc5d3 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 01:41:32 +0100 Subject: [PATCH 05/21] Implemented the GetMeEndpoint endpoint. --- .../APIService/Endpoints/GetMeEndpoint.swift | 33 ++++++++++ .../Extensions/String+Headers.swift | 20 ++++++ .../APIService/Extensions/String+Hosts.swift | 13 ++++ .../Extensions/String+Schemes.swift | 13 ++++ .../APIService/Protocols/Endpoint.swift | 4 +- .../Endpoints/GetMeEndpoint+InitTests.swift | 63 +++++++++++++++++++ 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift create mode 100644 Libraries/Sources/APIService/Extensions/String+Headers.swift create mode 100644 Libraries/Sources/APIService/Extensions/String+Hosts.swift create mode 100644 Libraries/Sources/APIService/Extensions/String+Schemes.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift diff --git a/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift new file mode 100644 index 0000000..c2ff363 --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift @@ -0,0 +1,33 @@ +// +// GetMeEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +struct GetMeEndpoint: Endpoint { + let path: String + let method: RequestMethod + let credentials: BasicCredentials + let headers: [String : String] + let body: [String : String]? +} + +// MARK: - Initialisers + +extension GetMeEndpoint { + init( + username: String, + password: String + ) { + self.path = "/me" + self.method = .get + self.credentials = .init( + username: username, + password: password + ) + self.headers = [.Header.Keys.contentType: .Header.Values.contentTypeJSON] + self.body = nil + } +} diff --git a/Libraries/Sources/APIService/Extensions/String+Headers.swift b/Libraries/Sources/APIService/Extensions/String+Headers.swift new file mode 100644 index 0000000..f224c8a --- /dev/null +++ b/Libraries/Sources/APIService/Extensions/String+Headers.swift @@ -0,0 +1,20 @@ +// +// String+Headers.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +extension String { + enum Header { + enum Keys { + static let authorization = "Authorization" + static let contentType = "Content-Type" + } + + enum Values { + static let contentTypeJSON = "application/json" + } + } +} diff --git a/Libraries/Sources/APIService/Extensions/String+Hosts.swift b/Libraries/Sources/APIService/Extensions/String+Hosts.swift new file mode 100644 index 0000000..5daa9c4 --- /dev/null +++ b/Libraries/Sources/APIService/Extensions/String+Hosts.swift @@ -0,0 +1,13 @@ +// +// String+Hosts.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +extension String { + enum Hosts { + static let `default` = "163.172.147.216:8080" + } +} diff --git a/Libraries/Sources/APIService/Extensions/String+Schemes.swift b/Libraries/Sources/APIService/Extensions/String+Schemes.swift new file mode 100644 index 0000000..5f7063b --- /dev/null +++ b/Libraries/Sources/APIService/Extensions/String+Schemes.swift @@ -0,0 +1,13 @@ +// +// String+Schemes.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +extension String { + enum Schemes { + static let http = "http" + } +} diff --git a/Libraries/Sources/APIService/Protocols/Endpoint.swift b/Libraries/Sources/APIService/Protocols/Endpoint.swift index 8834991..af12ed8 100644 --- a/Libraries/Sources/APIService/Protocols/Endpoint.swift +++ b/Libraries/Sources/APIService/Protocols/Endpoint.swift @@ -19,8 +19,8 @@ protocol Endpoint { // MARK: - Defaults extension Endpoint { - var scheme: String { "http" } - var host: String { "163.172.147.216:8080" } + var scheme: String { .Schemes.http } + var host: String { .Hosts.default } var authorizationHeader: [String: String] { let makeAuthHeader = MakeAuthorizationHeaderUseCase() diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift new file mode 100644 index 0000000..9befdf0 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift @@ -0,0 +1,63 @@ +// +// GetMeEndpoint+InitTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import XCTest + +@testable import APIService + +final class GetMeEndpointInitTests: XCTestCase { + + // MARK: Properties + + var endpoint: GetMeEndpoint! + var username: String! + var password: String! + + // MARK: Test cases + + func test_withProperUsernameAndPassword() throws { + // GIVEN + username = "username" + password = "password" + + // WHEN + endpoint = .init(username: username, password: password) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/me") + XCTAssertEqual(endpoint.method, .get) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [.Header.Keys.contentType: .Header.Values.contentTypeJSON]) + XCTAssertEqual(endpoint.authorizationHeader, [.Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="]) + XCTAssertNil(endpoint.body) + } + + func test_withEmptyUsernameOrPassword() async throws { + // GIVEN + username = "" + password = "password" + + // WHEN + endpoint = .init(username: username, password: password) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/me") + XCTAssertEqual(endpoint.method, .get) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [.Header.Keys.contentType: .Header.Values.contentTypeJSON]) + XCTAssertEqual(endpoint.authorizationHeader, [:]) + XCTAssertNil(endpoint.body) + } + +} From 6e03a97622a2dd1f34a65e8879cd74fedfc916cc Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 02:00:02 +0100 Subject: [PATCH 06/21] Implemented the GetItemsEndpoint endpoint. --- .../Endpoints/GetItemsEndpoint.swift | 42 +++++++++++ .../GetItemsEndpoint+InitTests.swift | 74 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift diff --git a/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift new file mode 100644 index 0000000..e5564fc --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift @@ -0,0 +1,42 @@ +// +// GetItemsEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +struct GetItemsEndpoint: Endpoint { + let path: String + let method: RequestMethod + let credentials: BasicCredentials + let headers: [String : String] + let body: [String : String]? +} + +// MARK: - Initialisers + +extension GetItemsEndpoint { + init( + itemId: String, + username: String, + password: String + ) { + self.path = .init(format: .Formats.itemsWithId, itemId) + self.method = .get + self.credentials = .init( + username: username, + password: password + ) + self.headers = [.Header.Keys.contentType: .Header.Values.contentTypeJSON] + self.body = nil + } +} + +// MARK: - String+Formats + +private extension String { + enum Formats { + static let itemsWithId = "/items/%@" + } +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift new file mode 100644 index 0000000..5d1fc3d --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift @@ -0,0 +1,74 @@ +// +// GetItemsEndpoint+InitTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import XCTest +import Foundation + +@testable import APIService + +final class GetItemsEndpointInitTests: XCTestCase { + + // MARK: Properties + + let itemId = UUID().uuidString + + var endpoint: GetItemsEndpoint! + var username: String! + var password: String! + + // MARK: Test cases + + func test_withProperUsernameAndPassword() throws { + // GIVEN + username = "username" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/" + itemId) + XCTAssertEqual(endpoint.method, .get) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [.Header.Keys.contentType: .Header.Values.contentTypeJSON]) + XCTAssertEqual(endpoint.authorizationHeader, [.Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="]) + XCTAssertNil(endpoint.body) + } + + func test_withEmptyUsernameOrPassword() async throws { + // GIVEN + username = "" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/" + itemId) + XCTAssertEqual(endpoint.method, .get) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [.Header.Keys.contentType: .Header.Values.contentTypeJSON]) + XCTAssertEqual(endpoint.authorizationHeader, [:]) + XCTAssertNil(endpoint.body) + } + +} From 90d13b265115fdf47a42bc4349b68330f3c8217d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 02:13:40 +0100 Subject: [PATCH 07/21] Implemented the GetDataEndpoint endpoint. --- .../Endpoints/GetDataEndpoint.swift | 42 +++++++++++ .../Endpoints/GetDataEndpoint+InitTests.swift | 74 +++++++++++++++++++ .../GetItemsEndpoint+InitTests.swift | 6 +- 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift diff --git a/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift new file mode 100644 index 0000000..e3ab99a --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift @@ -0,0 +1,42 @@ +// +// GetDataEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +struct GetDataEndpoint: Endpoint { + let path: String + let method: RequestMethod + let credentials: BasicCredentials + let headers: [String : String] + let body: [String : String]? +} + +// MARK: - Initialisers + +extension GetDataEndpoint { + init( + itemId: String, + username: String, + password: String + ) { + self.path = .init(format: .Formats.itemsDataWithId, itemId) + self.method = .get + self.credentials = .init( + username: username, + password: password + ) + self.headers = [:] + self.body = nil + } +} + +// MARK: - String+Formats + +private extension String { + enum Formats { + static let itemsDataWithId = "/items/%@/data" + } +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift new file mode 100644 index 0000000..3803111 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift @@ -0,0 +1,74 @@ +// +// GetDataEndpoint+InitTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class GetDataEndpointInitTests: XCTestCase { + + // MARK: Properties + + let itemId = UUID().uuidString + + var endpoint: GetDataEndpoint! + var username: String! + var password: String! + + // MARK: Test cases + + func test_withItemId_andProperUsernameAndPassword() throws { + // GIVEN + username = "username" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)/data") + XCTAssertEqual(endpoint.method, .get) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [:]) + XCTAssertEqual(endpoint.authorizationHeader, [.Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="]) + XCTAssertNil(endpoint.body) + } + + func test_withItemId_andEmptyUsernameOrPassword() async throws { + // GIVEN + username = "" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)/data") + XCTAssertEqual(endpoint.method, .get) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [:]) + XCTAssertEqual(endpoint.authorizationHeader, [:]) + XCTAssertNil(endpoint.body) + } + +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift index 5d1fc3d..1834a34 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // -import XCTest import Foundation +import XCTest @testable import APIService @@ -23,7 +23,7 @@ final class GetItemsEndpointInitTests: XCTestCase { // MARK: Test cases - func test_withProperUsernameAndPassword() throws { + func test_withItemId_andProperUsernameAndPassword() throws { // GIVEN username = "username" password = "password" @@ -47,7 +47,7 @@ final class GetItemsEndpointInitTests: XCTestCase { XCTAssertNil(endpoint.body) } - func test_withEmptyUsernameOrPassword() async throws { + func test_withItemId_andEmptyUsernameOrPassword() async throws { // GIVEN username = "" password = "password" From 01742cc4c644b2bc12f2ea2410821ee72e11162f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 02:29:46 +0100 Subject: [PATCH 08/21] Implemented the DeleteItemEndpoint endpoint. --- .../Endpoints/DeleteItemEndpoint.swift | 34 +++++++++ .../Endpoints/GetDataEndpoint.swift | 6 +- .../Endpoints/GetItemsEndpoint.swift | 8 -- .../Extensions/String+Formats.swift | 13 ++++ .../MakeAuthorizationHeaderUseCase.swift | 8 +- .../DeleteItemEndpoint+InitTests.swift | 74 +++++++++++++++++++ 6 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift create mode 100644 Libraries/Sources/APIService/Extensions/String+Formats.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift diff --git a/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift b/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift new file mode 100644 index 0000000..7ada098 --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift @@ -0,0 +1,34 @@ +// +// DeleteItemEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +struct DeleteItemEndpoint: Endpoint { + let path: String + let method: RequestMethod + let credentials: BasicCredentials + let headers: [String : String] + let body: [String : String]? +} + +// MARK: - Initialisers + +extension DeleteItemEndpoint { + init( + itemId: String, + username: String, + password: String + ) { + self.path = .init(format: .Formats.itemsWithId, itemId) + self.method = .delete + self.credentials = .init( + username: username, + password: password + ) + self.headers = [:] + self.body = nil + } +} diff --git a/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift index e3ab99a..c21bdf2 100644 --- a/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift +++ b/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift @@ -35,8 +35,6 @@ extension GetDataEndpoint { // MARK: - String+Formats -private extension String { - enum Formats { - static let itemsDataWithId = "/items/%@/data" - } +private extension String.Formats { + static let itemsDataWithId = "/items/%@/data" } diff --git a/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift index e5564fc..c60b263 100644 --- a/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift +++ b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift @@ -32,11 +32,3 @@ extension GetItemsEndpoint { self.body = nil } } - -// MARK: - String+Formats - -private extension String { - enum Formats { - static let itemsWithId = "/items/%@" - } -} diff --git a/Libraries/Sources/APIService/Extensions/String+Formats.swift b/Libraries/Sources/APIService/Extensions/String+Formats.swift new file mode 100644 index 0000000..91ee174 --- /dev/null +++ b/Libraries/Sources/APIService/Extensions/String+Formats.swift @@ -0,0 +1,13 @@ +// +// String+Formats.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +extension String { + enum Formats { + static let itemsWithId = "/items/%@" + } +} diff --git a/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift b/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift index 20b3080..49d7a54 100644 --- a/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift +++ b/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift @@ -38,9 +38,7 @@ enum MakeAuthorizationHeaderError: Error { // MARK: - String+Formats -private extension String { - enum Formats { - static let usernameAndPassword = "%@:%@" - static let authorizationValue = "Basic %@" - } +private extension String.Formats { + static let usernameAndPassword = "%@:%@" + static let authorizationValue = "Basic %@" } diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift new file mode 100644 index 0000000..77c888c --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift @@ -0,0 +1,74 @@ +// +// DeleteItemEndpoint+InitTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class DeleteItemEndpoint_InitTests: XCTestCase { + + // MARK: Properties + + let itemId = UUID().uuidString + + var endpoint: DeleteItemEndpoint! + var username: String! + var password: String! + + // MARK: Test cases + + func test_withItemId_andProperUsernameAndPassword() throws { + // GIVEN + username = "username" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)") + XCTAssertEqual(endpoint.method, .delete) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [:]) + XCTAssertEqual(endpoint.authorizationHeader, [.Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="]) + XCTAssertNil(endpoint.body) + } + + func test_withItemId_andEmptyUsernameOrPassword() async throws { + // GIVEN + username = "" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)") + XCTAssertEqual(endpoint.method, .delete) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [:]) + XCTAssertEqual(endpoint.authorizationHeader, [:]) + XCTAssertNil(endpoint.body) + } + +} From 01d060d5832ba0341a38c75dd8fdcb256a8718ed Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 03:04:57 +0100 Subject: [PATCH 09/21] Updated the type of the "body" property in the Endpoint protocol to the Data type from Foundation. --- .../Sources/APIService/Endpoints/DeleteItemEndpoint.swift | 4 +++- Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift | 4 +++- Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift | 4 +++- Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift | 4 +++- Libraries/Sources/APIService/Protocols/Endpoint.swift | 4 +++- .../Cases/Endpoints/DeleteItemEndpoint+InitTests.swift | 2 +- .../Cases/Endpoints/GetDataEndpoint+InitTests.swift | 2 +- .../Cases/Endpoints/GetItemsEndpoint+InitTests.swift | 2 +- .../Cases/Endpoints/GetMeEndpoint+InitTests.swift | 2 +- 9 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift b/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift index 7ada098..f5b44c7 100644 --- a/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift +++ b/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift @@ -6,12 +6,14 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import Foundation + struct DeleteItemEndpoint: Endpoint { let path: String let method: RequestMethod let credentials: BasicCredentials let headers: [String : String] - let body: [String : String]? + let body: Data? } // MARK: - Initialisers diff --git a/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift index c21bdf2..026bedd 100644 --- a/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift +++ b/Libraries/Sources/APIService/Endpoints/GetDataEndpoint.swift @@ -6,12 +6,14 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import Foundation + struct GetDataEndpoint: Endpoint { let path: String let method: RequestMethod let credentials: BasicCredentials let headers: [String : String] - let body: [String : String]? + let body: Data? } // MARK: - Initialisers diff --git a/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift index c60b263..6e74867 100644 --- a/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift +++ b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift @@ -6,12 +6,14 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import Foundation + struct GetItemsEndpoint: Endpoint { let path: String let method: RequestMethod let credentials: BasicCredentials let headers: [String : String] - let body: [String : String]? + let body: Data? } // MARK: - Initialisers diff --git a/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift index c2ff363..7dbd729 100644 --- a/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift +++ b/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift @@ -6,12 +6,14 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import Foundation + struct GetMeEndpoint: Endpoint { let path: String let method: RequestMethod let credentials: BasicCredentials let headers: [String : String] - let body: [String : String]? + let body: Data? } // MARK: - Initialisers diff --git a/Libraries/Sources/APIService/Protocols/Endpoint.swift b/Libraries/Sources/APIService/Protocols/Endpoint.swift index af12ed8..bac6dfa 100644 --- a/Libraries/Sources/APIService/Protocols/Endpoint.swift +++ b/Libraries/Sources/APIService/Protocols/Endpoint.swift @@ -6,6 +6,8 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import Foundation + protocol Endpoint { var scheme: String { get } var host: String { get } @@ -13,7 +15,7 @@ protocol Endpoint { var method: RequestMethod { get } var credentials: BasicCredentials { get } var headers: [String: String] { get } - var body: [String: String]? { get } + var body: Data? { get } } // MARK: - Defaults diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift index 77c888c..6382881 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift @@ -47,7 +47,7 @@ final class DeleteItemEndpoint_InitTests: XCTestCase { XCTAssertNil(endpoint.body) } - func test_withItemId_andEmptyUsernameOrPassword() async throws { + func test_withItemId_andEmptyUsernameOrPassword() throws { // GIVEN username = "" password = "password" diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift index 3803111..23885cf 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift @@ -47,7 +47,7 @@ final class GetDataEndpointInitTests: XCTestCase { XCTAssertNil(endpoint.body) } - func test_withItemId_andEmptyUsernameOrPassword() async throws { + func test_withItemId_andEmptyUsernameOrPassword() throws { // GIVEN username = "" password = "password" diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift index 1834a34..1e7a2f1 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift @@ -47,7 +47,7 @@ final class GetItemsEndpointInitTests: XCTestCase { XCTAssertNil(endpoint.body) } - func test_withItemId_andEmptyUsernameOrPassword() async throws { + func test_withItemId_andEmptyUsernameOrPassword() throws { // GIVEN username = "" password = "password" diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift index 9befdf0..ddd722e 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift @@ -40,7 +40,7 @@ final class GetMeEndpointInitTests: XCTestCase { XCTAssertNil(endpoint.body) } - func test_withEmptyUsernameOrPassword() async throws { + func test_withEmptyUsernameOrPassword() throws { // GIVEN username = "" password = "password" From a594e75712d2e2e5eea23928cc527ddc5c404dfa Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 03:05:21 +0100 Subject: [PATCH 10/21] Implemented the CreateFolderEndpoint endpoint. --- .../Endpoints/CreateFolderEndpoint.swift | 54 +++++++++++++ .../CreateFolderEndpoint+InitTests.swift | 79 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 Libraries/Sources/APIService/Endpoints/CreateFolderEndpoint.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift diff --git a/Libraries/Sources/APIService/Endpoints/CreateFolderEndpoint.swift b/Libraries/Sources/APIService/Endpoints/CreateFolderEndpoint.swift new file mode 100644 index 0000000..4aa5b3e --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/CreateFolderEndpoint.swift @@ -0,0 +1,54 @@ +// +// CreateFolderEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +struct CreateFolderEndpoint: Endpoint { + let path: String + let method: RequestMethod + let credentials: BasicCredentials + let headers: [String : String] + let body: Data? +} + +// MARK: - Initialisers + +extension CreateFolderEndpoint { + init( + itemId: String, + folderName: String, + username: String, + password: String + ) { + self.path = .init(format: .Formats.itemsWithId, itemId) + self.method = .post + self.credentials = .init( + username: username, + password: password + ) + self.headers = [.Header.Keys.contentType: .Header.Values.contentTypeJSON] + self.body = { + let encoder = JSONEncoder() + let folder = Folder(name: folderName) + + do { + return try encoder.encode(folder) + } catch { + return nil + } + }() + } +} + +// MARK: - Models + +private extension CreateFolderEndpoint { + struct Folder: Encodable { + let name: String + } +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift new file mode 100644 index 0000000..11464f0 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift @@ -0,0 +1,79 @@ +// +// CreateFolderEndpoint+InitTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class CreateFolderEndpointInitTests: XCTestCase { + + // MARK: Properties + + let itemId = UUID().uuidString + + var endpoint: CreateFolderEndpoint! + var folderName: String! + var username: String! + var password: String! + + // MARK: Test cases + + func test_withItemId_someFolderName_andProperUsernameAndPassword() throws { + // GIVEN + folderName = "some-folder" + username = "username" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + folderName: folderName, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)") + XCTAssertEqual(endpoint.method, .post) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [.Header.Keys.contentType: .Header.Values.contentTypeJSON]) + XCTAssertEqual(endpoint.authorizationHeader, [.Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="]) + XCTAssertEqual(endpoint.body, "{\"name\":\"some-folder\"}".data(using: .utf8)) + } + + func test_withItemId_someFolderName_andEmptyUsernameOrPassword() throws { + // GIVEN + folderName = "some-folder" + username = "" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + folderName: folderName, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)") + XCTAssertEqual(endpoint.method, .post) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [.Header.Keys.contentType: .Header.Values.contentTypeJSON]) + XCTAssertEqual(endpoint.authorizationHeader, [:]) + XCTAssertEqual(endpoint.body, "{\"name\":\"some-folder\"}".data(using: .utf8)) + } + +} From 7c8d520a9c562d6ad4daa31fd6d51a1d62b163c9 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 03:28:29 +0100 Subject: [PATCH 11/21] Implemented the UploadFileEndpoint endpoint. --- .../Endpoints/UploadFileEndpoint.swift | 47 ++++++++++ .../Extensions/String+Headers.swift | 2 + .../UploadFileEndpoint+InitTests.swift | 90 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 Libraries/Sources/APIService/Endpoints/UploadFileEndpoint.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift diff --git a/Libraries/Sources/APIService/Endpoints/UploadFileEndpoint.swift b/Libraries/Sources/APIService/Endpoints/UploadFileEndpoint.swift new file mode 100644 index 0000000..75ecdc5 --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/UploadFileEndpoint.swift @@ -0,0 +1,47 @@ +// +// UploadFileEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +struct UploadFileEndpoint: Endpoint { + let path: String + let method: RequestMethod + let credentials: BasicCredentials + let headers: [String : String] + let body: Data? +} + +// MARK: - Initialisers + +extension UploadFileEndpoint { + init( + itemId: String, + fileName: String, + fileData: Data, + username: String, + password: String + ) { + self.path = .init(format: .Formats.itemsWithId, itemId) + self.method = .post + self.credentials = .init( + username: username, + password: password + ) + self.headers = [ + .Header.Keys.contentType: .Header.Values.contentTypeOctet, + .Header.Keys.contentDisposition: .init(format: .Formats.attachment, fileName) + ] + self.body = fileData + } +} + +// MARK: - String+Formats + +private extension String.Formats { + static let attachment = "attachment;filename*=utf-8''%@" +} diff --git a/Libraries/Sources/APIService/Extensions/String+Headers.swift b/Libraries/Sources/APIService/Extensions/String+Headers.swift index f224c8a..cd4eefd 100644 --- a/Libraries/Sources/APIService/Extensions/String+Headers.swift +++ b/Libraries/Sources/APIService/Extensions/String+Headers.swift @@ -11,10 +11,12 @@ extension String { enum Keys { static let authorization = "Authorization" static let contentType = "Content-Type" + static let contentDisposition = "Content-Disposition" } enum Values { static let contentTypeJSON = "application/json" + static let contentTypeOctet = "application/octet-stream" } } } diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift new file mode 100644 index 0000000..4aaf70d --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift @@ -0,0 +1,90 @@ +// +// UploadFileEndpoint+InitTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class UploadFileEndpoint_InitTests: XCTestCase { + + // MARK: Properties + + let itemId = UUID().uuidString + + var endpoint: UploadFileEndpoint! + var fileName: String! + var fileData: Data! + var username: String! + var password: String! + + // MARK: Test cases + + func test_withItemId_someFileNameAndData_andProperUsernameAndPassword() throws { + // GIVEN + fileName = "some-filename.txt" + fileData = "This is some raw text data for testing...".data(using: .utf8) + username = "username" + password = "password" + + // WHEN + endpoint = .init( + itemId: itemId, + fileName: fileName, + fileData: fileData, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)") + XCTAssertEqual(endpoint.method, .post) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [ + .Header.Keys.contentType: .Header.Values.contentTypeOctet, + .Header.Keys.contentDisposition: "attachment;filename*=utf-8''" + fileName + ]) + XCTAssertEqual(endpoint.authorizationHeader, [.Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="]) + XCTAssertEqual(endpoint.body, fileData) + } + + func test_withItemId_someFileNameAndData_andEmptyUsernameOrPassword() throws { + // GIVEN + fileName = "some-filename.txt" + fileData = "This is some raw text data for testing...".data(using: .utf8) + username = "username" + password = "" + + // WHEN + endpoint = .init( + itemId: itemId, + fileName: fileName, + fileData: fileData, + username: username, + password: password + ) + + // THEN + XCTAssertEqual(endpoint.scheme, .Schemes.http) + XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.path, "/items/\(itemId)") + XCTAssertEqual(endpoint.method, .post) + XCTAssertEqual(endpoint.credentials.username, username) + XCTAssertEqual(endpoint.credentials.password, password) + XCTAssertEqual(endpoint.headers, [ + .Header.Keys.contentType: .Header.Values.contentTypeOctet, + .Header.Keys.contentDisposition: "attachment;filename*=utf-8''" + fileName + ]) + XCTAssertEqual(endpoint.authorizationHeader, [:]) + XCTAssertEqual(endpoint.body, fileData) + } + +} From acce70b6bc99231fee2b89c05ed52968fa3fb8b6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 03:49:37 +0100 Subject: [PATCH 12/21] Changed the accessor of the properties in the existing test cases to "private". --- .../Endpoints/CreateFolderEndpoint+InitTests.swift | 10 +++++----- .../Endpoints/DeleteItemEndpoint+InitTests.swift | 8 ++++---- .../Cases/Endpoints/GetDataEndpoint+InitTests.swift | 8 ++++---- .../Cases/Endpoints/GetItemsEndpoint+InitTests.swift | 8 ++++---- .../Cases/Endpoints/GetMeEndpoint+InitTests.swift | 6 +++--- .../Endpoints/UploadFileEndpoint+InitTests.swift | 12 ++++++------ .../MakeAuthorizationHeaderUseCaseTests.swift | 8 ++++---- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift index 11464f0..212a0b3 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift @@ -15,12 +15,12 @@ final class CreateFolderEndpointInitTests: XCTestCase { // MARK: Properties - let itemId = UUID().uuidString + private let itemId = UUID().uuidString - var endpoint: CreateFolderEndpoint! - var folderName: String! - var username: String! - var password: String! + private var endpoint: CreateFolderEndpoint! + private var folderName: String! + private var username: String! + private var password: String! // MARK: Test cases diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift index 6382881..c5be642 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift @@ -15,11 +15,11 @@ final class DeleteItemEndpoint_InitTests: XCTestCase { // MARK: Properties - let itemId = UUID().uuidString + private let itemId = UUID().uuidString - var endpoint: DeleteItemEndpoint! - var username: String! - var password: String! + private var endpoint: DeleteItemEndpoint! + private var username: String! + private var password: String! // MARK: Test cases diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift index 23885cf..f158e3d 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift @@ -15,11 +15,11 @@ final class GetDataEndpointInitTests: XCTestCase { // MARK: Properties - let itemId = UUID().uuidString + private let itemId = UUID().uuidString - var endpoint: GetDataEndpoint! - var username: String! - var password: String! + private var endpoint: GetDataEndpoint! + private var username: String! + private var password: String! // MARK: Test cases diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift index 1e7a2f1..ebf18bf 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift @@ -15,11 +15,11 @@ final class GetItemsEndpointInitTests: XCTestCase { // MARK: Properties - let itemId = UUID().uuidString + private let itemId = UUID().uuidString - var endpoint: GetItemsEndpoint! - var username: String! - var password: String! + private var endpoint: GetItemsEndpoint! + private var username: String! + private var password: String! // MARK: Test cases diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift index ddd722e..151e552 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift @@ -14,9 +14,9 @@ final class GetMeEndpointInitTests: XCTestCase { // MARK: Properties - var endpoint: GetMeEndpoint! - var username: String! - var password: String! + private var endpoint: GetMeEndpoint! + private var username: String! + private var password: String! // MARK: Test cases diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift index 4aaf70d..d91c854 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift @@ -15,13 +15,13 @@ final class UploadFileEndpoint_InitTests: XCTestCase { // MARK: Properties - let itemId = UUID().uuidString + private let itemId = UUID().uuidString - var endpoint: UploadFileEndpoint! - var fileName: String! - var fileData: Data! - var username: String! - var password: String! + private var endpoint: UploadFileEndpoint! + private var fileName: String! + private var fileData: Data! + private var username: String! + private var password: String! // MARK: Test cases diff --git a/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift b/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift index f4d7b33..792e694 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeAuthorizationHeaderUseCaseTests.swift @@ -14,12 +14,12 @@ final class MakeAuthorizationHeaderUseCaseTests: XCTestCase { // MARK: Properties - let makeAuthHeader = MakeAuthorizationHeaderUseCase() + private let makeAuthHeader = MakeAuthorizationHeaderUseCase() - var username: String! - var password: String! + private var username: String! + private var password: String! - var result: [String: String]! + private var result: [String: String]! // MARK: Test cases From 47a8db48ffa63db9585afbb0af3144ea7663280b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 04:07:03 +0100 Subject: [PATCH 13/21] Added the "port" property to the Endpoint protocol. --- .../Sources/APIService/Extensions/Int+Ports.swift | 13 +++++++++++++ .../APIService/Extensions/String+Hosts.swift | 2 +- .../Sources/APIService/Protocols/Endpoint.swift | 2 ++ .../Endpoints/CreateFolderEndpoint+InitTests.swift | 2 ++ .../Endpoints/DeleteItemEndpoint+InitTests.swift | 2 ++ .../Cases/Endpoints/GetDataEndpoint+InitTests.swift | 2 ++ .../Endpoints/GetItemsEndpoint+InitTests.swift | 2 ++ .../Cases/Endpoints/GetMeEndpoint+InitTests.swift | 2 ++ .../Endpoints/UploadFileEndpoint+InitTests.swift | 2 ++ 9 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Libraries/Sources/APIService/Extensions/Int+Ports.swift diff --git a/Libraries/Sources/APIService/Extensions/Int+Ports.swift b/Libraries/Sources/APIService/Extensions/Int+Ports.swift new file mode 100644 index 0000000..73ad179 --- /dev/null +++ b/Libraries/Sources/APIService/Extensions/Int+Ports.swift @@ -0,0 +1,13 @@ +// +// Int+Ports.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +extension Int { + enum Ports { + static let `default` = 8080 + } +} diff --git a/Libraries/Sources/APIService/Extensions/String+Hosts.swift b/Libraries/Sources/APIService/Extensions/String+Hosts.swift index 5daa9c4..b6c6f48 100644 --- a/Libraries/Sources/APIService/Extensions/String+Hosts.swift +++ b/Libraries/Sources/APIService/Extensions/String+Hosts.swift @@ -8,6 +8,6 @@ extension String { enum Hosts { - static let `default` = "163.172.147.216:8080" + static let `default` = "163.172.147.216" } } diff --git a/Libraries/Sources/APIService/Protocols/Endpoint.swift b/Libraries/Sources/APIService/Protocols/Endpoint.swift index bac6dfa..71cd132 100644 --- a/Libraries/Sources/APIService/Protocols/Endpoint.swift +++ b/Libraries/Sources/APIService/Protocols/Endpoint.swift @@ -11,6 +11,7 @@ import Foundation protocol Endpoint { var scheme: String { get } var host: String { get } + var port: Int { get } var path: String { get } var method: RequestMethod { get } var credentials: BasicCredentials { get } @@ -23,6 +24,7 @@ protocol Endpoint { extension Endpoint { var scheme: String { .Schemes.http } var host: String { .Hosts.default } + var port: Int { .Ports.default } var authorizationHeader: [String: String] { let makeAuthHeader = MakeAuthorizationHeaderUseCase() diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift index 212a0b3..e1a0c99 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift @@ -41,6 +41,7 @@ final class CreateFolderEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)") XCTAssertEqual(endpoint.method, .post) XCTAssertEqual(endpoint.credentials.username, username) @@ -67,6 +68,7 @@ final class CreateFolderEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)") XCTAssertEqual(endpoint.method, .post) XCTAssertEqual(endpoint.credentials.username, username) diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift index c5be642..abd63f7 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift @@ -38,6 +38,7 @@ final class DeleteItemEndpoint_InitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)") XCTAssertEqual(endpoint.method, .delete) XCTAssertEqual(endpoint.credentials.username, username) @@ -62,6 +63,7 @@ final class DeleteItemEndpoint_InitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)") XCTAssertEqual(endpoint.method, .delete) XCTAssertEqual(endpoint.credentials.username, username) diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift index f158e3d..158dbe5 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift @@ -38,6 +38,7 @@ final class GetDataEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)/data") XCTAssertEqual(endpoint.method, .get) XCTAssertEqual(endpoint.credentials.username, username) @@ -62,6 +63,7 @@ final class GetDataEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)/data") XCTAssertEqual(endpoint.method, .get) XCTAssertEqual(endpoint.credentials.username, username) diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift index ebf18bf..956d23c 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift @@ -38,6 +38,7 @@ final class GetItemsEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/" + itemId) XCTAssertEqual(endpoint.method, .get) XCTAssertEqual(endpoint.credentials.username, username) @@ -62,6 +63,7 @@ final class GetItemsEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/" + itemId) XCTAssertEqual(endpoint.method, .get) XCTAssertEqual(endpoint.credentials.username, username) diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift index 151e552..235ed6c 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift @@ -31,6 +31,7 @@ final class GetMeEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/me") XCTAssertEqual(endpoint.method, .get) XCTAssertEqual(endpoint.credentials.username, username) @@ -51,6 +52,7 @@ final class GetMeEndpointInitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/me") XCTAssertEqual(endpoint.method, .get) XCTAssertEqual(endpoint.credentials.username, username) diff --git a/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift b/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift index d91c854..6a7edc8 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift @@ -44,6 +44,7 @@ final class UploadFileEndpoint_InitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)") XCTAssertEqual(endpoint.method, .post) XCTAssertEqual(endpoint.credentials.username, username) @@ -75,6 +76,7 @@ final class UploadFileEndpoint_InitTests: XCTestCase { // THEN XCTAssertEqual(endpoint.scheme, .Schemes.http) XCTAssertEqual(endpoint.host, .Hosts.default) + XCTAssertEqual(endpoint.port, .Ports.default) XCTAssertEqual(endpoint.path, "/items/\(itemId)") XCTAssertEqual(endpoint.method, .post) XCTAssertEqual(endpoint.credentials.username, username) From a894a9de47a834ad0c5660c5989595254435212f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 04:29:50 +0100 Subject: [PATCH 14/21] Implemented the MakeURLRequestUseCase use case. --- .../Use Cases/MakeURLRequestUseCase.swift | 36 ++++ .../MakeURLRequestUseCaseTests.swift | 168 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 Libraries/Sources/APIService/Use Cases/MakeURLRequestUseCase.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeURLRequestUseCaseTests.swift diff --git a/Libraries/Sources/APIService/Use Cases/MakeURLRequestUseCase.swift b/Libraries/Sources/APIService/Use Cases/MakeURLRequestUseCase.swift new file mode 100644 index 0000000..f0febbe --- /dev/null +++ b/Libraries/Sources/APIService/Use Cases/MakeURLRequestUseCase.swift @@ -0,0 +1,36 @@ +// +// MakeURLRequestUseCase.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +struct MakeURLRequestUseCase { + func callAsFunction(endpoint: some Endpoint) throws -> URLRequest { + var urlComponents = URLComponents() + + urlComponents.scheme = endpoint.scheme + urlComponents.host = endpoint.host + urlComponents.port = endpoint.port + urlComponents.path = endpoint.path + + guard let url = urlComponents.url else { throw MakeURLRequestError.urlNotCreated } + + var urlRequest = URLRequest(url: url) + + urlRequest.httpMethod = endpoint.method.rawValue + urlRequest.httpBody = endpoint.body + urlRequest.allHTTPHeaderFields = endpoint.authorizationHeader.merging(endpoint.headers) { first, _ in first } + + return urlRequest + } +} + +// MARK: - Errors + +enum MakeURLRequestError: Error { + case urlNotCreated +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeURLRequestUseCaseTests.swift b/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeURLRequestUseCaseTests.swift new file mode 100644 index 0000000..fa6b62c --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Use Cases/MakeURLRequestUseCaseTests.swift @@ -0,0 +1,168 @@ +// +// MakeURLRequestUseCaseTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class MakeURLRequestUseCaseTests: XCTestCase { + + // MARK: Properties + + private let makeURLRequest = MakeURLRequestUseCase() + + private var result: URLRequest! + + // MARK: Test cases + + func test_withGetMeEndpoint() throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + // WHEN + result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://163.172.147.216:8080/me") + XCTAssertEqual(result.httpMethod, "GET") + XCTAssertEqual(result.allHTTPHeaderFields, [ + .Header.Keys.contentType: .Header.Values.contentTypeJSON, + .Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ]) + XCTAssertNil(result.httpBody) + } + + func test_withGetItemsEndpoint() throws { + // GIVEN + let itemId = UUID().uuidString + let endpoint = GetItemsEndpoint( + itemId: itemId, + username: "username", + password: "password" + ) + + // WHEN + result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://163.172.147.216:8080/items/\(itemId)") + XCTAssertEqual(result.httpMethod, "GET") + XCTAssertEqual(result.allHTTPHeaderFields, [ + .Header.Keys.contentType: .Header.Values.contentTypeJSON, + .Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ]) + XCTAssertNil(result.httpBody) + } + + func test_withUploadFileEndpoint() throws { + // GIVEN + let itemId = UUID().uuidString + let fileName = "some-file-name.txt" + let fileData = "This is just a line of text to make some test data".data(using: .utf8)! + let endpoint = UploadFileEndpoint( + itemId: itemId, + fileName: fileName, + fileData: fileData, + username: "username", + password: "password" + ) + + // WHEN + result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://163.172.147.216:8080/items/\(itemId)") + XCTAssertEqual(result.httpMethod, "POST") + XCTAssertEqual(result.allHTTPHeaderFields, [ + .Header.Keys.contentType: .Header.Values.contentTypeOctet, + .Header.Keys.contentDisposition: "attachment;filename*=utf-8''\(fileName)", + .Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ]) + XCTAssertNotNil(result.httpBody) + XCTAssertEqual(result.httpBody, fileData) + } + + func test_withCreateFolderEndpoint() throws { + // GIVEN + let itemId = UUID().uuidString + let folderName = "some-folder-name" + let endpoint = CreateFolderEndpoint( + itemId: itemId, + folderName: folderName, + username: "username", + password: "password" + ) + let body = "{\"name\":\"\(folderName)\"}".data(using: .utf8) + + // WHEN + result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://163.172.147.216:8080/items/\(itemId)") + XCTAssertEqual(result.httpMethod, "POST") + XCTAssertEqual(result.allHTTPHeaderFields, [ + .Header.Keys.contentType: .Header.Values.contentTypeJSON, + .Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ]) + XCTAssertNotNil(result.httpBody) + XCTAssertEqual(result.httpBody, body) + } + + func test_withDeleteItemEndpoint() throws { + // GIVEN + let itemId = UUID().uuidString + let endpoint = DeleteItemEndpoint( + itemId: itemId, + username: "username", + password: "password" + ) + + // WHEN + result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://163.172.147.216:8080/items/\(itemId)") + XCTAssertEqual(result.httpMethod, "DELETE") + XCTAssertEqual(result.allHTTPHeaderFields, [ + .Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ]) + XCTAssertNil(result.httpBody) + } + + func test_withGetDataEndpoint() throws { + // GIVEN + let itemId = UUID().uuidString + let endpoint = GetDataEndpoint( + itemId: itemId, + username: "username", + password: "password" + ) + + // WHEN + result = try makeURLRequest(endpoint: endpoint) + + // THEN + XCTAssertNotNil(result) + XCTAssertEqual(result.url?.absoluteString, "http://163.172.147.216:8080/items/\(itemId)/data") + XCTAssertEqual(result.httpMethod, "GET") + XCTAssertEqual(result.allHTTPHeaderFields, [ + .Header.Keys.authorization: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ]) + XCTAssertNil(result.httpBody) + } + +} From 777a50367cfb95ee7424183a2462fb2db107a7b7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 15:16:31 +0100 Subject: [PATCH 15/21] Defined the Client protocol. --- .../Sources/APIService/Protocols/Client.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Libraries/Sources/APIService/Protocols/Client.swift diff --git a/Libraries/Sources/APIService/Protocols/Client.swift b/Libraries/Sources/APIService/Protocols/Client.swift new file mode 100644 index 0000000..48a3727 --- /dev/null +++ b/Libraries/Sources/APIService/Protocols/Client.swift @@ -0,0 +1,14 @@ +// +// Client.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +protocol Client { + func request(endpoint: some Endpoint, model: Model.Type) async throws -> Model + @discardableResult func request(endpoint: some Endpoint) async throws -> Data +} From 3a87c57371446971a0e67dd993d3e597fbf57481 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 15:57:59 +0100 Subject: [PATCH 16/21] Implemented the MockURLProtocol class. --- .../APIService/Classes/MockURLProtocol.swift | 65 +++++++++++++++++++ .../Enumerations/ResponseStatus.swift | 15 +++++ .../Sources/APIService/Models/Item.swift | 2 +- 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 Libraries/Sources/APIService/Classes/MockURLProtocol.swift create mode 100644 Libraries/Sources/APIService/Enumerations/ResponseStatus.swift diff --git a/Libraries/Sources/APIService/Classes/MockURLProtocol.swift b/Libraries/Sources/APIService/Classes/MockURLProtocol.swift new file mode 100644 index 0000000..3c1e321 --- /dev/null +++ b/Libraries/Sources/APIService/Classes/MockURLProtocol.swift @@ -0,0 +1,65 @@ +// +// MockURLProtocol.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +class MockURLProtocol: URLProtocol { + + // MARK: Properties + + static var mockData: [URL: MockURLResponse] = [:] + + // MARK: Functions + + override class func canInit(with task: URLSessionTask) -> Bool { + true + } + + override class func canInit(with request: URLRequest) -> Bool { + true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + guard + let url = request.url, + let response = Self.mockData[url] + else { + client?.urlProtocolDidFinishLoading(self) + return + } + + if let data = response.data { + client?.urlProtocol(self, didLoad: data) + } + + if let httpResponse = HTTPURLResponse( + url: url, + statusCode: response.status.rawValue, + httpVersion: nil, + headerFields: response.headers + ) { + client?.urlProtocol(self, didReceive: httpResponse, cacheStoragePolicy: .allowedInMemoryOnly) + } + + client?.urlProtocolDidFinishLoading(self) + } + + override func stopLoading() {} +} + +// MARK: - Structs + +struct MockURLResponse { + let status: ResponseStatus + let headers: [String: String] + let data: Data? +} diff --git a/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift new file mode 100644 index 0000000..1338d68 --- /dev/null +++ b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift @@ -0,0 +1,15 @@ +// +// ResponseStatus.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +enum ResponseStatus: Int { + case ok = 200 + case created = 201 + case noContent = 204 + case badRequest = 400 + case notFound = 404 +} diff --git a/Libraries/Sources/APIService/Models/Item.swift b/Libraries/Sources/APIService/Models/Item.swift index b061629..78b7a24 100644 --- a/Libraries/Sources/APIService/Models/Item.swift +++ b/Libraries/Sources/APIService/Models/Item.swift @@ -8,7 +8,7 @@ import Foundation -struct Item { +public struct Item { let idParent: String? let id: String let name: String From 74a22d5844358d526efce5e39d2c80414a53c9d6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 17:21:11 +0100 Subject: [PATCH 17/21] Implemented the APIClient client. --- .../Extensions/DateFormatter+Formatters.swift | 27 ++ .../Sources/APIService/Models/Item.swift | 4 + Libraries/Sources/APIService/Models/Me.swift | 4 + .../APIService/Structs/APIClient.swift | 113 ++++++ .../Structs/APIClient+RequestTests.swift | 337 ++++++++++++++++++ 5 files changed, 485 insertions(+) create mode 100644 Libraries/Sources/APIService/Extensions/DateFormatter+Formatters.swift create mode 100644 Libraries/Sources/APIService/Structs/APIClient.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift diff --git a/Libraries/Sources/APIService/Extensions/DateFormatter+Formatters.swift b/Libraries/Sources/APIService/Extensions/DateFormatter+Formatters.swift new file mode 100644 index 0000000..323e44d --- /dev/null +++ b/Libraries/Sources/APIService/Extensions/DateFormatter+Formatters.swift @@ -0,0 +1,27 @@ +// +// DateFormatter+Formatter.swift +// APIServices +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +extension DateFormatter { + static let iso8601 = { + let dateFormatter = DateFormatter() + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + + return dateFormatter + }() + + static let isoZulu = { + let dateFormatter = DateFormatter() + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + + return dateFormatter + }() +} diff --git a/Libraries/Sources/APIService/Models/Item.swift b/Libraries/Sources/APIService/Models/Item.swift index 78b7a24..e607a77 100644 --- a/Libraries/Sources/APIService/Models/Item.swift +++ b/Libraries/Sources/APIService/Models/Item.swift @@ -31,3 +31,7 @@ extension Item: Decodable { case contentType } } + +// MARK: - Equatable + +extension Item: Equatable {} diff --git a/Libraries/Sources/APIService/Models/Me.swift b/Libraries/Sources/APIService/Models/Me.swift index 43b9bcf..35c1b87 100644 --- a/Libraries/Sources/APIService/Models/Me.swift +++ b/Libraries/Sources/APIService/Models/Me.swift @@ -15,3 +15,7 @@ public struct Me { // MARK: - Decodable extension Me: Decodable {} + +// MARK: - Equatable + +extension Me: Equatable {} diff --git a/Libraries/Sources/APIService/Structs/APIClient.swift b/Libraries/Sources/APIService/Structs/APIClient.swift new file mode 100644 index 0000000..47751cd --- /dev/null +++ b/Libraries/Sources/APIService/Structs/APIClient.swift @@ -0,0 +1,113 @@ +// +// APIClient.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +struct APIClient { + private let session: URLSession + private let decoder: JSONDecoder + private let makeURLRequest: MakeURLRequestUseCase +} + +// MARK: - Initialisers + +extension APIClient { + init(configuration: URLSessionConfiguration = .default) { + self.session = .init(configuration: configuration) + self.decoder = .init() + self.makeURLRequest = .init() + } +} + + +// MARK: - Client + +extension APIClient: Client { + func request( + endpoint: some Endpoint, + model: Model.Type + ) async throws -> Model where Model : Decodable { + let urlRequest = try makeURLRequest(endpoint: endpoint) + let (data, response) = try await session.data(for: urlRequest) + + guard + let httpResponse = response as? HTTPURLResponse, + let responseStatus = ResponseStatus(rawValue: httpResponse.statusCode) + else { + throw APIClientError.responseNotReturned + } + + switch responseStatus { + case .ok, + .created: + return try await decode(data, as: model) + case .noContent: + throw APIClientError.notSupported + case .badRequest: + throw APIClientError.itemAlreadyExist + case .notFound: + throw APIClientError.itemDoesNotExist + } + } + + @discardableResult + func request( + endpoint: some Endpoint + ) async throws -> Data { + let urlRequest = try makeURLRequest(endpoint: endpoint) + let (data, response) = try await session.data(for: urlRequest) + + guard + let httpResponse = response as? HTTPURLResponse, + let responseStatus = ResponseStatus(rawValue: httpResponse.statusCode) + else { + throw APIClientError.responseNotReturned + } + + switch responseStatus { + case .ok, + .noContent: + return data + case .created: + throw APIClientError.notSupported + case .badRequest: + throw APIClientError.itemIsNotFile + case .notFound: + throw APIClientError.itemDoesNotExist + } + } +} + +// MARK: - Helpers + +private extension APIClient { + func decode( + _ data: Data, + as model: Model.Type + ) async throws -> Model { + do { + decoder.dateDecodingStrategy = .formatted(.isoZulu) + + return try decoder.decode(model, from: data) + } catch { + decoder.dateDecodingStrategy = .iso8601 + + return try decoder.decode(model, from: data) + } + } +} + +// MARK: - Errors + +public enum APIClientError: Error { + case responseNotReturned + case itemIsNotFile + case itemAlreadyExist + case itemDoesNotExist + case notSupported +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift new file mode 100644 index 0000000..00611a3 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift @@ -0,0 +1,337 @@ +// +// APIClient+RequestTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class APIClientRequestTests: XCTestCase { + + // MARK: Properties + + private let makeURLRequest = MakeURLRequestUseCase() + private let dateFormatter = DateFormatter.iso8601 + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private var client: APIClient! + private var url: URL! + private var data: Data! + + // MARK: Setup + + override func setUp() async throws { + client = .init(configuration: sessionConfiguration) + } + + // MARK: Request cases + + func test_withSomeEndpoint_andSomeModel_whenResponseStatusOk() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"parentId\":\"\",\"name\":\"dossierTest\",\"isDir\":true,\"modificationDate\":\"2021-11-29T10:57:13Z\"}}\n".data(using: .utf8) + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .ok, + headers: [:], + data: data + ) + + // WHEN + let result = try await client.request(endpoint: endpoint, model: Me.self) + + // THEN + XCTAssertEqual(result, Me( + firstName: "Noel", + lastName: "Flantier", + rootItem: .init( + idParent: "", + id: "4b8e41fd4a6a89712f15bbf102421b9338cfab11", + name: "dossierTest", + isDirectory: true, + lastModifiedAt: dateFormatter.date(from: "2021-11-29T10:57:13Z")!, + size: nil, + contentType: nil + ) + )) + } + + func test_withSomeEndpoint_andSomeModel_whenResponseStatusCreated() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"parentId\":\"\",\"name\":\"dossierTest\",\"isDir\":true,\"modificationDate\":\"2021-11-29T10:57:13Z\"}}\n".data(using: .utf8) + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .created, + headers: [:], + data: data + ) + + // WHEN + let result = try await client.request(endpoint: endpoint, model: Me.self) + + // THEN + XCTAssertEqual(result, Me( + firstName: "Noel", + lastName: "Flantier", + rootItem: .init( + idParent: "", + id: "4b8e41fd4a6a89712f15bbf102421b9338cfab11", + name: "dossierTest", + isDirectory: true, + lastModifiedAt: dateFormatter.date(from: "2021-11-29T10:57:13Z")!, + size: nil, + contentType: nil + ) + )) + } + + func test_withSomeEndpoint_andSomeModel_whenResponseStatusNoContent() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .noContent, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await client.request(endpoint: endpoint, model: Me.self) + } catch APIClientError.notSupported { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withSomeEndpoint_andSomeModel_whenResponseStatusBadRequest() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await client.request(endpoint: endpoint, model: Me.self) + } catch APIClientError.itemAlreadyExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withSomeEndpoint_andSomeModel_whenResponseStatusNotFound() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await client.request(endpoint: endpoint, model: Me.self) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withSomeEndpoint_andSomeModel_whenResponseDataIncorrect() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\"\"isDir\":true}}\n".data(using: .utf8) + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .ok, + headers: [:], + data: data + ) + + // WHEN & THEN + do { + _ = try await client.request(endpoint: endpoint, model: Me.self) + } catch is DecodingError { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withSomeEndpoint_whenResponseStatusOk() async throws { + // GIVEN + let endpoint = GetDataEndpoint( + itemId: UUID().uuidString, + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + data = "This is just some dummy data for testing purposes".data(using: .utf8) + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .ok, + headers: [:], + data: data + ) + + // WHEN + let result = try await client.request(endpoint: endpoint) + + // THEN + XCTAssertEqual(result, data) + } + + func test_withSomeEndpoint_whenResponseStatusCreated() async throws { + // GIVEN + let endpoint = GetDataEndpoint( + itemId: UUID().uuidString, + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .created, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await client.request(endpoint: endpoint) + } catch APIClientError.notSupported { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withSomeEndpoint_whenResponseStatusNoContent() async throws { + // GIVEN + let endpoint = GetDataEndpoint( + itemId: UUID().uuidString, + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + data = .init() + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .noContent, + headers: [:], + data: nil + ) + + // WHEN + let result = try await client.request(endpoint: endpoint) + + // THEN + XCTAssertEqual(result, data) + } + + func test_withSomeEndpoint_whenResponseStatusBadRequest() async throws { + // GIVEN + let endpoint = GetDataEndpoint( + itemId: UUID().uuidString, + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await client.request(endpoint: endpoint) + } catch APIClientError.itemIsNotFile { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withSomeEndpoint_whenResponseStatusNotFound() async throws { + // GIVEN + let endpoint = GetDataEndpoint( + itemId: UUID().uuidString, + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await client.request(endpoint: endpoint) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} From 29e19d8fdf1b649f328b95d6f98dfcd53ed2d753 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 17:50:15 +0100 Subject: [PATCH 18/21] Added support for the "forbidden" response status in the APIClient client. --- .../Enumerations/ResponseStatus.swift | 1 + .../APIService/Structs/APIClient.swift | 5 ++ .../Structs/APIClient+RequestTests.swift | 51 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift index 1338d68..8d09e02 100644 --- a/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift +++ b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift @@ -11,5 +11,6 @@ enum ResponseStatus: Int { case created = 201 case noContent = 204 case badRequest = 400 + case forbidden = 403 case notFound = 404 } diff --git a/Libraries/Sources/APIService/Structs/APIClient.swift b/Libraries/Sources/APIService/Structs/APIClient.swift index 47751cd..2397fd3 100644 --- a/Libraries/Sources/APIService/Structs/APIClient.swift +++ b/Libraries/Sources/APIService/Structs/APIClient.swift @@ -50,6 +50,8 @@ extension APIClient: Client { throw APIClientError.notSupported case .badRequest: throw APIClientError.itemAlreadyExist + case .forbidden: + throw APIClientError.authenticationFailed case .notFound: throw APIClientError.itemDoesNotExist } @@ -77,6 +79,8 @@ extension APIClient: Client { throw APIClientError.notSupported case .badRequest: throw APIClientError.itemIsNotFile + case .forbidden: + throw APIClientError.authenticationFailed case .notFound: throw APIClientError.itemDoesNotExist } @@ -106,6 +110,7 @@ private extension APIClient { public enum APIClientError: Error { case responseNotReturned + case authenticationFailed case itemIsNotFile case itemAlreadyExist case itemDoesNotExist diff --git a/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift index 00611a3..2c2e977 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift @@ -157,6 +157,31 @@ final class APIClientRequestTests: XCTestCase { } } + func test_withSomeEndpoint_andSomeModel_whenResponseStatusForbidden() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .forbidden, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await client.request(endpoint: endpoint, model: Me.self) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + func test_withSomeEndpoint_andSomeModel_whenResponseStatusNotFound() async throws { // GIVEN let endpoint = GetMeEndpoint( @@ -308,6 +333,32 @@ final class APIClientRequestTests: XCTestCase { } } + func test_withSomeEndpoint_whenResponseStatusForbidden() async throws { + // GIVEN + let endpoint = GetDataEndpoint( + itemId: UUID().uuidString, + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .forbidden, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await client.request(endpoint: endpoint) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + func test_withSomeEndpoint_whenResponseStatusNotFound() async throws { // GIVEN let endpoint = GetDataEndpoint( From 7670d5b7a04c544d7da96352a23e47b17a316da2 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 17:59:06 +0100 Subject: [PATCH 19/21] Added support for the "unauthorized" response status in the APIClient client. --- .../Enumerations/ResponseStatus.swift | 1 + .../APIService/Structs/APIClient.swift | 6 ++- .../Structs/APIClient+RequestTests.swift | 51 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift index 8d09e02..0895820 100644 --- a/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift +++ b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift @@ -11,6 +11,7 @@ enum ResponseStatus: Int { case created = 201 case noContent = 204 case badRequest = 400 + case unauthorized = 401 case forbidden = 403 case notFound = 404 } diff --git a/Libraries/Sources/APIService/Structs/APIClient.swift b/Libraries/Sources/APIService/Structs/APIClient.swift index 2397fd3..ca7c67f 100644 --- a/Libraries/Sources/APIService/Structs/APIClient.swift +++ b/Libraries/Sources/APIService/Structs/APIClient.swift @@ -50,7 +50,8 @@ extension APIClient: Client { throw APIClientError.notSupported case .badRequest: throw APIClientError.itemAlreadyExist - case .forbidden: + case .unauthorized, + .forbidden: throw APIClientError.authenticationFailed case .notFound: throw APIClientError.itemDoesNotExist @@ -79,7 +80,8 @@ extension APIClient: Client { throw APIClientError.notSupported case .badRequest: throw APIClientError.itemIsNotFile - case .forbidden: + case .unauthorized, + .forbidden: throw APIClientError.authenticationFailed case .notFound: throw APIClientError.itemDoesNotExist diff --git a/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift index 2c2e977..40851f0 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift @@ -157,6 +157,31 @@ final class APIClientRequestTests: XCTestCase { } } + func test_withSomeEndpoint_andSomeModel_whenResponseStatusUnauthorized() async throws { + // GIVEN + let endpoint = GetMeEndpoint( + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await client.request(endpoint: endpoint, model: Me.self) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + func test_withSomeEndpoint_andSomeModel_whenResponseStatusForbidden() async throws { // GIVEN let endpoint = GetMeEndpoint( @@ -333,6 +358,32 @@ final class APIClientRequestTests: XCTestCase { } } + func test_withSomeEndpoint_whenResponseStatusUnauthorized() async throws { + // GIVEN + let endpoint = GetDataEndpoint( + itemId: UUID().uuidString, + username: "username", + password: "password" + ) + + url = try makeURLRequest(endpoint: endpoint).url + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await client.request(endpoint: endpoint) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + func test_withSomeEndpoint_whenResponseStatusForbidden() async throws { // GIVEN let endpoint = GetDataEndpoint( From f748c11886480cbbd9771866fff2b0bcf8b50a98 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 23:03:42 +0100 Subject: [PATCH 20/21] Defined the Service protocol. --- .../APIService/Protocols/Service.swift | 30 +++++++++++++++++++ .../APIService/Structs/APIClient.swift | 4 +-- .../Structs/APIClient+RequestTests.swift | 6 +++- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 Libraries/Sources/APIService/Protocols/Service.swift diff --git a/Libraries/Sources/APIService/Protocols/Service.swift b/Libraries/Sources/APIService/Protocols/Service.swift new file mode 100644 index 0000000..0f559f3 --- /dev/null +++ b/Libraries/Sources/APIService/Protocols/Service.swift @@ -0,0 +1,30 @@ +// +// Service.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +public protocol Service { + func getUser(credentials: Credentials) async throws -> Me + func getItems(id: String, credentials: Credentials) async throws -> [Item] + func getData(id: String, credentials: Credentials) async throws -> Data + func createFolder(id: String, name: String, credentials: Credentials) async throws -> Item + func uploadFile(id: String, file: File, credentials: Credentials) async throws -> Item + func deleteItem(id: String, credentials: Credentials) async throws +} + +// MARK: - Structs + +public struct Credentials { + let username: String + let password: String +} + +public struct File { + let name: String + let data: Data +} diff --git a/Libraries/Sources/APIService/Structs/APIClient.swift b/Libraries/Sources/APIService/Structs/APIClient.swift index ca7c67f..2295426 100644 --- a/Libraries/Sources/APIService/Structs/APIClient.swift +++ b/Libraries/Sources/APIService/Structs/APIClient.swift @@ -49,7 +49,7 @@ extension APIClient: Client { case .noContent: throw APIClientError.notSupported case .badRequest: - throw APIClientError.itemAlreadyExist + throw APIClientError.itemNameInvalidOrDefined case .unauthorized, .forbidden: throw APIClientError.authenticationFailed @@ -114,7 +114,7 @@ public enum APIClientError: Error { case responseNotReturned case authenticationFailed case itemIsNotFile - case itemAlreadyExist + case itemNameInvalidOrDefined case itemDoesNotExist case notSupported } diff --git a/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift index 40851f0..0949ea0 100644 --- a/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift +++ b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift @@ -35,6 +35,10 @@ final class APIClientRequestTests: XCTestCase { client = .init(configuration: sessionConfiguration) } + override func tearDown() async throws { + client = nil + } + // MARK: Request cases func test_withSomeEndpoint_andSomeModel_whenResponseStatusOk() async throws { @@ -150,7 +154,7 @@ final class APIClientRequestTests: XCTestCase { // WHEN & THEN do { _ = try await client.request(endpoint: endpoint, model: Me.self) - } catch APIClientError.itemAlreadyExist { + } catch APIClientError.itemNameInvalidOrDefined { XCTAssertTrue(true) } catch { XCTAssertTrue(false) From 571724b012bf544ad31371c22639cd74f39ae5d4 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 4 Dec 2022 23:04:22 +0100 Subject: [PATCH 21/21] Implemented the APIService service. --- .../APIService/Actors/APIService.swift | 112 ++++++++ .../Actors/APIService+CreateFolderTests.swift | 228 ++++++++++++++++ .../Actors/APIService+DeleteItemTests.swift | 136 ++++++++++ .../Actors/APIService+GetDataTests.swift | 164 ++++++++++++ .../Actors/APIService+GetItemsTests.swift | 183 +++++++++++++ .../Actors/APIService+GetUserTests.swift | 120 +++++++++ .../Actors/APIService+UploadFileTests.swift | 247 ++++++++++++++++++ 7 files changed, 1190 insertions(+) create mode 100644 Libraries/Sources/APIService/Actors/APIService.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Actors/APIService+CreateFolderTests.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Actors/APIService+DeleteItemTests.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetDataTests.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetItemsTests.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetUserTests.swift create mode 100644 Libraries/Tests/APIServiceTests/Cases/Actors/APIService+UploadFileTests.swift diff --git a/Libraries/Sources/APIService/Actors/APIService.swift b/Libraries/Sources/APIService/Actors/APIService.swift new file mode 100644 index 0000000..47d59d4 --- /dev/null +++ b/Libraries/Sources/APIService/Actors/APIService.swift @@ -0,0 +1,112 @@ +// +// APIService.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +public actor APIService { + + // MARK: Properties + + private let client: Client + + // MARK: Initialisers + + public init(configuration: URLSessionConfiguration = .default) { + self.client = APIClient(configuration: configuration) + } + +} + +// MARK: - Service + +extension APIService: Service { + public func getUser( + credentials: Credentials + ) async throws -> Me { + try await client.request( + endpoint: GetMeEndpoint( + username: credentials.username, + password: credentials.password + ), + model: Me.self + ) + } + + public func getItems( + id: String, + credentials: Credentials + ) async throws -> [Item] { + try await client.request( + endpoint: GetItemsEndpoint( + itemId: id, + username: credentials.username, + password: credentials.password + ), + model: [Item].self + ) + } + + public func getData( + id: String, + credentials: Credentials + ) async throws -> Data { + try await client.request( + endpoint: GetDataEndpoint( + itemId: id, + username: credentials.username, + password: credentials.password + ) + ) + } + + public func createFolder( + id: String, + name: String, + credentials: Credentials + ) async throws -> Item { + try await client.request( + endpoint: CreateFolderEndpoint( + itemId: id, + folderName: name, + username: credentials.username, + password: credentials.password + ), + model: Item.self + ) + } + + public func uploadFile( + id: String, + file: File, + credentials: Credentials + ) async throws -> Item { + try await client.request( + endpoint: UploadFileEndpoint( + itemId: id, + fileName: file.name, + fileData: file.data, + username: credentials.username, + password: credentials.password + ), + model: Item.self + ) + } + + public func deleteItem( + id: String, + credentials: Credentials + ) async throws { + try await client.request( + endpoint: DeleteItemEndpoint( + itemId: id, + username: credentials.username, + password: credentials.password + ) + ) + } +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+CreateFolderTests.swift b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+CreateFolderTests.swift new file mode 100644 index 0000000..68b2f22 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+CreateFolderTests.swift @@ -0,0 +1,228 @@ +// +// APIService+CreateFolderTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class APIServiceCreateFolderTests: XCTestCase { + + // MARK: Properties + + private let dateFormatter = DateFormatter.isoZulu + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private let itemId = UUID().uuidString + private let url = URL(string: "http://163.172.147.216:8080/items/")! + + private var service: APIService! + private var data: Data! + private var result: Item! + + // MARK: Setup + + override func setUp() async throws { + service = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + service = nil + } + + // MARK: Test cases + + func test_withExistingParentFolderId_someFolderName_andRightCredentials() async throws { + // GIVEN + data = "{\"id\":\"058c53609cac8d8388b96792cbc42bea31c73def\",\"parentId\":\"\(itemId)\",\"name\":\"Some new folder name\",\"isDir\":true,\"modificationDate\":\"2022-12-04T20:54:12.513214855Z\"}".data(using: .utf8) + + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .created, + headers: [.Header.Keys.contentType: .Header.Values.contentTypeJSON], + data: data + ) + + // WHEN + result = try await service.createFolder( + id: itemId, + name: "Some new folder name", + credentials: .init( + username: "username", + password: "password" + ) + ) + + // THEN + XCTAssertEqual(result, Item( + idParent: itemId, + id: "058c53609cac8d8388b96792cbc42bea31c73def", + name: "Some new folder name", + isDirectory: true, + lastModifiedAt: dateFormatter.date(from: "2022-12-04T20:54:12.513214855Z")!, + size: nil, + contentType: nil + )) + } + + func test_withExistingParentFolderId_someFolderName_andWrongCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.createFolder( + id: itemId, + name: "Some new folder name", + credentials: .init( + username: "usrname", + password: "passwrd" + ) + ) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withNonExistingParentFolderId_someFolderName_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.createFolder( + id: itemId, + name: "Some new folder name", + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withEmptyParentFolderId_someFolderName_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent("")] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.createFolder( + id: "", + name: "Some new folder name", + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withExistingParentFolderId_someInvalidFolderName_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.createFolder( + id: itemId, + name: "./Some:new:folder:name\\", + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withExistingParentFolderId_someExistingFolderName_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.createFolder( + id: itemId, + name: "Some new folder name", + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withExistingParentFolderId_someEmptyFolderName_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.createFolder( + id: itemId, + name: "", + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+DeleteItemTests.swift b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+DeleteItemTests.swift new file mode 100644 index 0000000..b912e16 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+DeleteItemTests.swift @@ -0,0 +1,136 @@ +// +// APIService+DeleteItemTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class APIServiceDeleteItemTests: XCTestCase { + + // MARK: Properties + + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private let itemId = UUID().uuidString + private let url = URL(string: "http://163.172.147.216:8080/items/")! + + private var service: APIService! + + // MARK: Setup + + override func setUp() async throws { + service = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + service = nil + } + + // MARK: Test cases + + func test_withExistingId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .noContent, + headers: [:], + data: nil + ) + + // WHEN + try await service.deleteItem( + id: itemId, + credentials: .init( + username: "username", + password: "password" + ) + ) + + // THEN + XCTAssertTrue(true) + } + + func test_withExistingId_andWrongCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await service.deleteItem( + id: itemId, + credentials: .init( + username: "usrname", + password: "passwrd" + ) + ) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withNonExistingId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await service.deleteItem( + id: itemId, + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withEmptyId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent("")] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + try await service.deleteItem( + id: "", + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetDataTests.swift b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetDataTests.swift new file mode 100644 index 0000000..26d063b --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetDataTests.swift @@ -0,0 +1,164 @@ +// +// APIService+GetDataTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class APIServiceGetDataTests: XCTestCase { + + // MARK: Properties + + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private let itemId = UUID().uuidString + private let url = URL(string: "http://163.172.147.216:8080/items/")! + + private var service: APIService! + private var data: Data! + private var result: Data! + + // MARK: Setup + + override func setUp() async throws { + service = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + service = nil + } + + // MARK: Test cases + + func test_withExistingFileId_andRightCredentials() async throws { + // GIVEN + data = "This is just a simple text for testing purposes.".data(using: .utf8) + + MockURLProtocol.mockData[url.appendingPathComponent(itemId + "/data")] = MockURLResponse( + status: .ok, + headers: [.Header.Keys.contentType: .Header.Values.contentTypeOctet], + data: data + ) + + // WHEN + result = try await service.getData( + id: itemId, + credentials: .init( + username: "username", + password: "password" + ) + ) + + // THEN + XCTAssertEqual(result, data) + } + + func test_withExistingFileId_andWrongCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId + "/data")] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getData( + id: itemId, + credentials: .init( + username: "usrname", + password: "passwrd" + ) + ) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withNonExistingFileId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId + "/data")] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getData( + id: itemId, + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withEmptyFileId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent("/data")] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getData( + id: "", + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemIsNotFile { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withSomeFolderId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId + "/data")] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getData( + id: itemId, + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemIsNotFile { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetItemsTests.swift b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetItemsTests.swift new file mode 100644 index 0000000..2446f7a --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetItemsTests.swift @@ -0,0 +1,183 @@ +// +// APIService+GetItemsTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class APIServiceGetItemsTests: XCTestCase { + + // MARK: Properties + + private let dateFormatter = DateFormatter.isoZulu + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private let itemId = UUID().uuidString + private let url = URL(string: "http://163.172.147.216:8080/items/")! + + private var service: APIService! + private var data: Data! + private var result: [Item]! + + // MARK: Setup + + override func setUp() async throws { + service = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + service = nil + } + + // MARK: Test cases + + func test_withExistingId_andRightCredentials_whenDataArrayPopulated() async throws { + // GIVEN + data = "[{\"id\":\"a077432ceb69b4f2dcbd4932d3ec63c3a4f14784\",\"parentId\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"name\":\"Tokyo\",\"isDir\":true,\"modificationDate\":\"2022-11-25T17:33:57.095027128Z\"},{\"id\":\"f5d351f7e532cae7c7be28488564b567ffeb425a\",\"parentId\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"name\":\"Meme.jpg\",\"isDir\":false,\"size\":43144,\"contentType\":\"image/jpeg\",\"modificationDate\":\"2022-12-01T15:24:12.29816675Z\"}]\n".data(using: .utf8) + + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .ok, + headers: [.Header.Keys.contentType: .Header.Values.contentTypeJSON], + data: data + ) + + // WHEN + result = try await service.getItems( + id: itemId, + credentials: .init( + username: "username", + password: "password" + ) + ) + + // THEN + XCTAssertEqual(result, [ + .init( + idParent: "4b8e41fd4a6a89712f15bbf102421b9338cfab11", + id: "a077432ceb69b4f2dcbd4932d3ec63c3a4f14784", + name: "Tokyo", + isDirectory: true, + lastModifiedAt: dateFormatter.date(from: "2022-11-25T17:33:57.095027128Z")!, + size: nil, + contentType: nil + ), + .init( + idParent: "4b8e41fd4a6a89712f15bbf102421b9338cfab11", + id: "f5d351f7e532cae7c7be28488564b567ffeb425a", + name: "Meme.jpg", + isDirectory: false, + lastModifiedAt: dateFormatter.date(from: "2022-12-01T15:24:12.29816675Z")!, + size: 43144, + contentType: "image/jpeg" + ), + ]) + } + + func test_withExistingId_andRightCredentials_whenDataArrayEmpty() async throws { + // GIVEN + data = "[]".data(using: .utf8) + + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .ok, + headers: [.Header.Keys.contentType: .Header.Values.contentTypeJSON], + data: data + ) + + // WHEN + result = try await service.getItems( + id: itemId, + credentials: .init( + username: "username", + password: "password" + ) + ) + + // THEN + XCTAssertEqual(result, []) + } + + func test_withExistingId_andWrongCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getItems( + id: itemId, + credentials: .init( + username: "usrname", + password: "passwrd" + ) + ) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withNonExistingId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent("xxx")] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getItems( + id: "xxx", + credentials: .init( + username: "usrname", + password: "passwrd" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withEmptyId_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent("")] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getItems( + id: "", + credentials: .init( + username: "usrname", + password: "passwrd" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetUserTests.swift b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetUserTests.swift new file mode 100644 index 0000000..b91c972 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+GetUserTests.swift @@ -0,0 +1,120 @@ +// +// APIService+GetUserTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class APIServiceGetUserTests: XCTestCase { + + // MARK: Properties + + private let dateFormatter = DateFormatter.iso8601 + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private let url = URL(string: "http://163.172.147.216:8080/me")! + + private var service: APIService! + private var data: Data! + private var result: Me! + + // MARK: Setup + + override func setUp() async throws { + service = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + service = nil + } + + // MARK: Test cases + + func test_withRightCredentials() async throws { + // GIVEN + data = "{\"firstName\":\"Noel\",\"lastName\":\"Flantier\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\",\"parentId\":\"\",\"name\":\"dossierTest\",\"isDir\":true,\"modificationDate\":\"2021-11-29T10:57:13Z\"}}\n".data(using: .utf8) + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .ok, + headers: [.Header.Keys.contentType: .Header.Values.contentTypeJSON], + data: data + ) + + // WHEN + result = try await service.getUser(credentials: .init( + username: "username", + password: "password" + )) + + // THEN + XCTAssertEqual(result, Me( + firstName: "Noel", + lastName: "Flantier", + rootItem: .init( + idParent: "", + id: "4b8e41fd4a6a89712f15bbf102421b9338cfab11", + name: "dossierTest", + isDirectory: true, + lastModifiedAt: dateFormatter.date(from: "2021-11-29T10:57:13Z")!, + size: nil, + contentType: nil + ) + )) + } + + func test_withWrongCredentials() async throws { + MockURLProtocol.mockData[url] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.getUser(credentials: .init( + username: "usrname", + password: "passwrd" + )) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withMalformedResponseData() async throws { + // GIVEN + data = "{\"firstName\":\"Noel\",\"rootItem\":{\"id\":\"4b8e41fd4a6a89712f15bbf102421b9338cfab11\"\"name\":\"dossierTest\",\"isDir\":true}}\n".data(using: .utf8) + + MockURLProtocol.mockData[url] = MockURLResponse( + status: .ok, + headers: [.Header.Keys.contentType: .Header.Values.contentTypeJSON], + data: data + ) + + // WHEN & THEN + do { + _ = try await service.getUser(credentials: .init( + username: "usrname", + password: "passwrd" + )) + } catch is DecodingError { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +} diff --git a/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+UploadFileTests.swift b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+UploadFileTests.swift new file mode 100644 index 0000000..6b9890b --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Actors/APIService+UploadFileTests.swift @@ -0,0 +1,247 @@ +// +// APIService+UploadFileTests.swift +// APIServiceTests +// +// Created by Javier Cicchelli on 04/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation +import XCTest + +@testable import APIService + +final class APIServiceUploadFileTests: XCTestCase { + + // MARK: Properties + + private let dateFormatter = DateFormatter.isoZulu + private let sessionConfiguration = { + let configuration = URLSessionConfiguration.default + + configuration.protocolClasses = [MockURLProtocol.self] + + return configuration + }() + + private let itemId = UUID().uuidString + private let url = URL(string: "http://163.172.147.216:8080/items/")! + + private var service: APIService! + private var data: Data! + private var result: Item! + + // MARK: Setup + + override func setUp() async throws { + service = .init(configuration: sessionConfiguration) + } + + override func tearDown() async throws { + service = nil + } + + // MARK: Test cases + + func test_withExistingParentFolderId_someFileNameAndData_andRightCredentials() async throws { + // GIVEN + data = "{\"id\":\"eb34443b0f1cd1b0a53dc889aa7ccb5a63edb2f8\",\"parentId\":\"\(itemId)\",\"name\":\"some-text-file.txt\",\"isDir\":false,\"size\":43,\"contentType\":\"text/plain; charset=utf-8\",\"modificationDate\":\"2022-12-04T21:20:01.218032276Z\"}".data(using: .utf8) + + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .created, + headers: [.Header.Keys.contentType: .Header.Values.contentTypeJSON], + data: data + ) + + // WHEN + result = try await service.uploadFile( + id: itemId, + file: .init( + name: "some-text-file.txt", + data: "This is just a dummy content in text format".data(using: .utf8)! + ), + credentials: .init( + username: "username", + password: "password" + ) + ) + + // THEN + XCTAssertEqual(result, Item( + idParent: itemId, + id: "eb34443b0f1cd1b0a53dc889aa7ccb5a63edb2f8", + name: "some-text-file.txt", + isDirectory: false, + lastModifiedAt: dateFormatter.date(from: "2022-12-04T21:20:01.218032276Z")!, + size: 43, + contentType: "text/plain; charset=utf-8" + )) + } + + func test_withExistingParentFolderId_someFileNameAndData_andWrongCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .unauthorized, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.uploadFile( + id: itemId, + file: .init( + name: "some-text-file.txt", + data: .init() + ), + credentials: .init( + username: "usrname", + password: "passwrd" + ) + ) + } catch APIClientError.authenticationFailed { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withNonExistingParentFolderId_someFileNameAndData_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .notFound, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.uploadFile( + id: itemId, + file: .init( + name: "some-text-file.txt", + data: .init() + ), + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemDoesNotExist { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withEmptyParentFolderId_someFileNameAndData_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent("")] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.uploadFile( + id: "", + file: .init( + name: "some-text-file.txt", + data: .init() + ), + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withExistingParentFolderId_someInvalidFileNameAndData_andRightCredentials() async throws { + // GIVEN + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.uploadFile( + id: itemId, + file: .init( + name: "some-text-file.txt", + data: .init() + ), + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withEmptyParentFolderId_someExistingNameAndData_andRightCredentials() async throws { + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.uploadFile( + id: itemId, + file: .init( + name: "some-text-file.txt", + data: .init() + ), + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + func test_withEmptyParentFolderId_someEmptyFileNameAndData_andRightCredentials() async throws { + MockURLProtocol.mockData[url.appendingPathComponent(itemId)] = MockURLResponse( + status: .badRequest, + headers: [:], + data: nil + ) + + // WHEN & THEN + do { + _ = try await service.uploadFile( + id: itemId, + file: .init( + name: "", + data: .init() + ), + credentials: .init( + username: "username", + password: "password" + ) + ) + } catch APIClientError.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + +}