iosswiftaws-sdkamazon-cognitoaws-sdk-ios

Testing Cognito Authentication in iOS App


I have an iOS app with Cognito authentication implemented very similar to CognitoYourUserPoolsSample. Most important fragments are in SignInViewController.swift:

  1. When user taps Sign In, asynch task is added:

    var passwordAuthenticationCompletion: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>?
    ...
    @IBAction func signInPressed<...> {
        ...
        let authDetails = AWSCognitoIdentityPasswordAuthenticationDetails(username: self.username.text!, password: self.password.text! )
        self.passwordAuthenticationCompletion?.set(result: authDetails)
        ...
    
  2. Later we get either success or error response:

    extension SignInViewController: AWSCognitoIdentityPasswordAuthentication {
          public func getDetails<...> {
              DispatchQueue.main.async {
                  // do something in case of success
          ...
    
          public func didCompleteStepWithError<...> {
              DispatchQueue.main.async {
                 // do something in case of failure
          ...
    

I also have a UI test, which fills username and password, clicks Sign In and validates response:

class MyAppUITests: XCTestCase {
    ...
    func loginTest() {
        let usernameField = <...>
        usernameField.tap()
        usernameField.typeText("user@domain.com")
        ... 
        // same for password field
        // then click Sign In
        <...>.buttons["Sign In"].tap()

Currently this test is working against the actual AWS infrastructure, which is not ideal for many reasons. What I want is to simulate various responses from AWS instead.

How can I do that?

I think the best would be mocking or stabbing task queue, but I'm not sure how to approach that. Any direction will be much appreciated. If you handled similar task in an alternative way, I'd like to hear your ideas too, thanks.


Solution

  • Okay, I am not that familiar with the AWS iOS SDK and how exactly it implements the auth flow, so take the following with a grain of salt. It's less a full answer but more a general "strategy" I hope. I implemented a similar approach in my current project, not just for the login, but actually all remote connections I make.

    There's three things you need to do:

    1. Run a small local webserver inside your UI test target. I use Embassy and Ambassador for that in my current project. Configure it to return whatever response Cognito (or another endpoint) usually gives. I simply curled a request manually and saved the response somewhere, but in my case I received plain data (and not, for example, a complete login page to show in a webview...). My guess is that Cognito actually shows a login (web)view and on successful login uses a deep link to "go back" to your app, which eventually calls your AWSCognitoIdentityPasswordAuthentication methods (success or error). You could have the test target, i.e. the webserver call the deep link directly if you know what it looks like (which should be possible to find out?).

    2. Add some mechanism to switch the Cognito endpoint during the test. This unfortunately requires addition of production code, but if done right it shouldn't be too difficult. I did it by using a launch environment variable I set during the test (see below). Unless your webserver supports https (Embassy does not out of the box) this also requires somehow configuring App Transport Security. The hardest part is surely to figure out where in the SDK that endpoint is constructed and how to change it. A quick look at the documentation leads me to believe that webDomain is where it's saved, but I don't see how it's set up. That property is even read-only, which complicates things. I assume, though, that you can change it in some configuration in your project? Otherwise it reeks like a case for method swizzling... Sorry I can't offer more sound direction here.

    3. During your tests, ensure the real endpoints that are going to be accessed during the relevant app flows are switched to http://localhost/.... I did so by using XCUIApplication().launchEnvironment["somekey"] = "TESTINGKEY", which matched my production code preparation in the second step. In my case I could simply load different endpoints (which had the localhost domain and otherwise the same paths as the original domains). Configure your webserver's responses according to the test case (successful login, invalid credentials etc.).

    I admit this was/is a lot of work, but for me it was worth it since I could easily run the entire app flow (which involved a lot of outgoing requests) without any network access. I had to implement our auth system on my own anyway, which gave me a lot of control over what URLs were used and where, making it easy to have a single place to stub them out depending on the launch environment variable. The ugliest part in my case was actually enabling ATS exceptions in my tests only, for which I had to use a run script for various reasons.