Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Yosemite/Yosemite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
02E4F5E423CD5628003B0010 /* NSOrderedSet+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4F5E323CD5628003B0010 /* NSOrderedSet+Array.swift */; };
02E7FFD52562226B00C53030 /* ShippingLabelStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E7FFD42562226B00C53030 /* ShippingLabelStoreTests.swift */; };
02E7FFD92562234F00C53030 /* MockShippingLabelRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E7FFD82562234F00C53030 /* MockShippingLabelRemote.swift */; };
02EF1666292DB65000D90AD6 /* PaymentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF1665292DB65000D90AD6 /* PaymentStore.swift */; };
02EF1668292DB68C00D90AD6 /* PaymentAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF1667292DB68C00D90AD6 /* PaymentAction.swift */; };
02F2722D292F18BF00C36419 /* PaymentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2722C292F18BF00C36419 /* PaymentStoreTests.swift */; };
02F2722F292F18FD00C36419 /* MockPaymentRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2722E292F18FD00C36419 /* MockPaymentRemote.swift */; };
02F6AAAC270556A4002425D0 /* Models+Copiable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F6AAAB270556A4002425D0 /* Models+Copiable.generated.swift */; };
02FF054D23D983F30058E6E7 /* MediaFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FF054523D983F30058E6E7 /* MediaFileManager.swift */; };
02FF054E23D983F30058E6E7 /* MediaImageExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FF054623D983F30058E6E7 /* MediaImageExporter.swift */; };
Expand Down Expand Up @@ -507,6 +511,10 @@
02E4F5E323CD5628003B0010 /* NSOrderedSet+Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSOrderedSet+Array.swift"; sourceTree = "<group>"; };
02E7FFD42562226B00C53030 /* ShippingLabelStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelStoreTests.swift; sourceTree = "<group>"; };
02E7FFD82562234F00C53030 /* MockShippingLabelRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShippingLabelRemote.swift; sourceTree = "<group>"; };
02EF1665292DB65000D90AD6 /* PaymentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentStore.swift; sourceTree = "<group>"; };
02EF1667292DB68C00D90AD6 /* PaymentAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentAction.swift; sourceTree = "<group>"; };
02F2722C292F18BF00C36419 /* PaymentStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentStoreTests.swift; sourceTree = "<group>"; };
02F2722E292F18FD00C36419 /* MockPaymentRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaymentRemote.swift; sourceTree = "<group>"; };
02F6AAAB270556A4002425D0 /* Models+Copiable.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Models+Copiable.generated.swift"; sourceTree = "<group>"; };
02FF054523D983F30058E6E7 /* MediaFileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaFileManager.swift; sourceTree = "<group>"; };
02FF054623D983F30058E6E7 /* MediaImageExporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaImageExporter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1206,6 +1214,7 @@
021BA0C328576940006E9886 /* MockDotcomAccountRemote.swift */,
02DAE7F9291A9F36009342B7 /* MockDomainRemote.swift */,
02616F932921E1CD0095BC00 /* MockSiteRemote.swift */,
02F2722E292F18FD00C36419 /* MockPaymentRemote.swift */,
);
path = Remote;
sourceTree = "<group>";
Expand Down Expand Up @@ -1419,6 +1428,7 @@
02E3B622290267D3007E0F13 /* AccountCreationStore.swift */,
02393066291A02AC00B2632F /* DomainStore.swift */,
021940E5291E8AD80090354E /* SiteStore.swift */,
02EF1665292DB65000D90AD6 /* PaymentStore.swift */,
);
path = Stores;
sourceTree = "<group>";
Expand Down Expand Up @@ -1480,6 +1490,7 @@
02E3B629290622DE007E0F13 /* AccountCreationStoreTests.swift */,
02DAE7F7291A9F11009342B7 /* DomainStoreTests.swift */,
02616F912921E1530095BC00 /* SiteStoreTests.swift */,
02F2722C292F18BF00C36419 /* PaymentStoreTests.swift */,
);
path = Stores;
sourceTree = "<group>";
Expand Down Expand Up @@ -1667,6 +1678,7 @@
02E3B624290267F2007E0F13 /* AccountCreationAction.swift */,
02393064291A018600B2632F /* DomainAction.swift */,
021940E3291E8A660090354E /* SiteAction.swift */,
02EF1667292DB68C00D90AD6 /* PaymentAction.swift */,
);
path = Actions;
sourceTree = "<group>";
Expand Down Expand Up @@ -2001,6 +2013,7 @@
261F94E6242EFF8700762B58 /* ProductCategoryStore.swift in Sources */,
57DFCC7925003C4000251E0C /* FetchResultSnapshotsProvider.swift in Sources */,
7492FAD9217FAD1000ED2C69 /* SiteSetting+ReadOnlyConvertible.swift in Sources */,
02EF1666292DB65000D90AD6 /* PaymentStore.swift in Sources */,
E1BD4D0027ABF84D006416D9 /* CardPresentPaymentsConfiguration.swift in Sources */,
CE3B7AD52225EBF10050FE4B /* OrderStatusAction.swift in Sources */,
7493750C224987D9007D85D1 /* ProductAttribute+ReadOnlyConvertible.swift in Sources */,
Expand Down Expand Up @@ -2115,6 +2128,7 @@
B52E002E211A3F5500700FDE /* ReadOnlyType.swift in Sources */,
5726456F250BD4E4005BBD7C /* OrdersUpsertUseCase.swift in Sources */,
3147030C2670333200EF253A /* WCPayAccount+PaymentGatewayAccount.swift in Sources */,
02EF1668292DB68C00D90AD6 /* PaymentAction.swift in Sources */,
B5C9DE162087FF0E006B910A /* Store.swift in Sources */,
02FF056123D98FD40058E6E7 /* ImageSourceWriter.swift in Sources */,
0212AC5E242C67FA00C51F6C /* ProductsSortOrder.swift in Sources */,
Expand Down Expand Up @@ -2279,6 +2293,7 @@
02FF055B23D9846A0058E6E7 /* MediaDirectoryTests.swift in Sources */,
265BCA0024301ACD004E53EE /* ProductCategoryStoreTests.swift in Sources */,
02FF056D23DEDCB90058E6E7 /* MockImageSourceWriter.swift in Sources */,
02F2722F292F18FD00C36419 /* MockPaymentRemote.swift in Sources */,
FE28F6F2268462A6004465C7 /* UserStoreTests.swift in Sources */,
02DAE7FA291A9F36009342B7 /* MockDomainRemote.swift in Sources */,
741F34842195F752005F5BD9 /* CommentStoreTests.swift in Sources */,
Expand All @@ -2295,6 +2310,7 @@
031FD8A026FC970400B315C7 /* RosettaTestingHelper.swift in Sources */,
077F39E526A5C98200ABEADC /* SystemStatusStoreTests.swift in Sources */,
022F931D257F27B40011CD94 /* MockShippingLabelAddress.swift in Sources */,
02F2722D292F18BF00C36419 /* PaymentStoreTests.swift in Sources */,
02FF056923DECD5B0058E6E7 /* MediaImageExporterTests.swift in Sources */,
0202B6972387AFBF00F3EBE0 /* MockInMemoryStorage.swift in Sources */,
028BCE2422DE22BB00056966 /* SiteVisitStatsStoreErrorTests.swift in Sources */,
Expand Down
22 changes: 22 additions & 0 deletions Yosemite/Yosemite/Actions/PaymentAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

/// PaymentAction: Defines all of the Actions supported by the PaymentStore.
///
public enum PaymentAction: Action {
/// Loads a specific WPCOM plan.
/// - Parameters:
/// - productID: The ID of the WPCOM product to return.
/// - completion: Invoked when the WPCOM plan that matches the given ID is loaded.
case loadPlan(productID: Int64,
completion: (Result<WPComPlan, Error>) -> Void)

/// Creates a cart with a WPCOM plan.
/// - Parameters:
/// - productID: The ID of the WPCOM plan product. It is of string type to integrate with `InAppPurchasesForWPComPlansProtocol`.
/// If the value is not a string of integer value, an error `CreateCartError.invalidProductID` is returned.
/// - siteID: The site ID for the WPCOM plan to be attached to.
/// - completion: The result of cart creation.
case createCart(productID: String,
siteID: Int64,
completion: (Result<Void, Error>) -> Void)
}
93 changes: 93 additions & 0 deletions Yosemite/Yosemite/Stores/PaymentStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Foundation
import Networking
import protocol Storage.StorageManagerType

/// Handles `PaymentAction`
///
public final class PaymentStore: Store {
// Keeps a strong reference to remote to keep requests alive.
private let remote: PaymentRemoteProtocol

public init(remote: PaymentRemoteProtocol,
dispatcher: Dispatcher,
storageManager: StorageManagerType,
network: Network) {
self.remote = remote
super.init(dispatcher: dispatcher, storageManager: storageManager, network: network)
}

public convenience override init(dispatcher: Dispatcher,
storageManager: StorageManagerType,
network: Network) {
let remote = PaymentRemote(network: network)
self.init(remote: remote,
dispatcher: dispatcher,
storageManager: storageManager,
network: network)
}

public override func registerSupportedActions(in dispatcher: Dispatcher) {
dispatcher.register(processor: self, for: PaymentAction.self)
}

/// Called whenever a given Action is dispatched.
///
public override func onAction(_ action: Action) {
guard let action = action as? PaymentAction else {
assertionFailure("PaymentStore received an unsupported action: \(action)")
return
}
switch action {
case .loadPlan(let productID, let completion):
loadPlan(productID: productID, completion: completion)
case .createCart(let productID, let siteID, let completion):
createCart(productID: productID, siteID: siteID, completion: completion)
}
}
}

private extension PaymentStore {
func loadPlan(productID: Int64,
completion: @escaping (Result<WPComPlan, Error>) -> Void) {
Task { @MainActor in
do {
let plan = try await remote.loadPlan(thatMatchesID: productID)
completion(.success(plan))
} catch {
completion(.failure(error))
}
}
}

func createCart(productID: String,
siteID: Int64,
completion: @escaping (Result<Void, Error>) -> Void) {
Task { @MainActor in
do {
guard let productID = Int64(productID) else {
return completion(.failure(CreateCartError.invalidProductID))
}
_ = try await remote.createCart(siteID: siteID, productID: productID)
completion(.success(()))
} catch {
switch error {
case let networkError as Networking.CreateCartError:
switch networkError {
case .productNotInCart:
completion(.failure(CreateCartError.productNotInCart))
}
default:
completion(.failure(error))
}
}
}
}
}

/// Possible cart creation errors.
public enum CreateCartError: Error, Equatable {
/// Product ID is not in the correct format for WPCOM plans.
case invalidProductID
/// The expected product is not in the created cart.
case productNotInCart
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Networking
import XCTest

/// Mock for `PaymentRemote`.
///
final class MockPaymentRemote {
/// The results to return in `loadPlan`.
private var loadPlanResult: Result<WPComPlan, Error>?

/// The results to return in `createCart`.
private var createCartResult: Result<Void, Error>?

/// Returns the value when `loadPlan` is called.
func whenLoadingPlan(thenReturn result: Result<WPComPlan, Error>) {
loadPlanResult = result
}

/// Returns the value when `createCart` is called.
func whenCreatingCart(thenReturn result: Result<Void, Error>) {
createCartResult = result
}
}

extension MockPaymentRemote: PaymentRemoteProtocol {
func loadPlan(thatMatchesID productID: Int64) async throws -> Networking.WPComPlan {
guard let result = loadPlanResult else {
XCTFail("Could not find result for loading a plan.")
throw NetworkError.notFound
}
return try result.get()
}

func createCart(siteID: Int64, productID: Int64) async throws {
guard let result = createCartResult else {
XCTFail("Could not find result for creating a cart.")
throw NetworkError.notFound
}
return try result.get()
}
}
140 changes: 140 additions & 0 deletions Yosemite/YosemiteTests/Stores/PaymentStoreTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import TestKit
import XCTest
@testable import Networking
@testable import Yosemite

final class PaymentStoreTests: 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: MockPaymentRemote!
private var store: PaymentStore!

override func setUp() {
super.setUp()
dispatcher = Dispatcher()
storageManager = MockStorageManager()
network = MockNetwork()
remote = MockPaymentRemote()
store = PaymentStore(remote: remote, dispatcher: dispatcher, storageManager: storageManager, network: network)
}

override func tearDown() {
store = nil
remote = nil
network = nil
storageManager = nil
dispatcher = nil
super.tearDown()
}

// MARK: - `loadPlan`

func test_loadPlan_returns_plan_on_success() throws {
// Given
remote.whenLoadingPlan(thenReturn: .success(.init(productID: 12, name: "woo", formattedPrice: "$16.8")))

// When
let result = waitFor { promise in
self.store.onAction(PaymentAction.loadPlan(productID: 12) { result in
promise(result)
})
}

// Then
XCTAssertTrue(result.isSuccess)
let plan = try XCTUnwrap(result.get())
XCTAssertEqual(plan, .init(productID: 12, name: "woo", formattedPrice: "$16.8"))
}

func test_loadPlan_returns_failure_on_error() throws {
// Given
remote.whenLoadingPlan(thenReturn: .failure(NetworkError.timeout))

// When
let result = waitFor { promise in
self.store.onAction(PaymentAction.loadPlan(productID: 12) { result in
promise(result)
})
}

// Then
XCTAssertTrue(result.isFailure)
let error = try XCTUnwrap(result.failure)
XCTAssertEqual(error as? NetworkError, .timeout)
}

// MARK: - `createCart`

func test_createCart_returns_on_success() throws {
// Given
remote.whenCreatingCart(thenReturn: .success(()))

// When
let result = waitFor { promise in
self.store.onAction(PaymentAction.createCart(productID: "12", siteID: 62) { result in
promise(result)
})
}

// Then
XCTAssertTrue(result.isSuccess)
}

func test_createCart_returns_invalidProductID_error_when_productID_is_not_integer() throws {
// Given
remote.whenCreatingCart(thenReturn: .failure(NetworkError.timeout))

// When
let result = waitFor { promise in
self.store.onAction(PaymentAction.createCart(productID: "wo0", siteID: 62) { result in
promise(result)
})
}

// Then
XCTAssertTrue(result.isFailure)
let error = try XCTUnwrap(result.failure)
XCTAssertEqual(error as? Yosemite.CreateCartError, .invalidProductID)
}

func test_createCart_relays_networking_CreateCartError_failure() throws {
// Given
remote.whenCreatingCart(thenReturn: .failure(Networking.CreateCartError.productNotInCart))

// When
let result = waitFor { promise in
self.store.onAction(PaymentAction.createCart(productID: "12", siteID: 62) { result in
promise(result)
})
}

// Then
XCTAssertTrue(result.isFailure)
let error = try XCTUnwrap(result.failure)
XCTAssertEqual(error as? Yosemite.CreateCartError, .productNotInCart)
}

func test_createCart_returns_failure_on_error() throws {
// Given
remote.whenCreatingCart(thenReturn: .failure(NetworkError.timeout))

// When
let result = waitFor { promise in
self.store.onAction(PaymentAction.createCart(productID: "12", siteID: 62) { result in
promise(result)
})
}

// Then
XCTAssertTrue(result.isFailure)
let error = try XCTUnwrap(result.failure)
XCTAssertEqual(error as? NetworkError, .timeout)
}
}