Lesson 18

Testing Storage and Reauthentication

WARNING: This module is deprecated and no longer receives updates. Protractor is likely being removed as the default from Angular applications and Protractor itself will likely stop receiving updates and development in the future. I would recommend checking out the Test Driven Development with Cypress/Jest as a replacement.

Dealing with complicated scenarios in E2E tests and more unit tests

DEPRECATED

Lesson Outline

Testing Storage

We have one last E2E test to deal with before all of our initial requirements for the application are finished:

  • On subsequent visits to the application, the user should be taken directly to the logged in view

This requirement will give us a chance to write some tests for interacting with Ionic's Storage API, and it will also present some interesting challenges for our existing E2E tests.

Create the E2E Test

Let's start by creating the E2E test for this requirement. We will add it to the e2e/src/login.e2e-spec.ts file, but we are going to quickly run into an issue that needs to be solved.

Add the following test to e2e/src/login.e2e-spec.ts:

it('should take the user directly to the home page if they have logged in previously', async () => {
  const input = loginPage.getKeyInput();
  const loginButton = loginPage.getLoginButton();

  await input.sendKeys('abcd-egfh-ijkl-mnop');

  await loginButton.click();

  await browser.wait(protractor.ExpectedConditions.urlContains('home'));

  await browser.get('');

  await browser.wait(protractor.ExpectedConditions.urlContains('home'));

  expect(homePage.getModuleListItems().first().getText()).toContain(
    'Module One'
  );
});

In the first part of the test we do exactly what we did for the test for checking if a user can log in, but once we have successfully logged in we make a call to browser.get(''); which will reload the application. We then do nothing and wait for it to take us to the home page. This is the behaviour we want - if we have already logged in previously then the next time we load the application we should automatically be taken to the home page.

All of our tests start from a logged out state, so this is fine for now. We should be able to get this E2E test passing by implementing the functionality in the application. Once we do implement it, though, the browser is going to automatically log the user in for all of the tests which is going to break what we have currently. We will need to make a decision on how to handle this, but we will get to that later. Let's focus on getting the E2E passing for now.

If we run the E2E tests with npm run e2e the tests will not be able to complete.

What's happening with our test is that it is logging in successfully, then it reloads the browser with browser.get('') and then nothing happens because it is just sitting on the login screen doing nothing as the tests wait for the URL to change to /home.

We need to implement some functionality so that on subsequent visits to the application, the user will automatically be logged in.

Extending the Auth Service

We will be extending our AuthService to include an additional function called reauthenticate that will handle automatically authenticating the user if there is a license key stored in local storage.

Let's create a new unit test for this.

Modify src/services/auth.service.spec.ts to reflect the following:

import { TestBed, inject, fakeAsync, tick } from '@angular/core/testing';
import {
  HttpClientTestingModule,
  HttpTestingController,
} from '@angular/common/http/testing';
import { IonicStorageModule } from '@ionic/storage-angular';

import { AuthService } from './auth.service';

describe('AuthService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [],

      providers: [AuthService],

      imports: [HttpClientTestingModule, IonicStorageModule.forRoot()],
    }).compileComponents();
  });

  it('checkKey should make a call to the server to check the validity of a key', inject(
    [AuthService, HttpTestingController],
    (authService, httpMock) => {
      const key = 'ewu0fef0ewuf08j3892jf98';
      const mockResponse = '{"isValid": true}';

      authService.checkKey(key).subscribe((result: any) => {
        expect(result).toEqual(mockResponse);
      });

      // Expect a request to the URL
      const mockReq = httpMock.expectOne('http://localhost:8080/api/check');

      // Execute the request using the mockResponse data
      mockReq.flush(mockResponse);
    }
  ));

  it('reauthenticate should automatically check key if in storage', inject(
    [AuthService, HttpTestingController],
    async (authService, httpMock) => {
      const mockResponse = '{"isValid": true}';

      await authService.init();

      spyOn(authService._storage, 'get').and.returnValue(
        Promise.resolve('abcde-fghi-jklm')
      );

      await authService.reauthenticate();

      // Expect a request to the URL
      const mockReq = httpMock.expectOne('http://localhost:8080/api/check');

      // Execute the request using the mockResponse data
      mockReq.flush(mockResponse);
    }
  ));
});

We've added another unit test to the end of this file, but we have also added imports for fakeAsync and tick, and we have also added the IonicStorageModule to the imports in the TestBed configuration. Since we will be using the Storage API we need to set this up for the testing environment.

NOTE: You will also need to install the Ionic Storage API using npm install @ionic/storage-angular

Just like in the other tests, we set up a mock backend since the reauthenticate function should be making a call to check the key against the server if one is present in storage.

We create a spy on authProvider.storage because we want to overwrite the behaviour of get to return our own promise with fake data (for the purpose of isolating the unit test, we don't want to retrieve the actual data from storage).

We then make a call to the reauthenticate. We make sure that the promise has resolved and then we check that the appropriate call to the server was made.

If we run our unit tests with npm test now, we will get this error:

AuthService reauthenticate should automatically check key if in storage FAILED
Error: <spyOn> : could not find an object to spy upon for get()

It's trying to spy on authProvider._storage but that does not exist yet, so it's time to implement that now. First of all, we will need to set up the storage module in our app.module.ts file.

Modify src/app/app.module.ts to reflect the following:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { IonicStorageModule } from '@ionic/storage-angular';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    IonicStorageModule.forRoot(),
    AppRoutingModule,
    HttpClientModule,
  ],
  providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
  bootstrap: [AppComponent],
})
export class AppModule {}

Modify src/services/auth.service.ts to reflect the following:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Storage } from '@ionic/storage-angular';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  public _storage: Storage;

  constructor(private http: HttpClient, private storage: Storage) {
    this.init();
  }

  async init(): Promise<void> {
    this._storage = await this.storage.create();
  }

  checkKey(key: string): Observable<any> {
    const body = {
      key: key,
    };

    return this.http.post('http://localhost:8080/api/check', body);
  }

  async reauthenticate(): Promise<void> {
    const key = await this._storage.get('eliteLicenseKey');

    if (!key) {
      throw new Error('No key found');
    }

    this.checkKey(key).subscribe((res) => {
      if (!res) {
        throw new Error('Invalid key');
      }
    });
  }
}

We've set up the Ionic Storage API now, and then we are using it in our reauthenticate function. This function will first retrieve the license key from storage and if it exists it will make a call to the checkKey function. If the key is valid it will resolve the promise, and if it isn't it will reject it.

If we run our unit tests with npm test now, you will see that everything passes:

Executed 21 of 21 SUCCESS (0.371 secs / 0.357 secs)

Adding Reauthentication to the Login Page

Now that we have a function to reauthenticate a user, we need to make use of it in the login page. We want to trigger this function when the user first loads the page, and if they do have a valid license key then we want them to be taken straight to the home page.

Let's create our unit test.

Add the following unit test to src/app/login/login.page.spec.ts:

it('if the user has a valid license key in storage then they should be taken straight to the home page', fakeAsync(() => {
  const authProvider = fixture.debugElement.injector.get(AuthService);
  const navCtrl = fixture.debugElement.injector.get(NavController);

  spyOn(navCtrl, 'navigateRoot');
  spyOn(authProvider, 'reauthenticate').and.returnValue(
    new Promise((resolve) => setTimeout(resolve, 0))
  );

  component.ngOnInit();

  tick();

  expect(navCtrl.navigateRoot).toHaveBeenCalledWith('/home');
}));

This is a pretty straightforward test. We are spying on navigateRoot of the navCtrl to see if it is called with /home at some point during the test. We also create a spy on our reauthenticate function so that it automatically responds with a resolved promise (again, isolating it from the actual AuthService).

The reauthentication should occur immediately, so we will be setting it up in ngOnInit. In the test, we make a call to ngOnInit, we flush any microtasks with tick(), and then we check that the root page was changed.

PRO

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).