diff --git a/Modules/Sources/Networking/Remote/ProductsRemote.swift b/Modules/Sources/Networking/Remote/ProductsRemote.swift index 5fddc32cbc5..18a81fbb7b3 100644 --- a/Modules/Sources/Networking/Remote/ProductsRemote.swift +++ b/Modules/Sources/Networking/Remote/ProductsRemote.swift @@ -8,7 +8,7 @@ public protocol ProductsRemoteProtocol { func addProduct(product: Product, completion: @escaping (Result) -> Void) func deleteProduct(for siteID: Int64, productID: Int64, completion: @escaping (Result) -> Void) func loadProduct(for siteID: Int64, productID: Int64, completion: @escaping (Result) -> 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, @@ -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) -> Void) @@ -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) } } @@ -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) } @@ -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(siteID: siteID) - enqueue(request, mapper: mapper, completion: completion) + return try await enqueue(request, mapper: mapper) } @@ -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, @@ -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: ",") @@ -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(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. @@ -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, @@ -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(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), @@ -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(siteID: siteID) + return try await enqueue(request, mapper: mapper) } /// Retrieves a product SKU if available. diff --git a/Modules/Sources/Yosemite/Stores/ProductStore.swift b/Modules/Sources/Yosemite/Stores/ProductStore.swift index 82b5d10e8b2..1a73dba5a2e 100644 --- a/Modules/Sources/Yosemite/Stores/ProductStore.swift +++ b/Modules/Sources/Yosemite/Stores/ProductStore.swift @@ -231,35 +231,31 @@ private extension ProductStore { productCategory: ProductCategory?, excludedProductIDs: [Int64], onCompletion: @escaping (Result) -> Void) { - switch filter { - case .all: - remote.searchProducts(for: siteID, - keyword: keyword, - pageNumber: pageNumber, - pageSize: pageSize, - stockStatus: stockStatus, - productStatus: productStatus, - productType: productType, - productCategory: productCategory, - excludedProductIDs: excludedProductIDs) { [weak self] result in - self?.handleSearchResults(siteID: siteID, - keyword: keyword, - filter: filter, - pageSize: pageSize, - result: result, - onCompletion: onCompletion) - } - case .sku: - remote.searchProductsBySKU(for: siteID, - keyword: keyword, - pageNumber: pageNumber, - pageSize: pageSize) { [weak self] result in - self?.handleSearchResults(siteID: siteID, - keyword: keyword, - filter: filter, - pageSize: pageSize, - result: result, - onCompletion: onCompletion) + Task { @MainActor in + do { + let products: [Product] + switch filter { + case .all: + products = try await remote.searchProducts(for: siteID, + keyword: keyword, + pageNumber: pageNumber, + pageSize: pageSize, + stockStatus: stockStatus, + productStatus: productStatus, + productType: productType, + productCategory: productCategory, + excludedProductIDs: excludedProductIDs) + case .sku: + products = try await remote.searchProductsBySKU(for: siteID, + keyword: keyword, + pageNumber: pageNumber, + pageSize: pageSize) + } + await upsertSearchResultsInBackground(siteID: siteID, keyword: keyword, filter: filter, readOnlyProducts: products) + let hasNextPage = products.count == pageSize + onCompletion(.success(hasNextPage)) + } catch { + onCompletion(.failure(error)) } } } @@ -340,13 +336,12 @@ private extension ProductStore { return } - remote.loadProducts(for: order.siteID, by: missingIDs) { [weak self] result in - switch result { - case .success(let products): - self?.upsertStoredProductsInBackground(readOnlyProducts: products, siteID: order.siteID, onCompletion: { - onCompletion(nil) - }) - case .failure(let error): + Task { @MainActor in + do { + let products = try await remote.loadProducts(for: order.siteID, by: missingIDs) + await upsertStoredProductsInBackground(readOnlyProducts: products, siteID: order.siteID) + onCompletion(nil) + } catch { onCompletion(error) } } @@ -365,14 +360,13 @@ private extension ProductStore { return } - remote.loadProducts(for: siteID, by: productIDs, pageNumber: pageNumber, pageSize: pageSize) { [weak self] result in - switch result { - case .success(let products): - self?.upsertStoredProductsInBackground(readOnlyProducts: products, siteID: siteID, onCompletion: { - let hasNextPage = products.count == pageSize - onCompletion(.success((products: products, hasNextPage: hasNextPage))) - }) - case .failure(let error): + Task { @MainActor in + do { + let products = try await remote.loadProducts(for: siteID, by: productIDs, pageNumber: pageNumber, pageSize: pageSize) + await upsertStoredProductsInBackground(readOnlyProducts: products, siteID: siteID) + let hasNextPage = products.count == pageSize + onCompletion(.success((products: products, hasNextPage: hasNextPage))) + } catch { onCompletion(.failure(error)) } } @@ -1271,6 +1265,23 @@ private extension ProductStore { }, completion: onCompletion, on: .main) } + func upsertSearchResultsInBackground(siteID: Int64, + keyword: String, + filter: ProductSearchFilter, + readOnlyProducts: [Networking.Product]) async { + await withCheckedContinuation { [weak self] continuation in + guard let self else { + return continuation.resume() + } + upsertSearchResultsInBackground(siteID: siteID, + keyword: keyword, + filter: filter, + readOnlyProducts: readOnlyProducts) { + continuation.resume() + } + } + } + /// Upserts the Products, and associates them to the Search Results Entity (in the specified Storage) /// private func upsertStoredResults(siteID: Int64, @@ -1319,35 +1330,17 @@ private extension ProductStore { } func searchProductsBySKU(for siteID: Int64, keyword: String) async throws -> [Product] { - try await withCheckedThrowingContinuation { continuation in - remote.searchProductsBySKU(for: siteID, - keyword: keyword, - pageNumber: Remote.Default.firstPageNumber, - pageSize: ProductsRemote.Default.pageSize) { result in - switch result { - case let .success(products): - continuation.resume(returning: products) - case let .failure(error): - continuation.resume(throwing: error) - } - } - } + try await remote.searchProductsBySKU(for: siteID, + keyword: keyword, + pageNumber: Remote.Default.firstPageNumber, + pageSize: ProductsRemote.Default.pageSize) } func searchProductsByGlobalUniqueIdentifier(for siteID: Int64, keyword: String) async throws -> [Product] { - try await withCheckedThrowingContinuation { continuation in - remote.searchProductsByGlobalUniqueIdentifier(for: siteID, - keyword: keyword, - pageNumber: Remote.Default.firstPageNumber, - pageSize: ProductsRemote.Default.pageSize) { result in - switch result { - case let .success(products): - continuation.resume(returning: products) - case let .failure(error): - continuation.resume(throwing: error) - } - } - } + try await remote.searchProductsByGlobalUniqueIdentifier(for: siteID, + keyword: keyword, + pageNumber: Remote.Default.firstPageNumber, + pageSize: ProductsRemote.Default.pageSize) } } diff --git a/Networking/NetworkingTests/Remote/ProductsRemoteTests.swift b/Networking/NetworkingTests/Remote/ProductsRemoteTests.swift index 7551d802c4b..9d237756d6d 100644 --- a/Networking/NetworkingTests/Remote/ProductsRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/ProductsRemoteTests.swift @@ -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. @@ -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) } diff --git a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductsRemote.swift b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductsRemote.swift index 8d9e4df2ebe..a41c62769e3 100644 --- a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductsRemote.swift +++ b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductsRemote.swift @@ -233,19 +233,20 @@ extension MockProductsRemote: ProductsRemoteProtocol { } } - 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] { requestedProductIDsForLoading = productIDs - DispatchQueue.main.async { [weak self] in - guard let self = self else { - return - } - let key = ResultKey(siteID: siteID, productIDs: productIDs) - if let result = self.productsLoadingResults[key] { - completion(result) - } else { - XCTFail("\(String(describing: self)) Could not find Result for \(key)") - } + let key = ResultKey(siteID: siteID, productIDs: productIDs) + guard let result = productsLoadingResults[key] else { + XCTFail("\(String(describing: self)) Could not find Result for \(key)") + throw NetworkError.notFound() + } + + switch result { + case let .success(products): + return products + case let .failure(error): + throw error } } @@ -291,39 +292,52 @@ extension MockProductsRemote: ProductsRemoteProtocol { productStatus: ProductStatus?, productType: ProductType?, productCategory: ProductCategory?, - excludedProductIDs: [Int64], - completion: @escaping (Result<[Product], Error>) -> Void) { + excludedProductIDs: [Int64]) async throws -> [Product] { searchProductTriggered = true searchProductWithStockStatus = stockStatus searchProductWithProductType = productType searchProductWithProductStatus = productStatus searchProductWithProductCategory = productCategory - if let result = searchProductsResultsByQuery[keyword] { - completion(result) + guard let result = searchProductsResultsByQuery[keyword] else { + throw NetworkError.notFound() + } + switch result { + case let .success(products): + return products + case let .failure(error): + throw error } } func searchProductsBySKU(for siteID: Int64, keyword: String, pageNumber: Int, - pageSize: Int, - completion: @escaping (Result<[Product], Error>) -> Void) { - if let result = searchProductsBySKUResultsBySKU[keyword] { - completion(result) - } else { + pageSize: Int) async throws -> [Product] { + guard let result = searchProductsBySKUResultsBySKU[keyword] else { XCTFail("\(String(describing: self)) Could not find result for SKU \(keyword)") + throw NetworkError.notFound() + } + switch result { + case let .success(products): + return products + case let .failure(error): + throw error } } func searchProductsByGlobalUniqueIdentifier(for siteID: Int64, keyword: String, pageNumber: Int, - pageSize: Int, - completion: @escaping (Result<[Product], Error>) -> Void) { - if let result = searchProductsByGlobalUniqueIdentifierResults[keyword] { - completion(result) - } else { + pageSize: Int) async throws -> [Product] { + guard let result = searchProductsByGlobalUniqueIdentifierResults[keyword] else { XCTFail("\(String(describing: self)) Could not find result for Global Unique Identifier \(keyword)") + throw NetworkError.notFound() + } + switch result { + case let .success(products): + return products + case let .failure(error): + throw error } } diff --git a/Yosemite/YosemiteTests/Stores/ProductStoreTests.swift b/Yosemite/YosemiteTests/Stores/ProductStoreTests.swift index 2ece6794770..f3d6c466dd0 100644 --- a/Yosemite/YosemiteTests/Stores/ProductStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/ProductStoreTests.swift @@ -1216,17 +1216,20 @@ final class ProductStoreTests: XCTestCase { let filteredProductCategory: Networking.ProductCategory = .init(categoryID: 123, siteID: sampleSiteID, parentID: 1, name: "Test", slug: "test") // When - let searchAction = ProductAction.searchProducts(siteID: sampleSiteID, - keyword: "hiii", - pageNumber: defaultPageNumber, - pageSize: defaultPageSize, - stockStatus: filteredStockStatus, - productStatus: filteredProductStatus, - productType: filteredProductType, - productCategory: filteredProductCategory, - excludedProductIDs: [], - onCompletion: { _ in }) - productStore.onAction(searchAction) + waitFor { promise in + productStore.onAction(ProductAction.searchProducts(siteID: self.sampleSiteID, + keyword: "hiii", + pageNumber: self.defaultPageNumber, + pageSize: self.defaultPageSize, + stockStatus: filteredStockStatus, + productStatus: filteredProductStatus, + productType: filteredProductType, + productCategory: filteredProductCategory, + excludedProductIDs: [], + onCompletion: { _ in + promise(()) + })) + } // Then XCTAssertTrue(remote.searchProductTriggered) @@ -1705,8 +1708,14 @@ final class ProductStoreTests: XCTestCase { // Action let pageNumber = 6 let pageSize = 36 - let action = ProductAction.retrieveProducts(siteID: sampleSiteID, productIDs: [sampleProductID], pageNumber: pageNumber, pageSize: pageSize) { _ in } - productStore.onAction(action) + waitFor { promise in + productStore.onAction(ProductAction.retrieveProducts(siteID: self.sampleSiteID, + productIDs: [self.sampleProductID], + pageNumber: pageNumber, + pageSize: pageSize) { _ in + promise(()) + }) + } // Assert guard let queryParameters = network.queryParameters else {