import Foundation import KeychainAccess enum APIError : Error { case accessTokenExpired case networkError // Add more error cases as needed } class APIManager { private let keychain = Keychain (service: "com.example.app.refreshToken" ) private let refreshTokenKey = "refreshToken" private var accessToken: String ? func callAPI < T : Codable >( urlString : String , method : String , parameters : [ String : Any ] ? , completion : @escaping ( Result < T , APIError >) -> Void ) { guard let url = URL (string: urlString) else { completion(.failure(.networkError)) return } var request = URLRequest (url: url) request.httpMethod = method // Add access token to the request headers if available if let token = accessToken { request.setValue( "Bearer \(token) " , forHTTPHeaderField: "Aut...
import Foundation
import KeychainAccessenum APIError: Error {
case accessTokenExpired
case networkError
// Add more error cases as needed
}
class APIManager {
private let keychain = Keychain(service: "com.example.app.refreshToken")
private let refreshTokenKey = "refreshToken"
private var accessToken: String?
func callAPI<T: Codable>(urlString: String, method: String, parameters: [String: Any]?, completion: @escaping (Result<T, APIError>) -> Void) {
guard let url = URL(string: urlString) else {
completion(.failure(.networkError))
return
}
var request = URLRequest(url: url)
request.httpMethod = method
// Add access token to the request headers if available
if let token = accessToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
// Add JSON body for POST method
if method == "POST", let parameters = parameters {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)
}
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(.failure(.networkError))
return
}
guard let data = data else {
completion(.failure(.networkError))
return
}
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(T.self, from: data)
completion(.success(decodedData))
} catch {
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 401 {
self.refreshAccessToken { result in
switch result {
case .success:
// Retry the original request
self.callAPI(urlString: urlString, method: method, parameters: parameters, completion: completion)
case .failure:
completion(.failure(.accessTokenExpired))
}
}
} else {
completion(.failure(.networkError))
}
}
}
task.resume()
}
func refreshAccessToken(completion: @escaping (Result<Void, APIError>) -> Void) {
guard let refreshToken = try? keychain.get(refreshTokenKey) else {
completion(.failure(.accessTokenExpired))
return
}
let refreshTokenURL = "https://api.example.com/refresh_token"
let parameters = ["refresh_token": refreshToken]
guard let url = URL(string: refreshTokenURL) else {
completion(.failure(.networkError))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(.failure(.networkError))
return
}
guard let data = data else {
completion(.failure(.networkError))
return
}
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(TokenResponse.self, from: data)
self.accessToken = decodedData.accessToken
completion(.success(()))
} catch {
completion(.failure(.accessTokenExpired))
}
}
task.resume()
}
func saveRefreshToken(_ token: String) {
do {
try keychain.set(token, key: refreshTokenKey)
} catch {
print("Error saving refresh token to Keychain: \(error)")
}
}
}
struct TokenResponse: Codable {
let accessToken: String
}
// Example usage from a ViewController:
class MyViewController: UIViewController {
let apiManager = APIManager()
func testAPICall() {
let urlString = "https://api.example.com/data"
// Test POST method
let postParameters = ["name": "John", "age": 30]
apiManager.callAPI(urlString: urlString, method: "POST", parameters: postParameters) { (result: Result<MyResponse, APIError>) in
switch result {
case .success(let response):
// Handle successful response
print(response)
case .failure(let error):
if error == .accessTokenExpired {
self.handleAccessTokenExpired {
// Retry the original request after refreshing the access token
self.testAPICall()
}
} else {
// Handle other API errors
print("API error: \(error)")
}
}
}
// Test GET method
apiManager.callAPI(urlString: urlString, method: "GET", parameters: nil) { (result: Result<MyResponse, APIError>) in
// Handle response and errors
}
// Test DELETE method
apiManager.callAPI(urlString: urlString, method: "DELETE", parameters: nil) { (result: Result<MyResponse, APIError>) in
// Handle response and errors
}
}
func handleAccessTokenExpired(completion: @escaping () -> Void) {
// Implement your logic to handle access token expiration here
// For example, you can present a login screen to the user
// After the user successfully logs in and obtains a new access token, call the following method to save it
let newAccessToken = "your_new_access_token"
apiManager.saveAccessToken(newAccessToken)
// Call the completion closure to retry the original request
completion()
}
}
Test case class for testing the API calls using XCTest Framework
import XCTest
class APIManagerTests: XCTestCase {
var apiManager: APIManager!
override func setUp() {
super.setUp()
apiManager = APIManager()
}
override func tearDown() {
apiManager = nil
super.tearDown()
}
func testAPICallWithValidAccessToken() {
// Set up a valid access token
let validAccessToken = "valid_access_token"
apiManager.saveAccessToken(validAccessToken)
// Define the expected response
let expectedResponse = MyResponse(name: "John", age: 30)
// Mock URLSessionDataTask for a successful response
let mockDataTask = MockURLSessionDataTask(data: validAccessToken.data(using: .utf8), response: HTTPURLResponse(url: URL(string: "https://api.example.com/data")!, statusCode: 200, httpVersion: nil, headerFields: nil), error: nil)
// Mock URLSession for the test case
let mockURLSession = MockURLSession(dataTask: mockDataTask)
apiManager.urlSession = mockURLSession
// Perform the API call
let urlString = "https://api.example.com/data"
apiManager.callAPI(urlString: urlString, method: "GET", parameters: nil) { (result: Result<MyResponse, APIError>) in
switch result {
case .success(let response):
// Compare the response with the expected response
XCTAssertEqual(response, expectedResponse)
case .failure(let error):
XCTFail("Unexpected failure: \(error)")
}
}
// Check if the data task was called with the correct URL
XCTAssertEqual(mockDataTask.url, URL(string: urlString))
// Check if the data task was called with the correct method
XCTAssertEqual(mockDataTask.httpMethod, "GET")
// Check if the access token was added to the request headers
XCTAssertEqual(mockDataTask.requestHeaders?["Authorization"], "Bearer \(validAccessToken)")
}
func testAPICallWithExpiredAccessToken() {
// Set up an expired access token
let expiredAccessToken = "expired_access_token"
apiManager.saveAccessToken(expiredAccessToken)
// Mock URLSessionDataTask for a 401 unauthorized response
let mockDataTask = MockURLSessionDataTask(data: nil, response: HTTPURLResponse(url: URL(string: "https://api.example.com/data")!, statusCode: 401, httpVersion: nil, headerFields: nil), error: nil)
// Mock URLSession for the test case
let mockURLSession = MockURLSession(dataTask: mockDataTask)
apiManager.urlSession = mockURLSession
// Perform the API call
let urlString = "https://api.example.com/data"
apiManager.callAPI(urlString: urlString, method: "GET", parameters: nil) { (result: Result<MyResponse, APIError>) in
switch result {
case .success:
XCTFail("Expected failure: .accessTokenExpired")
case .failure(let error):
XCTAssertEqual(error, .accessTokenExpired)
}
}
// Check if the data task was called with the correct URL
XCTAssertEqual(mockDataTask.url, URL(string: urlString))
// Check if the data task was called with the correct method
XCTAssertEqual(mockDataTask.httpMethod, "GET")
// Check if the access token was added to the request headers
XCTAssertEqual(mockDataTask.requestHeaders?["Authorization"], "Bearer \(expiredAccessToken)")
// Check if the access token was refreshed
XCTAssertTrue(mockURLSession.refreshAccessTokenCalled)
}
}
// Helper classes for mocking URLSession and URLSessionDataTask
class MockURLSessionDataTask: URLSessionDataTask {
private let data: Data?
private let response: URLResponse?
private let error: Error?
var url: URL?
var httpMethod: String?
var requestHeaders: [String: String]?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
override func resume() {
// Perform any necessary assertions or validations
}
override func cancel() {
// Perform any necessary assertions or validations
}
override func suspend() {
// Perform any necessary assertions or validations
}
override func getTaskTimesOut() -> Bool {
return false
}
override func getResponse() -> URLResponse? {
return response
}
override func getOriginalRequest() -> URLRequest? {
guard let url = url, let httpMethod = httpMethod else {
return nil
}
var request = URLRequest(url: url)
request.httpMethod = httpMethod
request.allHTTPHeaderFields = requestHeaders
return request
}
override func getError() -> Error? {
return error
}
override func getResponseData() -> Data? {
return data
}
}
class MockURLSession: URLSession {
var refreshAccessTokenCalled = false
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func dataTask(with url: URL) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func refreshAccessToken(completion: @escaping (Result<Void, APIError>) -> Void) {
// Perform any necessary assertions or validations
refreshAccessTokenCalled = true
completion(.success(()))
}
}
// Example response model for testing
struct MyResponse: Codable, Equatable {
let name: String
let age: Int
}
class APIManagerTests: XCTestCase {
var apiManager: APIManager!
override func setUp() {
super.setUp()
apiManager = APIManager()
}
override func tearDown() {
apiManager = nil
super.tearDown()
}
func testAPICallWithValidAccessToken() {
// Set up a valid access token
let validAccessToken = "valid_access_token"
apiManager.saveAccessToken(validAccessToken)
// Define the expected response
let expectedResponse = MyResponse(name: "John", age: 30)
// Mock URLSessionDataTask for a successful response
let mockDataTask = MockURLSessionDataTask(data: validAccessToken.data(using: .utf8), response: HTTPURLResponse(url: URL(string: "https://api.example.com/data")!, statusCode: 200, httpVersion: nil, headerFields: nil), error: nil)
// Mock URLSession for the test case
let mockURLSession = MockURLSession(dataTask: mockDataTask)
apiManager.urlSession = mockURLSession
// Perform the API call
let urlString = "https://api.example.com/data"
apiManager.callAPI(urlString: urlString, method: "GET", parameters: nil) { (result: Result<MyResponse, APIError>) in
switch result {
case .success(let response):
// Compare the response with the expected response
XCTAssertEqual(response, expectedResponse)
case .failure(let error):
XCTFail("Unexpected failure: \(error)")
}
}
// Check if the data task was called with the correct URL
XCTAssertEqual(mockDataTask.url, URL(string: urlString))
// Check if the data task was called with the correct method
XCTAssertEqual(mockDataTask.httpMethod, "GET")
// Check if the access token was added to the request headers
XCTAssertEqual(mockDataTask.requestHeaders?["Authorization"], "Bearer \(validAccessToken)")
}
func testAPICallWithExpiredAccessToken() {
// Set up an expired access token
let expiredAccessToken = "expired_access_token"
apiManager.saveAccessToken(expiredAccessToken)
// Mock URLSessionDataTask for a 401 unauthorized response
let mockDataTask = MockURLSessionDataTask(data: nil, response: HTTPURLResponse(url: URL(string: "https://api.example.com/data")!, statusCode: 401, httpVersion: nil, headerFields: nil), error: nil)
// Mock URLSession for the test case
let mockURLSession = MockURLSession(dataTask: mockDataTask)
apiManager.urlSession = mockURLSession
// Perform the API call
let urlString = "https://api.example.com/data"
apiManager.callAPI(urlString: urlString, method: "GET", parameters: nil) { (result: Result<MyResponse, APIError>) in
switch result {
case .success:
XCTFail("Expected failure: .accessTokenExpired")
case .failure(let error):
XCTAssertEqual(error, .accessTokenExpired)
}
}
// Check if the data task was called with the correct URL
XCTAssertEqual(mockDataTask.url, URL(string: urlString))
// Check if the data task was called with the correct method
XCTAssertEqual(mockDataTask.httpMethod, "GET")
// Check if the access token was added to the request headers
XCTAssertEqual(mockDataTask.requestHeaders?["Authorization"], "Bearer \(expiredAccessToken)")
// Check if the access token was refreshed
XCTAssertTrue(mockURLSession.refreshAccessTokenCalled)
}
}
// Helper classes for mocking URLSession and URLSessionDataTask
class MockURLSessionDataTask: URLSessionDataTask {
private let data: Data?
private let response: URLResponse?
private let error: Error?
var url: URL?
var httpMethod: String?
var requestHeaders: [String: String]?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
override func resume() {
// Perform any necessary assertions or validations
}
override func cancel() {
// Perform any necessary assertions or validations
}
override func suspend() {
// Perform any necessary assertions or validations
}
override func getTaskTimesOut() -> Bool {
return false
}
override func getResponse() -> URLResponse? {
return response
}
override func getOriginalRequest() -> URLRequest? {
guard let url = url, let httpMethod = httpMethod else {
return nil
}
var request = URLRequest(url: url)
request.httpMethod = httpMethod
request.allHTTPHeaderFields = requestHeaders
return request
}
override func getError() -> Error? {
return error
}
override func getResponseData() -> Data? {
return data
}
}
class MockURLSession: URLSession {
var refreshAccessTokenCalled = false
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func dataTask(with url: URL) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
// Perform any necessary assertions or validations
return MockURLSessionDataTask(data: nil, response: nil, error: nil)
}
override func refreshAccessToken(completion: @escaping (Result<Void, APIError>) -> Void) {
// Perform any necessary assertions or validations
refreshAccessTokenCalled = true
completion(.success(()))
}
}
// Example response model for testing
struct MyResponse: Codable, Equatable {
let name: String
let age: Int
}
Comments
Post a Comment