Skip to content

Commit a12af5e

Browse files
committed
Allow more flexible orderItem.image format
- Object with valid src → image exists - Direct string URL → image exists
1 parent ab2d58e commit a12af5e

File tree

3 files changed

+189
-15
lines changed

3 files changed

+189
-15
lines changed

Modules/Sources/NetworkingCore/Model/OrderItem.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,18 @@ public struct OrderItem: Codable, Equatable, Hashable, Sendable, GeneratedFakeab
127127
forKey: .attributes)
128128
.first(where: { $0.key == "_pao_ids" })?.value ?? []
129129

130-
// Order item product image
131-
let image = try container.decodeIfPresent(OrderItemProductImage.self, forKey: .image)
130+
// Order item product image can be either a string URL or an object with src field
131+
// Use failsafeDecodeIfPresent with alternative types to handle both formats gracefully
132+
let image: OrderItemProductImage? = {
133+
if let imageObject = container.failsafeDecodeIfPresent(OrderItemProductImage.self, forKey: .image) {
134+
return imageObject
135+
}
136+
if let urlString = container.failsafeDecodeIfPresent(stringForKey: .image),
137+
!urlString.isEmpty {
138+
return OrderItemProductImage(src: urlString)
139+
}
140+
return nil
141+
}()
132142

133143
// Product Bundle extension properties:
134144
// If the order item is part of a product bundle, `bundledBy` is the parent order item (product bundle).
@@ -240,16 +250,20 @@ private struct OrderItemProductAddOnContainer: Decodable {
240250
// MARK: - Order Item Product Image
241251
//
242252
public struct OrderItemProductImage: Codable, Equatable, Hashable, Sendable {
243-
public let src: String?
253+
public let src: String
254+
255+
public init(src: String) {
256+
self.src = src
257+
}
244258

245259
public init(from decoder: Decoder) throws {
246260
let container = try decoder.container(keyedBy: CodingKeys.self)
247-
self.src = try? container.decodeIfPresent(String.self, forKey: .src)
261+
self.src = try container.decode(String.self, forKey: .src)
248262
}
249263

250264
public func encode(to encoder: Encoder) throws {
251265
var container = encoder.container(keyedBy: CodingKeys.self)
252-
try container.encodeIfPresent(src, forKey: .src)
266+
try container.encode(src, forKey: .src)
253267
}
254268

255269
private enum CodingKeys: String, CodingKey {

Modules/Tests/NetworkingTests/Mapper/OrderListMapperTests.swift

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,30 +166,67 @@ class OrderListMapperTests: XCTestCase {
166166

167167
/// Verifies that OrderItem decoding is robust for various image field scenarios from WooCommerce API
168168
///
169+
/// Tests all possible image field formats:
170+
/// 1. String URL - should parse successfully with image object
171+
/// 2. Object with src field - should parse successfully with image object
172+
/// 3. Object without src field - should be nil
173+
/// 4. Invalid types should fail gracefully with nil image
174+
/// 5. Nested objects - should be nil
175+
/// 6. Empty values - should be nil
176+
/// 7. Missing field - should be nil
177+
///
169178
func test_order_item_image_decoding_robustness() {
170179
let orders = mapOrders(from: "order-item-image")
171180
XCTAssertEqual(orders.count, 1)
172181

173182
let order = orders[0]
174-
XCTAssertEqual(order.items.count, 4)
183+
XCTAssertEqual(order.items.count, 11)
175184

176-
// Item 1: Product with valid image src
185+
// Item 1: Product with valid image object with src field (standard WooCommerce format)
177186
let item1 = order.items[0]
178-
XCTAssertNotNil(item1.image)
187+
XCTAssertNotNil(item1.image, "Image object should exist for object with valid src field")
179188
XCTAssertEqual(item1.image?.src, "https://example.com/image.jpg")
180189

181-
// Item 2: Product with no image field
190+
// Item 2: Product with no image field - should be nil
182191
let item2 = order.items[1]
183-
XCTAssertNil(item2.image)
192+
XCTAssertNil(item2.image, "Image should be nil when field is missing")
184193

185-
// Item 3: Product with image but no src field
194+
// Item 3: Product with image object but no src field - should be nil
186195
let item3 = order.items[2]
187-
XCTAssertNotNil(item3.image)
188-
XCTAssertNil(item3.image?.src)
196+
XCTAssertNil(item3.image, "Image should be nil when object has no src field")
189197

190-
// Item 4: Product with wrong src type
198+
// Item 4: Product with image object with wrong src type (number instead of string) - should be nil
191199
let item4 = order.items[3]
192-
XCTAssertNil(item4.image?.src)
200+
XCTAssertNil(item4.image, "Image should be nil when src is not a string")
201+
202+
// Item 5: Product with direct string URL image (WordPress format) - should parse successfully
203+
let item5 = order.items[4]
204+
XCTAssertNotNil(item5.image, "Image object should exist for valid string URL")
205+
XCTAssertEqual(item5.image?.src, "https://wordpress.example/wp-content/uploads/product-image-150x150.jpeg")
206+
207+
// Item 6: Product with unexpected image type (array) - should fail gracefully with nil
208+
let item6 = order.items[5]
209+
XCTAssertNil(item6.image, "Image should be nil for array type")
210+
211+
// Item 7: Product with nested object in image.src - should be nil
212+
let item7 = order.items[6]
213+
XCTAssertNil(item7.image, "Image should be nil when src is a nested object instead of string")
214+
215+
// Item 8: Product with image as number - should be parsed to string if possible
216+
let item8 = order.items[7]
217+
XCTAssertNotNil(item8.image?.src, "Image src should be not nil for number type")
218+
219+
// Item 9: Product with image as boolean - should fail gracefully with nil
220+
let item9 = order.items[8]
221+
XCTAssertNil(item9.image, "Image should be nil for boolean type")
222+
223+
// Item 10: Product with empty string image - should be nil
224+
let item10 = order.items[9]
225+
XCTAssertNil(item10.image, "Image should be nil for empty string")
226+
227+
// Item 11: Product with empty object image - should be nil
228+
let item11 = order.items[10]
229+
XCTAssertNil(item11.image, "Image should be nil for empty object")
193230
}
194231
}
195232

Modules/Tests/NetworkingTests/Responses/order-item-image.json

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,129 @@
129129
"image": {
130130
"src": 12345
131131
}
132+
},
133+
{
134+
"id": 5,
135+
"name": "Product with direct string URL image",
136+
"product_id": 127,
137+
"variation_id": 0,
138+
"quantity": 1,
139+
"price": 10.0,
140+
"sku": "",
141+
"subtotal": "10.00",
142+
"subtotal_tax": "0.00",
143+
"tax_class": "",
144+
"taxes": [],
145+
"total": "10.00",
146+
"total_tax": "0.00",
147+
"meta_data": [],
148+
"image": "https://wordpress.example/wp-content/uploads/product-image-150x150.jpeg"
149+
},
150+
{
151+
"id": 6,
152+
"name": "Product with unexpected image type (array)",
153+
"product_id": 128,
154+
"variation_id": 0,
155+
"quantity": 1,
156+
"price": 10.0,
157+
"sku": "",
158+
"subtotal": "10.00",
159+
"subtotal_tax": "0.00",
160+
"tax_class": "",
161+
"taxes": [],
162+
"total": "10.00",
163+
"total_tax": "0.00",
164+
"meta_data": [],
165+
"image": ["unexpected", "array"]
166+
},
167+
{
168+
"id": 7,
169+
"name": "Product with nested object in image",
170+
"product_id": 129,
171+
"variation_id": 0,
172+
"quantity": 1,
173+
"price": 10.0,
174+
"sku": "",
175+
"subtotal": "10.00",
176+
"subtotal_tax": "0.00",
177+
"tax_class": "",
178+
"taxes": [],
179+
"total": "10.00",
180+
"total_tax": "0.00",
181+
"meta_data": [],
182+
"image": {
183+
"src": {
184+
"url": "https://nested.com/image.jpg"
185+
}
186+
}
187+
},
188+
{
189+
"id": 8,
190+
"name": "Product with image as number",
191+
"product_id": 130,
192+
"variation_id": 0,
193+
"quantity": 1,
194+
"price": 10.0,
195+
"sku": "",
196+
"subtotal": "10.00",
197+
"subtotal_tax": "0.00",
198+
"tax_class": "",
199+
"taxes": [],
200+
"total": "10.00",
201+
"total_tax": "0.00",
202+
"meta_data": [],
203+
"image": 12345
204+
},
205+
{
206+
"id": 9,
207+
"name": "Product with image as boolean",
208+
"product_id": 131,
209+
"variation_id": 0,
210+
"quantity": 1,
211+
"price": 10.0,
212+
"sku": "",
213+
"subtotal": "10.00",
214+
"subtotal_tax": "0.00",
215+
"tax_class": "",
216+
"taxes": [],
217+
"total": "10.00",
218+
"total_tax": "0.00",
219+
"meta_data": [],
220+
"image": true
221+
},
222+
{
223+
"id": 10,
224+
"name": "Product with empty string image",
225+
"product_id": 132,
226+
"variation_id": 0,
227+
"quantity": 1,
228+
"price": 10.0,
229+
"sku": "",
230+
"subtotal": "10.00",
231+
"subtotal_tax": "0.00",
232+
"tax_class": "",
233+
"taxes": [],
234+
"total": "10.00",
235+
"total_tax": "0.00",
236+
"meta_data": [],
237+
"image": ""
238+
},
239+
{
240+
"id": 11,
241+
"name": "Product with empty object image",
242+
"product_id": 133,
243+
"variation_id": 0,
244+
"quantity": 1,
245+
"price": 10.0,
246+
"sku": "",
247+
"subtotal": "10.00",
248+
"subtotal_tax": "0.00",
249+
"tax_class": "",
250+
"taxes": [],
251+
"total": "10.00",
252+
"total_tax": "0.00",
253+
"meta_data": [],
254+
"image": {}
132255
}
133256
],
134257
"coupon_lines": [],

0 commit comments

Comments
 (0)