From 2e6985ab2a75176a90d47c424eac73fda4127721 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Wed, 9 Nov 2022 08:39:50 +0800 Subject: [PATCH 1/2] Add `DomainRemote` for domain suggestions endpoint. --- .../Networking.xcodeproj/project.pbxproj | 12 ++++ .../Networking/Remote/DomainRemote.swift | 68 +++++++++++++++++++ .../Remote/DomainRemoteTests.swift | 47 +++++++++++++ .../Responses/domain-suggestions.json | 12 ++++ 4 files changed, 139 insertions(+) create mode 100644 Networking/Networking/Remote/DomainRemote.swift create mode 100644 Networking/NetworkingTests/Remote/DomainRemoteTests.swift create mode 100644 Networking/NetworkingTests/Responses/domain-suggestions.json diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 00aed7b1931..d5dcb4fa631 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ 021EAA5625493B3600AA8CCD /* OrderItemAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021EAA5525493B3600AA8CCD /* OrderItemAttribute.swift */; }; 022902D422E2436400059692 /* stats_module_disabled_error.json in Resources */ = {isa = PBXBuildFile; fileRef = 022902D122E2436300059692 /* stats_module_disabled_error.json */; }; 022902D622E2436400059692 /* no_stats_permission_error.json in Resources */ = {isa = PBXBuildFile; fileRef = 022902D322E2436400059692 /* no_stats_permission_error.json */; }; + 023930632918FF5400B2632F /* DomainRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023930622918FF5400B2632F /* DomainRemote.swift */; }; + 0239306B291A96F800B2632F /* DomainRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0239306A291A96F800B2632F /* DomainRemoteTests.swift */; }; + 0239306D291A973F00B2632F /* domain-suggestions.json in Resources */ = {isa = PBXBuildFile; fileRef = 0239306C291A973F00B2632F /* domain-suggestions.json */; }; 025CA2C0238EB8CB00B05C81 /* ProductShippingClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2BF238EB8CB00B05C81 /* ProductShippingClass.swift */; }; 025CA2C2238EBBAA00B05C81 /* ProductShippingClassListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2C1238EBBAA00B05C81 /* ProductShippingClassListMapper.swift */; }; 025CA2C4238EBC4300B05C81 /* ProductShippingClassRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2C3238EBC4300B05C81 /* ProductShippingClassRemote.swift */; }; @@ -767,6 +770,9 @@ 021EAA5525493B3600AA8CCD /* OrderItemAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderItemAttribute.swift; sourceTree = ""; }; 022902D122E2436300059692 /* stats_module_disabled_error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = stats_module_disabled_error.json; sourceTree = ""; }; 022902D322E2436400059692 /* no_stats_permission_error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = no_stats_permission_error.json; sourceTree = ""; }; + 023930622918FF5400B2632F /* DomainRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainRemote.swift; sourceTree = ""; }; + 0239306A291A96F800B2632F /* DomainRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainRemoteTests.swift; sourceTree = ""; }; + 0239306C291A973F00B2632F /* domain-suggestions.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "domain-suggestions.json"; sourceTree = ""; }; 025CA2BF238EB8CB00B05C81 /* ProductShippingClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductShippingClass.swift; sourceTree = ""; }; 025CA2C1238EBBAA00B05C81 /* ProductShippingClassListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductShippingClassListMapper.swift; sourceTree = ""; }; 025CA2C3238EBC4300B05C81 /* ProductShippingClassRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductShippingClassRemote.swift; sourceTree = ""; }; @@ -1720,6 +1726,7 @@ DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */, 68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */, 68F48B0E28E3BB850045C15B /* WCAnalyticsCustomerRemoteTests.swift */, + 0239306A291A96F800B2632F /* DomainRemoteTests.swift */, ); path = Remote; sourceTree = ""; @@ -1859,6 +1866,7 @@ AEF94584272974F2001DCCFB /* TelemetryRemote.swift */, 68CB800F28D89A0400E169F8 /* CustomerRemote.swift */, 68F48B0A28E3B1CD0045C15B /* WCAnalyticsCustomerRemote.swift */, + 023930622918FF5400B2632F /* DomainRemote.swift */, ); path = Remote; sourceTree = ""; @@ -1985,6 +1993,7 @@ 028CB713290223CB00331C09 /* create-account-error-password.json */, 028CB71A290224D700331C09 /* create-account-error-username.json */, 028CB712290223CB00331C09 /* create-account-success.json */, + 0239306C291A973F00B2632F /* domain-suggestions.json */, DE50295F28C609A300551736 /* jetpack-connected-user.json */, DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */, DE50296228C609DE00551736 /* jetpack-user-not-connected.json */, @@ -2886,6 +2895,7 @@ 3158FE8426129F3A00E566B9 /* wcpay-account-unknown-status.json in Resources */, 314EDF2B27C02CD300A56B6F /* stripe-account-complete-null-descriptor.json in Resources */, 31054720262E2F9D00C5C02B /* wcpay-payment-intent-processing.json in Resources */, + 0239306D291A973F00B2632F /* domain-suggestions.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2964,6 +2974,7 @@ 26455E2425F66982008A1D32 /* ProductAttributeTermRemote.swift in Sources */, 036563DD29069BE400D84BFD /* JustInTimeMessageListMapper.swift in Sources */, 7426CA0D21AF27B9004E9FFC /* SiteAPIRemote.swift in Sources */, + 023930632918FF5400B2632F /* DomainRemote.swift in Sources */, 451A97D1260A03900059D135 /* ShippingLabelCustomPackage.swift in Sources */, DE34051328BDCA5100CF0D97 /* WordPressOrgRequest.swift in Sources */, D88D5A45230BC6F9007B6E01 /* ProductReviewsRemote.swift in Sources */, @@ -3338,6 +3349,7 @@ 74AB5B4D21AF354E00859C12 /* SiteAPIMapperTests.swift in Sources */, 68F48B0F28E3BB850045C15B /* WCAnalyticsCustomerRemoteTests.swift in Sources */, 93D8BC01226BC20600AD2EB3 /* AccountSettingsRemoteTests.swift in Sources */, + 0239306B291A96F800B2632F /* DomainRemoteTests.swift in Sources */, 262E5AD5255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift in Sources */, 03DCB7442624AD9B00C8953D /* CouponListMapperTests.swift in Sources */, AE2D6623272A8F6E004A2C3A /* TelemetryRemoteTests.swift in Sources */, diff --git a/Networking/Networking/Remote/DomainRemote.swift b/Networking/Networking/Remote/DomainRemote.swift new file mode 100644 index 00000000000..309d890d80e --- /dev/null +++ b/Networking/Networking/Remote/DomainRemote.swift @@ -0,0 +1,68 @@ +import Foundation + +/// Protocol for `DomainRemote` mainly used for mocking. +public protocol DomainRemoteProtocol { + /// Loads domain suggestions that are free (`*.wordpress.com` only) based on the query. + /// - Parameter query: What the domain suggestions are based on. + /// - Returns: The result of free domain suggestions. + func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> +} + +/// Domain: Remote Endpoints +/// +public class DomainRemote: Remote, DomainRemoteProtocol { + public func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> { + let path = Path.domainSuggestions + let parameters: [String: Any] = [ + ParameterKey.query: query, + ParameterKey.quantity: Defaults.domainSuggestionsQuantity, + ParameterKey.wordPressDotComSubdomainsOnly: true + ] + let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters) + do { + let suggestions: [FreeDomainSuggestion] = try await enqueue(request) + return .success(suggestions) + } catch { + return .failure(error) + } + } +} + +/// Necessary data for a free domain suggestion. +public struct FreeDomainSuggestion: Decodable, Equatable { + /// Domain name. + public let name: String + /// Theoretically `true` for all domains in the result, but the client side can still filter any exceptions in the UI. + public let isFree: Bool + + public init(name: String, isFree: Bool) { + self.name = name + self.isFree = isFree + } + + private enum CodingKeys: String, CodingKey { + case name = "domain_name" + case isFree = "is_free" + } +} + +// MARK: - Constants +// +private extension DomainRemote { + enum Defaults { + static let domainSuggestionsQuantity = 20 + } + + enum ParameterKey { + /// Term (e.g "flowers") or domain name (e.g. "flowers.com") to search alternative domain names from. + static let query = "query" + /// Maximum number of suggestions to return. + static let quantity = "quantity" + /// Whether to restrict suggestions to only wordpress.com subdomains. If `true`, only `quantity` and `query` parameters are respected. + static let wordPressDotComSubdomainsOnly = "only_wordpressdotcom" + } + + enum Path { + static let domainSuggestions = "domains/suggestions" + } +} diff --git a/Networking/NetworkingTests/Remote/DomainRemoteTests.swift b/Networking/NetworkingTests/Remote/DomainRemoteTests.swift new file mode 100644 index 00000000000..a05cdb23225 --- /dev/null +++ b/Networking/NetworkingTests/Remote/DomainRemoteTests.swift @@ -0,0 +1,47 @@ +import XCTest +@testable import Networking + +final class DomainRemoteTests: XCTestCase { + /// Mock network wrapper. + private var network: MockNetwork! + + override func setUp() { + super.setUp() + network = MockNetwork() + } + + override func tearDown() { + network = nil + super.tearDown() + } + + func test_loadFreeDomainSuggestions_returns_suggestions_on_success() async throws { + // Given + let remote = DomainRemote(network: network) + network.simulateResponse(requestUrlSuffix: "domains/suggestions", filename: "domain-suggestions") + + // When + let result = await remote.loadFreeDomainSuggestions(query: "domain") + + // Then + XCTAssertTrue(result.isSuccess) + let suggestions = try XCTUnwrap(result.get()) + XCTAssertEqual(suggestions, [ + .init(name: "domaintestingtips.wordpress.com", isFree: true), + .init(name: "domaintestingtoday.wordpress.com", isFree: true), + ]) + } + + func test_loadFreeDomainSuggestions_returns_error_on_empty_response() async throws { + // Given + let remote = DomainRemote(network: network) + + // When + let result = await remote.loadFreeDomainSuggestions(query: "domain") + + // Then + XCTAssertTrue(result.isFailure) + let error = try XCTUnwrap(result.failure as? NetworkError) + XCTAssertEqual(error, .notFound) + } +} diff --git a/Networking/NetworkingTests/Responses/domain-suggestions.json b/Networking/NetworkingTests/Responses/domain-suggestions.json new file mode 100644 index 00000000000..32075e6919e --- /dev/null +++ b/Networking/NetworkingTests/Responses/domain-suggestions.json @@ -0,0 +1,12 @@ +[ + { + "domain_name": "domaintestingtips.wordpress.com", + "cost": "Free", + "is_free": true + }, + { + "domain_name": "domaintestingtoday.wordpress.com", + "cost": "Free", + "is_free": true + } +] From 10b495f76dc05386ecdef60f1a48438823f74757 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Wed, 9 Nov 2022 08:46:10 +0800 Subject: [PATCH 2/2] Add `DomainStore` for domain suggestions. --- Yosemite/Yosemite.xcodeproj/project.pbxproj | 16 +++++ Yosemite/Yosemite/Actions/DomainAction.swift | 7 ++ Yosemite/Yosemite/Model/Model.swift | 1 + Yosemite/Yosemite/Stores/DomainStore.swift | 46 ++++++++++++ .../Networking/Remote/MockDomainRemote.swift | 24 +++++++ .../Stores/DomainStoreTests.swift | 71 +++++++++++++++++++ 6 files changed, 165 insertions(+) create mode 100644 Yosemite/Yosemite/Actions/DomainAction.swift create mode 100644 Yosemite/Yosemite/Stores/DomainStore.swift create mode 100644 Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift create mode 100644 Yosemite/YosemiteTests/Stores/DomainStoreTests.swift diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index ad652f6bf66..8ae110d33da 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ 022F9319257F24730011CD94 /* MockShippingLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022F9318257F24730011CD94 /* MockShippingLabel.swift */; }; 022F931D257F27B40011CD94 /* MockShippingLabelAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022F931C257F27B40011CD94 /* MockShippingLabelAddress.swift */; }; 0232372922F7DA6E00715FAB /* StatsTimeRangeV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0232372822F7DA6E00715FAB /* StatsTimeRangeV4.swift */; }; + 02393065291A018600B2632F /* DomainAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02393064291A018600B2632F /* DomainAction.swift */; }; + 02393067291A02AC00B2632F /* DomainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02393066291A02AC00B2632F /* DomainStore.swift */; }; 0248B3652459018100A271A4 /* ResultsController+FilterProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248B3642459018100A271A4 /* ResultsController+FilterProducts.swift */; }; 0248B3672459020500A271A4 /* ResultsController+FilterProductTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248B3662459020500A271A4 /* ResultsController+FilterProductTests.swift */; }; 0248B36924590FC300A271A4 /* ProductStore+FilterProductsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248B36824590FC300A271A4 /* ProductStore+FilterProductsTests.swift */; }; @@ -65,6 +67,8 @@ 02C254FA2563B66600A04423 /* ShippingLabelRefund+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C254F92563B66600A04423 /* ShippingLabelRefund+ReadOnlyConvertible.swift */; }; 02C254FE2563C6E500A04423 /* ShippingLabelSettings+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C254FD2563C6E500A04423 /* ShippingLabelSettings+ReadOnlyConvertible.swift */; }; 02C255022563C76A00A04423 /* ShippingLabel+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C255012563C76A00A04423 /* ShippingLabel+ReadOnlyConvertible.swift */; }; + 02DAE7F8291A9F11009342B7 /* DomainStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DAE7F7291A9F11009342B7 /* DomainStoreTests.swift */; }; + 02DAE7FA291A9F36009342B7 /* MockDomainRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DAE7F9291A9F36009342B7 /* MockDomainRemote.swift */; }; 02E262BD238CE46A00B79588 /* ShippingSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E262BC238CE46A00B79588 /* ShippingSettingsService.swift */; }; 02E262C0238CE80100B79588 /* StorageShippingSettingsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E262BF238CE80100B79588 /* StorageShippingSettingsServiceTests.swift */; }; 02E262C2238CF74D00B79588 /* StorageShippingSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E262C1238CF74D00B79588 /* StorageShippingSettingsService.swift */; }; @@ -456,6 +460,8 @@ 022F9318257F24730011CD94 /* MockShippingLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShippingLabel.swift; sourceTree = ""; }; 022F931C257F27B40011CD94 /* MockShippingLabelAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShippingLabelAddress.swift; sourceTree = ""; }; 0232372822F7DA6E00715FAB /* StatsTimeRangeV4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangeV4.swift; sourceTree = ""; }; + 02393064291A018600B2632F /* DomainAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainAction.swift; sourceTree = ""; }; + 02393066291A02AC00B2632F /* DomainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainStore.swift; sourceTree = ""; }; 0248B3642459018100A271A4 /* ResultsController+FilterProducts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsController+FilterProducts.swift"; sourceTree = ""; }; 0248B3662459020500A271A4 /* ResultsController+FilterProductTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsController+FilterProductTests.swift"; sourceTree = ""; }; 0248B36824590FC300A271A4 /* ProductStore+FilterProductsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductStore+FilterProductsTests.swift"; sourceTree = ""; }; @@ -482,6 +488,8 @@ 02C254F92563B66600A04423 /* ShippingLabelRefund+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabelRefund+ReadOnlyConvertible.swift"; sourceTree = ""; }; 02C254FD2563C6E500A04423 /* ShippingLabelSettings+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabelSettings+ReadOnlyConvertible.swift"; sourceTree = ""; }; 02C255012563C76A00A04423 /* ShippingLabel+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabel+ReadOnlyConvertible.swift"; sourceTree = ""; }; + 02DAE7F7291A9F11009342B7 /* DomainStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainStoreTests.swift; sourceTree = ""; }; + 02DAE7F9291A9F36009342B7 /* MockDomainRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDomainRemote.swift; sourceTree = ""; }; 02E262BC238CE46A00B79588 /* ShippingSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingSettingsService.swift; sourceTree = ""; }; 02E262BF238CE80100B79588 /* StorageShippingSettingsServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageShippingSettingsServiceTests.swift; sourceTree = ""; }; 02E262C1238CF74D00B79588 /* StorageShippingSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageShippingSettingsService.swift; sourceTree = ""; }; @@ -1188,6 +1196,7 @@ 02A26F1D2744FE97008E4EDB /* MockAccountRemote.swift */, 029249E7274B8AEE002E9C34 /* MockMediaRemote.swift */, 021BA0C328576940006E9886 /* MockDotcomAccountRemote.swift */, + 02DAE7F9291A9F36009342B7 /* MockDomainRemote.swift */, ); path = Remote; sourceTree = ""; @@ -1399,6 +1408,7 @@ DE3404FD28BC5F4200CF0D97 /* JetpackConnectionStore.swift */, 68BD37B428DB2E9800C2A517 /* CustomerStore.swift */, 02E3B622290267D3007E0F13 /* AccountCreationStore.swift */, + 02393066291A02AC00B2632F /* DomainStore.swift */, ); path = Stores; sourceTree = ""; @@ -1458,6 +1468,7 @@ DE50296628C7114800551736 /* JetpackConnectionStoreTests.swift */, 68BD37B828DB323D00C2A517 /* CustomerStoreTests.swift */, 02E3B629290622DE007E0F13 /* AccountCreationStoreTests.swift */, + 02DAE7F7291A9F11009342B7 /* DomainStoreTests.swift */, ); path = Stores; sourceTree = ""; @@ -1643,6 +1654,7 @@ B9AECD3D2850F41100E78584 /* OrderCardPresentPaymentEligibilityAction.swift */, DE3404FB28BC5E7800CF0D97 /* JetpackConnectionAction.swift */, 02E3B624290267F2007E0F13 /* AccountCreationAction.swift */, + 02393064291A018600B2632F /* DomainAction.swift */, ); path = Actions; sourceTree = ""; @@ -2036,6 +2048,7 @@ 2618707C2540B6A4006522A1 /* ShippingLineTax+ReadOnlyConvertible.swift in Sources */, 749375002249605E007D85D1 /* ProductAction.swift in Sources */, D831E2E4230E3524000037D0 /* ProductReviewAction.swift in Sources */, + 02393065291A018600B2632F /* DomainAction.swift in Sources */, D831E2E6230E7149000037D0 /* ProductReview+ReadOnlyConvertible.swift in Sources */, CE4FD4502350F27C00A16B31 /* OrderItemTax+ReadOnlyConvertible.swift in Sources */, 02FF055123D983F30058E6E7 /* MediaAssetExporter.swift in Sources */, @@ -2162,6 +2175,7 @@ 453305F7245AE68C00264E50 /* SitePostStore.swift in Sources */, 026CF62A237D92C6009563D4 /* ProductVariation+ReadOnlyConvertible.swift in Sources */, 02F6AAAC270556A4002425D0 /* Models+Copiable.generated.swift in Sources */, + 02393067291A02AC00B2632F /* DomainStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2192,6 +2206,7 @@ E1F54D0427AD4DAF00012983 /* CardPresentConfigurationTests.swift in Sources */, 025CA2D0238F54E800B05C81 /* ProductShippingClassStoreTests.swift in Sources */, 74A7688E20D45ED400F9D437 /* OrderStoreTests.swift in Sources */, + 02DAE7F8291A9F11009342B7 /* DomainStoreTests.swift in Sources */, D8652E322630741000350F37 /* PaymentIntent+ReceiptParametersTests.swift in Sources */, 029249E8274B8AEE002E9C34 /* MockMediaRemote.swift in Sources */, 021BA0C428576940006E9886 /* MockDotcomAccountRemote.swift in Sources */, @@ -2249,6 +2264,7 @@ 265BCA0024301ACD004E53EE /* ProductCategoryStoreTests.swift in Sources */, 02FF056D23DEDCB90058E6E7 /* MockImageSourceWriter.swift in Sources */, FE28F6F2268462A6004465C7 /* UserStoreTests.swift in Sources */, + 02DAE7FA291A9F36009342B7 /* MockDomainRemote.swift in Sources */, 741F34842195F752005F5BD9 /* CommentStoreTests.swift in Sources */, B5C9DE282087FF20006B910A /* MockSiteStore.swift in Sources */, FEEB2F5F268A1C5E0075A6E0 /* User+RolesTests.swift in Sources */, diff --git a/Yosemite/Yosemite/Actions/DomainAction.swift b/Yosemite/Yosemite/Actions/DomainAction.swift new file mode 100644 index 00000000000..24982628d2d --- /dev/null +++ b/Yosemite/Yosemite/Actions/DomainAction.swift @@ -0,0 +1,7 @@ +import Foundation + +// MARK: - DomainAction: Defines all of the Actions supported by the DomainStore. +// +public enum DomainAction: Action { + case loadFreeDomainSuggestions(query: String, completion: (Result<[FreeDomainSuggestion], Error>) -> Void) +} diff --git a/Yosemite/Yosemite/Model/Model.swift b/Yosemite/Yosemite/Model/Model.swift index 7f1d6a4942e..c226e0e7c9e 100644 --- a/Yosemite/Yosemite/Model/Model.swift +++ b/Yosemite/Yosemite/Model/Model.swift @@ -27,6 +27,7 @@ public typealias Customer = Networking.Customer public typealias DotcomDevice = Networking.DotcomDevice public typealias DotcomUser = Networking.DotcomUser public typealias Feature = WordPressKit.Feature +public typealias FreeDomainSuggestion = Networking.FreeDomainSuggestion public typealias InboxNote = Networking.InboxNote public typealias InboxAction = Networking.InboxAction public typealias JetpackUser = Networking.JetpackUser diff --git a/Yosemite/Yosemite/Stores/DomainStore.swift b/Yosemite/Yosemite/Stores/DomainStore.swift new file mode 100644 index 00000000000..41d80cb17bb --- /dev/null +++ b/Yosemite/Yosemite/Stores/DomainStore.swift @@ -0,0 +1,46 @@ +import Foundation +import Networking +import protocol Storage.StorageManagerType + +/// Handles `DomainAction`. +/// +public final class DomainStore: Store { + // Keeps a strong reference to remote to keep requests alive. + private let remote: DomainRemoteProtocol + + public init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network, remote: DomainRemoteProtocol) { + self.remote = remote + super.init(dispatcher: dispatcher, storageManager: storageManager, network: network) + } + + public override convenience init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) { + let remote = DomainRemote(network: network) + self.init(dispatcher: dispatcher, storageManager: storageManager, network: network, remote: remote) + } + + public override func registerSupportedActions(in dispatcher: Dispatcher) { + dispatcher.register(processor: self, for: DomainAction.self) + } + + /// Called whenever a given Action is dispatched. + /// + public override func onAction(_ action: Action) { + guard let action = action as? DomainAction else { + assertionFailure("DomainStore received an unsupported action: \(action)") + return + } + switch action { + case .loadFreeDomainSuggestions(let query, let completion): + loadFreeDomainSuggestions(query: query, completion: completion) + } + } +} + +private extension DomainStore { + func loadFreeDomainSuggestions(query: String, completion: @escaping (Result<[FreeDomainSuggestion], Error>) -> Void) { + Task { @MainActor in + let result = await remote.loadFreeDomainSuggestions(query: query) + completion(result) + } + } +} diff --git a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift new file mode 100644 index 00000000000..820ef55db59 --- /dev/null +++ b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockDomainRemote.swift @@ -0,0 +1,24 @@ +import Networking +import XCTest + +/// Mock for `DomainRemote`. +/// +final class MockDomainRemote { + /// The results to return in `loadDomainSuggestions`. + private var loadDomainSuggestionsResult: Result<[FreeDomainSuggestion], Error>? + + /// Returns the value when `loadDomainSuggestions` is called. + func whenLoadingDomainSuggestions(thenReturn result: Result<[FreeDomainSuggestion], Error>) { + loadDomainSuggestionsResult = result + } +} + +extension MockDomainRemote: DomainRemoteProtocol { + func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> { + guard let result = loadDomainSuggestionsResult else { + XCTFail("Could not find result for loading domain suggestions.") + return .failure(NetworkError.notFound) + } + return result + } +} diff --git a/Yosemite/YosemiteTests/Stores/DomainStoreTests.swift b/Yosemite/YosemiteTests/Stores/DomainStoreTests.swift new file mode 100644 index 00000000000..1f214142e01 --- /dev/null +++ b/Yosemite/YosemiteTests/Stores/DomainStoreTests.swift @@ -0,0 +1,71 @@ +import XCTest +@testable import Networking +@testable import Yosemite + +final class DomainStoreTests: XCTestCase { + /// Mock Dispatcher. + private var dispatcher: Dispatcher! + + /// Mock Storage: InMemory. + private var storageManager: MockStorageManager! + + /// Mock Network: Allows us to inject predefined responses. + private var network: MockNetwork! + + private var remote: MockDomainRemote! + private var store: DomainStore! + + override func setUp() { + super.setUp() + dispatcher = Dispatcher() + storageManager = MockStorageManager() + network = MockNetwork() + remote = MockDomainRemote() + store = DomainStore(dispatcher: dispatcher, storageManager: storageManager, network: network, remote: remote) + } + + override func tearDown() { + store = nil + remote = nil + network = nil + storageManager = nil + dispatcher = nil + super.tearDown() + } + + func test_loadFreeDomainSuggestions_returns_suggestions_on_success() throws { + // Given + remote.whenLoadingDomainSuggestions(thenReturn: .success([.init(name: "freedomaintesting", isFree: false)])) + + // When + let result: Result<[FreeDomainSuggestion], Error> = waitFor { promise in + let action = DomainAction.loadFreeDomainSuggestions(query: "domain") { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + let suggestions = try XCTUnwrap(result.get()) + XCTAssertEqual(suggestions, [.init(name: "freedomaintesting", isFree: false)]) + } + + func test_loadFreeDomainSuggestions_returns_error_on_failure() throws { + // Given + remote.whenLoadingDomainSuggestions(thenReturn: .failure(NetworkError.timeout)) + + // When + let result: Result<[FreeDomainSuggestion], Error> = waitFor { promise in + let action = DomainAction.loadFreeDomainSuggestions(query: "domain") { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + XCTAssertTrue(result.isFailure) + let error = try XCTUnwrap(result.failure) + XCTAssertEqual(error as? NetworkError, .timeout) + } +}