Is it possible to mock a function imported from a Swift package? - swift

I have written a simple Swift package called JwtApiClient that provides a few helper functions for making HTTP requests to JWToken-protected APIs that transfer data as JSON.
I am consuming one of these functions (postJsonDictionary) in another package:
import Foundation
import JwtApiClient
public func requestToken<T: Decodable>(
_ username: String,
_ password: String
) async throws -> T {
let endpoint = "/users/login" // in the actual code this is an instance of URLComponents
// not String but it's irrelevant here
let credentials = [
"email": username,
"password": password
]
// I'd like to be able to mock this call
return try await postJsonDictionary(endpoint!, credentials)
}
I was just wondering if it is possible to mock postJsonDictionary in a unit test to avoid making an underlying network request and to be able to assert on the parameters passed to it.
I have tried tricking my unit test into believing the function is a global but it doesn't work:
extension NSObject {
public func postJsonDictionary<T: Decodable>(_ url: URL!, _ dictionary: [String: Any]) async throws -> T {
try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
let fakeTokenResponse: TokenResponse = TokenResponse(token: "fake-token")
return fakeTokenResponse as! T
}
}
// XCTestCase below

Related

Returning parsed JSON data using Alamofire?

Hello new to Swift and Alamofire,
The issue i'm having is when I call this fetchAllUsers() the code will return the empty users array and after it's done executing it will go inside the AF.request closure and execute the rest.
I've done some research and I was wondering is this is caused by Alamofire being an Async function.
Any suggestions?
func fetchAllUsers() -> [User] {
var users = [User]()
let allUsersUrl = baseUrl + "users/"
if let url = URL(string: allUsersUrl) {
AF.request(url).response { response in
if let data = response.data {
users = self.parse(json: data)
}
}
}
return users
}
You need to handle the asynchrony in some way. This this means passing a completion handler for the types you need. Other times it means you wrap it in other async structures, like promises or a publisher (which Alamofire also provides).
In you case, I'd suggest making your User type Decodable and allow Alamofire to do the decoding for you.
func fetchAllUsers(completionHandler: #escaping ([User]) -> Void) {
let allUsersUrl = baseUrl + "users/"
if let url = URL(string: allUsersUrl) {
AF.request(url).responseDecodable(of: [User].self) { response in
if let users = response.value {
completionHandler(users)
}
}
}
}
However, I would suggest returning the full Result from the response rather than just the [User] value, otherwise you'll miss any errors that occur.

Parsing Alamofire JSON

I am building an app that will work with Plaid. Plaid provides a nice little LinkKit that I need to use to grab my link_token. I provide that link_token to authenticate to a bank. I have written a request using Alamofire to send the .post to get the new link_token when someone would want to add another account. My issue is when I decode the JSON to a struct that I have built I cant seem to use that stored link_token value.
Code to retrieve link_token
let parameters = PlaidAPIKeys(client_id: K.plaidCreds.client_id,
secret: K.plaidCreds.secret,
client_name: K.plaidCreds.client_name,
language: K.plaidCreds.language,
country_codes: [K.plaidCreds.country_codes],
user: [K.plaidCreds.client_user_id: K.plaidCreds.unique_user_id],
products: [K.plaidCreds.products])
func getLinkToken() {
let linkTokenRequest = AF.request(K.plaidCreds.plaidLinkTokenURL,
method: .post,
parameters: parameters,
encoder: JSONParameterEncoder.default).responseDecodable(of: GeneratedLinkToken.self) { response in
print(response)
}
}
Struct I have built:
struct GeneratedLinkToken: Decodable {
let expiration: String
let linkToken: String
let requestID: String
enum CodingKeys: String, CodingKey {
case expiration = "expiration"
case linkToken = "link_token"
case requestID = "request_id"
}
}
I have tested by calling the function getLinkToken() when pressing my add account or dummy button, I do get the data back that I am needing. Why wouldnt I be able to access GeneratedLinkToken.linkToken directly after the request?
You aren't able to access linkToken property like this: GeneratedLinkToken.linkToken, because linkToken is as instance property(read here)
If you want to get an instance after your request, you can do it like this:
func getLinkToken(completion: #escaping ((GeneratedLinkToken) -> Void)) {
let linkTokenRequest = AF.request(K.plaidCreds.plaidLinkTokenURL,
method: .post,
parameters: parameters,
encoder: JSONParameterEncoder.default).responseDecodable(of: GeneratedLinkToken.self) { response in
print(response)
// if response is an object of type GeneratedLinkToken
switch response.result {
case .success(let object):
completion(object)
case .failure(let error):
// hanlde error
}
}
}
Later you can call as:
getLinkToken { linkObject in
print("My tokne: \(linkObject.linkToken)")
}
I added completion(read here) to your method, since the request executing async, you can take a look at this Q/A: read here. I also suggest you, pass parameters as a parameter to this function, not declare it globally.

Alamofire request or Firebase query is not working in a non UIViewController Class

Imperial trying to perform a request to a website using alamofire and my problem is the following:
When I use the corresponding code in an ViewController cocoaTOuch class viewDidLoad() function everything works fine.(here is the code)
super.viewDidLoad()
let loginActionUrl = url
do{
let parameters = [
"p_user":user,
"p_password": password
]
AF.request(loginActionUrl, method: .post, parameters: parameters).responseJSON
{response in
if let header = response.response?.allHeaderFields as? [String: String],
let responseUrl = response.request?.url{
let sessionCookies = HTTPCookie.cookies(withResponseHeaderFields: header, for: responseUrl)
......
If I repeat the same code inside a private function on a swift (non cocoa touch) class, then,I have no response, While debugging it tries to perform the request task twice and then jumps out of the {response in code block.
The code is the following:
private func checkInWithAeA(withLogIn: String, password: String) -> (Bool){
var companyUSerRecognized: Bool = false
var startIndex: String.Index!
let loginActionUrl = url
do{
let parameters = [
"p_user" : withLogIn,
"p_password": password
]
AF.request(loginActionUrl, method: .post, parameters: parameters).responseJSON
{response in
if let header = response.response?.allHeaderFields as? [String: String],
let responseUrl = response.request?.url{
let sessionCookies = HTTPCookie.cookies(withResponseHeaderFields: header, for: responseUrl)
companyUSerRecognized = true
......
I don't now what is happening but is the second time I have the same problem. What I'm dong is trying to avoid to set up to much code in the viewController using other support classes, following best practices, but I already tried to do this with firebase, and I have the same problem, the query to the database only worked in UIViewcontroller classes (in certain) and now is the same, I am not able to obtain any result when I execute the code in the clean swift file.
Is there any kind of limitation on this. Why I cannot do anything like an alamofire request or a firebase query to the realtime database out of a UIViewController class?
Here I add some information:
var myConnectionController: ConnectionController = ConnectionController()
let (companyUSerRecognized, error) = myConnectionController.chekUserIDandPassWord(forTheCompany: self.companyName, logInName: self.companyLogIn, password: self.companyPassword)
This call to the ConnectionController class (that is a swift plain class) asks for a connexion to a web page. If the response is good, then a true is obtained and the process is continued.
The function called has a switch statement:
public func chekUserIDandPassWord(forTheCompany: String, logInName: String, password: String) -> (Bool, String){
var companyUSerRecognized: Bool!
var error: String!
switch forTheCompany {
case "(AEA)":
companyUSerRecognized = checkInWithAeA(withLogIn: logInName, password: password)
break
.....
This is what calls Check in With AeA. (The function I just mentioned before). What I want to is get the cookies of the connection in return check them and if they are good, true is returned.
I already have done this in the viewDidLoad() function of a ViewController, In fact I can parse the response with SwiftSoup, etc. But If I do it this way I am not able to do it.
Thanks again
I finally made up the solution by reviewing some bibliography. I did not notice that, any alamofire call opens a pipeline. That is, we are obviously talking about asynchronous operations. There are two ways to handle with this kind of operations in swift. One is the use of future objects. This option allows the continuation of the execution by substituting the results from the async call when they are ready. Depending on the situation this is not so good. The other is to make the execution wait for the response of the async call. This is done with a closure. I took this lastoption.
The closer is to be performed by using a completion handler function that is called at the end of the async call block to return any value you need from the async call. In this case. This is what I called completion
private func checkInWithAeA(completion: #escaping (Bool)-> Void){
let loginActionUrl = url1
let postLoginUrl = url2
let parameters = [
"p_user" : logInName,
"p_password": password
]
AF.request(loginActionUrl, method: .post, parameters: parameters).responseData
{(response) in
if let header = response.response?.allHeaderFields as? [String: String],
let responseUrl = response.request?.url{
let sessionCookies = HTTPCookie.cookies(withResponseHeaderFields: header, for: responseUrl)
let cookieToSend = sessionCookies[0]
//Debug
print(cookieToSend)
AF.session.configuration.httpCookieStorage?.setCookie(cookieToSend)
completion(true)
}else{
completion(false)
}
}
}
That's it. Hope it helps
BTW. I think that this is the same problem with the firebase queries.
Thanks!

Create a Swift HTTP mock with alternate data

I have a mocked HTTPManager, and I want it to either return a userIDResonse or a tokenResponse.
To be able to do this I made the mock conform to a protocol to allow this to be set within the test.
let userIDResponse = """
{\"user_id\":\"5a7ab957a225856b38f49bb4\"}
"""
let tokenResponse = """
{\"access_token\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IjczMUE3OUEyMjY3QjY4Q0EwNTc5QjYzRjdFMkY0QjlBQkZFMENEMTUiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJjeHA1b2laN2FNb0ZlYllfZmk5TG1yX2d6UlUifQ.eyJuYmYiOjE1MTI5NjU3NTgsImV4cCI6MTUxMjk2OTM1OCwiaXNzIjoiaHR0cHM6Ly9pZG0uYWxwaGFub3ZhLnNnIiwiYXVkIjpbImh0dHBzOi8vaWRtLmFscGhhbm92YS5zZy9yZXNvdXJjZXMiLCJhcGkyIl0sImNsaWVudF9pZCI6ImNhcGFwb3QtbmciLCJzdWIiOiI1YTFjMWU5MjY0MjUzYjFlMWU2N2ZhZDIiLCJhdXRoX3RpbWUiOjE1MTI5NjU3NTgsImlkcCI6ImFscGhhbm92YSIsImZpcnN0X25hbWUiOiJTdGV2ZW4iLCJsYXN0X25hbWUiOiJDdXJ0aXMiLCJuYW1lIjoiU3RldmVuIEN1cnRpcyIsImVtYWlsIjoic3RldmVuQGFscGhhbm92YS5zZyIsInNjb3BlIjpbImNhcGFwb3QucHJvZmlsZSIsImVtYWlsIiwib3BlbmlkIiwiYXBpMi5yZWFkX29ubHkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsicGFzc3dvcmQiXX0.q4-SF5KBVSwN4bFhcQ88icR9X2jzz_JH2K4EpDgS-oZjjppNruckxfTjauVqcwG8zPR0eGzx5CBXiAfMeg9akShWajqBZ9rkCsqjXw6Ef74J9cTBDhxTEUL0v7P0zm_fVNOutM_UJQ-DiQr2gAO0mfAxMhOiQ_uXlKoM2RYGKjfMkH6Ym7kBjtRAhho8pPVmtQiBmVFI5OUVXNU3rPVgB7sx-I1LZmUZBZoy7T4s14TAuE4yiUyTBgO5joyRsZtMdFybna8CRK_ylS3WC6wOBNm74O9IrZlbsiradtLzMG-9E8AnjbvH4RYR68H2xpt562PfnGD_VC9NXFQ7iRrRMw\"}
"""
Used by the Mock
protocol HTTPManagerMockProtocol {
func setResponse(response: String.UTF8View)
}
typealias HTTPMock = HTTPManagerProtocol & HTTPManagerMockProtocol
class HTTPManagerMock: HTTPMock {
var data = Data(userIDResponse.utf8)
func setResponse(response: String.UTF8View) {
data = Data(response)
}
func get(urlString: String, parameters: [String : String], completionBlock: #escaping (Result<Data, Error>) -> Void) {
completionBlock(.success(data))
}
}
So then in my test I have to set the reponse:
let httpMock = HTTPManagerMock()
httpMock.setResponse(response: tokenResponse.utf8)
sut = Login(serverString: "serverURL", headers: [:], httpManager: httpMock )
In some ways this seems ok, however it means I cannot use the setup function in my tests which results in repeated code within my test classes.
Which approach can mean I can have a mock with different output without generating extra test code?
Make a parameterized helper method to create your System Under Test.
private func makeLogin(response: String) -> Login {
let httpMock = HTTPManagerMock()
httpMock.setResponse(response: response.utf8)
return Login(serverString: "serverURL", headers: [:], httpManager: httpMock)
}
That way, you can vary the response across different tests. And if you have tests where you don't really care about the response and want to provide dummy data, that can be a default value in the helper.

Alamofire retry request - reactive way

I was looking at those two:
http://sapandiwakar.in/refresh-oauth-tokens-using-moya-rxswift/
Using retryWhen to update tokens based on http error code
And trying to create similiar thing, but without Moya, using Alamofire + RxSwift.
First of all is obviously where should I stick this, since my implementation is divided into a couple smaller parts.
First of all I have my custom method for generating reactive requests:
static func rx_request<T>(requestConvertible: URLRequestConvertible, completion: (Request) -> Observable<T> ) -> Observable<T> {
let manager: Manager = Manager.sharedInstance
return manager
.rx_request { manager -> Request in
return Alamofire.request(requestConvertible)
}
.flatMap { request -> Observable<T> in
return completion(request)
}
.shareReplay(1)
}
Which is later used by specific Requests convenience classes. For example my UserRequests has this private extension to extract some common code from it's methods:
private extension Request {
func rx_userRequest() -> Observable<User> {
return self
.validate()
.rx_responseJSON()
.flatMap{ (request, json) -> Observable<User> in
guard
let dict = json as? [ String: AnyObject ],
let parsedUser: User = try? Unbox(dict) else {
return Observable.error(RequestError.ParsingError)
}
return Observable.just(parsedUser)
}
.rx_storeCredentials()
}
}
Because of how things looks like I wonder whare's the right place to put a retry method and also how to implement it? Because depending on the location I can get different input arguments.
The retry code has to go after the first try, which is rx_responseJSON so the way you have things setup now, it must go between that and the flatMap after it.