How to read a parameter in a Vapor middleware without consuming it - vapor

I am brand new to the Vapor framework, and am trying to protect multiple routes. Basically, I want to make sure that all routes under /campaigns/:id can only be accessed if the user actually has access to that particular campaign with that ID. So that I can't just enter any ID into the url and access any campaign.
Now, instead of adding logic for this to every single route (already 6 so far), I figured I'd use a middleware for this. This is what I came up with so far, with the help of some friendly folk over at the Vapor Discord:
final class CampaignMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
let user = try request.requireAuthenticated(User.self)
return try request.parameters.next(Campaign.self).flatMap(to: Response.self) { campaign in
guard try campaign.userID == user.requireID() else {
throw Abort(.forbidden, reason: "Campaign doesn't belong to you!")
}
return try next.respond(to: request)
}
}
}
struct CampaignController: RouteCollection {
func boot(router: Router) throws {
let route = router.grouped("campaigns")
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardMiddleware = User.guardAuthMiddleware()
let tokenAuthGroup = route.grouped(tokenAuthMiddleware, guardMiddleware)
tokenAuthGroup.get(use: getAllHandler)
tokenAuthGroup.post(CampaignCreateData.self, use: createHandler)
// Everything under /campaigns/:id/*, CampaignMiddleware makes sure that the campaign actually belongs to you
let campaignRoute = tokenAuthGroup.grouped(Campaign.parameter)
let campaignMiddleware = CampaignMiddleware()
let protectedCampaignRoute = campaignRoute.grouped(campaignMiddleware)
protectedCampaignRoute.get(use: getOneHandler)
protectedCampaignRoute.delete(use: deleteHandler)
protectedCampaignRoute.put(use: updateHandler)
// Add /campaigns/:id/entries routes
let entryController = EntryController()
try protectedCampaignRoute.register(collection: entryController)
}
func getAllHandler(_ req: Request) throws -> Future<[Campaign]> {
let user = try req.requireAuthenticated(User.self)
return try user.campaigns.query(on: req).all()
}
func getOneHandler(_ req: Request) throws -> Future<Campaign> {
return try req.parameters.next(Campaign.self)
}
// ...deleted some other route handlers...
}
The problem here is that the middleware is "eating up" the campaign parameter by doing request.parameters.next(Campaign.self). So in getOneHandler, where it also tries to access req.parameters.next(Campaign.self), it fails with error "Insufficient parameters". Which makes sense, since .next actually removes that param from the internal array of parameters.
Now, how can I write a middleware, that uses the parameter, without eating it up? Do I need to use the raw values and query the Campaign model myself? Or can I somehow reset the parameters after using .next? Or is there another better way to deal with model authorization in Vapor 3?

Heeey, it looks like you could get your Campaign from request without dropping it like this
guard let parameter = req.parameters.values.first else {
throw Abort(.forbidden)
}
try Campaign.resolveParameter(parameter.value, on: req)
So your final code may look like
final class CampaignMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
let user = try request.requireAuthenticated(User.self)
guard let parameter = request.parameters.values.first else {
throw Abort(.forbidden)
}
return try Campaign.resolveParameter(parameter.value, on: request).flatMap { campaign in
guard try campaign.userID == user.requireID() else {
throw Abort(.forbidden, reason: "Campaign doesn't belong to you!")
}
return try next.respond(to: request)
}
}
}

Related

Swift: how to detect the error type using the debugger?

I'm new in iOS development, so maybe I'm thinking in the wrong way. I coded a view model with a function that calls an API, and everything works fine.
class SearchCityViewModel : ViewModelProtocol {
//OBSERVABLES
var cities = PublishSubject<[City]>()
var networkError = PublishSubject<Void>()
var generalError = PublishSubject<Void>()
init(){
print("Init SearchCityViewModel")
reinit()
}
func reinit(){}
func searchCity(stringToSearch: String){
async {
do {
if stringToSearch.count>=2 {
let cities = try await(api.getCities(cityToSearch: stringToSearch)).payload!
self.cities.onNext(cities)
}
else {
self.cities.onNext([])
}
}
catch {
self.generalError.onNext(Void())
}
}
}
Now I want to handle errors. In the catch block I want to distinguish all the errors I want to handle gracefully, and for the other ones I just want to emit a general error. To do that, firstly I need to know which error is thrown when the situation I want to handle occurs. I usually do this with the debugger. For instance, I disable the internet connection, and i create a breakpoint inside the catch block. The idea is to check which error is thrown when the internet connection is disabled, in order to create a catch block for that kind of error.
Image of the debugger
I'm struggling because with the debugger I only see that is an AFError instance, but it's not telling me nothing more that can help me to catch it.
What is wrong with my workflow? Do I really need to read all the docs every time? For each library I use?
Thank you!
Perhaps you can read the articles and then you will know how to do it better, you can use the framework -oslog instead of using print function.
debugging your logging info
I found the way. What I was missing is casting the error as NSError. In this way, with the debugger is possible to see the domain and the code of the error. In the case of Alamofire, the real error is wrapped, and it's accessible through the underlyingError attribute. Once I had the domain and the code of the error, I wrote the following code:
class SearchCityViewModel : ViewModelProtocol {
//OBSERVABLES
var cities = PublishSubject<[City]>()
var networkError = PublishSubject<Void>()
var generalError = PublishSubject<Void>()
init(){
print("Init SearchCityViewModel")
reinit()
}
func reinit(){}
func searchCity(stringToSearch: String){
async {
do {
if stringToSearch.count>=2 {
let cities = try await(api.getCities(cityToSearch: stringToSearch)).payload!
self.cities.onNext(cities)
}
else {
self.cities.onNext([])
}
}
catch {
if let afError = asAFError, let underlyingError = afError.underlyingError as NSError?, underlyingError.domain == NSURLErrorDomain, underlyingError.code == NSURLErrorNotConnectedToInternet || underlyingError.code == NSURLErrorTimedOut {
self.networkError.onNext(Void())
}
else {
self.generalError.onNext(Void())
}
}
}
}

How to get an auth code using ASWebAuthenticationSession?

I'm trying to obtain an auth code from Stripe's OAuth endpoint using ASWebAuthenticationSession - this event happens after the my Stripe redirect url gets called.
Unfortunately, the authSession's completion handler doesn't call back with a callbackURL. And I need this callbackURL to query the auth code. I've read different articles on this topic but I can't figure out why my implementation doesn't work they way I expect it to.
Here's my code:
class CreateStripeConnectAccountController: UIViewController {
var authSession: ASWebAuthenticationSession!
override func viewDidLoad() {
super.viewDidLoad()
configureAuthSession()
}
private func configureAuthSession() {
let urlString = Constants.URLs.stripeConnectOAuth // Sample URL
guard let url = URL(string: urlString) else { return }
let callbackScheme = "myapp:auth"
authSession = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackScheme, completionHandler: { (callbackURL, error) in
guard error == nil, let successURL = callbackURL else {
print("Nothing")
return
}
let oauthToken = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
print(successURL.absoluteString)
})
authSession.presentationContextProvider = self
authSession.start()
}
}
extension CreateStripeConnectAccountController: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
self.view.window ?? ASPresentationAnchor()
}
}
I believe the issue is that you are giving nil for callbackURLScheme. You need to give a URL scheme that redirects to your app:
See apple's authentication example: https://developer.apple.com/documentation/authenticationservices/authenticating_a_user_through_a_web_service
And here's apple's docs on how to create a custom URL scheme for your app: https://developer.apple.com/documentation/uikit/inter-process_communication/allowing_apps_and_websites_to_link_to_your_content/defining_a_custom_url_scheme_for_your_app.
I know it's old, but anyway.
Make sure, that callbackScheme and scheme that is used in redirect_uri are the same.
Your callbackScheme myapp:auth is incorrect format.
The symbol : cannot be used in the scheme name of a URI.
See the following RFC definition of Scheme.
Scheme names consist of a sequence of characters beginning with a
letter and followed by any combination of letters, digits, plus
("+"), period ("."), or hyphen ("-").
https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
Thefore, revising the callbackURLscheme as myapp-auth or myapp.auth works well.
try setting your callbackScheme = "myapp" to receive callback
and from your server-side it should return "myapp://auth?token=1234"
Hope it helps.

Siesta REST login

How to translate my login user URLSession code into Siesta framework code? My current attempt isn't working.
I've looked at the example in the GithubBrowser but the API I have doesn't work like that.
The issue is that the user structure is kind of split by how the endpoint in the API I'm consuming works. The endpoint is http://server.com/api/key. Yes, it really is called key and not user or login. Its called that by the authors because you post a user/pass pair and get a key back. So it takes in (via post) a json struct like:
{"name": "bob", "password": "s3krit"}
and returns as a response:
{"token":"AEWasBDasd...AAsdga"}
I have a SessionUser struct:
struct SessionUser: Codable
{
let name: String
let password: String
let token: String
}
...which encapsulates the state (the "S" in REST) for the user. The trouble is name & password get posted and token is the response.
When this state changes I do my:
service.invalidateConfiguration() // So requests get config change
service.wipeResources() // Scrub all unauthenticated data
An instance is stored in a singleton, which is picked up by the configure block so that the token from the API is put in the header for all other API requests:
configure("**") {
// This block ^ is run AGAIN when the configuration is invalidated
// even if loadManifest is not called again.
if let haveToken = SessionManager.shared.currentUser?.token
{
$0.headers["Authorization"] = haveToken
}
}
That token injection part is already working well, by the way. Yay, Siesta!
URLSession version
This is bloated compared to Siesta, and I'm now not using this but here is what it used to be:
func login(user: SessionUser, endpoint: URL)
{
DDLogInfo("Logging in: \(user.name) with \(user.password)")
let json: [String: Any] = ["name": user.name, "password": user.password]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.httpBody = jsonData
_currentStatus = .Unknown
weak var welf = self
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
handleLogin(error: error, message: "No data from login attempt")
return
}
let jsonData:Any
do {
jsonData = try JSONSerialization.jsonObject(with: data, options: [])
}
catch let jsonDecodeError {
handleLogin(error: jsonDecodeError, message: "Could not get JSON from login response data")
return
}
guard let jsonDecoded = jsonData as? [String: Any] else {
handleLogin(error: error, message: "Could not decode JSON as dictionary")
return
}
guard let token = jsonDecoded["token"] as? String else {
handleLogin(error: error, message: "No auth token in login response")
return
}
let newUser = SessionUser(name: user.name, password: "", token: token)
welf?.currentUser = newUser
welf?.saveCurrentSession()
welf?._currentStatus = .LoggedIn
DDLogInfo("User \(newUser.name) logged in")
loginUpdate(user: newUser, status: .LoggedIn, message: nil, error: nil)
}
task.resume()
}
Siesta Version
Here is my attempt right now:
func login(user: String, pass: String, status: #escaping (String?) -> ())
{
let json = [ "name": user, "password": pass]
let req = ManifestCloud.shared.keys.request(.post, json: json)
req.onSuccess { (tokenInfo) in
if let token = tokenInfo.jsonDict["token"] as? String
{
let newUser = SessionUser(name: user, password: pass, token: token)
self.currentUser = newUser
}
status("success")
}
req.onFailure { (error) in
status(error.userMessage)
}
req.onCompletion { (response) in
status(nil)
}
}
Its sort of working, but the log in credentials are not saved by Siesta and I've had to rig up a new notification system for login state which I'd hoped Siesta would do for me.
I want to use Siesta's caching so that the SessionUser object is cached locally and I can use it to get a new token, if required, using the cached credentials. At the moment I have a jury-rigged system using UserDefaults.
Any help appreciated!
The basic problem here is that you are requesting but not loading the resource. Siesta draws a distinction between those two things: the first is essentially a fancied-up URLSession request; the second means that Siesta hangs on to some state and notifies observers about it.
Funny thing, I just answered a different but related question about this a few minutes ago! You might find that answer a helpful starting point.
In your case, the problem is here:
let req = ManifestCloud.shared.keys.request(.post, json: json)
That .request(…) means that only your request hooks (onSuccess etc.) receive a notification when your POST request finishes, and Siesta doesn’t keep the state around for others to observe.
You would normally accomplish that by using .load(); however, that creates a GET request and you need a POST. You probably want to promote your POST to be a full-fledge load request like this:
let keysResource = ManifestCloud.shared.keys
let req = keysResource.load(using:
keysResource.request(.post, json: json))
This will take whatever that POST request returns and make it the (observable) latestData of ManifestCloud.shared.keys, which should give you the “notification system for login state” that you’re looking for.

File Provider iOS11 startProvidingItem not invoked

I'm implementing a File Provider Extension for iOS 11.
Dispite watching the conference at https://developer.apple.com/videos/play/wwdc2017/243/ and navigating through Apple's Documentation, I still can't seem to understand how to implement some of the methods for NSFileProviderExtension and NSFileProviderEnumerator objects.
I successfully implemented NSFileProviderItem, having all of them listed in the Navite iOS 11 Files App. However, I can't trigger any document based app to open upon selecting a file.
I overrided all the methods for the NSFileProviderExtension. Some are still empty, but I placed a breakpoint to check whenever they are called.
The NSFileProviderExtension looks something like this:
class FileProviderExtension: NSFileProviderExtension {
var db : [FileProviderItem] = [] //Used "as" a database
...
override func item(for identifier: NSFileProviderItemIdentifier) throws -> NSFileProviderItem {
for i in db {
if i.itemIdentifier.rawValue == identifier.rawValue {
return i
}
}
throw NSError(domain: NSCocoaErrorDomain, code: NSNotFound, userInfo:[:])
}
override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
guard let item = try? item(for: identifier) else {
return nil
}
// in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
let manager = NSFileProviderManager.default
let perItemDirectory = manager.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
return perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
}
// MARK: - Enumeration
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
var maybeEnumerator: NSFileProviderEnumerator? = nil
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
maybeEnumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
self.db = CustomData.getData(pid: containerItemIdentifier)
} else if (containerItemIdentifier == NSFileProviderItemIdentifier.workingSet) {
// TODO: instantiate an enumerator for the working set
} else {
}
guard let enumerator = maybeEnumerator else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
My enumerateItems looks something like so:
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
override func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
let itens = CustomData.getData(pid: enumeratedItemIdentifier)
observer.didEnumerate(itens)
observer.finishEnumerating(upTo: nil)
}
The static function CustomData.getData is used for testing. It returns an array of NSFileProviderItem with the desired properties. It should be replaced with a database, as explained in the conference.
class CustomData {
static func getData(pid : NSFileProviderItemIdentifier) -> [FileProviderItem] {
return [
FileProviderItem(uid: "0", pid: pid, name: "garden", remoteUrl : "https://img2.10bestmedia.com/Images/Photos/338373/GettyImages-516844708_54_990x660.jpg"),
FileProviderItem(uid: "1", pid: pid, name: "car", remoteUrl : "https://static.pexels.com/photos/170811/pexels-photo-170811.jpeg"),
FileProviderItem(uid: "2", pid: pid, name: "cat", remoteUrl : "http://www.petmd.com/sites/default/files/what-does-it-mean-when-cat-wags-tail.jpg"),
FileProviderItem(uid: "3", pid: pid, name: "computer", remoteUrl : "http://mrslamarche.com/wp-content/uploads/2016/08/dell-xps-laptop-620.jpg")
]
}
}
The problem is, when the user presses a document, urlForItem is successfully called but nothing happens upon returning the item url.
What am I doing wrong?
I can't find any examples on the internet.
Cheers
-nls
Turns out, I did not correctly implement providePlaceholder(at url:).
It is now solved.
Cheers
-nls
EDIT:
In order to list the items in your file provider, the method enumerator(for:) should be implemented.
This method will receive a containerItemIdentifier, as if telling you "what folder the user is trying to access". It returns a NSFileProviderEnumerator object, that should also be implemented by you.
Here is an example of how a simple enumerator(for:) method should look like:
class FileProviderExtension: NSFileProviderExtension {
override func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
var enumerator: NSFileProviderEnumerator? = nil
if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
else {
enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
}
if enumerator == nill {
throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
}
return enumerator
}
(...)
}
Again, as I said, the FileProviderEnumerator should be implemented by you. The important method here is the enumerateItems(for observer:, startingAt page:)
Here it is how it should look:
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
if (enumeratedItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
//Creating an example of a folder item
let folderItem = FileProviderFolder()
folderItem.parentItemIdentifier = enumeratedItemIdentifier //<-- Very important
folderItem.typeIdentifier = "public.folder"
folderItem.name = "ExampleFolder"
folderItem.id = "ExampleFolderID"
//Creating an example of a file item
let fileItem = FileProviderFile()
fileItem.parentItemIdentifier = enumeratedItemIdentifier //<-- Very important
fileItem.typeIdentifier = "public.plain-text"
fileItem.name = "ExampleFile.txt"
fileItem.id = "ExampleFileID"
self.itemList.append(contentsOf: [folderItem, fileItem])
observer.didEnumerate(self.itemList)
observer.finishEnumerating(upTo: nil)
}
else {
//1 > Find directory name using "enumeratedItemIdentifier" property
//2 > Fetch data from the desired directory
//3 > Create File or Folder Items
//4 > Send items back using didEnumerate and finishEnumerating
}
}
(...)
}
Remember that we were creating these FileProviderEnumerators, giving them the containerItemIdentifier. This property is used to determine what folder the user is trying to access.
Very important note: Each item, File or Folder, should have its parentItemIdentifier property defined. If this property is not set, the items won't appear when the user tries to open the parent folder.
Also, as the name suggests, typeIdentifier will hold the Uniform Type Identifier (UTI) for the item.
Finally, the last object we should implement is the NSFileProviderItem. Both File and Folder items are very similar, and should differ in their typeIdentifier property.
Here is a very simple example of a folder:
class FileProviderFolder: NSObject, NSFileProviderItem {
public var id: String?
public var name: String?
var parentItemIdentifier: NSFileProviderItemIdentifier
var typeIdentifier: String
init() {
}
var itemIdentifier: NSFileProviderItemIdentifier {
return NSFileProviderItemIdentifier(self.id!)
}
var filename: String {
return self.name!
}
}
The itemIdentifier is very important because, as stated before, this property will provide the directory name for the folder item when trying to enumerate its contents (refer to enumerator(for:) method).
EDIT2
If the user selects a file, the method startProvidingItem(at url:) should be called.
This method should perform 3 tasks:
1 - Find the selected item ID (usualy using the provided url, but you can use a database too)
2 - Download the file to the local device, making it available at the specified url. Alamofire does this;
3 - Call completionHandler;
Here is a simple example of this method:
class FileProviderExtension: NSFileProviderExtension {
override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
// resolve the given identifier to a file on disk
guard let item = try? item(for: identifier) else {
return nil
}
// in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
let perItemDirectory = NSFileProviderManager.default.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
let allDir = perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
return allDir
}
override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {
// exploit that the path structure has been defined as <base storage directory>/<item identifier>/<item file name>, at urlForItem
let pathComponents = url.pathComponents
assert(pathComponents.count > 2)
return NSFileProviderItemIdentifier(pathComponents[pathComponents.count - 2])
}
override func startProvidingItem(at url: URL, completionHandler: #escaping (Error?) -> Void) {
guard
let itemID = persistentIdentifierForItem(at: url),
let item = try? self.item(for: itemID) as! FileProviderFile else {
return
}
DownloadfileAsync(
file: item,
toLocalDirectory: url,
success: { (response) in
// Do necessary processing on the FileProviderFile object
// Example: setting isOffline flag to True
completionHandler(nil)
},
fail: { (response) in
completionHandler(NSFileProviderError(.serverUnreachable))
}
)
}
(...)
}
Note that, to get the ID from the URL, I'm using the recomended method: the URL it self contains the item ID.
This URL is definedin the urlForItem method.
Hope this helps.
-nls
I thought I'd provide a followup answer, the primary answer is great as a first step. In my case startProvidingItem was not called because I was not storing the files in exactly the directory the system was looking for, that is to say:
<Your container path>/File Provider Storage/<itemIdentifier>/My Awesome Image.png
That is on the slide from WWDC17 on the FileProvider extension, but I did not think it must follow that format so exactly.
I had a directory not named "File Provider Storage" into which I was putting files directly, and startProvidingItem was never called. It was only when I made a directory for the uniqueFileID into which the file was placed, AND renamed my overall storage directory to "File Provider Storage" that startProvidingItem was called.
Also note that with iOS11, you'll need to provide a providePlaceholder call as well to the FileProviderExtension, use EXACTLY the code that is in the docs for that and do not deviate unless you are sure of what you are doing.

Unit Testing private functions that require keychain authentication in swift

Thanks in advance for any advice!
I'm setting up some unit tests in swift for iOS development. The method requires a call to the keychain to create an authToken to successfully run the function. I'm not sure how to approach creating a unit test for this kind of environment. Do I mock up a local file that I can use to bypass the authentication? Do I try to skip the authentication step entirely?
The function I'm testing is a private function as well and I'm having a hard time conceptualizing how I can test it through the public methods. Here is the code I'm working with:
override func viewDidLoad() {
super.viewDidLoad()
self.setMyDoctorsNavBarTitle()
self.setBackgroundWaterMark()
self.getDoctors()
//self.refreshControl?.addTarget(self, action: #selector(MyDoctorsViewController.refresh(_:)), forControlEvents: UIControlEvents.ValueChanged)
}
private func getDoctors() {
let authToken: [String: AnyObject] = [ "Authorization": keychain["Authorization"]!, // creates an authToken with the current values stored in
"UUID": keychain["UUID"]!, "LifeTime": keychain["LifeTime"]! ] // the keychain
RestApiManager.sharedInstance.postMyDocs(authToken) { (json, statusCode) in // passes the created authToken to postMyDocs in RestAPI to see if
if statusCode == 200 { // the token matches what's on the server
if let results = json["Doctors"].array { // If the credentials pass, we grab the json file and create an array of Doctors
for entry in results {
self.buildDoctorObject(entry) // Doctors information is parsed into individual objects
}
}
} else if statusCode == 401 {
/* If statucCode is 401, the user's AuthToken has expired. The historical AuthToken data will be removed from the iOS KeyChain and the user will be redirected to the login screen to reauthorize with the API
*/
self.keychain["Authorization"] = nil
self.keychain["UUID"] = nil
self.keychain["LifeTime"] = nil
let loginController = self.storyboard?.instantiateViewControllerWithIdentifier("LoginViewController") as! LoginViewController
NSOperationQueue.mainQueue().addOperationWithBlock {
self.presentViewController(loginController, animated: true, completion: nil)
}
} else if statusCode == 503 {
print("Service Unavailable Please Try Again Later")
}
}
}
private func buildDoctorObject(json: JSON){
let fName = json["FirstName"].stringValue
let lName = json["LastName"].stringValue
let city = json["City"].stringValue
let phNum = json["PhoneNumber"].stringValue
let faxNum = json["FaxNumber"].stringValue
let state = json["State"].stringValue
let addr = json["Address"].stringValue
let img = json["Image"].stringValue
let zip = json["Zip"].stringValue
let tax = json["Taxonomy"].stringValue
let docObj = DoctorObject(fName: fName, lName: lName, addr: addr, city: city, state: state, zip: zip, phNum: phNum, faxNum: faxNum, img: img, tax: tax)
self.docs.append(docObj)
self.tableView.reloadData()
}
I want to be able to Unit Test the getDoctors() and buildDoctorObject() functions, but I can only do that indirectly through viewDidLoad() since they're private.
I want to be able to test that the statusCode is being brought down correctly from the server and the appropriate steps take place if it comes back as 200 or 401.
I'm not necessarily looking for complete code, but simply a way to approach the problem. If you know of resources that might be helpful I would be grateful. I'm very new to this and I tried looking into resources online but couldn't find anything. A lot of what I found was you don't want to test private functions directly, and isn't advised to change the functions to public for the sake of testing.
Thanks again for looking into it!
Sean W.
Define that private method in the Test Class, with the same signature. Just try to call that method it will call your actual class method.

Resources