diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index a491dcbffbd..ba2ef3dce7a 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -47,6 +47,9 @@ B567AF2F20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B567AF2C20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift */; }; B567AF3020A0FB8F00AB6C62 /* DotcomRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B567AF2D20A0FB8F00AB6C62 /* DotcomRequestTests.swift */; }; B567AF3120A0FB8F00AB6C62 /* JetpackRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B567AF2E20A0FB8F00AB6C62 /* JetpackRequestTests.swift */; }; + B56C1EB620EA757B00D749F9 /* SiteListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */; }; + B56C1EB820EA76F500D749F9 /* Site.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C1EB720EA76F500D749F9 /* Site.swift */; }; + B56C1EBA20EA7D2C00D749F9 /* sites.json in Resources */ = {isa = PBXBuildFile; fileRef = B56C1EB920EA7D2C00D749F9 /* sites.json */; }; B5969E1520A47F99005E9DF1 /* RemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5969E1420A47F99005E9DF1 /* RemoteTests.swift */; }; B5BB1D0C20A2050300112D92 /* DateFormatter+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BB1D0B20A2050300112D92 /* DateFormatter+Woo.swift */; }; B5BB1D1020A237FB00112D92 /* Address.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BB1D0F20A237FB00112D92 /* Address.swift */; }; @@ -113,6 +116,9 @@ B567AF2C20A0FB8F00AB6C62 /* AuthenticatedRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticatedRequestTests.swift; sourceTree = ""; }; B567AF2D20A0FB8F00AB6C62 /* DotcomRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotcomRequestTests.swift; sourceTree = ""; }; B567AF2E20A0FB8F00AB6C62 /* JetpackRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JetpackRequestTests.swift; sourceTree = ""; }; + B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteListMapper.swift; sourceTree = ""; }; + B56C1EB720EA76F500D749F9 /* Site.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Site.swift; sourceTree = ""; }; + B56C1EB920EA7D2C00D749F9 /* sites.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = sites.json; sourceTree = ""; }; B5969E1420A47F99005E9DF1 /* RemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteTests.swift; sourceTree = ""; }; B5BB1D0B20A2050300112D92 /* DateFormatter+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Woo.swift"; sourceTree = ""; }; B5BB1D0F20A237FB00112D92 /* Address.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Address.swift; sourceTree = ""; }; @@ -298,6 +304,7 @@ B5C6FCCE20A3592900A4F8E4 /* OrderItem.swift */, 74C8F06320EEB44800B6EDC9 /* OrderNote.swift */, B5BB1D1120A255EC00112D92 /* OrderStatus.swift */, + B56C1EB720EA76F500D749F9 /* Site.swift */, ); path = Model; sourceTree = ""; @@ -312,6 +319,7 @@ 74C8F06B20EEBD5D00B6EDC9 /* broken-order.json */, 74C8F06520EEB76400B6EDC9 /* order-notes.json */, 74C8F06F20EEC3A800B6EDC9 /* broken-notes.json */, + B56C1EB920EA7D2C00D749F9 /* sites.json */, ); path = Responses; sourceTree = ""; @@ -323,6 +331,7 @@ B505F6CC20BEE37E00BB1B69 /* AccountMapper.swift */, B5C6FCD320A373BA00A4F8E4 /* OrderMapper.swift */, B567AF2A20A0FA4200AB6C62 /* OrderListMapper.swift */, + B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */, 74C8F06720EEB7BC00B6EDC9 /* OrderNotesMapper.swift */, ); path = Mapper; @@ -473,6 +482,7 @@ B505F6D520BEE4E700BB1B69 /* me.json in Resources */, B5C6FCD620A3768900A4F8E4 /* order.json in Resources */, B559EBAA20A0B5CD00836CD4 /* orders-load-all.json in Resources */, + B56C1EBA20EA7D2C00D749F9 /* sites.json in Resources */, CE20179320E3EFA7005B4C18 /* broken-orders.json in Resources */, 74C8F07020EEC3A800B6EDC9 /* broken-notes.json in Resources */, ); @@ -544,6 +554,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B56C1EB620EA757B00D749F9 /* SiteListMapper.swift in Sources */, B557DA1A20979D66005962F4 /* Settings.swift in Sources */, 741B950120EBC8A700DD6E2D /* OrderCouponLine.swift in Sources */, 74C8F06420EEB44800B6EDC9 /* OrderNote.swift in Sources */, @@ -551,6 +562,7 @@ B567AF2520A0CCA300AB6C62 /* AuthenticatedRequest.swift in Sources */, B505F6EA20BEFC3700BB1B69 /* MockupNetwork.swift in Sources */, B557DA0220975500005962F4 /* JetpackRequest.swift in Sources */, + B56C1EB820EA76F500D749F9 /* Site.swift in Sources */, B505F6CD20BEE37E00BB1B69 /* AccountMapper.swift in Sources */, B557DA0D20975DB1005962F4 /* WordPressAPIVersion.swift in Sources */, B557DA1D20979E7D005962F4 /* Order.swift in Sources */, diff --git a/Networking/Networking/Mapper/SiteListMapper.swift b/Networking/Networking/Mapper/SiteListMapper.swift new file mode 100644 index 00000000000..61c51d322ad --- /dev/null +++ b/Networking/Networking/Mapper/SiteListMapper.swift @@ -0,0 +1,29 @@ +import Foundation + + +/// Mapper: SiteList +/// +class SiteListMapper: Mapper { + + /// (Attempts) to convert a dictionary into [Site]. + /// + func map(response: Data) throws -> [Site] { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter) + + return try decoder.decode(SiteListEnvelope.self, from: response).sites + } +} + + +/// SiteList Disposable Entity: +/// `Load All Sites` endpoint returns all of its orders within the `sites` key. This entity +/// allows us to do parse all the things with JSONDecoder. +/// +private struct SiteListEnvelope: Decodable { + let sites: [Site] + + private enum CodingKeys: String, CodingKey { + case sites = "sites" + } +} diff --git a/Networking/Networking/Model/Site.swift b/Networking/Networking/Model/Site.swift new file mode 100644 index 00000000000..0a1c1f52b57 --- /dev/null +++ b/Networking/Networking/Model/Site.swift @@ -0,0 +1,60 @@ +import Foundation + + +/// Represents a WordPress.com Site. +/// +public struct Site: Decodable { + + /// WordPress.com Site Identifier. + /// + let siteID: Int + + /// Site's Name. + /// + let name: String + + /// Site's Description. + /// + let description: String + + /// Site's URL. + /// + let url: String + + /// Indicates if this site hosts a WordPress Store. + /// + let isWordPressStore: Bool + + + /// Designated Initializer. + /// + public init(from decoder: Decoder) throws { + let siteContainer = try decoder.container(keyedBy: SiteKeys.self) + + siteID = try siteContainer.decode(Int.self, forKey: .siteID) + name = try siteContainer.decode(String.self, forKey: .name) + description = try siteContainer.decode(String.self, forKey: .description) + url = try siteContainer.decode(String.self, forKey: .url) + + let optionsContainer = try siteContainer.nestedContainer(keyedBy: OptionKeys.self, forKey: .options) + isWordPressStore = try optionsContainer.decode(Bool.self, forKey: .isWordPressStore) + } +} + + +/// Defines all of the Site CodingKeys. +/// +private extension Site { + + enum SiteKeys: String, CodingKey { + case siteID = "ID" + case name = "name" + case description = "description" + case url = "URL" + case options = "options" + } + + enum OptionKeys: String, CodingKey { + case isWordPressStore = "is_wpcom_store" + } +} diff --git a/Networking/Networking/Remote/AccountRemote.swift b/Networking/Networking/Remote/AccountRemote.swift index bdb1e2b22eb..e7c19715082 100644 --- a/Networking/Networking/Remote/AccountRemote.swift +++ b/Networking/Networking/Remote/AccountRemote.swift @@ -15,4 +15,19 @@ public class AccountRemote: Remote { enqueue(request, mapper: mapper, completion: completion) } + + + /// Loads the Sites collection associated with the WordPress.com User. + /// + public func loadSites(completion: @escaping ([Site]?, Error?) -> Void) { + let path = "me/sites" + let parameters = [ + "fields": "ID,name,description,URL,options" + ] + + let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters) + let mapper = SiteListMapper() + + enqueue(request, mapper: mapper, completion: completion) + } } diff --git a/Networking/NetworkingTests/Mapper/AccountMapperTests.swift b/Networking/NetworkingTests/Mapper/AccountMapperTests.swift index 3a66ad4a97b..a0bbb7b6fb7 100644 --- a/Networking/NetworkingTests/Mapper/AccountMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/AccountMapperTests.swift @@ -20,6 +20,27 @@ class AccountMapperTests: XCTestCase { XCTAssertEqual(account.userID, 78972699) XCTAssertEqual(account.username, "apiexamples") } + + /// Verifies that all of the Site fields are properly parsed. + /// + func testSiteFieldsAreProperlyParsed() { + let sites = mapLoadSitesResponse() + XCTAssert(sites?.count == 2) + + let first = sites!.first! + XCTAssertEqual(first.siteID, 1112233334444555) + XCTAssertEqual(first.name, "Testing Blog") + XCTAssertEqual(first.description, "Testing Tagline") + XCTAssertEqual(first.url, "https://some-testing-url.testing.blog") + XCTAssertEqual(first.isWordPressStore, true) + + let second = sites!.last! + XCTAssertEqual(second.siteID, 11122333344446666) + XCTAssertEqual(second.name, "Thoughts") + XCTAssertEqual(second.description, "Your Favorite Blog") + XCTAssertEqual(second.url, "https://thoughts.testing.blog") + XCTAssertEqual(second.isWordPressStore, false) + } } @@ -28,7 +49,7 @@ class AccountMapperTests: XCTestCase { // private extension AccountMapperTests { - /// Returns the AccountMapper output upon receiving `me` (Data Encoded) + /// Returns the AccountMapper output upon receiving `me` mockup response (Data Encoded). /// func mapLoadAccountResponse() -> Account? { guard let response = Loader.contentsOf("me") else { @@ -37,4 +58,14 @@ private extension AccountMapperTests { return try? AccountMapper().map(response: response) } + + /// Returns the SiteListMapper output upon receiving `me/sites` mockup response (Data Encoded). + /// + func mapLoadSitesResponse() -> [Site]? { + guard let response = Loader.contentsOf("sites") else { + return nil + } + + return try? SiteListMapper().map(response: response) + } } diff --git a/Networking/NetworkingTests/Responses/sites.json b/Networking/NetworkingTests/Responses/sites.json new file mode 100644 index 00000000000..1ac6b3ee09d --- /dev/null +++ b/Networking/NetworkingTests/Responses/sites.json @@ -0,0 +1,219 @@ +{ + "sites": [ + { + "ID": 1112233334444555, + "name": "Testing Blog", + "description": "Testing Tagline", + "URL": "https:\/\/some-testing-url.testing.blog", + "options": { + "timezone": "", + "gmt_offset": 0, + "blog_public": 1, + "videopress_enabled": false, + "upgraded_filetypes_enabled": true, + "login_url": "https:\/\/some-testing-url.here\/wp-login.php", + "admin_url": "https:\/\/some-testing-url.here\/wp-admin\/", + "is_mapped_domain": true, + "is_redirect": false, + "unmapped_url": "https:\/\/some-testing-url.here", + "featured_images_enabled": false, + "theme_slug": "storefront", + "header_image": false, + "background_color": false, + "image_default_link_type": "file", + "image_thumbnail_width": 150, + "image_thumbnail_height": 150, + "image_thumbnail_crop": 0, + "image_medium_width": 300, + "image_medium_height": 300, + "image_large_width": 1024, + "image_large_height": 1024, + "permalink_structure": "\/%year%\/%monthnum%\/%day%\/%postname%\/", + "post_formats": [], + "default_post_format": "0", + "default_category": 12, + "allowed_file_types": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "doc", + "ppt", + "odt", + "pptx", + "docx", + "pps", + "ppsx", + "xls", + "xlsx", + "key", + "ogv", + "mp4", + "m4v", + "mov", + "wmv", + "avi", + "mpg", + "3gp", + "3g2" + ], + "show_on_front": "page", + "default_likes_enabled": true, + "default_sharing_status": true, + "default_comment_status": true, + "default_ping_status": true, + "software_version": "4.9.6", + "created_at": "2014-03-28T15:03:03+00:00", + "wordads": false, + "publicize_permanently_disabled": false, + "frame_nonce": "73ae7163d8", + "page_on_front": 1455, + "page_for_posts": 0, + "headstart": false, + "headstart_is_fresh": false, + "ak_vp_bundle_enabled": false, + "advanced_seo_front_page_description": "", + "advanced_seo_title_formats": [], + "verification_services_codes": null, + "podcasting_archive": null, + "is_domain_only": false, + "is_automated_transfer": true, + "is_wpcom_store": true, + "woocommerce_is_active": true, + "design_type": null, + "site_goals": null, + "jetpack_version": "6.2.1", + "main_network_site": "https:\/\/some-testing-url.here", + "active_modules": [ + "after-the-deadline", + "contact-form", + "custom-content-types", + "custom-css", + "enhanced-distribution", + "gravatar-hovercards", + "json-api", + "latex", + "manage", + "notes", + "post-by-email", + "protect", + "publicize", + "sharedaddy", + "shortcodes", + "shortlinks", + "sitemaps", + "stats", + "subscriptions", + "verification-tools", + "widget-visibility", + "widgets", + "sso", + "tiled-gallery", + "likes", + "comments", + "comment-likes", + "videopress", + "carousel", + "photon", + "seo-tools", + "google-analytics", + "infinite-scroll", + "masterbar", + "vaultpress" + ], + "max_upload_size": false, + "wp_memory_limit": "268435456", + "wp_max_memory_limit": "268435456", + "is_multi_network": false, + "is_multi_site": false, + "file_mod_disabled": false + }, + "updates": { + "plugins": 3, + "themes": 1, + "wordpress": 0, + "translations": 0, + "total": 4 + } + }, + { + "ID": 11122333344446666, + "name": "Thoughts", + "description": "Your Favorite Blog", + "URL": "https:\/\/thoughts.testing.blog", + "options": { + "timezone": "", + "gmt_offset": 0, + "blog_public": 1, + "videopress_enabled": false, + "upgraded_filetypes_enabled": false, + "login_url": "https:\/\/thoughts.testing.blog\/wp-login.php", + "admin_url": "https:\/\/thoughts.testing.blog\/wp-admin\/", + "is_mapped_domain": false, + "is_redirect": false, + "unmapped_url": "https:\/\/thoughts.testing.blog", + "featured_images_enabled": false, + "theme_slug": "pub\/p2-breathe", + "header_image": { + "thumbnail_url": "https:\/\/thoughts.testing.blog\/2016\/07\/blur.jpg?resize=520,108.33333333333", + "url": "https:\/\/thoughts.testing.blog\/2016\/07\/blur.jpg?resize=1200,250", + "description": "Blurred Lights" + }, + "background_color": "2b2b2b", + "image_default_link_type": "file", + "image_thumbnail_width": 150, + "image_thumbnail_height": 150, + "image_thumbnail_crop": 0, + "image_medium_width": 300, + "image_medium_height": 300, + "image_large_width": 1024, + "image_large_height": 1024, + "permalink_structure": "\/%year%\/%monthnum%\/%day%\/%postname%\/", + "post_formats": [], + "default_post_format": "standard", + "default_category": 1, + "allowed_file_types": [ + "jpg", + "jpeg", + "png", + "gif", + "pdf", + "doc", + "ppt", + "odt", + "pptx", + "docx", + "pps", + "ppsx", + "xls", + "xlsx", + "key" + ], + "show_on_front": "posts", + "default_likes_enabled": true, + "default_sharing_status": false, + "default_comment_status": true, + "default_ping_status": true, + "software_version": "4.9.7-alpha-43298", + "created_at": "2013-08-05T16:39:48+00:00", + "wordads": false, + "publicize_permanently_disabled": false, + "frame_nonce": "e7bfd785f0", + "headstart": false, + "headstart_is_fresh": false, + "ak_vp_bundle_enabled": null, + "advanced_seo_front_page_description": "", + "advanced_seo_title_formats": [], + "verification_services_codes": null, + "podcasting_archive": null, + "is_domain_only": false, + "is_automated_transfer": false, + "is_wpcom_store": false, + "woocommerce_is_active": false, + "design_type": null, + "site_goals": null + } + } + ] +} diff --git a/Podfile.lock b/Podfile.lock index a6740dcf063..035ee4a84c5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -13,9 +13,9 @@ PODS: - CocoaLumberjack/Default - CocoaLumberjack/Swift (3.4.2): - CocoaLumberjack/Default - - Crashlytics (3.10.1): - - Fabric (~> 1.7.5) - - Fabric (1.7.7) + - Crashlytics (3.10.3): + - Fabric (~> 1.7.8) + - Fabric (1.7.8) - FormatterKit/Resources (1.8.2) - FormatterKit/TimeIntervalFormatter (1.8.2): - FormatterKit/Resources @@ -59,10 +59,10 @@ PODS: - UIDeviceIdentifier (~> 0.4) - WordPressShared (~> 1.0.3) - wpxmlrpc (= 0.8.3) - - WordPressShared (1.0.7): + - WordPressShared (1.0.8): - CocoaLumberjack (~> 3.4) - FormatterKit/TimeIntervalFormatter (= 1.8.2) - - WordPressUI (1.0.4) + - WordPressUI (1.0.5) - wpxmlrpc (0.8.3) DEPENDENCIES: @@ -118,8 +118,8 @@ SPEC CHECKSUMS: Alamofire: e4fa87002c137ba2d8d634d2c51fabcda0d5c223 Automattic-Tracks-iOS: d8c6c6c1351b1905a73e45f431b15598d71963b5 CocoaLumberjack: db7cc9e464771f12054c22ff6947c5a58d43a0fd - Crashlytics: aee1a064cbbf99b32efa3f056a5f458d846bc8ff - Fabric: bda89e242bce1b7b8ab264248cf3407774ce0095 + Crashlytics: 2aea305161a2122492698c75af27f2b98aa49a25 + Fabric: fba4684a95df789565b0b27fc5b6e68f1755af32 FormatterKit: 4b8f29acc9b872d5d12a63efb560661e8f2e1b98 GoogleSignInRepacked: d357702618c555f38923576924661325eb1ef22b GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f @@ -133,8 +133,8 @@ SPEC CHECKSUMS: UIDeviceIdentifier: a959a6d4f51036b4180dd31fb26483a820f1cc46 WordPressAuthenticator: 4c802aa18781858253daf984f873e3efe0dac7ef WordPressKit: a24baaa783c3a221f2d9a51c19318cbb27333373 - WordPressShared: db964b81e02ff9be1ea2ff65ca9a4d57c49e82ba - WordPressUI: f2348649b63b5a9392a72b1d2f46dd1d72e80ad9 + WordPressShared: 063e1e8b1a7aaf635abf17f091a2d235a068abdc + WordPressUI: ab90086350f1e26bf39e578e443d0abf0347e542 wpxmlrpc: bfc572f62ce7ee897f6f38b098d2ba08732ecef4 PODFILE CHECKSUM: b0058ebc467be802babc234b85e02282da5d9d42 diff --git a/Yosemite/Yosemite/Actions/AccountAction.swift b/Yosemite/Yosemite/Actions/AccountAction.swift index 10034099ce7..9e451ae836f 100644 --- a/Yosemite/Yosemite/Actions/AccountAction.swift +++ b/Yosemite/Yosemite/Actions/AccountAction.swift @@ -7,5 +7,4 @@ import Networking // public enum AccountAction: Action { case synchronizeAccount(onCompletion: (Account?, Error?) -> Void) - case retrieveAccount(userId: Int, onCompletion: (Account?) -> Void) } diff --git a/Yosemite/Yosemite/Stores/AccountStore.swift b/Yosemite/Yosemite/Stores/AccountStore.swift index e6efaf6b96e..933b99b9224 100644 --- a/Yosemite/Yosemite/Stores/AccountStore.swift +++ b/Yosemite/Yosemite/Stores/AccountStore.swift @@ -25,8 +25,6 @@ public class AccountStore: Store { switch action { case .synchronizeAccount(let onCompletion): synchronizeAccount(onCompletion: onCompletion) - case .retrieveAccount(let userId, let onCompletion): - retrieveAccount(userId: userId, onCompletion: onCompletion) } } } @@ -51,13 +49,6 @@ extension AccountStore { onCompletion(account, nil) } } - - /// Retrieves the account associated with a given User ID (if any!). - /// - func retrieveAccount(userId: Int, onCompletion: (Account?) -> Void) { - let account = loadStoredAccount(userId: userId)?.toReadOnly() - onCompletion(account) - } } diff --git a/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift b/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift index 517034cd825..54e0febeba9 100644 --- a/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/AccountStoreTests.swift @@ -122,33 +122,6 @@ class AccountStoreTests: XCTestCase { let storageAccount = accountStore.loadStoredAccount(userId: remoteAccount.userID)! compare(storageAccount: storageAccount, remoteAccount: remoteAccount) } - - /// Verifies that AccountAction.retrieveAccount returns the expected Account. - /// - func testRetrieveAccountReturnsEntityWithExpectedFields() { - let accountStore = AccountStore(dispatcher: dispatcher, storageManager: storageManager, network: network) - let sampleAccount = sampleAccountPristine() - - let expectation = self.expectation(description: "Synchronize") - accountStore.upsertStoredAccount(remote: sampleAccount) - - let retrieveAccountAction = AccountAction.retrieveAccount(userId: sampleAccount.userID) { account in - guard let retrieved = account else { - XCTFail() - return - } - - XCTAssertEqual(retrieved.displayName, sampleAccount.displayName) - XCTAssertEqual(retrieved.email, sampleAccount.email) - XCTAssertEqual(retrieved.gravatarUrl, sampleAccount.gravatarUrl) - XCTAssertEqual(retrieved.userID, sampleAccount.userID) - XCTAssertEqual(retrieved.username, sampleAccount.username) - expectation.fulfill() - } - - accountStore.onAction(retrieveAccountAction) - wait(for: [expectation], timeout: Constants.expectationTimeout) - } }