diff --git a/Modules/Sources/Networking/Model/SiteAPI.swift b/Modules/Sources/Networking/Model/SiteAPI.swift index cc8acbb75f9..f8d59dc755a 100644 --- a/Modules/Sources/Networking/Model/SiteAPI.swift +++ b/Modules/Sources/Networking/Model/SiteAPI.swift @@ -45,7 +45,17 @@ public struct SiteAPI: Decodable, Equatable, GeneratedFakeable { } let siteAPIContainer = try decoder.container(keyedBy: SiteAPIKeys.self) - let namespaces = siteAPIContainer.failsafeDecodeIfPresent([String].self, forKey: .namespaces) ?? [] + + /// Some third-party plugins (like CoCart API) alter the response of `namespaces` field into a dictionary instead of array. + /// This workaround transforms the unexpected dictionary to extract the values in the dictionary. + let namespaces = siteAPIContainer.failsafeDecodeIfPresent( + targetType: [String].self, + forKey: .namespaces, + alternativeTypes: [ + .dictionary(transform: { Array($0.values) }) + ] + ) ?? [] + let authentication = try? siteAPIContainer.decode(Authentication.self, forKey: .authentication) let applicationPasswordAvailable = authentication?.applicationPasswords?.endpoints?.authorization != nil diff --git a/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift b/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift index 7bce142c4dc..093b2c4467d 100644 --- a/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift +++ b/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift @@ -6,6 +6,7 @@ public enum AlternativeDecodingType { case string(transform: (String) -> T) case bool(transform: (Bool) -> T) case integer(transform: (Int) -> T) + case dictionary(transform: ([String: String]) -> T) } // MARK: - KeyedDecodingContainer: Bulletproof JSON Decoding. @@ -60,6 +61,10 @@ public extension KeyedDecodingContainer { if let result = failsafeDecodeIfPresent(integerForKey: key) { return transform(result) } + case .dictionary(let transform): + if let result = failsafeDecodeIfPresent([String: String].self, forKey: key) { + return transform(result) + } } } return nil diff --git a/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift b/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift index c7373302d1d..4d3ce308b5a 100644 --- a/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift +++ b/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift @@ -5,7 +5,7 @@ import XCTest /// SiteAPIMapperTests Unit Tests /// -class SiteAPIMapperTests: XCTestCase { +final class SiteAPIMapperTests: XCTestCase { /// Dummy Site ID. /// @@ -54,6 +54,18 @@ class SiteAPIMapperTests: XCTestCase { XCTAssertEqual(apiSettings?.namespaces, dummyBrokenNamespaces) XCTAssertEqual(apiSettings?.highestWooVersion, WooAPIVersion.none) } + + /// Verifies the SiteSetting fields are parsed correctly. + /// + func test_SiteSetting_with_malformed_namespaces_fields_are_properly_parsed() { + let apiSettings = mapLoadSiteAPIResponseWithMalformedNamespaces() + + XCTAssertNotNil(apiSettings) + XCTAssertEqual(apiSettings?.siteID, dummySiteID) + XCTAssertNotNil(apiSettings?.namespaces) + XCTAssertEqual(apiSettings?.namespaces.sorted(), dummyNamespaces.sorted()) + XCTAssertEqual(apiSettings?.highestWooVersion, WooAPIVersion.mark3) + } } @@ -88,4 +100,10 @@ private extension SiteAPIMapperTests { func mapLoadBrokenSiteAPIResponse() -> SiteAPI? { return mapSiteAPIData(from: "site-api-no-woo") } + + /// Returns the SiteAPIMapper output with malformed namespaces + /// + func mapLoadSiteAPIResponseWithMalformedNamespaces() -> SiteAPI? { + return mapSiteAPIData(from: "site-api-malformed") + } } diff --git a/Modules/Tests/NetworkingTests/Responses/site-api-malformed.json b/Modules/Tests/NetworkingTests/Responses/site-api-malformed.json new file mode 100644 index 00000000000..c6b4c2d4ab4 --- /dev/null +++ b/Modules/Tests/NetworkingTests/Responses/site-api-malformed.json @@ -0,0 +1,23 @@ +{ + "data": { + "namespaces": { + "0": "oembed\/1.0", + "1": "akismet\/v1", + "2": "jetpack\/v4", + "3": "wpcom\/v2", + "4": "wc\/v1", + "5": "wc\/v2", + "6": "wc\/v3", + "7": "wc-pb\/v3", + "8": "wp\/v2" + }, + "authentication": [], + "_links": { + "help": [ + { + "href": "http:\/\/v2.wp-api.org\/" + } + ] + } + } +} diff --git a/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift b/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift index 2f5eb8c814f..a552090ee2a 100644 --- a/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift +++ b/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift @@ -166,8 +166,10 @@ class DefaultStoresManager: StoresManager { if case .wpcom = credentials { listenToWPCOMInvalidWPCOMTokenNotification() + applicationPasswordGenerationFailureObserver = nil } else { listenToApplicationPasswordGenerationFailureNotification() + invalidWPCOMTokenNotificationObserver = nil } return self @@ -250,6 +252,7 @@ class DefaultStoresManager: StoresManager { @discardableResult func deauthenticate() -> StoresManager { applicationPasswordGenerationFailureObserver = nil + invalidWPCOMTokenNotificationObserver = nil if isAuthenticated { let resetAction = CardPresentPaymentAction.reset