Skip to content

Commit

Permalink
Fix http mocking issues in unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
z4kn4fein committed Oct 28, 2022
1 parent c352bcd commit c80907d
Show file tree
Hide file tree
Showing 14 changed files with 489 additions and 482 deletions.
8 changes: 4 additions & 4 deletions Sources/ConfigCat/ConfigCatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
baseUrl: String = "",
flagOverrides: OverrideDataSource? = nil,
logLevel: LogLevel = .warning) {
self.init(sdkKey: sdkKey, pollingMode: refreshMode, session: URLSession(configuration: sessionConfiguration),
self.init(sdkKey: sdkKey, pollingMode: refreshMode, httpEngine: URLSessionEngine(session: URLSession(configuration: sessionConfiguration)),
configCache: configCache, baseUrl: baseUrl, dataGovernance: dataGovernance, flagOverrides: flagOverrides, logLevel: logLevel)
}

init(sdkKey: String,
pollingMode: PollingMode,
session: URLSession?,
httpEngine: HttpEngine?,
hooks: Hooks? = nil,
configCache: ConfigCache? = nil,
baseUrl: String = "",
Expand All @@ -79,7 +79,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
// configService is not needed in localOnly mode
configService = nil
} else {
let fetcher = ConfigFetcher(session: session ?? URLSession(configuration: URLSessionConfiguration.default),
let fetcher = ConfigFetcher(httpEngine: httpEngine ?? URLSessionEngine(session: URLSession(configuration: URLSessionConfiguration.default)),
logger: log,
sdkKey: sdkKey,
mode: pollingMode.identifier,
Expand Down Expand Up @@ -119,7 +119,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
let opts = options ?? ConfigCatOptions.default
let client = ConfigCatClient(sdkKey: sdkKey,
pollingMode: opts.pollingMode,
session: URLSession(configuration: opts.sessionConfiguration),
httpEngine: URLSessionEngine(session: URLSession(configuration: opts.sessionConfiguration)),
hooks: opts.hooks,
configCache: opts.configCache,
baseUrl: opts.baseUrl,
Expand Down
89 changes: 53 additions & 36 deletions Sources/ConfigCat/ConfigFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,36 @@ func ==(lhs: FetchResponse, rhs: FetchResponse) -> Bool {
}
}

protocol HttpEngine {
func get(request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void)
}

class URLSessionEngine: HttpEngine {
fileprivate let session: URLSession

init(session: URLSession) {
self.session = session
}

func get(request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
session.dataTask(with: request) { (data, resp, error) in
completion(data, resp, error)
}.resume()
}
}

class ConfigFetcher: NSObject {
private let log: Logger
private let session: URLSession
private let httpEngine: HttpEngine
@Synced private var baseUrl: String
private let mode: String
private let sdkKey: String
private let urlIsCustom: Bool

init(session: URLSession, logger: Logger, sdkKey: String, mode: String,
init(httpEngine: HttpEngine, logger: Logger, sdkKey: String, mode: String,
dataGovernance: DataGovernance, baseUrl: String = "") {
log = logger
self.session = session
self.httpEngine = httpEngine
self.sdkKey = sdkKey
urlIsCustom = !baseUrl.isEmpty
self.baseUrl = baseUrl.isEmpty
Expand Down Expand Up @@ -105,43 +123,42 @@ class ConfigFetcher: NSObject {

private func sendFetchRequest(url: String, eTag: String, completion: @escaping (FetchResponse) -> Void) {
let request = getRequest(url: url, eTag: eTag)
session.dataTask(with: request) { (data, resp, error) in
if let error = error {
var extraInfo = ""
if error._code == NSURLErrorTimedOut {
extraInfo = String(format: " Timeout interval for request: %.2f seconds.", self.session.configuration.timeoutIntervalForRequest)
}
let message = String(format: "An error occurred during the config fetch: %@%@", error.localizedDescription, extraInfo)
httpEngine.get(request: request) { (data, resp, error) in
if let error = error {
var extraInfo = ""
if error._code == NSURLErrorTimedOut, let engine = self.httpEngine as? URLSessionEngine {
extraInfo = String(format: " Timeout interval for request: %.2f seconds.", engine.session.configuration.timeoutIntervalForRequest)
}
let message = String(format: "An error occurred during the config fetch: %@%@", error.localizedDescription, extraInfo)
self.log.error(message: message)
completion(.failure(message))
} else {
let response = resp as! HTTPURLResponse
if response.statusCode >= 200 && response.statusCode < 300, let data = data {
self.log.debug(message: "Fetch was successful: new config fetched")
let etag = response.allHeaderFields["Etag"] as? String ?? ""
let jsonString = String(data: data, encoding: .utf8) ?? ""
let configResult = self.parseConfigFromJson(json: jsonString)
switch configResult {
case .success(let config):
completion(.fetched(ConfigEntry(config: config, eTag: etag, fetchTime: Date())))
case .failure(let error):
let message = String(format: "An error occurred during JSON deserialization. %@", error.localizedDescription)
self.log.error(message: message)
completion(.failure(message))
} else {
let response = resp as! HTTPURLResponse
if response.statusCode >= 200 && response.statusCode < 300, let data = data {
self.log.debug(message: "Fetch was successful: new config fetched")
let etag = response.allHeaderFields["Etag"] as? String ?? ""
let jsonString = String(data: data, encoding: .utf8) ?? ""
let configResult = self.parseConfigFromJson(json: jsonString)
switch configResult {
case .success(let config):
completion(.fetched(ConfigEntry(config: config, eTag: etag, fetchTime: Date())))
case .failure(let error):
let message = String(format: "An error occurred during JSON deserialization. %@", error.localizedDescription)
self.log.error(message: message)
completion(.failure(message))
}
} else if response.statusCode == 304 {
self.log.debug(message: "Fetch was successful: not modified")
completion(.notModified)
} else {
let message = String(format: """
Double-check your SDK Key at https://app.configcat.com/sdkkey. Non success status code: %@
""", String(response.statusCode))
self.log.error(message: message)
completion(.failure(message))
}
}
} else if response.statusCode == 304 {
self.log.debug(message: "Fetch was successful: not modified")
completion(.notModified)
} else {
let message = String(format: """
Double-check your SDK Key at https://app.configcat.com/sdkkey. Non success status code: %@
""", String(response.statusCode))
self.log.error(message: message)
completion(.failure(message))
}
.resume()
}
}
}

private func getRequest(url: String, eTag: String) -> URLRequest {
Expand Down
52 changes: 35 additions & 17 deletions Tests/ConfigCatTests/AsyncAwaitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@ class AsyncAwaitTests: XCTestCase {
let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [{"o":1,"a":"Email","t":2,"c":"@example.com","v":true,"i":"9f21c24c"}] } } }"#
let user = ConfigCatUser(identifier: "id", email: "[email protected]")

override func setUp() {
super.setUp()
MockHTTP.reset()
MockHTTP.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testGetValue() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
let value = await client.getValue(for: "key1", defaultValue: false)
XCTAssertTrue(value)
let value2 = await client.getValue(for: "key2", defaultValue: false, user: user)
Expand All @@ -23,7 +20,10 @@ class AsyncAwaitTests: XCTestCase {

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testGetVariationId() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
let id = await client.getVariationId(for: "key1", defaultVariationId: "")
XCTAssertEqual("fakeId1", id)
let id2 = await client.getVariationId(for: "key2", defaultVariationId: "", user: user)
Expand All @@ -32,53 +32,71 @@ class AsyncAwaitTests: XCTestCase {

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testGetKeyValue() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
let id = await client.getKeyAndValue(for: "fakeId1")
XCTAssertEqual(true, id?.value as? Bool)
XCTAssertEqual("key1", id?.key)
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testGetAllKeys() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
let keys = await client.getAllKeys()
XCTAssertEqual(2, keys.count)
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testGetAllValues() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
let values = await client.getAllValues()
XCTAssertEqual(2, values.count)
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testRefresh() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
let result = await client.forceRefresh()
let value = await client.getValue(for: "key2", defaultValue: true)
XCTAssertTrue(result.success)
XCTAssertFalse(value)
XCTAssertEqual(1, MockHTTP.requests.count)
XCTAssertEqual(1, engine.requests.count)
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testRefreshWithoutResult() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
await client.forceRefresh()
let value = await client.getValue(for: "key2", defaultValue: true)
XCTAssertFalse(value)
XCTAssertEqual(1, MockHTTP.requests.count)
XCTAssertEqual(1, engine.requests.count)
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func testDetails() async {
let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), session: MockHTTP.session())
let engine = MockEngine()
engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200))

let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine)
await client.forceRefresh()
let details = await client.getValueDetails(for: "key2", defaultValue: true)
XCTAssertFalse(details.isDefaultValue)
XCTAssertFalse(details.value)
XCTAssertEqual(1, MockHTTP.requests.count)
XCTAssertEqual(1, engine.requests.count)
}
#endif
}
Loading

0 comments on commit c80907d

Please sign in to comment.