Skip to content

Update ProductsRemote to load products with async/throws network methods to parse response in a background thread #15781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
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
56 changes: 22 additions & 34 deletions Modules/Sources/Networking/Remote/ProductsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public protocol ProductsRemoteProtocol {
func addProduct(product: Product, completion: @escaping (Result<Product, Error>) -> Void)
func deleteProduct(for siteID: Int64, productID: Int64, completion: @escaping (Result<Product, Error>) -> Void)
func loadProduct(for siteID: Int64, productID: Int64, completion: @escaping (Result<Product, Error>) -> Void)
func loadProducts(for siteID: Int64, by productIDs: [Int64], pageNumber: Int, pageSize: Int, completion: @escaping (Result<[Product], Error>) -> Void)
func loadProducts(for siteID: Int64, by productIDs: [Int64], pageNumber: Int, pageSize: Int) async throws -> [Product]
func loadAllProducts(for siteID: Int64,
context: String?,
pageNumber: Int,
Expand All @@ -29,18 +29,15 @@ public protocol ProductsRemoteProtocol {
productStatus: ProductStatus?,
productType: ProductType?,
productCategory: ProductCategory?,
excludedProductIDs: [Int64],
completion: @escaping (Result<[Product], Error>) -> Void)
excludedProductIDs: [Int64]) async throws -> [Product]
func searchProductsBySKU(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void)
pageSize: Int) async throws -> [Product]
func searchProductsByGlobalUniqueIdentifier(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void)
pageSize: Int) async throws -> [Product]
func searchSku(for siteID: Int64,
sku: String,
completion: @escaping (Result<String, Error>) -> Void)
Expand Down Expand Up @@ -102,12 +99,11 @@ public protocol ProductsRemoteProtocol {
}

extension ProductsRemoteProtocol {
public func loadProducts(for siteID: Int64, by productIDs: [Int64], completion: @escaping (Result<[Product], Error>) -> Void) {
loadProducts(for: siteID,
by: productIDs,
pageNumber: ProductsRemote.Default.pageNumber,
pageSize: ProductsRemote.Default.pageSize,
completion: completion)
public func loadProducts(for siteID: Int64, by productIDs: [Int64]) async throws -> [Product] {
try await loadProducts(for: siteID,
by: productIDs,
pageNumber: ProductsRemote.Default.pageNumber,
pageSize: ProductsRemote.Default.pageSize)
}
}

Expand Down Expand Up @@ -387,16 +383,13 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - productIDs: The array of product IDs that are requested.
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of products to be retrieved per page.
/// - completion: Closure to be executed upon completion.
///
public func loadProducts(for siteID: Int64,
by productIDs: [Int64],
pageNumber: Int = Default.pageNumber,
pageSize: Int = Default.pageSize,
completion: @escaping (Result<[Product], Error>) -> Void) {
pageSize: Int = Default.pageSize) async throws -> [Product] {
guard productIDs.isEmpty == false else {
completion(.success([]))
return
return []
}

let stringOfProductIDs = productIDs.map { String($0) }
Expand All @@ -409,9 +402,9 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
]
let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
let mapper = ListMapper<Product>(siteID: siteID)

enqueue(request, mapper: mapper, completion: completion)
return try await enqueue(request, mapper: mapper)
}


Expand All @@ -438,7 +431,6 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of products to be retrieved per page.
/// - excludedProductIDs: a list of product IDs to be excluded from the results.
/// - completion: Closure to be executed upon completion.
///
public func searchProducts(for siteID: Int64,
keyword: String,
Expand All @@ -448,8 +440,7 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
productStatus: ProductStatus? = nil,
productType: ProductType? = nil,
productCategory: ProductCategory? = nil,
excludedProductIDs: [Int64] = [],
completion: @escaping (Result<[Product], Error>) -> Void) {
excludedProductIDs: [Int64] = []) async throws -> [Product] {
let stringOfExcludedProductIDs = excludedProductIDs.map { String($0) }
.joined(separator: ",")

Expand All @@ -471,9 +462,9 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {

let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
let mapper = ListMapper<Product>(siteID: siteID)

enqueue(request, mapper: mapper, completion: completion)
return try await enqueue(request, mapper: mapper)
}

/// Retrieves all of the `Product`s that match the SKU. Partial SKU search is supported for WooCommerce version 6.6+, otherwise full SKU match is performed.
Expand All @@ -482,12 +473,10 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - keyword: Search string that should be matched by the SKU (partial or full depending on the WC version).
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of products to be retrieved per page.
/// - completion: Closure to be executed upon completion.
public func searchProductsBySKU(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void) {
pageSize: Int) async throws -> [Product] {
let parameters = [
ParameterKey.sku: keyword,
ParameterKey.partialSKUSearch: keyword,
Expand All @@ -497,15 +486,14 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
]
let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
enqueue(request, mapper: mapper, completion: completion)
let mapper = ListMapper<Product>(siteID: siteID)
return try await enqueue(request, mapper: mapper)
}

public func searchProductsByGlobalUniqueIdentifier(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void) {
pageSize: Int) async throws -> [Product] {
let parameters = [
ParameterKey.globalUniqueID: keyword,
ParameterKey.page: String(pageNumber),
Expand All @@ -514,8 +502,8 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
]
let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
enqueue(request, mapper: mapper, completion: completion)
let mapper = ListMapper<Product>(siteID: siteID)
return try await enqueue(request, mapper: mapper)
}

/// Retrieves a product SKU if available.
Expand Down
151 changes: 82 additions & 69 deletions Networking/NetworkingTests/Remote/ProductsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,43 @@ final class ProductsRemoteTests: XCTestCase {
XCTAssertEqual(result?.isFailure, true)
}

// MARK: - Load products tests

func test_loadProducts_properly_returns_parsed_products() async throws {
// Given
let remote = ProductsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "products-load-all")

// When
let products = try await remote.loadProducts(for: sampleSiteID, by: [6, 2])

// Then
XCTAssertEqual(products.count, 10)
}

func test_loadProducts_returns_empty_array_when_productIDs_is_empty() async throws {
// Given
let remote = ProductsRemote(network: network)

// When
let products = try await remote.loadProducts(for: sampleSiteID, by: [])

// Then
XCTAssertEqual(products.count, 0)
}

func test_loadProducts_properly_relays_netwoking_errors() async {
// Given
let remote = ProductsRemote(network: network)

do {
_ = try await remote.loadProducts(for: sampleSiteID, by: [6, 2])
XCTFail("Expected error to be thrown")
} catch {
XCTAssertEqual(error as? NetworkError, .notFound())
}
}

// MARK: - Load all products tests

/// Verifies that loadAllProducts properly parses the `products-load-all` sample response.
Expand Down Expand Up @@ -413,127 +450,103 @@ final class ProductsRemoteTests: XCTestCase {

/// Verifies that searchProducts properly parses the `products-load-all` sample response.
///
func test_searchProducts_properly_returns_parsed_products() throws {
func test_searchProducts_properly_returns_parsed_products() async throws {
// Given
let remote = ProductsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "products-search-photo")

// When
let result: Result<[Product], Error> = waitFor { promise in
remote.searchProducts(for: self.sampleSiteID,
keyword: "photo",
pageNumber: 0,
pageSize: 100) { result in
promise(result)
}
}
let products = try await remote.searchProducts(for: sampleSiteID,
keyword: "photo",
pageNumber: 0,
pageSize: 100)

// Then
XCTAssertTrue(result.isSuccess)
let products = try result.get()
XCTAssertEqual(products.count, 2)
}

/// Verifies that searchProducts properly relays Networking Layer errors.
///
func test_searchProducts_properly_relays_networking_errors() {
func test_searchProducts_properly_relays_networking_errors() async {
// Given
let remote = ProductsRemote(network: network)

// When
let result: Result<[Product], Error> = waitFor { promise in
remote.searchProducts(for: self.sampleSiteID,
keyword: String(),
pageNumber: 0,
pageSize: 100) { result in
promise(result)
}
// When & Then
do {
_ = try await remote.searchProducts(for: sampleSiteID,
keyword: String(),
pageNumber: 0,
pageSize: 100)
XCTFail("Expected error to be thrown")
} catch {
// Expected error
}

// Then
XCTAssertTrue(result.isFailure)
}

// MARK: - Search Products by SKU

func test_searchProductsBySKU_properly_returns_parsed_products() throws {
func test_searchProductsBySKU_properly_returns_parsed_products() async throws {
// Given
let remote = ProductsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "products-sku-search")

// When
let result: Result<[Product], Error> = waitFor { promise in
remote.searchProductsBySKU(for: self.sampleSiteID,
keyword: "choco",
pageNumber: 0,
pageSize: 100) { result in
promise(result)
}
}
let products = try await remote.searchProductsBySKU(for: sampleSiteID,
keyword: "choco",
pageNumber: 0,
pageSize: 100)

// Then
XCTAssertTrue(result.isSuccess)
let products = try result.get()
XCTAssertEqual(products.count, 1)
}

func test_searchProductsBySKU_properly_relays_networking_errors() {
func test_searchProductsBySKU_properly_relays_networking_errors() async {
// Given
let remote = ProductsRemote(network: network)

// When
let result: Result<[Product], Error> = waitFor { promise in
remote.searchProductsBySKU(for: self.sampleSiteID,
keyword: String(),
pageNumber: 0,
pageSize: 100) { result in
promise(result)
}
// When & Then
do {
_ = try await remote.searchProductsBySKU(for: sampleSiteID,
keyword: String(),
pageNumber: 0,
pageSize: 100)
XCTFail("Expected error to be thrown")
} catch {
// Expected error
}

// Then
XCTAssertTrue(result.isFailure)
}

// MARK: - Search Products by Global Unique Identifier

func test_searchProductsByGlobalUniqueIdentifier_properly_returns_parsed_products() throws {
func test_searchProductsByGlobalUniqueIdentifier_properly_returns_parsed_products() async throws {
// Given
let remote = ProductsRemote(network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "products-sku-search")

// When
let result: Result<[Product], Error> = waitFor { promise in
remote.searchProductsByGlobalUniqueIdentifier(for: self.sampleSiteID,
keyword: "12345",
pageNumber: 0,
pageSize: 100) { result in
promise(result)
}
}
let products = try await remote.searchProductsByGlobalUniqueIdentifier(for: sampleSiteID,
keyword: "12345",
pageNumber: 0,
pageSize: 100)

// Then
XCTAssertTrue(result.isSuccess)
let products = try result.get()
XCTAssertEqual(products.count, 1)
}

func test_searchProductsByGlobalUniqueIdentifier_properly_relays_networking_errors() {
func test_searchProductsByGlobalUniqueIdentifier_properly_relays_networking_errors() async {
// Given
let remote = ProductsRemote(network: network)

// When
let result: Result<[Product], Error> = waitFor { promise in
remote.searchProductsByGlobalUniqueIdentifier(for: self.sampleSiteID,
keyword: String(),
pageNumber: 0,
pageSize: 100) { result in
promise(result)
}
// When & Then
do {
_ = try await remote.searchProductsByGlobalUniqueIdentifier(for: sampleSiteID,
keyword: String(),
pageNumber: 0,
pageSize: 100)
XCTFail("Expected error to be thrown")
} catch {
// Expected error
}

// Then
XCTAssertTrue(result.isFailure)
}


Expand Down
Loading