Skip to content

Commit 3c3df4c

Browse files
joshhealdclaude
andcommitted
Add tests for server-side validation error extraction
Add comprehensive test coverage for the networking error extraction logic now in POSOrderService. This logic was previously untested when it resided in PointOfSaleOrderController. Test cases cover: - DotcomError with invalid_variation_id code - NetworkError with variation_id in data field (detailed error) - NetworkError without variation_id (generic error) - AFError wrapping DotcomError (proper unwrapping) - Unrecognized error codes (pass-through behavior) - Variation not found in cart (generic fallback) All tests use properly formatted JSON responses matching real NetworkError structure with code, message, and optional data fields. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent ddfccf5 commit 3c3df4c

File tree

2 files changed

+172
-6
lines changed

2 files changed

+172
-6
lines changed

Modules/Sources/PointOfSale/Controllers/PointOfSaleOrderController.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,15 @@ protocol PointOfSaleOrderControllerProtocol {
9595
return .success(.newOrder)
9696
} catch {
9797
self.order = nil
98-
trackOrderCreationFailed(error: error, cart: posCart)
99-
setOrderStateToError(error, cart: posCart, retryHandler: retryHandler)
98+
trackOrderCreationFailed(error: error)
99+
setOrderStateToError(error, retryHandler: retryHandler)
100100
return .failure(SyncOrderStateError.syncFailure)
101101
}
102102
}
103103

104104
private func setOrderStateToError(_ error: Error,
105-
cart: POSCart,
106105
retryHandler: @escaping () async -> Void) {
107-
orderState = .error(orderStateError(from: error, cart: cart), {
106+
orderState = .error(orderStateError(from: error), {
108107
Task {
109108
await retryHandler()
110109
}
@@ -189,7 +188,7 @@ private extension PointOfSaleOrderController {
189188
// MARK: - Error Handling
190189

191190
private extension PointOfSaleOrderController {
192-
func orderStateError(from error: Error, cart: POSCart) -> PointOfSaleOrderState.OrderStateError {
191+
func orderStateError(from error: Error) -> PointOfSaleOrderState.OrderStateError {
193192
// Check for missing products error first
194193
if case .missingProductsInOrder(let missingItems) = error as? POSOrderService.POSOrderServiceError {
195194
let missingProductInfo = missingItems.map {
@@ -266,7 +265,7 @@ extension PointOfSaleOrderController {
266265

267266

268267
private extension PointOfSaleOrderController {
269-
func trackOrderCreationFailed(error: Error, cart: POSCart) {
268+
func trackOrderCreationFailed(error: Error) {
270269
var errorType: WooAnalyticsEvent.Orders.OrderCreationErrorType?
271270

272271
if let _ = CouponsError(underlyingError: error) {

Modules/Tests/YosemiteTests/Tools/POS/POSOrderServiceTests.swift

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import Foundation
22
import Testing
33
@testable import Yosemite
4+
import enum Networking.DotcomError
5+
import enum Networking.NetworkError
6+
import enum Alamofire.AFError
7+
import struct Networking.AnyDecodable
48

59
struct POSOrderServiceTests {
610
let sut: POSOrderService
@@ -341,6 +345,169 @@ struct POSOrderServiceTests {
341345
return false
342346
})
343347
}
348+
349+
// MARK: - Server-side Validation Error Tests
350+
351+
@Test func syncOrder_throws_missingProducts_error_for_DotcomError_with_invalid_variation_code() async throws {
352+
// Given
353+
let cart = POSCart(items: [makePOSCartItem(productID: 100, quantity: 1)])
354+
let dotcomError = DotcomError.unknown(code: "order_item_product_invalid_variation_id", message: "Invalid variation")
355+
mockOrdersRemote.createPOSOrderResult = .failure(dotcomError)
356+
357+
// When/Then
358+
await #expect(performing: {
359+
try await sut.syncOrder(cart: cart, currency: .USD)
360+
}, throws: { error in
361+
if case .missingProductsInOrder(let missingItems) = error as? POSOrderService.POSOrderServiceError {
362+
#expect(missingItems.count == 1)
363+
#expect(missingItems.first?.productID == 0) // Generic error
364+
#expect(missingItems.first?.variationID == 0)
365+
#expect(missingItems.first?.name == "Unknown Product")
366+
return true
367+
}
368+
return false
369+
})
370+
}
371+
372+
@Test func syncOrder_throws_missingProducts_error_for_NetworkError_with_invalid_variation_code_and_variation_id() async throws {
373+
// Given
374+
let cart = POSCart(items: [
375+
POSCartItem(
376+
item: POSVariation(
377+
id: UUID(),
378+
name: "Large",
379+
formattedPrice: "$20",
380+
price: "20",
381+
productID: 100,
382+
variationID: 500,
383+
parentProductName: "T-Shirt"
384+
),
385+
quantity: 1
386+
)
387+
])
388+
389+
let errorJSON = """
390+
{
391+
"code": "order_item_product_invalid_variation_id",
392+
"message": "Invalid variation",
393+
"data": {
394+
"variation_id": 500
395+
}
396+
}
397+
"""
398+
let errorData = errorJSON.data(using: .utf8)!
399+
let networkError = NetworkError.unacceptableStatusCode(statusCode: 400, response: errorData)
400+
mockOrdersRemote.createPOSOrderResult = .failure(networkError)
401+
402+
// When/Then
403+
await #expect(performing: {
404+
try await sut.syncOrder(cart: cart, currency: .USD)
405+
}, throws: { error in
406+
if case .missingProductsInOrder(let missingItems) = error as? POSOrderService.POSOrderServiceError {
407+
#expect(missingItems.count == 1)
408+
#expect(missingItems.first?.productID == 100)
409+
#expect(missingItems.first?.variationID == 500)
410+
#expect(missingItems.first?.name == "T-Shirt - Large")
411+
return true
412+
}
413+
return false
414+
})
415+
}
416+
417+
@Test func syncOrder_throws_missingProducts_error_for_NetworkError_without_variation_id() async throws {
418+
// Given
419+
let cart = POSCart(items: [makePOSCartItem(productID: 100, quantity: 1)])
420+
let errorJSON = """
421+
{
422+
"code": "order_item_product_invalid_variation_id",
423+
"message": "Invalid variation"
424+
}
425+
"""
426+
let errorData = errorJSON.data(using: .utf8)!
427+
let networkError = NetworkError.unacceptableStatusCode(statusCode: 400, response: errorData)
428+
mockOrdersRemote.createPOSOrderResult = .failure(networkError)
429+
430+
// When/Then
431+
await #expect(performing: {
432+
try await sut.syncOrder(cart: cart, currency: .USD)
433+
}, throws: { error in
434+
if case .missingProductsInOrder(let missingItems) = error as? POSOrderService.POSOrderServiceError {
435+
#expect(missingItems.count == 1)
436+
#expect(missingItems.first?.productID == 0) // Generic error
437+
#expect(missingItems.first?.variationID == 0)
438+
return true
439+
}
440+
return false
441+
})
442+
}
443+
444+
@Test func syncOrder_throws_missingProducts_error_for_AFError_wrapping_DotcomError() async throws {
445+
// Given
446+
let cart = POSCart(items: [makePOSCartItem(productID: 100, quantity: 1)])
447+
let dotcomError = DotcomError.unknown(code: "order_item_product_invalid_variation_id", message: "Invalid")
448+
let afError = AFError.sessionTaskFailed(error: dotcomError)
449+
mockOrdersRemote.createPOSOrderResult = .failure(afError)
450+
451+
// When/Then
452+
await #expect(performing: {
453+
try await sut.syncOrder(cart: cart, currency: .USD)
454+
}, throws: { error in
455+
if case .missingProductsInOrder(let missingItems) = error as? POSOrderService.POSOrderServiceError {
456+
#expect(missingItems.count == 1)
457+
return true
458+
}
459+
return false
460+
})
461+
}
462+
463+
@Test func syncOrder_throws_original_error_for_unrecognized_DotcomError_code() async throws {
464+
// Given
465+
let cart = POSCart(items: [makePOSCartItem(productID: 100, quantity: 1)])
466+
let dotcomError = DotcomError.unknown(code: "some_other_error_code", message: "Different error")
467+
mockOrdersRemote.createPOSOrderResult = .failure(dotcomError)
468+
469+
// When/Then
470+
await #expect(performing: {
471+
try await sut.syncOrder(cart: cart, currency: .USD)
472+
}, throws: { error in
473+
// Should throw the original DotcomError, not missingProductsInOrder
474+
if case .unknown = error as? DotcomError {
475+
return true
476+
}
477+
return false
478+
})
479+
}
480+
481+
@Test func syncOrder_throws_missingProducts_with_generic_name_when_variation_not_in_cart() async throws {
482+
// Given
483+
let cart = POSCart(items: [makePOSCartItem(productID: 100, quantity: 1)])
484+
let errorJSON = """
485+
{
486+
"code": "order_item_product_invalid_variation_id",
487+
"message": "Invalid variation",
488+
"data": {
489+
"variation_id": 999
490+
}
491+
}
492+
"""
493+
let errorData = errorJSON.data(using: .utf8)!
494+
let networkError = NetworkError.unacceptableStatusCode(statusCode: 400, response: errorData)
495+
mockOrdersRemote.createPOSOrderResult = .failure(networkError)
496+
497+
// When/Then
498+
await #expect(performing: {
499+
try await sut.syncOrder(cart: cart, currency: .USD)
500+
}, throws: { error in
501+
if case .missingProductsInOrder(let missingItems) = error as? POSOrderService.POSOrderServiceError {
502+
#expect(missingItems.count == 1)
503+
#expect(missingItems.first?.productID == 0)
504+
#expect(missingItems.first?.variationID == 999)
505+
#expect(missingItems.first?.name == "Unknown Product")
506+
return true
507+
}
508+
return false
509+
})
510+
}
344511
}
345512

346513
private func makePOSCartItem(

0 commit comments

Comments
 (0)