1
1
//
2
- // Copyright 2022 , Optimizely, Inc. and contributors
2
+ // Copyright 2025 , Optimizely, Inc. and contributors
3
3
//
4
4
// Licensed under the Apache License, Version 2.0 (the "License");
5
5
// you may not use this file except in compliance with the License.
@@ -64,14 +64,16 @@ class DefaultCmabClientTests: XCTestCase {
64
64
let expectation = self . expectation ( description: " Completion called " )
65
65
client. fetchDecision (
66
66
ruleId: " abc " , userId: " user1 " ,
67
- attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
67
+ attributes: [ " foo " : " bar " ] ,
68
+ cmabUUID: " uuid "
68
69
) { result in
69
70
if case let . success( variationId) = result {
70
71
XCTAssertEqual ( variationId, " variation-123 " )
71
72
XCTAssertEqual ( self . mockSession. callCount, 1 )
72
73
} else {
73
74
XCTFail ( " Expected success result " )
74
75
}
76
+ self . verifyRequest ( ruleId: " abc " , userId: " user1 " , attributes: [ " foo " : " bar " ] , cmabUUID: " uuid " )
75
77
expectation. fulfill ( )
76
78
}
77
79
waitForExpectations ( timeout: 1 )
@@ -85,14 +87,16 @@ class DefaultCmabClientTests: XCTestCase {
85
87
let expectation = self . expectation ( description: " Completion called " )
86
88
client. fetchDecision (
87
89
ruleId: " abc " , userId: " user1 " ,
88
- attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
90
+ attributes: [ " foo " : " bar " ] ,
91
+ cmabUUID: " uuid "
89
92
) { result in
90
93
if case let . success( variationId) = result {
91
94
XCTAssertEqual ( variationId, " variation-retry " )
92
95
XCTAssertEqual ( self . mockSession. callCount, 2 )
93
96
} else {
94
97
XCTFail ( " Expected success after retry " )
95
98
}
99
+ self . verifyRequest ( ruleId: " abc " , userId: " user1 " , attributes: [ " foo " : " bar " ] , cmabUUID: " uuid " )
96
100
expectation. fulfill ( )
97
101
}
98
102
waitForExpectations ( timeout: 2 )
@@ -106,14 +110,16 @@ class DefaultCmabClientTests: XCTestCase {
106
110
let expectation = self . expectation ( description: " Completion called " )
107
111
client. fetchDecision (
108
112
ruleId: " abc " , userId: " user1 " ,
109
- attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
113
+ attributes: [ " foo " : " bar " ] ,
114
+ cmabUUID: " uuid "
110
115
) { result in
111
116
if case let . success( variationId) = result {
112
117
XCTAssertEqual ( variationId, " success-third " )
113
118
XCTAssertEqual ( self . mockSession. callCount, 3 )
114
119
} else {
115
120
XCTFail ( " Expected success after two retries " )
116
121
}
122
+ self . verifyRequest ( ruleId: " abc " , userId: " user1 " , attributes: [ " foo " : " bar " ] , cmabUUID: " uuid " )
117
123
expectation. fulfill ( )
118
124
}
119
125
waitForExpectations ( timeout: 2 )
@@ -126,14 +132,16 @@ class DefaultCmabClientTests: XCTestCase {
126
132
let expectation = self . expectation ( description: " Completion called " )
127
133
client. fetchDecision (
128
134
ruleId: " abc " , userId: " user1 " ,
129
- attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
135
+ attributes: [ " foo " : " bar " ] ,
136
+ cmabUUID: " uuid "
130
137
) { result in
131
138
if case let . failure( error) = result {
132
139
XCTAssertTrue ( " \( error) " . contains ( " Exhausted all retries " ) )
133
140
XCTAssertEqual ( self . mockSession. callCount, 3 )
134
141
} else {
135
142
XCTFail ( " Expected failure after all retries " )
136
143
}
144
+ self . verifyRequest ( ruleId: " abc " , userId: " user1 " , attributes: [ " foo " : " bar " ] , cmabUUID: " uuid " )
137
145
expectation. fulfill ( )
138
146
}
139
147
waitForExpectations ( timeout: 2 )
@@ -148,13 +156,15 @@ class DefaultCmabClientTests: XCTestCase {
148
156
let expectation = self . expectation ( description: " Completion called " )
149
157
client. fetchDecision (
150
158
ruleId: " abc " , userId: " user1 " ,
151
- attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
159
+ attributes: [ " foo " : " bar " ] ,
160
+ cmabUUID: " uuid "
152
161
) { result in
153
162
if case let . failure( error) = result {
154
163
XCTAssertTrue ( " \( error) " . contains ( " HTTP error code " ) )
155
164
} else {
156
165
XCTFail ( " Expected failure on HTTP error " )
157
166
}
167
+ self . verifyRequest ( ruleId: " abc " , userId: " user1 " , attributes: [ " foo " : " bar " ] , cmabUUID: " uuid " )
158
168
expectation. fulfill ( )
159
169
}
160
170
waitForExpectations ( timeout: 2 )
@@ -169,14 +179,16 @@ class DefaultCmabClientTests: XCTestCase {
169
179
let expectation = self . expectation ( description: " Completion called " )
170
180
client. fetchDecision (
171
181
ruleId: " abc " , userId: " user1 " ,
172
- attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
182
+ attributes: [ " foo " : " bar " ] ,
183
+ cmabUUID: " uuid "
173
184
) { result in
174
185
if case let . failure( error) = result {
175
186
XCTAssertTrue ( error is CmabClientError )
176
187
XCTAssertEqual ( self . mockSession. callCount, 1 )
177
188
} else {
178
189
XCTFail ( " Expected failure on invalid JSON " )
179
190
}
191
+ self . verifyRequest ( ruleId: " abc " , userId: " user1 " , attributes: [ " foo " : " bar " ] , cmabUUID: " uuid " )
180
192
expectation. fulfill ( )
181
193
}
182
194
waitForExpectations ( timeout: 2 )
@@ -193,18 +205,47 @@ class DefaultCmabClientTests: XCTestCase {
193
205
let expectation = self . expectation ( description: " Completion called " )
194
206
client. fetchDecision (
195
207
ruleId: " abc " , userId: " user1 " ,
196
- attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
208
+ attributes: [ " foo " : " bar " ] ,
209
+ cmabUUID: " uuid-1234 "
197
210
) { result in
198
211
if case let . failure( error) = result {
199
212
XCTAssertEqual ( error as? CmabClientError , . invalidResponse)
200
213
XCTAssertEqual ( self . mockSession. callCount, 1 )
201
214
} else {
202
215
XCTFail ( " Expected failure on invalid response structure " )
203
216
}
217
+ self . verifyRequest ( ruleId: " abc " , userId: " user1 " , attributes: [ " foo " : " bar " ] , cmabUUID: " uuid-1234 " )
204
218
expectation. fulfill ( )
205
219
}
206
220
waitForExpectations ( timeout: 2 )
221
+
207
222
}
223
+
224
+ private func verifyRequest( ruleId: String , userId: String , attributes: [ String : Any ] , cmabUUID: String ) {
225
+ // Assert request body
226
+ guard let request = mockSession. lastRequest else {
227
+ XCTFail ( " No request was sent " )
228
+ return
229
+ }
230
+ guard let body = request. httpBody else {
231
+ XCTFail ( " No HTTP body in request " )
232
+ return
233
+ }
234
+
235
+ let json = try ! JSONSerialization . jsonObject ( with: body, options: [ ] ) as! [ String : Any ]
236
+ let instances = json [ " instances " ] as? [ [ String : Any ] ]
237
+ XCTAssertNotNil ( instances)
238
+ let instance = instances? . first
239
+ XCTAssertEqual ( instance ? [ " visitorId " ] as? String , userId)
240
+ XCTAssertEqual ( instance ? [ " experimentId " ] as? String , ruleId)
241
+ XCTAssertEqual ( instance ? [ " cmabUUID " ] as? String , cmabUUID)
242
+ // You can add further assertions for the attributes, e.g.:
243
+ let payloadAttributes = instance ? [ " attributes " ] as? [ [ String : Any ] ]
244
+ XCTAssertEqual ( payloadAttributes? . first ? [ " id " ] as? String , attributes. keys. first)
245
+ XCTAssertEqual ( payloadAttributes? . first ? [ " value " ] as? String , attributes. values. first as? String )
246
+ XCTAssertEqual ( payloadAttributes? . first ? [ " type " ] as? String , " custom_attribute " )
247
+ }
248
+
208
249
}
209
250
210
251
// MARK: - MockURLSession for ordered responses
@@ -221,12 +262,13 @@ extension DefaultCmabClientTests {
221
262
typealias CompletionHandler = ( Data ? , URLResponse ? , Error ? ) -> Void
222
263
var responses : [ ( Data ? , URLResponse ? , Error ? ) ] = [ ]
223
264
var callCount = 0
265
+ var lastRequest : URLRequest ?
224
266
225
267
override func dataTask(
226
268
with request: URLRequest ,
227
269
completionHandler: @escaping CompletionHandler
228
270
) -> URLSessionDataTask {
229
-
271
+ self . lastRequest = request
230
272
let idx = callCount
231
273
callCount += 1
232
274
let tuple = idx < responses. count ? responses [ idx] : ( nil , nil , nil )
0 commit comments