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..99713d6 --- /dev/null +++ b/Libraries/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.7 + +import PackageDescription + +let package = Package( + name: "Libraries", + platforms: [.iOS(.v15)], + products: [ + .library( + name: "Libraries", + targets: ["APIService"]), + ], + dependencies: [], + targets: [ + .target(name: "APIService"), + .testTarget( + name: "APIServiceTests", + dependencies: ["APIService"] + ), + ] +) 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/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/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/Sources/APIService/Endpoints/DeleteItemEndpoint.swift b/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift new file mode 100644 index 0000000..f5b44c7 --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/DeleteItemEndpoint.swift @@ -0,0 +1,36 @@ +// +// DeleteItemEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// 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: Data? +} + +// 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 new file mode 100644 index 0000000..026bedd --- /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. +// + +import Foundation + +struct GetDataEndpoint: Endpoint { + let path: String + let method: RequestMethod + let credentials: BasicCredentials + let headers: [String : String] + let body: Data? +} + +// 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.Formats { + static let itemsDataWithId = "/items/%@/data" +} diff --git a/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift new file mode 100644 index 0000000..6e74867 --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/GetItemsEndpoint.swift @@ -0,0 +1,36 @@ +// +// GetItemsEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 04/12/2022. +// 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: Data? +} + +// 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 + } +} diff --git a/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift b/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift new file mode 100644 index 0000000..7dbd729 --- /dev/null +++ b/Libraries/Sources/APIService/Endpoints/GetMeEndpoint.swift @@ -0,0 +1,35 @@ +// +// GetMeEndpoint.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// 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: Data? +} + +// 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/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/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/Enumerations/ResponseStatus.swift b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift new file mode 100644 index 0000000..0895820 --- /dev/null +++ b/Libraries/Sources/APIService/Enumerations/ResponseStatus.swift @@ -0,0 +1,17 @@ +// +// 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 unauthorized = 401 + case forbidden = 403 + case notFound = 404 +} 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/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+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/Extensions/String+Headers.swift b/Libraries/Sources/APIService/Extensions/String+Headers.swift new file mode 100644 index 0000000..cd4eefd --- /dev/null +++ b/Libraries/Sources/APIService/Extensions/String+Headers.swift @@ -0,0 +1,22 @@ +// +// 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" + static let contentDisposition = "Content-Disposition" + } + + enum Values { + static let contentTypeJSON = "application/json" + static let contentTypeOctet = "application/octet-stream" + } + } +} diff --git a/Libraries/Sources/APIService/Extensions/String+Hosts.swift b/Libraries/Sources/APIService/Extensions/String+Hosts.swift new file mode 100644 index 0000000..b6c6f48 --- /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" + } +} 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/Models/Item.swift b/Libraries/Sources/APIService/Models/Item.swift new file mode 100644 index 0000000..e607a77 --- /dev/null +++ b/Libraries/Sources/APIService/Models/Item.swift @@ -0,0 +1,37 @@ +// +// Item.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +public 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 + } +} + +// MARK: - Equatable + +extension Item: Equatable {} diff --git a/Libraries/Sources/APIService/Models/Me.swift b/Libraries/Sources/APIService/Models/Me.swift new file mode 100644 index 0000000..35c1b87 --- /dev/null +++ b/Libraries/Sources/APIService/Models/Me.swift @@ -0,0 +1,21 @@ +// +// 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 {} + +// MARK: - Equatable + +extension Me: Equatable {} 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 +} diff --git a/Libraries/Sources/APIService/Protocols/Endpoint.swift b/Libraries/Sources/APIService/Protocols/Endpoint.swift new file mode 100644 index 0000000..71cd132 --- /dev/null +++ b/Libraries/Sources/APIService/Protocols/Endpoint.swift @@ -0,0 +1,47 @@ +// +// Endpoint.swift +// APIService +// +// Created by Javier Cicchelli on 03/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +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 } + var headers: [String: String] { get } + var body: Data? { get } +} + +// MARK: - Defaults + +extension Endpoint { + var scheme: String { .Schemes.http } + var host: String { .Hosts.default } + var port: Int { .Ports.default } + 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 +} 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 new file mode 100644 index 0000000..2295426 --- /dev/null +++ b/Libraries/Sources/APIService/Structs/APIClient.swift @@ -0,0 +1,120 @@ +// +// 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.itemNameInvalidOrDefined + case .unauthorized, + .forbidden: + throw APIClientError.authenticationFailed + 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 .unauthorized, + .forbidden: + throw APIClientError.authenticationFailed + 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 authenticationFailed + case itemIsNotFile + case itemNameInvalidOrDefined + case itemDoesNotExist + case notSupported +} diff --git a/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift b/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift new file mode 100644 index 0000000..49d7a54 --- /dev/null +++ b/Libraries/Sources/APIService/Use Cases/MakeAuthorizationHeaderUseCase.swift @@ -0,0 +1,44 @@ +// +// 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.Formats { + static let usernameAndPassword = "%@:%@" + static let authorizationValue = "Basic %@" +} 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/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) + } + } + +} 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..e1a0c99 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/CreateFolderEndpoint+InitTests.swift @@ -0,0 +1,81 @@ +// +// 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 + + private let itemId = UUID().uuidString + + private var endpoint: CreateFolderEndpoint! + private var folderName: String! + private var username: String! + private 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.port, .Ports.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.port, .Ports.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)) + } + +} 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..abd63f7 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/DeleteItemEndpoint+InitTests.swift @@ -0,0 +1,76 @@ +// +// 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 + + private let itemId = UUID().uuidString + + private var endpoint: DeleteItemEndpoint! + private var username: String! + private 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.port, .Ports.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() 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.port, .Ports.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) + } + +} 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..158dbe5 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetDataEndpoint+InitTests.swift @@ -0,0 +1,76 @@ +// +// 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 + + private let itemId = UUID().uuidString + + private var endpoint: GetDataEndpoint! + private var username: String! + private 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.port, .Ports.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() 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.port, .Ports.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 new file mode 100644 index 0000000..956d23c --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetItemsEndpoint+InitTests.swift @@ -0,0 +1,76 @@ +// +// GetItemsEndpoint+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 GetItemsEndpointInitTests: XCTestCase { + + // MARK: Properties + + private let itemId = UUID().uuidString + + private var endpoint: GetItemsEndpoint! + private var username: String! + private 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.port, .Ports.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_withItemId_andEmptyUsernameOrPassword() 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.port, .Ports.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) + } + +} 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..235ed6c --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/GetMeEndpoint+InitTests.swift @@ -0,0 +1,65 @@ +// +// 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 + + private var endpoint: GetMeEndpoint! + private var username: String! + private 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.port, .Ports.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() 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.port, .Ports.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) + } + +} 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..6a7edc8 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Endpoints/UploadFileEndpoint+InitTests.swift @@ -0,0 +1,92 @@ +// +// 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 + + private let itemId = UUID().uuidString + + private var endpoint: UploadFileEndpoint! + private var fileName: String! + private var fileData: Data! + private var username: String! + private 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.port, .Ports.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.port, .Ports.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) + } + +} 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..0949ea0 --- /dev/null +++ b/Libraries/Tests/APIServiceTests/Cases/Structs/APIClient+RequestTests.swift @@ -0,0 +1,443 @@ +// +// 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) + } + + override func tearDown() async throws { + client = nil + } + + // 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.itemNameInvalidOrDefined { + XCTAssertTrue(true) + } catch { + XCTAssertTrue(false) + } + } + + 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( + 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( + 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_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( + 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( + 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) + } + } + +} 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..792e694 --- /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 + + private let makeAuthHeader = MakeAuthorizationHeaderUseCase() + + private var username: String! + private var password: String! + + private 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) + } + } + +} 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) + } + +}