@@ -19,212 +19,218 @@ import XCTest
19
19
class DefaultCmabClientTests : XCTestCase {
20
20
var client : DefaultCmabClient !
21
21
var mockSession : MockURLSession !
22
+ var shortRetryConfig : CmabRetryConfig !
22
23
23
24
override func setUp( ) {
24
25
super. setUp ( )
25
26
mockSession = MockURLSession ( )
26
- client = DefaultCmabClient ( session: mockSession)
27
+ shortRetryConfig = CmabRetryConfig ( maxRetries: 2 , initialBackoff: 0.01 , maxBackoff: 0.05 , backoffMultiplier: 1.0 )
28
+ client = DefaultCmabClient ( session: mockSession, retryConfig: shortRetryConfig)
27
29
}
28
30
29
31
override func tearDown( ) {
30
32
client = nil
31
33
mockSession = nil
34
+ shortRetryConfig = nil
32
35
super. tearDown ( )
33
36
}
34
37
35
- func testFetchDecisionSuccess( ) {
36
- let expectedVariationId = " variation-123 "
37
- let responseJSON : [ String : Any ] = [
38
+ // MARK: - Helpers
39
+
40
+ func makeSuccessResponse( variationId: String ) -> ( Data , URLResponse , Error ? ) {
41
+ let json : [ String : Any ] = [
38
42
" predictions " : [
39
- [ " variation_id " : expectedVariationId ]
43
+ [ " variation_id " : variationId ]
40
44
]
41
45
]
42
- let responseData = try ! JSONSerialization . data ( withJSONObject: responseJSON, options: [ ] )
43
- mockSession. nextData = responseData
44
- mockSession. nextResponse = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
45
- statusCode: 200 , httpVersion: nil , headerFields: nil )
46
- mockSession. nextError = nil
46
+ let data = try ! JSONSerialization . data ( withJSONObject: json, options: [ ] )
47
+ let response = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
48
+ statusCode: 200 , httpVersion: nil , headerFields: nil ) !
49
+ return ( data, response, nil )
50
+ }
51
+
52
+ func makeFailureResponse( ) -> ( Data , URLResponse , Error ? ) {
53
+ let response = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
54
+ statusCode: 500 , httpVersion: nil , headerFields: nil ) !
55
+ return ( Data ( ) , response, nil )
56
+ }
57
+
58
+ // MARK: - Test Cases
59
+
60
+ func testFetchDecision_SuccessOnFirstTry( ) {
61
+ let ( successData, successResponse, _) = makeSuccessResponse ( variationId: " variation-123 " )
62
+ mockSession. responses = [ ( successData, successResponse, nil ) ]
47
63
48
64
let expectation = self . expectation ( description: " Completion called " )
49
-
50
65
client. fetchDecision (
51
- ruleId: " abc " ,
52
- userId: " user1 " ,
53
- attributes: [ " foo " : " bar " ] ,
54
- cmabUUID: " uuid "
66
+ ruleId: " abc " , userId: " user1 " ,
67
+ attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
55
68
) { result in
56
- switch result {
57
- case . success ( let variationId) :
58
- XCTAssertEqual ( variationId , expectedVariationId )
59
- case . failure ( let error ) :
60
- XCTFail ( " Expected success, got failure: \( error ) " )
69
+ if case let . success ( variationId ) = result {
70
+ XCTAssertEqual ( variationId, " variation-123 " )
71
+ XCTAssertEqual ( self . mockSession . callCount , 1 )
72
+ } else {
73
+ XCTFail ( " Expected success result " )
61
74
}
62
75
expectation. fulfill ( )
63
76
}
64
-
65
- waitForExpectations ( timeout: 2 , handler: nil )
77
+ waitForExpectations ( timeout: 1 )
66
78
}
67
79
68
- func testFetchDecisionHttpError( ) {
69
- mockSession. nextData = Data ( )
70
- mockSession. nextResponse = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
71
- statusCode: 500 , httpVersion: nil , headerFields: nil )
72
- mockSession. nextError = nil
80
+ func testFetchDecision_SuccessOnSecondTry( ) {
81
+ let ( successData, successResponse, _) = makeSuccessResponse ( variationId: " variation-retry " )
82
+ let fail = makeFailureResponse ( )
83
+ mockSession. responses = [ fail, ( successData, successResponse, nil ) ]
73
84
74
85
let expectation = self . expectation ( description: " Completion called " )
75
-
76
86
client. fetchDecision (
77
- ruleId: " abc " ,
78
- userId: " user1 " ,
79
- attributes: [ " foo " : " bar " ] ,
80
- cmabUUID: " uuid "
87
+ ruleId: " abc " , userId: " user1 " ,
88
+ attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
81
89
) { result in
82
- switch result {
83
- case . success ( _ ) :
84
- XCTFail ( " Expected failure, got success " )
85
- case . failure ( let error ) :
86
- XCTAssertTrue ( " \( error ) " . contains ( " HTTP error code " ) )
90
+ if case let . success ( variationId ) = result {
91
+ XCTAssertEqual ( variationId , " variation-retry " )
92
+ XCTAssertEqual ( self . mockSession . callCount , 2 )
93
+ } else {
94
+ XCTFail ( " Expected success after retry " )
87
95
}
88
96
expectation. fulfill ( )
89
97
}
90
-
91
- waitForExpectations ( timeout: 2 , handler: nil )
98
+ waitForExpectations ( timeout: 2 )
92
99
}
93
100
94
- func testFetchDecisionInvalidJson( ) {
95
- mockSession. nextData = Data ( " not a json " . utf8)
96
- mockSession. nextResponse = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
97
- statusCode: 200 , httpVersion: nil , headerFields: nil )
98
- mockSession. nextError = nil
101
+ func testFetchDecision_SuccessOnThirdTry( ) {
102
+ let ( successData, successResponse, _) = makeSuccessResponse ( variationId: " success-third " )
103
+ let fail = makeFailureResponse ( )
104
+ mockSession. responses = [ fail, fail, ( successData, successResponse, nil ) ]
99
105
100
106
let expectation = self . expectation ( description: " Completion called " )
101
-
102
107
client. fetchDecision (
103
- ruleId: " abc " ,
104
- userId: " user1 " ,
105
- attributes: [ " foo " : " bar " ] ,
106
- cmabUUID: " uuid "
108
+ ruleId: " abc " , userId: " user1 " ,
109
+ attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
107
110
) { result in
108
- switch result {
109
- case . success( _ ) :
110
- XCTFail ( " Expected failure, got success " )
111
- case . failure ( let error ) :
112
- XCTAssertTrue ( error is CmabClientError )
111
+ if case let . success ( variationId ) = result {
112
+ XCTAssertEqual ( variationId , " success-third " )
113
+ XCTAssertEqual ( self . mockSession . callCount , 3 )
114
+ } else {
115
+ XCTFail ( " Expected success after two retries " )
113
116
}
114
117
expectation. fulfill ( )
115
118
}
119
+ waitForExpectations ( timeout: 2 )
120
+ }
121
+
122
+ func testFetchDecision_ExhaustsAllRetries( ) {
123
+ let fail = makeFailureResponse ( )
124
+ mockSession. responses = [ fail, fail, fail]
116
125
117
- waitForExpectations ( timeout: 2 , handler: nil )
126
+ let expectation = self . expectation ( description: " Completion called " )
127
+ client. fetchDecision (
128
+ ruleId: " abc " , userId: " user1 " ,
129
+ attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
130
+ ) { result in
131
+ if case let . failure( error) = result {
132
+ XCTAssertTrue ( " \( error) " . contains ( " Exhausted all retries " ) )
133
+ XCTAssertEqual ( self . mockSession. callCount, 3 )
134
+ } else {
135
+ XCTFail ( " Expected failure after all retries " )
136
+ }
137
+ expectation. fulfill ( )
138
+ }
139
+ waitForExpectations ( timeout: 2 )
118
140
}
119
141
120
- func testFetchDecisionInvalidResponseStructure( ) {
121
- let responseJSON : [ String : Any ] = [
122
- " not_predictions " : [ ]
142
+ func testFetchDecision_HttpError( ) {
143
+ mockSession. responses = [
144
+ ( Data ( ) , HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
145
+ statusCode: 500 , httpVersion: nil , headerFields: nil ) , nil )
123
146
]
124
- let responseData = try ! JSONSerialization . data ( withJSONObject: responseJSON, options: [ ] )
125
- mockSession. nextData = responseData
126
- mockSession. nextResponse = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
127
- statusCode: 200 , httpVersion: nil , headerFields: nil )
128
- mockSession. nextError = nil
129
147
130
148
let expectation = self . expectation ( description: " Completion called " )
131
-
132
149
client. fetchDecision (
133
- ruleId: " abc " ,
134
- userId: " user1 " ,
135
- attributes: [ " foo " : " bar " ] ,
136
- cmabUUID: " uuid "
150
+ ruleId: " abc " , userId: " user1 " ,
151
+ attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
137
152
) { result in
138
- switch result {
139
- case . success( _) :
140
- XCTFail ( " Expected failure, got success " )
141
- case . failure( let error) :
142
- XCTAssertEqual ( error as? CmabClientError , . invalidResponse)
153
+ if case let . failure( error) = result {
154
+ XCTAssertTrue ( " \( error) " . contains ( " HTTP error code " ) )
155
+ } else {
156
+ XCTFail ( " Expected failure on HTTP error " )
143
157
}
144
158
expectation. fulfill ( )
145
159
}
146
-
147
- waitForExpectations ( timeout: 2 , handler: nil )
160
+ waitForExpectations ( timeout: 2 )
148
161
}
149
162
150
- func testFetchDecisionRetriesOnFailure( ) {
151
- let expectedVariationId = " variation-retry "
152
- var callCount = 0
153
-
154
- let responseJSON : [ String : Any ] = [
155
- " predictions " : [
156
- [ " variation_id " : expectedVariationId]
157
- ]
163
+ func testFetchDecision_InvalidJson( ) {
164
+ mockSession. responses = [
165
+ ( Data ( " not a json " . utf8) , HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
166
+ statusCode: 200 , httpVersion: nil , headerFields: nil ) , nil )
158
167
]
159
- let responseData = try ! JSONSerialization . data ( withJSONObject: responseJSON, options: [ ] )
160
168
161
- mockSession. onRequest = { _ in
162
- callCount += 1
163
- if callCount == 1 {
164
- self . mockSession. nextData = Data ( )
165
- self . mockSession. nextResponse = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
166
- statusCode: 500 , httpVersion: nil , headerFields: nil )
167
- self . mockSession. nextError = nil
169
+ let expectation = self . expectation ( description: " Completion called " )
170
+ client. fetchDecision (
171
+ ruleId: " abc " , userId: " user1 " ,
172
+ attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
173
+ ) { result in
174
+ if case let . failure( error) = result {
175
+ XCTAssertTrue ( error is CmabClientError )
176
+ XCTAssertEqual ( self . mockSession. callCount, 1 )
168
177
} else {
169
- self . mockSession. nextData = responseData
170
- self . mockSession. nextResponse = HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
171
- statusCode: 200 , httpVersion: nil , headerFields: nil )
172
- self . mockSession. nextError = nil
178
+ XCTFail ( " Expected failure on invalid JSON " )
173
179
}
180
+ expectation. fulfill ( )
174
181
}
182
+ waitForExpectations ( timeout: 2 )
183
+ }
184
+
185
+ func testFetchDecision_Invalid_Response_Structure( ) {
186
+ let responseJSON : [ String : Any ] = [ " not_predictions " : [ ] ]
187
+ let responseData = try ! JSONSerialization . data ( withJSONObject: responseJSON, options: [ ] )
188
+ mockSession. responses = [
189
+ ( responseData, HTTPURLResponse ( url: URL ( string: " https://prediction.cmab.optimizely.com/predict/abc " ) !,
190
+ statusCode: 200 , httpVersion: nil , headerFields: nil ) , nil )
191
+ ]
175
192
176
193
let expectation = self . expectation ( description: " Completion called " )
177
-
178
194
client. fetchDecision (
179
- ruleId: " abc " ,
180
- userId: " user1 " ,
181
- attributes: [ " foo " : " bar " ] ,
182
- cmabUUID: " uuid "
195
+ ruleId: " abc " , userId: " user1 " ,
196
+ attributes: [ " foo " : " bar " ] , cmabUUID: " uuid "
183
197
) { result in
184
- switch result {
185
- case . success( let variationId) :
186
- XCTAssertEqual ( variationId, expectedVariationId)
187
- XCTAssertTrue ( callCount >= 2 )
188
- case . failure( let error) :
189
- XCTFail ( " Expected success, got failure: \( error) " )
198
+ if case let . failure( error) = result {
199
+ XCTAssertEqual ( error as? CmabClientError , . invalidResponse)
200
+ XCTAssertEqual ( self . mockSession. callCount, 1 )
201
+ } else {
202
+ XCTFail ( " Expected failure on invalid response structure " )
190
203
}
191
204
expectation. fulfill ( )
192
205
}
193
-
194
- waitForExpectations ( timeout: 3 , handler: nil )
206
+ waitForExpectations ( timeout: 2 )
195
207
}
196
208
}
197
209
210
+ // MARK: - MockURLSession for ordered responses
211
+
198
212
extension DefaultCmabClientTests {
199
213
class MockURLSessionDataTask : URLSessionDataTask {
200
214
private let closure : ( ) -> Void
201
215
override var state : URLSessionTask . State { . completed }
202
- init ( closure: @escaping ( ) -> Void ) {
203
- self . closure = closure
204
- }
205
-
206
- override func resume( ) {
207
- closure ( )
208
- }
216
+ init ( closure: @escaping ( ) -> Void ) { self . closure = closure }
217
+ override func resume( ) { closure ( ) }
209
218
}
210
219
211
220
class MockURLSession : URLSession {
212
221
typealias CompletionHandler = ( Data ? , URLResponse ? , Error ? ) -> Void
213
-
214
- var nextData : Data ?
215
- var nextResponse : URLResponse ?
216
- var nextError : Error ?
217
- var onRequest : ( ( URLRequest ) -> Void ) ?
218
-
222
+ var responses : [ ( Data ? , URLResponse ? , Error ? ) ] = [ ]
223
+ var callCount = 0
224
+
219
225
override func dataTask(
220
226
with request: URLRequest ,
221
227
completionHandler: @escaping CompletionHandler
222
228
) -> URLSessionDataTask {
223
- onRequest ? ( request)
224
- return MockURLSessionDataTask {
225
- completionHandler ( self . nextData, self . nextResponse, self . nextError)
226
- }
229
+
230
+ let idx = callCount
231
+ callCount += 1
232
+ let tuple = idx < responses. count ? responses [ idx] : ( nil , nil , nil )
233
+ return MockURLSessionDataTask { completionHandler ( tuple. 0 , tuple. 1 , tuple. 2 ) }
227
234
}
228
235
}
229
-
230
236
}
0 commit comments