Skip to content

Commit

Permalink
Important bug fix and improved tests
Browse files Browse the repository at this point in the history
  • Loading branch information
NicholasMata committed Apr 24, 2024
1 parent c1808f6 commit 1dfcd8d
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 27 deletions.
13 changes: 7 additions & 6 deletions ApiKit-OAuth/Tests/OAuthTests.swift
Expand Up @@ -29,7 +29,7 @@ public class DummyProvider: OAuthTokenManagedProvider {

public func refreshToken(completion: @escaping (Result<String, Error>) -> Void) {
do {
Thread.sleep(forTimeInterval: 5)
Thread.sleep(forTimeInterval: 1)
let loginBody = DummyLoginInfo(username: "kminchelle", password: "0lelplR", expiresInMins: 60)
try Api.send(.post("https://dummyjson.com/auth/login", body: loginBody)) { (result: Result<DummyLoginResponse, Error>) in
switch result {
Expand All @@ -55,32 +55,33 @@ final class ApiKitOAuthTests: XCTestCase {
let interceptors: [ApiInterceptor] = [ConnectivityInterceptor(), OAuthInterceptor(provider: provider)]
let config = DefaultApiConfig(interceptors: interceptors)
let api = Api(config: config)
let expectation = self.expectation(description: "RequestSent")
let p1Expectation = self.expectation(description: "Product1")

var product: DummyProduct? = nil
var product2: DummyProduct? = nil

api.send(.get("https://dummyjson.com/auth/products/1")) { (result: Result<DummyProduct, Error>) in
switch result {
case let .success(response):
product = response
if product != nil && product2 != nil { expectation.fulfill() }
p1Expectation.fulfill()
case .failure:
XCTAssert(false)
}
}

let p2Expectation = self.expectation(description: "Product2")
var product2: DummyProduct? = nil
api.send(.get("https://dummyjson.com/auth/products/2")) { (result: Result<DummyProduct, Error>) in
switch result {
case let .success(response):
product2 = response
if product != nil && product2 != nil { expectation.fulfill() }
p2Expectation.fulfill()
case .failure:
XCTAssert(false)
}
}

waitForExpectations(timeout: 15) { _ in
waitForExpectations(timeout: 1.5) { _ in
XCTAssertNotNil(product)
XCTAssertEqual(product!.id, 1)

Expand Down
9 changes: 4 additions & 5 deletions ApiKit/Sources/Api.swift
Expand Up @@ -17,8 +17,6 @@ open class Api {
public var config: ApiConfig
/// The URLSession that is used when making network requests.
public let urlSession: URLSession

private var semaphoreByRequest:[UUID: DispatchSemaphore] = [:]

/// Creates an instance of Api using an ApiConfig
/// - Parameter config: The configuration information that the Api will use.
Expand Down Expand Up @@ -117,12 +115,12 @@ open class Api {
let requestId = UUID()
let semaphore = DispatchSemaphore(value: 0)
semaphore.signal() // This is important as I want to increment it before start so I have one available
self.semaphoreByRequest.updateValue(semaphore, forKey: requestId)
operation.semaphore = semaphore

// Called when you want to complete this function.
// Marks operation as finished and calls final completion.
let operationCompletion: RequiredHttpDataCompletion = { result in
self.semaphoreByRequest.removeValue(forKey: requestId)
operation.semaphore = nil
if operation.isCancelled { return }
operation.finished()
completion?(result)
Expand Down Expand Up @@ -152,6 +150,7 @@ open class Api {
}
operationCompletion(result)
}

let interceptors = self.config.interceptors

for interceptor in interceptors {
Expand Down Expand Up @@ -243,7 +242,7 @@ public extension Api {
/// - method: The HTTP method that will be used.
/// - headers: The headers to include if any.
/// - Returns: A URLRequest that can be sent via Api
func request(endpoint: EndpointInfo, path: String, method: HttpMethod, headers: [String: String] = [:], requireJsonResponse _: Bool = true) -> URLRequest {
func request(endpoint: EndpointInfo, path: String, method: HttpMethod, headers: [String: String] = [:]) -> URLRequest {
return request(host: endpoint.url,
path: path,
method: method,
Expand Down
27 changes: 21 additions & 6 deletions ApiKit/Sources/HttpOperation.swift
Expand Up @@ -12,18 +12,33 @@ public class HttpOperation: Operation {
private var block: (HttpOperation) -> URLSessionTask?
private var urlSessionTask: URLSessionTask?

private let lockQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).lock-queue", attributes: .concurrent)

private let lockQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).lock-queue",
attributes: .concurrent)
private let semaphoreLockQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).semaphore-lock-queue",
attributes: .concurrent)

public var onUrlSessionTask: ((URLSessionTask) -> Void)? = nil

override public var isAsynchronous: Bool {
return false
}

private var _semaphore: DispatchSemaphore? = nil
var semaphore: DispatchSemaphore? {
get {
return lockQueue.sync { _semaphore }
}
set {
lockQueue.async(flags: .barrier) {
self._semaphore = newValue
}
}
}

private var _isExecuting: Bool = false
override public private(set) var isExecuting: Bool {
get {
return lockQueue.sync { () -> Bool in
return lockQueue.sync {
_isExecuting
}
}
Expand All @@ -39,7 +54,7 @@ public class HttpOperation: Operation {
private var _isFinished: Bool = false
override public private(set) var isFinished: Bool {
get {
return lockQueue.sync { () -> Bool in
return lockQueue.sync {
_isFinished
}
}
Expand All @@ -62,10 +77,10 @@ public class HttpOperation: Operation {
if let task = task {
onUrlSessionTask?(task)
}
self.urlSessionTask = task
urlSessionTask = task
}

internal func finished() {
func finished() {
isExecuting = false
isFinished = true
}
Expand Down
36 changes: 34 additions & 2 deletions ApiKit/Tests/ApiIntegrationTests.swift
Expand Up @@ -71,6 +71,39 @@ final class ApiTests: XCTestCase {
}
}

func testMultipleRequests() throws {
let expectation = self.expectation(description: "Users")
var expectedUsers: [User]?

api.getAllUsers { result in
switch result {
case let .success(users):
expectedUsers = users
expectation.fulfill()
case .failure:
break
}
}

let nextExpectation = self.expectation(description: "NextUsers")
var nextExpectedUsers: [User]?

api.getAllUsers { result in
switch result {
case let .success(users):
nextExpectedUsers = users
nextExpectation.fulfill()
case .failure:
break
}
}

waitForExpectations(timeout: 10) { _ in
XCTAssertNotNil(expectedUsers)
XCTAssertNotNil(nextExpectedUsers)
}
}

func testPostRequest() throws {
let expectation = self.expectation(description: "Users")
var expectedUser: User?
Expand Down Expand Up @@ -108,14 +141,13 @@ final class ApiTests: XCTestCase {
var observation: NSKeyValueObservation?
task?.onUrlSessionTask = { urlSessionTask in
observation = urlSessionTask.progress.observe(\.fractionCompleted) { progress, _ in
print("Observing fractionCompleted")
if !progress.isIndeterminate {
print(progress.fractionCompleted)
}
}
}

waitForExpectations(timeout: 20) { err in
waitForExpectations(timeout: 20) { _ in
observation?.invalidate()

XCTAssertNotNil(filePath)
Expand Down
50 changes: 50 additions & 0 deletions ApiKit/Tests/ApiSerializationTests.swift
@@ -0,0 +1,50 @@
//
// ApiSerializationTests.swift
//
//
// Created by Nicholas Mata on 4/18/24.
//

import ApiKit
import XCTest

public struct TestObject: Encodable {
var string: String = "testing"

var int: Int = -1
var uint: UInt = 1
var float: Float = 1.1
var double: Double = 1.1

var date: Date = .init()
}

final class ApiSerializationTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testURLEncodingSerialiation() throws {
let encoder = URLEncodedFormEncoder(arrayEncoding: .indexInBrackets,
dateEncoding: .iso8601)
let now = Date()
let nowISO8601 = ISO8601DateFormatter().string(from: now)
let request = try HttpRequest.post("https://testing.com",
body: TestObject(date: now),
encoder: encoder)
guard let body = request.body else {
XCTAssert(false, "Request must have a body.")
return
}
let requestBody = String(decoding: body, as: UTF8.self)
guard let requestBodyNoPercents = requestBody.removingPercentEncoding else {
XCTAssert(false, "Unable to remove percent encoding")
return
}
XCTAssertEqual("date=\(nowISO8601)&double=1.1&float=1.1&int=-1&string=testing&uint=1", requestBodyNoPercents)
}
}
8 changes: 0 additions & 8 deletions ApiKit/Tests/OAuthInterceptorTests.swift

This file was deleted.

0 comments on commit 1dfcd8d

Please sign in to comment.