From 1640d5c6aee37d903d41b1e442a21175be21c237 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 19 Sep 2022 21:27:51 +0200 Subject: [PATCH 01/11] Update Networking layer with initial GET customer api --- .../Networking.xcodeproj/project.pbxproj | 20 ++++++ .../Networking/Mapper/CustomerMapper.swift | 27 ++++++++ Networking/Networking/Model/Customer.swift | 65 +++++++++++++++++++ .../Networking/Remote/CustomerRemote.swift | 23 +++++++ .../Mapper/CustomerMapperTests.swift | 28 ++++++++ .../NetworkingTests/Responses/customer.json | 53 +++++++++++++++ 6 files changed, 216 insertions(+) create mode 100644 Networking/Networking/Mapper/CustomerMapper.swift create mode 100644 Networking/Networking/Model/Customer.swift create mode 100644 Networking/Networking/Remote/CustomerRemote.swift create mode 100644 Networking/NetworkingTests/Mapper/CustomerMapperTests.swift create mode 100644 Networking/NetworkingTests/Responses/customer.json diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 6c5a38f9c97..1d65480f727 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -324,6 +324,11 @@ 57E8FED3246616AC0057CD68 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */; }; 6647C0161DAC6AB6570C53A7 /* Pods_Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */; }; 68C87B342862D40E00A99054 /* setting-all-except-countries.json in Resources */ = {isa = PBXBuildFile; fileRef = 68C87B332862D40E00A99054 /* setting-all-except-countries.json */; }; + 68CB800C28D87BC800E169F8 /* Customer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800B28D87BC800E169F8 /* Customer.swift */; }; + 68CB800E28D8901B00E169F8 /* CustomerMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800D28D8901B00E169F8 /* CustomerMapper.swift */; }; + 68CB801028D89A0400E169F8 /* CustomerRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800F28D89A0400E169F8 /* CustomerRemote.swift */; }; + 68CB801428D8A05200E169F8 /* CustomerMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB801328D8A05200E169F8 /* CustomerMapperTests.swift */; }; + 68CB801628D8A39700E169F8 /* customer.json in Resources */ = {isa = PBXBuildFile; fileRef = 68CB801528D8A39700E169F8 /* customer.json */; }; 68FBC5B828928C8C00A05461 /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68FBC5B728928C8C00A05461 /* WooFoundation.framework */; }; 74002D6A2118B26100A63C19 /* SiteVisitStatsMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74002D692118B26000A63C19 /* SiteVisitStatsMapperTests.swift */; }; 74002D6C2118B88200A63C19 /* SiteVisitStatsRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74002D6B2118B88200A63C19 /* SiteVisitStatsRemoteTests.swift */; }; @@ -1011,6 +1016,11 @@ 57BE08D72409B63700F6DCED /* reviews-missing-avatar-urls.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "reviews-missing-avatar-urls.json"; sourceTree = ""; }; 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = ""; }; 68C87B332862D40E00A99054 /* setting-all-except-countries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "setting-all-except-countries.json"; sourceTree = ""; }; + 68CB800B28D87BC800E169F8 /* Customer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Customer.swift; sourceTree = ""; }; + 68CB800D28D8901B00E169F8 /* CustomerMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerMapper.swift; sourceTree = ""; }; + 68CB800F28D89A0400E169F8 /* CustomerRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerRemote.swift; sourceTree = ""; }; + 68CB801328D8A05200E169F8 /* CustomerMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerMapperTests.swift; sourceTree = ""; }; + 68CB801528D8A39700E169F8 /* customer.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = customer.json; sourceTree = ""; }; 68FBC5B728928C8C00A05461 /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 69314EDE650855CAF927057E /* Pods_NetworkingTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkingTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74002D692118B26000A63C19 /* SiteVisitStatsMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteVisitStatsMapperTests.swift; sourceTree = ""; }; @@ -1748,6 +1758,7 @@ FE28F6E5268429B6004465C7 /* UserRemote.swift */, 077F39D526A58E4500ABEADC /* SystemStatusRemote.swift */, AEF94584272974F2001DCCFB /* TelemetryRemote.swift */, + 68CB800F28D89A0400E169F8 /* CustomerRemote.swift */, ); path = Remote; sourceTree = ""; @@ -1855,6 +1866,7 @@ FE28F6E126840DED004465C7 /* User.swift */, DE50295828C5BD0200551736 /* JetpackUser.swift */, DE50295A28C5F99700551736 /* DotcomUser.swift */, + 68CB800B28D87BC800E169F8 /* Customer.swift */, ); path = Model; sourceTree = ""; @@ -2089,6 +2101,7 @@ 4513382727A96DE700AE5E78 /* inbox-note.json */, 0205021B27C86B9700FB1C6B /* inbox-note-without-isRead.json */, 68C87B332862D40E00A99054 /* setting-all-except-countries.json */, + 68CB801528D8A39700E169F8 /* customer.json */, ); path = Responses; sourceTree = ""; @@ -2181,6 +2194,7 @@ 02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */, 0359EA1C27AADE000048DE2D /* WCPayChargeMapper.swift */, DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */, + 68CB800D28D8901B00E169F8 /* CustomerMapper.swift */, ); path = Mapper; sourceTree = ""; @@ -2303,6 +2317,7 @@ DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */, DE50296428C60A8000551736 /* JetpackUserMapperTests.swift */, 0359EA1E27AAE4680048DE2D /* WCPayChargeMapperTests.swift */, + 68CB801328D8A05200E169F8 /* CustomerMapperTests.swift */, ); path = Mapper; sourceTree = ""; @@ -2684,6 +2699,7 @@ 7497376A2141F2BE0008C490 /* top-performers-week-alt.json in Resources */, D865CE61278CA1AE002C8520 /* stripe-payment-intent-processing.json in Resources */, 743E84F222172D0A00FAC9D7 /* shipment_tracking_plugin_not_active.json in Resources */, + 68CB801628D8A39700E169F8 /* customer.json in Resources */, 451A97DE260B59870059D135 /* shipping-label-packages-success.json in Resources */, 31D27C8F2602B553002EDB1D /* plugins.json in Resources */, 261CF1B4255AD6B30090D8D3 /* payment-gateway-list.json in Resources */, @@ -2982,6 +2998,7 @@ B518662220A097C200037A38 /* Network.swift in Sources */, B572F69A21AC475C003EEFF0 /* DevicesRemote.swift in Sources */, 3192F220260D33BB0067FEF9 /* WCPayAccount.swift in Sources */, + 68CB800E28D8901B00E169F8 /* CustomerMapper.swift in Sources */, 45CCFCE227A2C9BF0012E8CB /* InboxNote.swift in Sources */, 311D412C2783BF7400052F64 /* StripeAccount.swift in Sources */, B518662420A099BF00037A38 /* AlamofireNetwork.swift in Sources */, @@ -3030,6 +3047,7 @@ 020D07BE23D8570800FD9580 /* MediaListMapper.swift in Sources */, 0359EA1327AAC6D00048DE2D /* WCPayCardPaymentDetails.swift in Sources */, CCB2CA9E262091CB00285CA0 /* SuccessDataResultMapper.swift in Sources */, + 68CB801028D89A0400E169F8 /* CustomerRemote.swift in Sources */, DE50295B28C5F99700551736 /* DotcomUser.swift in Sources */, 74C8F06820EEB7BD00B6EDC9 /* OrderNotesMapper.swift in Sources */, 24F98C582502EA8800F49B68 /* FeatureFlagMapper.swift in Sources */, @@ -3039,6 +3057,7 @@ 74046E1D217A6989007DD7BF /* SiteSetting.swift in Sources */, B5BB1D1020A237FB00112D92 /* Address.swift in Sources */, CE43066A23465F340073CBFF /* Refund.swift in Sources */, + 68CB800C28D87BC800E169F8 /* Customer.swift in Sources */, DE50295D28C6068B00551736 /* JetpackUserMapper.swift in Sources */, B524194121AC60A700D6FC0A /* DotcomDevice.swift in Sources */, D8EDFE2225EE88C9003D2213 /* ReaderConnectionToken.swift in Sources */, @@ -3124,6 +3143,7 @@ CEC4BF8F234E382F008D9195 /* RefundMapperTests.swift in Sources */, 24F98C5E2502EDCF00F49B68 /* BundleWooTests.swift in Sources */, 74AB0ACA21948CE4008220CD /* CommentResultMapperTests.swift in Sources */, + 68CB801428D8A05200E169F8 /* CustomerMapperTests.swift in Sources */, 02698CF824C183A5005337C4 /* ProductVariationListMapperTests.swift in Sources */, B524194921AC659500D6FC0A /* DevicesRemoteTests.swift in Sources */, 2685C0DA263B551300D9EE97 /* AddOnGroupMapperTests.swift in Sources */, diff --git a/Networking/Networking/Mapper/CustomerMapper.swift b/Networking/Networking/Mapper/CustomerMapper.swift new file mode 100644 index 00000000000..7ec42bf9082 --- /dev/null +++ b/Networking/Networking/Mapper/CustomerMapper.swift @@ -0,0 +1,27 @@ +import Foundation + +/// Mapper: Customer +/// +struct CustomerMapper: Mapper { + /// We're injecting this field by copying it in after parsing responses, because `siteID` is not returned in any of the Customer endpoints. + /// + let siteID: Int64 + + /// (Attempts) to convert a dictionary into a `Customer` entity + /// + func map(response: Data) throws -> Customer { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter) + decoder.userInfo = [.siteID: siteID] + let customer = try decoder.decode(CustomerEnvelope.self, from: response).customer + return customer + } +} + +private struct CustomerEnvelope: Decodable { + let customer: Customer + + private enum CodingKeys: String, CodingKey { + case customer = "data" + } +} diff --git a/Networking/Networking/Model/Customer.swift b/Networking/Networking/Model/Customer.swift new file mode 100644 index 00000000000..33ae47b853d --- /dev/null +++ b/Networking/Networking/Model/Customer.swift @@ -0,0 +1,65 @@ +import Foundation +import Codegen + +/// Represents a Customer entity: +/// https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties +/// +struct Customer: Codable { + + /// Unique identifier for the customer + let customerID: Int64 + + /// The email address for the customer + let email: String + + /// Customer first name + let firstName: String + + /// Customer last name + let lastName: String + // TODO: Check why both billing and shipping are making the decoder fail. + /// List of billing address data + //let billing: [String] + + /// List of shipping address data + //let shipping: [String] + + /// Customer struct initializer + /// + public init(customerID: Int64, + email: String, + firstName: String, + lastName: String) { + self.customerID = customerID + self.email = email + self.firstName = firstName + self.lastName = lastName + } + + /// Public initializer for the Customer + /// + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let customerID = try container.decode(Int64.self, forKey: .customerID) + let email = try container.decode(String.self, forKey: .email) + let firstName = try container.decodeIfPresent(String.self, forKey: .firstName) ?? "" + let lastName = try container.decodeIfPresent(String.self, forKey: .lastName) ?? "" + + self.init(customerID: customerID, + email: email, + firstName: firstName, + lastName: lastName + ) + } +} + +/// Defines all of the Customer CodingKeys +/// +extension Customer { + enum CodingKeys: String, CodingKey { + case customerID = "id" + case email + case firstName = "first_name" + case lastName = "last_name" + } +} diff --git a/Networking/Networking/Remote/CustomerRemote.swift b/Networking/Networking/Remote/CustomerRemote.swift new file mode 100644 index 00000000000..1df984ba788 --- /dev/null +++ b/Networking/Networking/Remote/CustomerRemote.swift @@ -0,0 +1,23 @@ +import Foundation + +public class CustomerRemote: Remote { + /// Retrieves a `Customer` + /// + /// - Parameters: + /// - customerID: ID of the customer that will be retrieved + /// - siteID: Site for which we'll fetch the customer. + /// - completion: Closure to be executed upon completion. + /// + func retrieveCustomer(for siteID: Int64, with customerID: Int64, completion: @escaping (Result) -> Void) { + let path = "/customers/\(customerID)" + let request = JetpackRequest(wooApiVersion: .mark3, + method: .get, + siteID: siteID, + path: path, + parameters: nil + ) + + let mapper = CustomerMapper(siteID: siteID) + enqueue(request, mapper: mapper, completion: completion) + } +} diff --git a/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift b/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift new file mode 100644 index 00000000000..186c8eaf489 --- /dev/null +++ b/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift @@ -0,0 +1,28 @@ +import XCTest +@testable import Networking + +class CustomerMapperTests: XCTestCase { + + func test_data_is_mapped() { + let mapper = CustomerMapper(siteID: 123) + guard let data = Loader.contentsOf("customer") else { + XCTFail("customer.json not found") + return + } + let customer = try? mapper.map(response: data) + XCTAssertNotNil(mapper) + XCTAssertNotNil(customer) + XCTAssertEqual(customer?.customerID, 25) + } +} + +private extension CustomerMapperTests { + func mapCustomer(from filename: String) throws -> Customer { + return Customer( + customerID: 25, + email: "", + firstName: "", + lastName: "" + ) + } +} diff --git a/Networking/NetworkingTests/Responses/customer.json b/Networking/NetworkingTests/Responses/customer.json new file mode 100644 index 00000000000..01b971bd4fb --- /dev/null +++ b/Networking/NetworkingTests/Responses/customer.json @@ -0,0 +1,53 @@ +{ + "data": { + "id": 25, + "date_created": "2017-03-21T16:09:28", + "date_created_gmt": "2017-03-21T19:09:28", + "date_modified": "2017-03-21T16:09:30", + "date_modified_gmt": "2017-03-21T19:09:30", + "email": "john.doe@example.com", + "first_name": "John", + "last_name": "Doe", + "role": "customer", + "username": "john.doe", + "billing": { + "first_name": "John", + "last_name": "Doe", + "company": "", + "address_1": "969 Market", + "address_2": "", + "city": "San Francisco", + "state": "CA", + "postcode": "94103", + "country": "US", + "email": "john.doe@example.com", + "phone": "(555) 555-5555" + }, + "shipping": { + "first_name": "John", + "last_name": "Doe", + "company": "", + "address_1": "969 Market", + "address_2": "", + "city": "San Francisco", + "state": "CA", + "postcode": "94103", + "country": "US" + }, + "is_paying_customer": false, + "avatar_url": "https://secure.gravatar.com/avatar/8eb1b522f60d11fa897de1dc6351b7e8?s=96", + "meta_data": [], + "_links": { + "self": [ + { + "href": "https://example.com/wp-json/wc/v3/customers/25" + } + ], + "collection": [ + { + "href": "https://example.com/wp-json/wc/v3/customers" + } + ] + } + } +} From 28826ced2859b76daf59566fbcc923c324261d1c Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 20 Sep 2022 10:18:03 +0200 Subject: [PATCH 02/11] Add billing & shipping params to Customer model --- Networking/Networking/Model/Customer.swift | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/Networking/Networking/Model/Customer.swift b/Networking/Networking/Model/Customer.swift index 33ae47b853d..5d2a226b65c 100644 --- a/Networking/Networking/Model/Customer.swift +++ b/Networking/Networking/Model/Customer.swift @@ -13,42 +13,51 @@ struct Customer: Codable { let email: String /// Customer first name - let firstName: String + let firstName: String? /// Customer last name - let lastName: String - // TODO: Check why both billing and shipping are making the decoder fail. + let lastName: String? + /// List of billing address data - //let billing: [String] + let billing: Address? /// List of shipping address data - //let shipping: [String] + let shipping: Address? /// Customer struct initializer /// public init(customerID: Int64, email: String, - firstName: String, - lastName: String) { + firstName: String?, + lastName: String?, + billing: Address?, + shipping: Address?) { self.customerID = customerID self.email = email self.firstName = firstName self.lastName = lastName + self.billing = billing + self.shipping = shipping } /// Public initializer for the Customer /// public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + let customerID = try container.decode(Int64.self, forKey: .customerID) let email = try container.decode(String.self, forKey: .email) let firstName = try container.decodeIfPresent(String.self, forKey: .firstName) ?? "" let lastName = try container.decodeIfPresent(String.self, forKey: .lastName) ?? "" + let billing = try? container.decode(Address.self, forKey: .billing) + let shipping = try? container.decode(Address.self, forKey: .shipping) self.init(customerID: customerID, - email: email, - firstName: firstName, - lastName: lastName + email: email, + firstName: firstName, + lastName: lastName, + billing: billing, + shipping: shipping ) } } @@ -61,5 +70,7 @@ extension Customer { case email case firstName = "first_name" case lastName = "last_name" + case billing + case shipping } } From 9e29b05a1601a3873811e49e2c197cb73c0e0dbe Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 20 Sep 2022 10:54:27 +0200 Subject: [PATCH 03/11] Add CustomerMapper unit tests --- .../Mapper/CustomerMapperTests.swift | 76 ++++++++++++++++--- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift b/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift index 186c8eaf489..1d29dbcf5e9 100644 --- a/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift @@ -1,28 +1,82 @@ import XCTest @testable import Networking +/// CustomerMapper Unit Tests +/// class CustomerMapperTests: XCTestCase { - func test_data_is_mapped() { - let mapper = CustomerMapper(siteID: 123) - guard let data = Loader.contentsOf("customer") else { + /// Dummy Site ID. + /// + private let dummySiteID: Int64 = 123 + + /// Local file that holds Customer data representing the API endpoint + /// + private let filename: String = "customer" + + /// Verifies that the Customer object can be mapped fron the Encoded data + /// + func test_Customer_is_mapped_from_encoded_data() { + // Given + let mapper = CustomerMapper(siteID: dummySiteID) + guard let data = Loader.contentsOf(filename) else { XCTFail("customer.json not found") return } + + // When let customer = try? mapper.map(response: data) + + // Then XCTAssertNotNil(mapper) XCTAssertNotNil(customer) - XCTAssertEqual(customer?.customerID, 25) + XCTAssertNotNil(customer?.customerID) + XCTAssertNotNil(customer?.email) + XCTAssertNotNil(customer?.firstName) + XCTAssertNotNil(customer?.lastName) + XCTAssertNotNil(customer?.billing) + XCTAssertNotNil(customer?.shipping) + } + + /// Verifies that all of the Customer response values are parsed correctly + /// + func test_Customer_response_values_are_correctly_parsed() throws { + // Given + guard let customer = try mapCustomer(from: filename) else { + XCTFail() + return + } + + // Then + XCTAssertNotNil(customer) + XCTAssertEqual(customer.customerID, 25) + XCTAssertEqual(customer.email, "john.doe@example.com") + XCTAssertEqual(customer.firstName, "John") + XCTAssertEqual(customer.lastName, "Doe") + + let dummyAddresses = [customer.shipping, customer.billing].compactMap({ $0 }) + XCTAssertEqual(dummyAddresses.count, 2) + + for address in dummyAddresses { + XCTAssertEqual(address.firstName, "John") + XCTAssertEqual(address.lastName, "Doe") + XCTAssertEqual(address.company, "") + XCTAssertEqual(address.address1, "969 Market") + XCTAssertEqual(address.address2, "") + XCTAssertEqual(address.city, "San Francisco") + XCTAssertEqual(address.state, "CA") + XCTAssertEqual(address.postcode, "94103") + XCTAssertEqual(address.country, "US") + } } } private extension CustomerMapperTests { - func mapCustomer(from filename: String) throws -> Customer { - return Customer( - customerID: 25, - email: "", - firstName: "", - lastName: "" - ) + /// Returns the CustomerMapper output upon receiving `filename` (Data Encoded) + /// + func mapCustomer(from filename: String) throws -> Customer? { + guard let response = Loader.contentsOf(filename) else { + return nil + } + return try! CustomerMapper(siteID: dummySiteID).map(response: response) } } From 4d4668a87eda47000515fc10d9c0950e34376870 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 20 Sep 2022 11:19:48 +0200 Subject: [PATCH 04/11] Add CustomerRemote unit tests --- .../Networking.xcodeproj/project.pbxproj | 4 ++ .../Remote/CustomerRemoteTests.swift | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 Networking/NetworkingTests/Remote/CustomerRemoteTests.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 1d65480f727..55a4286b4c9 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -323,6 +323,7 @@ 57BE08D82409B63800F6DCED /* reviews-missing-avatar-urls.json in Resources */ = {isa = PBXBuildFile; fileRef = 57BE08D72409B63700F6DCED /* reviews-missing-avatar-urls.json */; }; 57E8FED3246616AC0057CD68 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */; }; 6647C0161DAC6AB6570C53A7 /* Pods_Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */; }; + 68BD37B328D9B8BD00C2A517 /* CustomerRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */; }; 68C87B342862D40E00A99054 /* setting-all-except-countries.json in Resources */ = {isa = PBXBuildFile; fileRef = 68C87B332862D40E00A99054 /* setting-all-except-countries.json */; }; 68CB800C28D87BC800E169F8 /* Customer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800B28D87BC800E169F8 /* Customer.swift */; }; 68CB800E28D8901B00E169F8 /* CustomerMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800D28D8901B00E169F8 /* CustomerMapper.swift */; }; @@ -1015,6 +1016,7 @@ 5726F7332460A8F00031CAAC /* CopiableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiableTests.swift; sourceTree = ""; }; 57BE08D72409B63700F6DCED /* reviews-missing-avatar-urls.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "reviews-missing-avatar-urls.json"; sourceTree = ""; }; 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = ""; }; + 68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerRemoteTests.swift; sourceTree = ""; }; 68C87B332862D40E00A99054 /* setting-all-except-countries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "setting-all-except-countries.json"; sourceTree = ""; }; 68CB800B28D87BC800E169F8 /* Customer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Customer.swift; sourceTree = ""; }; 68CB800D28D8901B00E169F8 /* CustomerMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerMapper.swift; sourceTree = ""; }; @@ -1626,6 +1628,7 @@ FE28F6EB268436C9004465C7 /* UserRemoteTests.swift */, 077F39D926A58ED700ABEADC /* SystemStatusRemoteTests.swift */, DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */, + 68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */, ); path = Remote; sourceTree = ""; @@ -3218,6 +3221,7 @@ 45CCFCE827A2E5020012E8CB /* InboxNoteListMapperTests.swift in Sources */, 74002D6C2118B88200A63C19 /* SiteVisitStatsRemoteTests.swift in Sources */, 0212683524C046CB00F8A892 /* MockNetwork+Path.swift in Sources */, + 68BD37B328D9B8BD00C2A517 /* CustomerRemoteTests.swift in Sources */, B554FA932180C17200C54DFF /* NoteHashListMapperTests.swift in Sources */, CC07866526790B1100BA9AC1 /* ShippingLabelPurchaseMapperTests.swift in Sources */, 74002D6A2118B26100A63C19 /* SiteVisitStatsMapperTests.swift in Sources */, diff --git a/Networking/NetworkingTests/Remote/CustomerRemoteTests.swift b/Networking/NetworkingTests/Remote/CustomerRemoteTests.swift new file mode 100644 index 00000000000..dd57905807c --- /dev/null +++ b/Networking/NetworkingTests/Remote/CustomerRemoteTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import Networking + +class CustomerRemoteTests: XCTestCase { + + /// Dummy Network Wrapper + /// + private var network: MockNetwork! + + /// Dummy Site ID + /// + private let sampleSiteID: Int64 = 123 + + /// Dummy Customer ID + private let sampleCustomerID: Int64 = 25 + + override func setUp() { + super.setUp() + network = MockNetwork() + } + + override func tearDown() { + network = nil + super.tearDown() + } + + /// Verifies that retrieveCustomer properly parses the `wc/v3/customers/{customerID}` endpoint sample response. + /// + func test_retrieveCustomer_returns_parsed_customer_successfully() throws { + // Given + let remote = CustomerRemote(network: network) + network.simulateResponse(requestUrlSuffix: "customers/\(sampleCustomerID)", filename: "customer") + + // When + let result = waitFor { promise in + remote.retrieveCustomer(for: self.sampleSiteID, with: self.sampleCustomerID) { result in + promise(result) + } + } + + // Then + XCTAssert(result.isSuccess) + let customer = try XCTUnwrap(result.get()) + XCTAssertNotNil(customer) + } +} From 6fb5ee474d057fa20184ecda8050e0033ef517f4 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 20 Sep 2022 15:57:02 +0200 Subject: [PATCH 05/11] Added GeneratedCopiable to Customer model --- .../Copiable/Models+Copiable.generated.swift | 27 +++++++++++++++++++ Networking/Networking/Model/Customer.swift | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 8eb39c4e25e..117dc48d6bc 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -169,6 +169,33 @@ extension CouponReport { } } +extension Customer { + func copy( + customerID: CopiableProp = .copy, + email: CopiableProp = .copy, + firstName: NullableCopiableProp = .copy, + lastName: NullableCopiableProp = .copy, + billing: NullableCopiableProp
= .copy, + shipping: NullableCopiableProp
= .copy + ) -> Customer { + let customerID = customerID ?? self.customerID + let email = email ?? self.email + let firstName = firstName ?? self.firstName + let lastName = lastName ?? self.lastName + let billing = billing ?? self.billing + let shipping = shipping ?? self.shipping + + return Customer( + customerID: customerID, + email: email, + firstName: firstName, + lastName: lastName, + billing: billing, + shipping: shipping + ) + } +} + extension DotcomUser { public func copy( id: CopiableProp = .copy, diff --git a/Networking/Networking/Model/Customer.swift b/Networking/Networking/Model/Customer.swift index 5d2a226b65c..d68960d9d44 100644 --- a/Networking/Networking/Model/Customer.swift +++ b/Networking/Networking/Model/Customer.swift @@ -4,7 +4,7 @@ import Codegen /// Represents a Customer entity: /// https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties /// -struct Customer: Codable { +struct Customer: Codable, GeneratedCopiable { /// Unique identifier for the customer let customerID: Int64 From 449c9676e282462206c9a8d954a79ed88b07d042 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 20 Sep 2022 16:04:38 +0200 Subject: [PATCH 06/11] Add GeneratedFakeable conformance to Customer model --- Fakes/Fakes/Networking.generated.swift | 14 ++++++++++++++ Networking/Networking/Model/Customer.swift | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index db61a3eb451..12342cfcd63 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -181,6 +181,20 @@ extension CreateProductVariation { ) } } +extension Customer { + /// Returns a "ready to use" type filled with fake values. + /// + static func fake() -> Customer { + .init( + customerID: .fake(), + email: .fake(), + firstName: .fake(), + lastName: .fake(), + billing: .fake(), + shipping: .fake() + ) + } +} extension DotcomError { /// Returns a "ready to use" type filled with fake values. /// diff --git a/Networking/Networking/Model/Customer.swift b/Networking/Networking/Model/Customer.swift index d68960d9d44..a8c491cc4c5 100644 --- a/Networking/Networking/Model/Customer.swift +++ b/Networking/Networking/Model/Customer.swift @@ -4,7 +4,7 @@ import Codegen /// Represents a Customer entity: /// https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties /// -struct Customer: Codable, GeneratedCopiable { +struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { /// Unique identifier for the customer let customerID: Int64 From a0543fdf6b05d265832675a76c523e31db8905a2 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 20 Sep 2022 16:25:06 +0200 Subject: [PATCH 07/11] Make Customer public so fake() can be found As Fakes is on a different framework, the Customer object cannot be found due to its default internal access control. Catched this when CI ran tests. --- Fakes/Fakes/Networking.generated.swift | 2 +- Networking/Networking/Model/Customer.swift | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 12342cfcd63..ef4ecdf3fd1 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -184,7 +184,7 @@ extension CreateProductVariation { extension Customer { /// Returns a "ready to use" type filled with fake values. /// - static func fake() -> Customer { + public static func fake() -> Customer { .init( customerID: .fake(), email: .fake(), diff --git a/Networking/Networking/Model/Customer.swift b/Networking/Networking/Model/Customer.swift index a8c491cc4c5..eb5faead80f 100644 --- a/Networking/Networking/Model/Customer.swift +++ b/Networking/Networking/Model/Customer.swift @@ -4,25 +4,25 @@ import Codegen /// Represents a Customer entity: /// https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties /// -struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { +public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { /// Unique identifier for the customer - let customerID: Int64 + public let customerID: Int64 /// The email address for the customer - let email: String + public let email: String /// Customer first name - let firstName: String? + public let firstName: String? /// Customer last name - let lastName: String? + public let lastName: String? /// List of billing address data - let billing: Address? + public let billing: Address? /// List of shipping address data - let shipping: Address? + public let shipping: Address? /// Customer struct initializer /// From 5c6d445ed82d9800fd16530073085eb52094b49b Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 27 Sep 2022 08:50:08 +0900 Subject: [PATCH 08/11] Remove unused dateDecodingStrategy --- Networking/Networking/Mapper/CustomerMapper.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Networking/Networking/Mapper/CustomerMapper.swift b/Networking/Networking/Mapper/CustomerMapper.swift index 7ec42bf9082..f8b3f443676 100644 --- a/Networking/Networking/Mapper/CustomerMapper.swift +++ b/Networking/Networking/Mapper/CustomerMapper.swift @@ -11,7 +11,6 @@ struct CustomerMapper: Mapper { /// func map(response: Data) throws -> Customer { let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter) decoder.userInfo = [.siteID: siteID] let customer = try decoder.decode(CustomerEnvelope.self, from: response).customer return customer From f5c1c3e69305534f9b179ffb54a0c880844f1b79 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 27 Sep 2022 08:58:58 +0900 Subject: [PATCH 09/11] Remove unnecessary default optional values --- Networking/Networking/Model/Customer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Networking/Networking/Model/Customer.swift b/Networking/Networking/Model/Customer.swift index eb5faead80f..70d57332fe8 100644 --- a/Networking/Networking/Model/Customer.swift +++ b/Networking/Networking/Model/Customer.swift @@ -47,8 +47,8 @@ public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { let customerID = try container.decode(Int64.self, forKey: .customerID) let email = try container.decode(String.self, forKey: .email) - let firstName = try container.decodeIfPresent(String.self, forKey: .firstName) ?? "" - let lastName = try container.decodeIfPresent(String.self, forKey: .lastName) ?? "" + let firstName = try container.decodeIfPresent(String.self, forKey: .firstName) + let lastName = try container.decodeIfPresent(String.self, forKey: .lastName) let billing = try? container.decode(Address.self, forKey: .billing) let shipping = try? container.decode(Address.self, forKey: .shipping) From 2fe9688ec26da1d8c0186bdd67290d3b15ccb38d Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 27 Sep 2022 09:00:55 +0900 Subject: [PATCH 10/11] Remove unnecessary CustomerMapper asserts --- Networking/NetworkingTests/Mapper/CustomerMapperTests.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift b/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift index 1d29dbcf5e9..5eab4ee1d8c 100644 --- a/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/CustomerMapperTests.swift @@ -29,12 +29,6 @@ class CustomerMapperTests: XCTestCase { // Then XCTAssertNotNil(mapper) XCTAssertNotNil(customer) - XCTAssertNotNil(customer?.customerID) - XCTAssertNotNil(customer?.email) - XCTAssertNotNil(customer?.firstName) - XCTAssertNotNil(customer?.lastName) - XCTAssertNotNil(customer?.billing) - XCTAssertNotNil(customer?.shipping) } /// Verifies that all of the Customer response values are parsed correctly From 32dfe2a623957706b5e8eb660430e8f2490c9d71 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 27 Sep 2022 09:15:11 +0900 Subject: [PATCH 11/11] Split CustomerRemoteTests --- .../Remote/CustomerRemoteTests.swift | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Networking/NetworkingTests/Remote/CustomerRemoteTests.swift b/Networking/NetworkingTests/Remote/CustomerRemoteTests.swift index dd57905807c..914f8c18113 100644 --- a/Networking/NetworkingTests/Remote/CustomerRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/CustomerRemoteTests.swift @@ -7,6 +7,8 @@ class CustomerRemoteTests: XCTestCase { /// private var network: MockNetwork! + private var remote: CustomerRemote! + /// Dummy Site ID /// private let sampleSiteID: Int64 = 123 @@ -17,30 +19,47 @@ class CustomerRemoteTests: XCTestCase { override func setUp() { super.setUp() network = MockNetwork() + remote = CustomerRemote(network: network) } override func tearDown() { network = nil + remote = nil super.tearDown() } - /// Verifies that retrieveCustomer properly parses the `wc/v3/customers/{customerID}` endpoint sample response. + /// Verifies that retrieveCustomer simulated response is successful for a given customerID /// - func test_retrieveCustomer_returns_parsed_customer_successfully() throws { + func test_CustomerRemote_when_retrieveCustomer_then_returns_result_isSuccess() throws { // Given - let remote = CustomerRemote(network: network) network.simulateResponse(requestUrlSuffix: "customers/\(sampleCustomerID)", filename: "customer") // When let result = waitFor { promise in - remote.retrieveCustomer(for: self.sampleSiteID, with: self.sampleCustomerID) { result in + self.remote.retrieveCustomer(for: self.sampleSiteID, with: self.sampleCustomerID) { result in promise(result) } } // Then XCTAssert(result.isSuccess) + } + + /// Verifies that retrieveCustomer properly parses the `wc/v3/customers/{customerID}` endpoint sample response. + /// + func test_retrieveCustomer_returns_parsed_customer_successfully() throws { + // Given + network.simulateResponse(requestUrlSuffix: "customers/\(sampleCustomerID)", filename: "customer") + + // When + let result = waitFor { promise in + self.remote.retrieveCustomer(for: self.sampleSiteID, with: self.sampleCustomerID) { result in + promise(result) + } + } let customer = try XCTUnwrap(result.get()) + + // Then XCTAssertNotNil(customer) } }