Skip to content

task#28 build a registration dialog

bacn edited this page Sep 16, 2020 · 1 revision

Build a Registration Dialog

The goal of this task is to create a registration dialog, which handles the registration request for new users.

  1. Create Validators like no-white-space-validator.ts, password-validator.ts to make sure the user enters valid information.
  2. Create a custom-validation.service.ts to check if passwords match.
  3. Create the Register component. Add the Component to app.module.ts, app-routing.module.ts and app.component.spec.ts.
  4. Link the component with the authentication service, the new user shall get the default role user.
  5. Add the link for /register to the Home and Navigator component
  6. Create a UserLocation service to identify the location of the user based on his ip address
  7. Add getUserLocation to the user.service.ts.
  8. Refactor the mock backend in order to register users, refactor the error handling

Result

Verify the result by running the unit tests or by using the login form and check the token in the applications view -> localStore of the browser development tools.

Register a new user

task28-register

Error

task28-username-taken

Success

task28-success

Try to login

task28-logout

Hints

Validator

There are 3 different Types of validators neccessary:

  • NoWhiteSpaceValidator: makes sure there are no spaces in the name
  • PasswordValidator: makes sure the chosen password follows the password rules (Password must be between 5 and 50 characters, at least one upper and one lower case letter and at least one number.)
  • CustomValidationService with matchPassword method: makes sure the password confirmation matches the password

task28-register-error-fields

The no white space validator

Create a file no-white-space-validator.ts in the folder src/app/shared/validator. Implement a method cannotContainSpace. The method returns a ValidationErrors or null.

import { AbstractControl, ValidationErrors } from '@angular/forms';

export class NoWhiteSpaceValidator {
  static cannotContainSpace(control: AbstractControl): ValidationErrors | null {
    if ((control.value as string).indexOf(' ') >= 0) {
      return {cannotContainSpace: true};
    }
    return null;
  }
}

The password validator

Create a file password-validator.ts in the folder src/app/shared/validator. Implement a method passwordValidator. The method returns a ValidationErrors or null.

import { AbstractControl, ValidationErrors } from '@angular/forms';

export class PasswordValidator {
  static passwordValidator(control: AbstractControl): ValidationErrors | null {

    if (!control.value) { return null; }
    const regex = new RegExp('^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{5,50}$');
    const valid = regex.test(control.value);
    return valid ? null : { invalidPassword: true };
  }
}

The CustomValidationService

Generate the service

ng generate service shared/service/custom-validation

Implement a method matchPassword whcih takes 2 arguments password and confirmPassword.
The method returns a ValidationErrors or null.

import { Injectable } from '@angular/core';
import {AbstractControl, FormGroup, ValidationErrors, ValidatorFn} from '@angular/forms';

@Injectable({
  providedIn: 'root'
})
export class CustomValidationService {

  constructor() { }

  matchPassword(password: string, confirmPassword: string): ValidationErrors | null {
    return (formGroup: FormGroup) => {
      const passwordControl = formGroup.controls[password];
      const confirmPasswordControl = formGroup.controls[confirmPassword];

      if (!passwordControl || !confirmPasswordControl) {
        return null;
      }

      if (confirmPasswordControl.errors && !confirmPasswordControl.errors.passwordMismatch) {
        return null;
      }

      if (passwordControl.value !== confirmPasswordControl.value) {
        confirmPasswordControl.setErrors({ passwordMismatch: true });
      } else {
        confirmPasswordControl.setErrors(null);
      }
    };
  }
}

Register Component

Generate the Component:

ng generate component component/register

The register component /src/app/component/register/register.component.ts uses the authentication service to register to the application. If the user is already register an error message is presented.

It creates the form fields and validators using an Angular FormBuilder to create an instance of a FormGroup that is stored in the form property. The form is then bound to the

element in the register component template above using the [formGroup] directive.

The component contains a convenience getter property registerCtrl to make it a bit easier to access form controls, for example you can access the password field in the template using registerCtrl.password instead of form.controls.password.

Register Component

The register component template contains a simple registration form with fields for first name, last name, username and password. It displays validation messages for invalid fields when the submit button is clicked. The form element uses the [formGroup] directive to bind to the form FormGroup in the register component below, and it binds the form submit event to the onSubmit() handler in the register component using the angular event binding (ngSubmit)="onSubmit()".

Custom Validation

The method patternValidator is used to validate the password pattern in our form. The parameter for this method is of type AbstractControl which is a base class for the FormControl. We will use a regular expression to validate the password. We will validate the following four conditions using the regular expression:

  • The password should be a minimum of five characters (for testing only) long.
  • It has at least one lower case letter.
  • It has at least one upper case letter.
  • Contains at least one number.

If the password fails the regex check, we will set the invalidPassword property to true.

The method MatchPassword is used to compare the passwords in two fields. This method will accept two parameters of type string. These parameters represent the name of the fields to be matched. We will get the FormControl for these two fields and then match the values in them. If the values do not match, we will set the passwordMismatch property to true.

register.component.ts

import {Component, OnInit} from '@angular/core';
import {Router, ActivatedRoute} from '@angular/router';
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
import {first} from 'rxjs/operators';
import {AlertService} from '../../shared/component/alert/alert.service';
import {UserService} from '../../shared/service/user.service';
import {CustomValidationService} from '../../shared/service/custom-validation.service';
import {NoWhiteSpaceValidator} from '../../shared/validator/no-white-space-validator';
import {PasswordValidator} from '../../shared/validator/password-validator';


@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
  registerForm: FormGroup;
  loading = false;
  submitted = false;
  passwordFieldType;

  userNamePattern = '^[a-z0-9_-]{5,15}$';
  mobnumPattern = '^((\\+91-?)|0)?[0-9]{10}$';

  constructor(
    private formBuilder: FormBuilder,
    private route: ActivatedRoute,
    private router: Router,
    private userService: UserService,
    private alertService: AlertService,
    private customValidator: CustomValidationService
  ) {
  }

  ngOnInit(): void {
    this.registerForm = this.formBuilder.group({
        firstName: ['', [
          Validators.required,
          Validators.minLength(2),
          Validators.maxLength(30),
          NoWhiteSpaceValidator.cannotContainSpace]
        ],
        lastName: ['', [
          Validators.required, Validators.minLength(2),
          Validators.maxLength(30),
          NoWhiteSpaceValidator.cannotContainSpace]
        ],
        email: ['', [
          NoWhiteSpaceValidator.cannotContainSpace,
          Validators.required,
          Validators.email,
        ]
        ],
        username: ['', [
          Validators.required,
          Validators.pattern(this.userNamePattern)],
        ],
        password: ['', [
          Validators.required,
          PasswordValidator.passwordValidator]
        ],
        confirmPassword: ['', [Validators.required]],
      },
      {
        validator: this.customValidator.matchPassword('password', 'confirmPassword'),
      }
    );

  }

  togglePasswordFieldType(): void {
    this.passwordFieldType = !this.passwordFieldType;
  }

  // convenience getter for easy access to form fields
  get registerCtrl(): any {
    return this.registerForm.controls;
  }

  onSubmit(): void {
    this.submitted = true;

    // reset alerts on submit
    this.alertService.clear();

    // stop here if form is invalid
    if (this.registerForm.invalid) {
      return;
    }

    this.registerCtrl.email.setValue(this.registerCtrl.email.value.toLowerCase());

    this.loading = true;
    this.userService.register(this.registerForm.value)
      .pipe(first())
      .subscribe(
        data => {
          this.alertService.success('Registration successful', {keepAfterRouteChange: true});
          this.router.navigate(['../login'], {relativeTo: this.route});
        },
        err => {
          // console.log('error=' + error.error.message, JSON.stringify(error));
          this.alertService.error(err.error.message, {keepAfterRouteChange: true});
          this.loading = false;
        });
  }
}

register.component.html

<div class="col-md-8 offset-md-2 ">

  <div class="card">
    <h4 class="card-header">Register<br>Auction Case Study</h4>
    <div class="card-body">
      <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
        <div class="form-group row">
          <label for="firstName" class="col-sm-3 col-form-label col-form-label-sm">First Name</label>
          <div class="col-sm-9">
            <input type="text" formControlName="firstName" id="firstName" class="form-control form-control-sm"
                   [ngClass]="{ 'is-invalid': submitted && registerCtrl.firstName.errors }"/>
            <div *ngIf="(registerCtrl.firstName.touched || submitted)">
              <span class="text-danger" *ngIf="registerCtrl.firstName.errors?.required"> First Name is required. </span>
              <span class="text-danger" *ngIf="registerCtrl.firstName.errors?.minlength"> First Name must be at least 2 characters. </span>
              <span class="text-danger" *ngIf="registerCtrl.firstName.errors?.maxlength"> First Name must be at smaller than 30 characters. </span>
              <span class="text-danger" *ngIf="registerCtrl.firstName.errors?.cannotContainSpace"> White Spaces are not allowed. </span>
            </div>
          </div>
        </div>
        <div class="form-group row">
          <label for="lastName" class="col-sm-3 col-form-label col-form-label-sm">Last Name</label>
          <div class="col-sm-9">
            <input type="text" formControlName="lastName" id="lastName" class="form-control form-control-sm"
                   [ngClass]="{ 'is-invalid': submitted && registerCtrl.lastName.errors }"/>
            <div *ngIf="(registerCtrl.lastName.touched || submitted)">
              <span class="text-danger" *ngIf="registerCtrl.lastName.errors?.required"> Last Name is required. </span>
              <span class="text-danger" *ngIf="registerCtrl.lastName.errors?.minlength"> Last Name must be at least 2 characters. </span>
              <span class="text-danger" *ngIf="registerCtrl.lastName.errors?.maxlength"> Last Name must be at smaller than 30 characters. </span>
              <span class="text-danger" *ngIf="registerCtrl.lastName.errors?.cannotContainSpace"> White Spaces are not allowed. </span>
            </div>
          </div>
        </div>
        <div class="form-group row">
          <label for="email" class="col-sm-3 col-form-label col-form-label-sm">E-Mail</label>
          <div class="col-sm-9">
            <input type="text" autocomplete="email" formControlName="email" id="email"
                   class="form-control form-control-sm"
                   [ngClass]="{ 'is-invalid': submitted && registerCtrl.email.errors }"/>
            <div *ngIf="(registerCtrl.email.touched || submitted)">
              <span class="text-danger" *ngIf="registerCtrl.email.errors?.required"> E-Mail is required. </span>
              <span class="text-danger" *ngIf="registerCtrl.email.errors?.cannotContainSpace"> White Spaces are not allowed. </span>
              <span class="text-danger" *ngIf="registerCtrl.email.errors?.email"> The E-Mail address is not in the correct format. </span>
            </div>
          </div>
        </div>
        <div class="form-group row">
          <label for="username" class="col-sm-3 col-form-label col-form-label-sm">Username</label>
          <div class="col-sm-9">
            <input type="text" autocomplete="off" formControlName="username" id="username"
                   class="form-control form-control-sm"
                   [ngClass]="{ 'is-invalid': submitted && registerCtrl.username.errors }"/>
            <div *ngIf="(registerCtrl.username.touched || submitted)">
              <span class="text-danger" *ngIf="registerCtrl.username.errors?.userNameNotAvailable"> User Name is already taken </span>
              <span class="text-danger" *ngIf="registerCtrl.username.errors?.required"> User Name is required </span>
              <span class="text-danger" *ngIf="registerCtrl.username.errors?.pattern"> User Name must be between 5 and 15 characters. Allowed characters are a-z, - and _. </span>
            </div>
          </div>
        </div>
        <div class="form-group row">
          <label for="password" class="col-sm-3 col-form-label col-form-label-sm">Password</label>
          <div class="col-sm-9">
            <div class="input-group">
              <input [type]="passwordFieldType ? 'text' : 'password'" autocomplete="current-password"
                     formControlName="password" id="password"
                     class="form-control form-control-sm"
                     [ngClass]="{ 'is-invalid': submitted && registerCtrl.password.errors }"/>
              <div class="input-group-append">
               <span class="input-group-text">
                  <i
                    class="fa"
                    [ngClass]="{
                      'fa-eye-slash': !passwordFieldType,
                      'fa-eye': passwordFieldType
                    }"
                    (click)="togglePasswordFieldType()"
                  ></i>
               </span>
              </div>
            </div>
            <div *ngIf="(registerCtrl.password.touched || submitted)">
              <span class="text-danger" *ngIf="registerCtrl.password.errors?.required"> Password is required </span>
              <span class="text-danger" *ngIf="registerCtrl.password.errors?.invalidPassword"> Password must be between 5 and 50 characters, at least one upper and one lower case letter and at least one number.</span>
            </div>
          </div>
        </div>
        <div class="form-group row">
          <label for="confirm-password" class="col-sm-3 col-form-label col-form-label-sm">Confirm Password</label>
          <div class="col-sm-9">
            <div class="input-group">
              <input [type]="passwordFieldType ? 'text' : 'password'" autocomplete="confirm-password"
                     formControlName="confirmPassword"
                     id="confirm-password"
                     class="form-control form-control-sm"
                     [ngClass]="{ 'is-invalid': submitted && registerCtrl.confirmPassword.errors }"/>
              <div class="input-group-append">
               <span class="input-group-text">
                  <i
                    class="fa"
                    [ngClass]="{
                      'fa-eye-slash': !passwordFieldType,
                      'fa-eye': passwordFieldType
                    }"
                    (click)="togglePasswordFieldType()"
                  ></i>
               </span>
              </div>
            </div>
            <div *ngIf="(registerCtrl.confirmPassword.touched || submitted)">
              <span class="text-danger" *ngIf="registerCtrl.confirmPassword.errors?.required"> Confirm Password is required </span>
              <span class="text-danger" *ngIf="registerCtrl.confirmPassword.errors?.passwordMismatch"> Passwords do not match.</span>
            </div>
          </div>
        </div>

        <div class="form-group row">
          <div class="col-sm-9">
            <button [disabled]="loading || this.registerForm.invalid" class="btn btn-primary">
              <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
              Register
            </button>
            <a routerLink="../login" class="btn btn-link">Cancel</a>
          </div>
        </div>
      </form>
    </div>
  </div>
</div>

register.component.scss

.input-group-text {
  background: white;
  margin-bottom: 4px;
}

.form-control-sm {
  margin-bottom: 4px;
}

register.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { RegisterComponent } from './register.component';
import {RouterTestingModule} from '@angular/router/testing';
import {ReactiveFormsModule} from '@angular/forms';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {AuthenticationService} from '../../shared/service/authentication.service';

describe('RegisterComponent', () => {
  let component: RegisterComponent;
  let fixture: ComponentFixture<RegisterComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([
          {path: 'login', redirectTo: ''}]),
        ReactiveFormsModule,
        HttpClientTestingModule,
      ],
      declarations: [ RegisterComponent ],
      providers: [
        AuthenticationService,
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(RegisterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

});

Add the register component to the app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AngularDateHttpInterceptorService, MockModule } from './shared/helper';

import { NavBarComponent } from './nav-bar/nav-bar.component';
import { HomeComponent } from './home/home.component';
import { AppRoutingModule } from './app-routing.module';
import { environment } from '../environments/environment';
import { AlertModule } from './shared/component/alert/alert.module';
import { AlertTestComponent } from './shared/component/alert-test/alert-test.component';
import { LoginComponent } from './component/login/login.component';
import { RegisterComponent } from './component/register/register.component';

// import/use mock module only if configured (mock module will not be included in prod build!):
const mockModule = environment.useMockBackend ? [MockModule] : [];

@NgModule({
  declarations: [
    AppComponent,
    NavBarComponent,
    HomeComponent,
    AlertTestComponent,
    LoginComponent,
    RegisterComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    AppRoutingModule,
    AlertModule,
    ...mockModule
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AngularDateHttpInterceptorService,
      multi: true
    },
  ],

  bootstrap: [AppComponent]
})
export class AppModule {}

Add a routing for the register component to the app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomeComponent} from './home/home.component';
import {LoginComponent} from './component/login/login.component';
import {RegisterComponent} from './component/register/register.component';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent
  }, {
    path: 'login',
    component: LoginComponent
  }, {
    path: 'register',
    component: RegisterComponent
  }, {
    path: 'auctions',
    loadChildren: () => import('./auction/auction.module').then(m => m.AuctionModule)
  }, {
    path: '',
    pathMatch: 'full',
    redirectTo: '/home'
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Refactor the app.component.spec.ts

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpClientModule} from '@angular/common/http';
import {HomeComponent} from './home/home.component';
import {NavBarComponent} from './nav-bar/nav-bar.component';
import {AlertModule} from './shared/component/alert/alert.module';
import {AlertTestComponent} from './shared/component/alert-test/alert-test.component';
import {AlertService} from './shared/component/alert/alert.service';
import {BrowserModule} from '@angular/platform-browser';



describe('AppComponent', () => {
  let alertService: AlertService;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        BrowserModule,
        FormsModule,
        ReactiveFormsModule,
        HttpClientModule,
        RouterTestingModule.withRoutes([
          {path: 'login', redirectTo: ''},
          {path: 'register', redirectTo: ''},
          ]),
        AlertModule,
      ],
      declarations: [
        AppComponent,
        HomeComponent,
        NavBarComponent,
        AlertTestComponent,
      ],
      providers: [
        AlertService,
      ]
    }).compileComponents();
    alertService = TestBed.inject(AlertService);
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'app works!'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('app works!');
  });

});

Refactor the Authentication Service

The Refactoring is stopping some console.log output. The refactored file shared/service/authentication.service.ts:

import {Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {BehaviorSubject, Observable} from 'rxjs';
import {map, retry} from 'rxjs/operators';
import {User} from '../model/user';
import {environment} from '../../../environments/environment';
import {JwtTokenService} from './jwt-token.service';
import {BrowserStorageService} from './browser-storage.service';
import {init} from '../helper/util';


@Injectable({providedIn: 'root'})
export class AuthenticationService {
  private currentUserSubject: BehaviorSubject<User>;
  public currentUser: Observable<User>;
  private authApiUrl: string;
  private refreshTokenTimeout;

  constructor(
    private browserStorageService: BrowserStorageService,
    private http: HttpClient,
    private router: Router,
    private tokenService: JwtTokenService
  ) {
    this.authApiUrl = environment.endpoints.backendAuthUrl;

    // Check localstore or sessionstore
    if (!browserStorageService.getItem('currentUser'))  {
      browserStorageService.rememberMe = true;
    }
    const user: User = init(User, JSON.parse(browserStorageService.getItem('currentUser')));
    this.currentUserSubject = new BehaviorSubject<User>(user);
    this.currentUser = this.currentUserSubject.asObservable();
  }

  public get currentUserValue(): User {
    return this.currentUserSubject.value;
  }

  login(username: string, password: string, rememberMe = true): Observable<any> {

    const data = {
      username: username,
      password: password
    };
    const httpOptions = {
      headers: new HttpHeaders({'Content-Type': 'application/json'}),
      observe: 'response' as 'response'
    };
    return this.http.post<any>(`${this.authApiUrl}/users/authenticate`, data, httpOptions)
      .pipe(
        map(response => {
        const user = response.body;
        if (this.tokenService.verifyToken(user.token, user.publicKey)) {
          this.browserStorageService.rememberMe = rememberMe;
          return this.saveUser(user);
        }
      }),
        retry(2)
    );
  }


  logout(): void {
    // remove user from local storage and set current user to null
    this.browserStorageService.removeItem('currentUser');
    this.currentUserSubject.next(null);
    this.stopRefreshTokenTimer();
    this.router.navigate(['/login']); // is needed after we have a login dialog
  }

  getBasicAuthHeader(username: string, password: string): string {
    return `Basic ${btoa(username + ':' + password)}`;
  }

  refreshToken(): Observable<User> {

    const jwtToken = this.currentUserSubject.value.token;
    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + jwtToken
      }), observe: 'response' as 'response'
    };

    const data = {
      username: this.currentUserValue.username,
      refreshToken: this.currentUserValue.refreshToken
    };

    return this.http.post<any>(`$${this.authApiUrl}/users/refresh_token`, data, httpOptions)
      .pipe(map(response => {
        // console.log(response);
        const user = response.body;
        if (this.tokenService.verifyToken(user.token, user.publicKey)) {
          return this.saveUser(user);
        } else {
          console.log('refresh token not successful');
        }
      }));
  }

  private startRefreshTokenTimer(): void {

    // set a timeout to refresh the token a minute before it expires
    const expires = new Date(this.currentUserValue.expires * 1000);
    const timeout = expires.getTime() - Date.now() - (60 * 1000); // debug 59 minutes before (every minute a new token)
    this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(), timeout);
  }

  private stopRefreshTokenTimer(): void {
    clearTimeout(this.refreshTokenTimeout);
  }

  private saveUser(user: User): User {

    user.extractTokenInfo();
    this.browserStorageService.setItem('currentUser', JSON.stringify(user));
    this.currentUserSubject.next(user);
    this.startRefreshTokenTimer();
    return user;
  }

}

The Refactoring is stopping some console.log output. The refactored file shared/service/authentication.service.spec.ts:

import {inject, TestBed} from '@angular/core/testing';

import { AuthenticationService } from './authentication.service';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { MockBackendInterceptor } from '../helper/mock/mock-backend-interceptor.service';
import { CreateUserService } from '../helper/create-user.service';
import {decodeToken, Jwt, verifyToken} from '../helper/helper.jwt';

describe('AuthenticationService', () => {
  let service: AuthenticationService;
  let singletonService: AuthenticationService;
  let userService: CreateUserService;

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([
          {path: 'login', redirectTo: ''}]),
        HttpClientTestingModule,

      ],
      providers: [
        AuthenticationService,
        CreateUserService,
        {
          provide: HTTP_INTERCEPTORS,
          useClass: MockBackendInterceptor,
          multi: true
        }
      ]
    });
    service = TestBed.inject(AuthenticationService);
    userService = TestBed.inject(CreateUserService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('Service injected via inject(...) and TestBed.inject(...) should be the same instance',
    inject([AuthenticationService], (injectService: AuthenticationService) => {
      expect(injectService).toBe(service);
    })
  );

  it('should authenticate an admin', (done) => {
    service.login('admin', 'admin').subscribe(
      (data) => {
        // console.log('authenticateResponse', data, service.currentUserValue);
        expect(service.currentUserValue.username).toBe('admin');
        // use the same service for further tests
        singletonService = service;
        checkTheTokens();
        checkTheRoles(['admin', 'user']);
        checkCurrentUserStoreExist();
        done();
      },
    (err) => {
      // console.log('authenticateResponse', err);
      fail();
      done();
    }, () => {
      // console.log('Test complete');
      done();
    });
  });

  it('should refresh a token', (done) => {
    const oldRefreshToken = singletonService.currentUserValue.refreshToken;
    singletonService.refreshToken().subscribe(
      (data) => {
        // console.log('refreshTokenResponse', data, service.currentUserValue);
        expect(singletonService.currentUserValue.username).toBe('admin');
        const newRefreshToken = singletonService.currentUserValue.refreshToken;
        expect(newRefreshToken).not.toEqual(oldRefreshToken);
        checkTheTokens();
        checkTheRoles(['admin', 'user']);
        checkCurrentUserStoreExist();
        done();
      },
      (err) => {
        // console.log('authenticateResponse', err);
        fail();
        done();
      }, () => {
        // console.log('Test complete');
        done();
      });
  });

  it('should logout', (done) => {
    singletonService.logout();
    expect(singletonService.currentUserValue).toBeNull();
    checkCurrentUserStoreNotExist();
    done();
  });

  function checkCurrentUserStoreExist(): void {
     expect(localStorage.getItem('currentUser')).toBeTruthy();
  }

  function checkCurrentUserStoreNotExist(): void {
    expect(localStorage.getItem('currentUser')).toBeFalsy();
  }

  function checkTheTokens(): void {

    const refreshToken = decodeToken(singletonService.currentUserValue.refreshToken);
    if (refreshToken instanceof Jwt) {
      expect(verifyToken(singletonService.currentUserValue.refreshToken, singletonService.currentUserValue.publicKey)).toBeTrue();
      expect(refreshToken.body.sub).toEqual(singletonService.currentUserValue.username);
    }
    const token = decodeToken(singletonService.currentUserValue.token);
    if (token instanceof Jwt) {
      expect(verifyToken(singletonService.currentUserValue.token, singletonService.currentUserValue.publicKey)).toBeTrue();
      expect(token.body.sub).toEqual(singletonService.currentUserValue.username);
    }
  }

  function checkTheRoles(checkRoles: Array<string>): void {
    singletonService.currentUserValue.roles.forEach(role => {
      expect(checkRoles.includes(role)).toBeTrue();
    });
  }

});

Refactor the Home component

Add the link /register to the Home Component src/app/home/home.component.html.

<div class="jumbotron">

  <h1>Welcome to Auction App</h1>

  <p>Tutorial for Angular 10 Frontend - Auction Case Study.</p>
  <a [hidden]="isLoggedIn" class="btn btn-lg btn-primary" routerLink="/register" role="button">New User? Get started: sign-up</a>
</div>

<div class="container">
  <div class="row">
    <div class="col-sm">
      <div class="row justify-content-center">Box with hover effect</div>
      <div class="row justify-content-center">
        <div class="box boxsh row justify-content-center">
          <img class="boximage" src="../../assets/01-yamaha-blue.png"  alt="bike"/>
        </div>
      </div>
    </div>
    <div class="col-sm">
      <div class="row justify-content-center">Box with hover effect</div>
      <div class="row justify-content-center">
        <div class="box boxsh row justify-content-center">
          <img class="boximage" src="../../assets/02-yamaha-aquamarine.png"  alt="bike"/>
        </div>
      </div>
    </div>
    <div class="col-sm">
      <div class="row justify-content-center">Box with hover effect</div>
      <div class="row justify-content-center">
        <div class="box boxsh row justify-content-center">
          <img class="boximage" src="../../assets/03-yamaha-red.png"  alt="bike"/>
        </div>
      </div>
    </div>
    <div class="col-sm">
      <div class="row justify-content-center">Box with hover effect</div>
      <div class="row justify-content-center">
        <div class="box boxsh row justify-content-center">
          <img class="boximage" src="../../assets/01-yamaha-blue.png"  alt="bike"/>
        </div>
      </div>
    </div>
  </div>
</div>

<app-alert-test></app-alert-test>

Refactor the Login component

Remove some log output and change the title from Angular 10 to Login.

login.component.ts

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
import { first } from 'rxjs/operators';
import {AuthenticationService} from '../../shared/service/authentication.service';


@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {

  loginForm: FormGroup;
  loading = false;
  submitted = false;
  returnUrl: string;
  error = '';

  constructor(
    private formBuilder: FormBuilder,
    private route: ActivatedRoute,
    public router: Router,
    private authenticationService: AuthenticationService
  ) {
    // redirect to home if already logged in
    if (this.authenticationService.currentUserValue) {
      if (this.authenticationService.currentUserValue.isLoggedIn()) {
        // console.log('already logged in');
        this.router.navigate(['/']);
      }

    }
  }

  ngOnInit(): void {
    this.loginForm = this.formBuilder.group({
      username: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(30)]],
      password: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(30)]],
      rememberMe: true
    });

    // get return url from route parameters or default to '/'
    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
  }

  // convenience getter for easy access to form fields
  get loginCtrl(): { [p: string]: AbstractControl } { return this.loginForm.controls; }

  onSubmit(): void {
    this.submitted = true;

    // stop here if form is invalid
    if (this.loginForm.invalid) {
      return;
    }

    this.loading = true;
    this.authenticationService.login(this.loginCtrl.username.value, this.loginCtrl.password.value, this.loginCtrl.rememberMe.value)
      .pipe(first())
      .subscribe({
        next: () => {
          this.router.navigate([this.returnUrl]);
        },
        error: err => {
          this.error = err.error.message;
          this.loading = false;
        }
      });
  }
}

login.component.html

<div class="col-md-6 offset-md-3 mt-5">
  <div class="alert alert-info">
    Username: admin, or user<br />
    Password:  admin, or user
  </div>
  <div class="card">
    <h4 class="card-header">Login<br>Auction Case Study</h4>
    <div class="card-body">
      <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label for="username">Username</label>
          <input type="text" autocomplete="username" formControlName="username" id="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && loginCtrl.username.errors }" />
          <div *ngIf="submitted && loginCtrl.username.errors" class="invalid-feedback">
            <div *ngIf="loginCtrl.username.errors.required">Username is required</div>
          </div>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" autocomplete="current-password" formControlName="password" id="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && loginCtrl.password.errors }" />
          <div *ngIf="submitted && loginCtrl.password.errors" class="invalid-feedback">
            <div *ngIf="loginCtrl.password.errors.required">Password is required</div>
          </div>
        </div>
        <div class="form-check">
          <input type="checkbox" name="rememberMe" id="rememberMe" formControlName="rememberMe" class="form-check-input" >
          <label class="login-checkbox" for="rememberMe">Remember Me</label>
        </div>
        <button [disabled]="loading || this.loginForm.invalid" class="btn btn-primary">
          <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
          Login
        </button>
        <div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
      </form>
    </div>
  </div>
</div>

Create a user location model and add getUserLocation to the user.service.ts.

Create a file user-location.ts in the folder src/app/shared/model.

export class UserLocation {

  constructor(init?: Partial<UserLocation>) {
    Object.assign(this, init);
  }

  countryCode: string;
  iso3CountryCode: string;
  countryName: string;
  currency: string;
  countryCallingCode: string;
  countryLanguages: string[];
  browserLanguage: string;

}

Add a service to call the user location in the file src/app/shared/serivce/user.service.ts. The service is calling the site: https://ipapi.co/json

import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {User} from '../model/user';
import {environment} from '../../../environments/environment';
import {Observable} from 'rxjs';
import {UserLocation} from '../model/user-location';
import {map} from 'rxjs/operators';


@Injectable({providedIn: 'root'})
export class UserService {

  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json',
      Accept: 'application/json'
    }),
    observe: 'response' as 'response'
  };

  private authApiUrl: string;
  constructor(private http: HttpClient) {
    this.authApiUrl = environment.endpoints.backendAuthUrl;
  }

  get(id: number): Observable<User> {
    return this.http.get<User>(`${this.authApiUrl}/users/${id}`);
  }

  getByName(name: string): Observable<any> {
    return this.http.get<User>(`${this.authApiUrl}/users/${name}`)
      .pipe(map(data => {
        return data;
      }));
  }

  getAll(): Observable<User[]> {
    return this.http.get<User[]>(`${this.authApiUrl}/users`);
  }

  register(user: User): Observable<any> {

    return this.http.post<Partial<User>>(`${this.authApiUrl}/users/register`, user, this.httpOptions);
  }

  getUserLocation(): Observable<UserLocation> {

    return this.http.get<UserLocation>('https://ipapi.co/json', this.httpOptions)
      .pipe(map(data => {
        // console.log(response);
        const userLocation: UserLocation = {
          countryCode: data.body['country_code'],
          iso3CountryCode: data.body['country_code_iso3'],
          countryName: data.body['country_name'],
          currency: data.body['currency'],
          countryCallingCode: data.body['country_calling_code'],
          countryLanguages: data.body['languages'],
          browserLanguage: navigator.language
        };
        return userLocation;

      }));
  }

  delete(id: number): Observable<any> {
    return this.http.delete<any>(`this.authApiUrl/users/${id}`);
  }
}

Verify the service

Add a new spec file user.service.ext.spec.ts the the folder src/app/shared/serivce.

import {TestBed} from '@angular/core/testing';
import {UserService} from './user.service';
import {RouterTestingModule} from '@angular/router/testing';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {MockBackendInterceptor} from '../helper/mock/mock-backend-interceptor.service';
import {AuthenticationService} from './authentication.service';


describe('UserServiceExtended', () => {
  let service: UserService;
  let authenticationService: AuthenticationService;
  let expectedResult;
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule,
        HttpClientModule
      ],
      providers: [
        UserService,
        AuthenticationService,
        {
          provide: HTTP_INTERCEPTORS,
          useClass: MockBackendInterceptor,
          multi: true
        }
      ]
    });
    expectedResult = {};
    service = TestBed.inject(UserService);
    authenticationService = TestBed.inject(AuthenticationService);

  });


  it('should be created', () => {
    // const service = TestBed.inject(UserService);
    expect(service).toBeTruthy();
  });


  it('should return user location', (done) => {
    service.getUserLocation().subscribe((userLocation) => {

        console.log(userLocation);
        expect(userLocation.browserLanguage).toEqual(navigator.language);
        console.log('getUserLocation=' + JSON.stringify(expectedResult));
        done();

      }, (err) => {
        // console.log > (err.status);
        expect(err.status).toBe(404);
        done();
      }
    );

  });

  it('should return an error 401 Unauthorised', (done) => {
    // const loginToken = getLoginToken();
    service.getByName('admin').subscribe((data) => {
        console.log(data);
        expect(data.username).toEqual('admin');
        fail();
        done();

      }, (err) => {
        console.log('getByName err=' + JSON.stringify(err));
        expect(err.status).toEqual(401);
        done();
      }
    );

  });

  function getLoginToken(): string {
    authenticationService.login('admin', 'admin').subscribe(
      (data) => {
        // console.log('authenticateResponse', data, service.currentUserValue);
        expect(authenticationService.currentUserValue.username).toBe('admin');
        return authenticationService.currentUserValue.token;
      }, (err) => {
        console.log('getByName err=' + JSON.stringify(err));
      }
    );
    return null;
  }
});

Refactor the Mock Backend

The refactoring is using the api url from the environment. The error status code return is refactored to throwError. Change the file mock-backend-interceptor.service.ts.

import {Injectable} from '@angular/core';
import {
  HttpRequest,
  HttpResponse,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HTTP_INTERCEPTORS, HttpHeaders, HttpErrorResponse
} from '@angular/common/http';
import {Observable, of, throwError} from 'rxjs';
import {delay, mergeMap, materialize, dematerialize, tap} from 'rxjs/operators';
import {AUCTION_DATA} from '../../../auction/shared/auction-data';
import {
  createMockUser,
  createTestRefreshToken,
  createTestToken, createUser, mockAdminData, MockUser, mockUserData,
  publicKey,
  verifyRsaJwtToken
} from './jwt-backend.data';
import {decodeToken, Jwt} from '../helper.jwt';
import {User} from '../../model/user';
import {nowEpochSeconds} from '../util';
import {environment} from '../../../../environments/environment';

/**
 * The mock backend interceptor is used to simulate a backend. The interceptor allows
 * to write individual route functions in order to support all different http verbs (GET, POST, PUT, GET)
 * The interceptor simulated a backend delay of 500ms. The traffic to the interceptor
 * is visible in the console of the browser.
 *
 * At the end of this file you will find a method mockBackendProvider which can be used in the module provider
 * to activate the interceptor
 *
 * Based on: https://jasonwatmore.com/post/2019/05/02/angular-7-mock-backend-example-for-backendless-development
 *
 */

// array in local storage for users
const usersKey = 'users';
// const users = JSON.parse(localStorage.getItem(usersKey)) || [];

const usersData = [];
usersData.push(mockAdminData);
usersData.push(mockUserData);
localStorage.setItem(usersKey, JSON.stringify(usersData));


@Injectable()
export class MockBackendInterceptor implements HttpInterceptor {

  /**
   * Overwritten method of HttpInterceptor
   * @param request the HttpRequest
   * @param next the next Handler
   */
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const {url, method, headers, body} = request;
    if (url.startsWith('http') && !url.startsWith(environment.endpoints.backendBaseUrl)) {
      console.log('call external=' + url);
      return next.handle(request);
    }
    // wrap in delayed observable to simulate server api call
    return of(null)

      .pipe(mergeMap(() => handleRoute()))
      .pipe(materialize()) // call materialize and dematerialize to ensure delay
      .pipe(delay(100))
      .pipe(dematerialize())
      .pipe(tap({
        next: data => {
          console.log('mockResponse', data);
        },
        error: err => {
          console.log('mockResponseError', JSON.stringify(err));
        },
        // complete: () => console.log('mockResponse: on complete')
      }));

    /**
     * The handle route function is used to individually support the
     * different end points
     */
    function handleRoute(): Observable<HttpEvent<any>> {
      console.log('mockRequest: ' + url, method, headers, body);
      let response: Observable<HttpEvent<any>>;


      switch (true) {
        case url.endsWith('/users/authenticate') && method === 'POST':
          response = authenticate();
          break;

        case url.endsWith('/users/register') && method === 'POST':
          response = register();
          break;

        case url.endsWith('/users/refresh_token') && method === 'POST':
          response = refreshTheToken();
          break;

        case url.endsWith('/users') && method === 'GET':
          response = getUsers();
          break;

        case url.match(/(\/users[\/])/) && method === 'GET':
          if (isNumber(nameFromUrl())) {
            response = getUser();
          }
          else {
            response = getUserByName();
          }
          break;

        case url.match(/\/users\/\d+$/) && method === 'DELETE':
          response = deleteUser();
          break;

        case url.endsWith('/users') && method === 'POST':
          response = register();
          break;

        case url.match(/\/users\/\d+$/) && method === 'PUT':
          response = changeUser();
          break;

        case url.match(/\/auctions\/\d+$/) && method === 'GET':
          response = getAuction();
          break;

        case url.endsWith('/auctions') && method === 'GET':
          response = getAuctions();
          break;

        default:
          // pass through any requests not handled above
          response = next.handle(request);
      }

      return response;
    }

    // --- route functions ---

    function authenticate(): Observable<HttpResponse<unknown>> {
      const {username, password} = body;
      const mockUsers: Array<MockUser> = JSON.parse(localStorage.getItem(usersKey)) || [];
      const mockUser: MockUser = mockUsers.find(x => x.username === username && x.password === password);
      if (!mockUser) {
        return unauthorizedMessage('Username or password is incorrect');
      } else {
        mockUser.tokens.push(createTestToken(username));
        mockUser.refreshTokens.push(createTestRefreshToken(username));
        localStorage.setItem(usersKey, JSON.stringify(mockUsers));

        const user: User = createUser(mockUser);
        user.publicKey = publicKey;

        const responseHeader: HttpHeaders = createHeader( user.token);
        return ok(user, responseHeader);
      }
    }

    function register(): Observable<HttpResponse<unknown>> {
      const user = body;
      const mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];

      if (mockUsers.find(x => x.username === user.username)) {
        return error('Username "' + user.username + '" is already taken');
      }
      user.id = mockUsers.length ? Math.max(...mockUsers.map(x => x.id)) + 1 : 1;
      const mockUser: MockUser = createMockUser(user);
      mockUsers.push(mockUser);
      localStorage.setItem(usersKey, JSON.stringify(mockUsers));

      const responseHeaders = createResisterHeader();
      // const responseHeaders: HttpHeaders = createHeader( 'ok');
      return ok(user, responseHeaders);
    }

    function refreshTheToken(): Observable<HttpResponse<unknown>> {
      if (!isLoggedIn()) { return unauthorized(); }
      if (!isTokenExpired()) { return expired(); }

      const {username, refreshToken} = body;
      if (!isRefreshTokenExpired(refreshToken)) { return expired(); }

      const mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
      const mockUser: MockUser = mockUsers.find(x => x.username === username);

      if (!mockUser) {
        return error(`Username ${mockUser} not found`);
      } else {
        const refreshTok = mockUser.refreshTokens.find(x => x === refreshToken);

        if (!refreshTok) {
          return error('refreshToken not found');
        } else {
          mockUser.tokens.push(createTestToken(username));
          mockUser.refreshTokens.push(createTestRefreshToken(username));
          localStorage.setItem(usersKey, JSON.stringify(mockUsers));

          const user: User = createUser(mockUser);
          user.publicKey = publicKey;

          const responseHeader: HttpHeaders  = createHeader(user.token);
          JSON.stringify(responseHeader);
          return ok(user, responseHeader);
        }
      }
    }

    function getUsers(): Observable<HttpResponse<unknown>> {
      if (!isLoggedIn()) { return unauthorized(); }
      if (!isTokenExpired()) { return expired(); }
      if (!isInRole('admin')) { return notInRole(); }

      const mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
      if (!isLoggedIn()) { return unauthorized(); }

      const users = [];
      mockUsers.forEach(mockUser => users.push(createUser(mockUser)));
      return ok(users);
    }

    function getUser(): Observable<HttpResponse<unknown>> {
      if (!isLoggedIn()) { return unauthorized(); }
      let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
      mockUsers = mockUsers.filter(x => x.id === idFromUrl());
      if (mockUsers.length > 0) {
        return ok(createUser(mockUsers[0]));
      } else {
        return noContent('User with id ' + idFromUrl() + ' not found.');
      }
    }

    function getUserByName(): Observable<HttpResponse<unknown>> {
      if (!isLoggedIn()) { return unauthorized(); }
      let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
      mockUsers = mockUsers.filter(x => x.username === nameFromUrl());
      if (mockUsers.length > 0) {
        return ok(createUser(mockUsers[0]));
      } else {
        return noContent('User with name ' + nameFromUrl() + ' not found.');
      }
    }

    function deleteUser(): Observable<HttpResponse<unknown>>  {
      if (!isLoggedIn()) { return unauthorized(); }
      let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
      mockUsers = mockUsers.filter(x => x.id !== idFromUrl());
      localStorage.setItem(usersKey, JSON.stringify(mockUsers));
      return ok();
    }

    function changeUser(): Observable<HttpResponse<unknown>> {
      if (!isLoggedIn()) { return unauthorized(); }
      const user = body;
      if (!user.id) { user.id = idFromUrl(); }
      let mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
      mockUsers = mockUsers.filter(x => x.id === idFromUrl());
      if (mockUsers.length > 0) {
        // delete this user
        mockUsers = JSON.parse(localStorage.getItem(usersKey)) || [];
        mockUsers = mockUsers.filter(x => x.id !== idFromUrl());
        // add changed user
        mockUsers.push(user);
        localStorage.setItem(usersKey, JSON.stringify(mockUsers));
        return ok(user);
      } else {
        return noContent('User with id ' + idFromUrl() + ' not found.');
      }
    }

    function getAuctions(): Observable<HttpResponse<unknown>> {
      return ok(AUCTION_DATA);
    }

    function getAuction(): Observable<HttpResponse<unknown>> {
      const auctions = AUCTION_DATA.filter(x => x.id === idFromUrl());
      if (auctions.length > 0) {
        return ok(auctions[0]);
      } else {
        return noContent('Auction item with id ' + idFromUrl() + ' not found.');
      }
    }


    // helper functions

    function ok(httpBody?, httpHeaders?: HttpHeaders): Observable<HttpResponse<unknown>> {
      const resp = new HttpResponse({body: httpBody, headers: httpHeaders, status: 200});
      return of(new HttpResponse(resp));
    }

    function error(message): Observable<HttpResponse<unknown>> {
      return throwError({ headers: headers, status: 404, statusText: message, error: { message: message } });
    }

    function unauthorized(): Observable<HttpResponse<unknown>> {
      return throwError({ headers: headers, status: 401, statusText: 'Unauthorised', error: { message: 'Unauthorised' } });
    }

    function unauthorizedMessage(message: string): Observable<HttpResponse<unknown>> {
      return throwError({ headers: headers, status: 401, statusText: message, error: { message: message } });
    }


    function expired(): Observable<HttpResponse<unknown>> {
      return throwError({ headers: headers, status: 401, statusText: 'Unauthorised - Token expired', error: { message: 'Unauthorised - Token expired' } });
    }

    function notInRole(): Observable<HttpResponse<unknown>> {
      return throwError({ headers: headers, status: 403, statusText: 'Forbidden - not correct role', error: { message: 'Forbidden - not correct role' } });
    }

    function notFound(message): Observable<HttpResponse<unknown>> {
      return throwError({ headers: headers, status: 404, statusText: message, error: { message: message } });
    }

    function noContent(message): Observable<HttpResponse<unknown>> {
      const resp = new HttpResponse({body: message, headers: headers, status: 204});
      return of(new HttpResponse(resp));
    }


    function isLoggedIn(): boolean {
      const bearerToken = headers.get('Authorization');
      if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
        const jwtToken = bearerToken.slice(7, bearerToken.length);
        try {
          if (verifyRsaJwtToken(jwtToken)) { return true; }
        } catch (e) {
          if (e instanceof Error) {
            throwError({status: 401, error: {message: 'Unauthorised - Token invalid'}});
          }
        }
      }
      return (headers.get('Authorization') === 'Bearer fake-jwt-token');
    }

    function isTokenExpired(): boolean {
      const bearerToken = headers.get('Authorization');
      if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
        const jwtToken = bearerToken.slice(7, bearerToken.length);
        const token = decodeToken(jwtToken);
        if (token instanceof Jwt) {
          if (token.body.exp >= nowEpochSeconds()) { return true; }
        }
      }
      return false;
    }

    function isRefreshTokenExpired(refreshToken: string): boolean {
      const token = decodeToken(refreshToken);
      if (token instanceof Jwt) {
        if (token.body.exp >= nowEpochSeconds()) { return true; }
      }
      return false;
    }

    function isInRole(role): boolean {
      const bearerToken = headers.get('Authorization');
      if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
        const jwtToken = bearerToken.slice(7, bearerToken.length);
        const token = decodeToken(jwtToken);
        if (token instanceof Jwt && token.body['roles']) {
          const roles: Array<string> = token.body['roles'];
          // console.log('roles', roles, role);
          if (roles.indexOf(role) > -1) { return true; }
        }
      }
      return false;
    }

    function idFromUrl(): number {
      const urlParts = url.split('/');
      return parseInt(urlParts[urlParts.length - 1], 10);
    }

    function nameFromUrl(): string {
      const urlParts = url.split('/');
      return urlParts[urlParts.length - 1];
    }

    function createHeader(token: string): HttpHeaders {
      return new HttpHeaders({
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Authorization: `Bearer ${token}`
      });
    }

    function createResisterHeader(): HttpHeaders {
      return new HttpHeaders({
        'Content-Type': 'application/json',
        Accept: '"application/json'
      });
    }


    function addTokenToHeader(httpHeaders: HttpHeaders, token: string): HttpHeaders {
      return addItemToHeader(httpHeaders, 'Authorization', `Bearer ${token}`);
    }

    function addContentTypeToHeader(httpHeaders: HttpHeaders): HttpHeaders {
      return addItemToHeader(httpHeaders, 'Content-Type', 'application/json');
    }

    function addAcceptToHeader(httpHeaders: HttpHeaders): HttpHeaders {
      return addItemToHeader(httpHeaders, 'Accept', 'application/json');
    }

    function addItemToHeader(httpHeaders: HttpHeaders, key: string, item: string): HttpHeaders {
      return httpHeaders.append(key, item);
    }

    function isNumber(value: string | number): boolean {
      return ((value != null) && !isNaN(Number(value.toString())));
    }

  }
}

/**
 * Put the method call to the provider section of your NgModule
 */
export const mockBackendProvider = {
  // use fake backend in place of Http service for backend-less development
  provide: HTTP_INTERCEPTORS,
  useClass: MockBackendInterceptor,
  multi: true
};

The refactored spec file mock-backend-interceptor.service.spec.ts.

import {TestBed} from '@angular/core/testing';

import {MockBackendInterceptor} from './mock-backend-interceptor.service';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {HTTP_INTERCEPTORS, HttpClient, HttpHeaders} from '@angular/common/http';
import {Auction} from '../../../auction/shared/auction';
import {decodeToken, Jwt} from '../helper.jwt';
import {environment} from '../../../../environments/environment';

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type': 'application/json',
    Authorization: 'my-auth-token'
  }),
  observe: 'response' as 'response'
};

const httpOptionsJwtToken = {
  headers: null,
  observe: 'response' as 'response'
};

const httpOptionsExpiredJwtToken = {
  headers: null,
  observe: 'response' as 'response'
};

const httpOptionsInvalidJwtToken = {
  headers: null,
  observe: 'response' as 'response'
};

class User {
  id?: number;
  username: string;
  password: string;
  firstName?: string;
  lastName?: string;
  email?: string;
  thresholdOpenPayment?: number;
  locked?: boolean;
  token?: string;
  refreshToken?: string;
  expires?: number;
}

const postUser: User = {
  firstName: 'post',
  lastName: 'post',
  username: 'post',
  password: 'post',
  email: 'post' + '@mail.com',
  thresholdOpenPayment: 1000,
  locked: false,
  token: null,
  refreshToken: null,
  expires: null
};

const regUserId = 0;
let postUserId = 0;
const adminUserId = 0;

const regUser: User = {
  id: 0,
  firstName: 'default',
  lastName: 'default',
  username: 'default',
  password: 'default',
  email: 'default1' + '@mail.com',
  thresholdOpenPayment: 1000,
  locked: false,
  token: null,
  refreshToken: null,
  expires: null
};

const adminRegUser: User = {
  id: 0,
  firstName: 'admin',
  lastName: 'admin',
  username: 'admin',
  password: 'admin',
  email: 'admin' + '@mail.com',
  thresholdOpenPayment: 1000,
  locked: false,
  token: null,
  refreshToken: null,
  expires: null
};


const loginUser: User = {
  username: 'default',
  password: 'default'
};

const loginUserRefresh = {
  username: 'default',
  refreshToken: 'default'
};


const loginAdminUser: User = {
  username: 'admin',
  password: 'admin'
};

const unknowLoginUser: User = {
  username: 'unknown',
  password: 'default'
};

const wrongPasswordLoginUser: User = {
  username: 'default',
  password: 'unknown'
};

const expiredRsa256Token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NjgzOTQyNzUsImV4cCI6MTU2ODM5Nzg3NSwiaXNzIjoiWkhBVyIsImF1ZCI6IkFTRTIiLCJzdWIiOiJhZG1pbiIsInJvbGVzIjpbImFkbWluIiwidXNlciJdLCJhY2Nlc3NUb2tlbiI6InNlY3JldGFjY2Vzc3Rva2VuIn0.cJF7Z_4dbFkHdbx-2TogMqppa3MoLzjj7O0XOyl7ZMDSZDiyRvSZhwKOT40gdYO1iW65ZYnpeumEcCrYM_KnfMV3i9d9LOPBDYakerpA-lHD_tfaB2rNWFgjjtg1IhvI-_1tSYfTjosPB2KB110t3Jz_iTSAFV8AxM02UubddDo';


describe('MockBackendInterceptor', () => {

  let http: HttpTestingController;
  let httpClient: HttpClient;
  const authApiUrl: string = environment.endpoints.backendAuthUrl;

  beforeEach(() => {
    const testBed = TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule,
      ],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: MockBackendInterceptor,
          multi: true
        }
      ]
    });
    http = testBed.inject(HttpTestingController);
    httpClient = testBed.inject(HttpClient);
  });

  it('should catch 401', (done) => {
    httpClient.get(`${authApiUrl}/error`, httpOptions)
      .subscribe((data) => {
        expect(data.status).toBe(401);
        done();
      }, (err) => {
        expect(err.status).toBe(401);
        // Perform test
        done();
      });

    http.expectOne(`${authApiUrl}/error`).error(new ErrorEvent('Unauthorized error'), {
      status: 401
    });
    http.verify();
  });

  it('should catch 401 at get users', (done) => {
    httpClient.get(`${authApiUrl}/users`, httpOptions)
      .subscribe((data) => {
        expect(data.status).toBe(401);
        done();
      }, (err) => {
        expect(err.status).toBe(401);
        done();
      });
  });

  it('should return an auction', (done) => {
    httpClient.get<Auction>(`${authApiUrl}/auctions/1`, httpOptions)
      .subscribe((data) => {
          const auction: Auction = data.body;
          const status: number = data.status;
          expect(status).toBe(200);
          expect(auction.id).toBe(1);
          done();
        },
        (err) => {
          fail();
          done();
        }, () => {
          done();
        });
  });

  it('should register a user', (done) => {
    httpClient.post<User>(`${authApiUrl}//users/register`, regUser, httpOptions)
      .subscribe((data) => {
          const user: User = data.body;
          const status: number = data.status;
          expect(status).toBe(200);
          expect(user.username).toEqual(regUser.username);
          done();
        },
        (err) => {
          done();
        }, () => {
          done();
        });
  });

  it('should not register an admin because already registered', (done) => {
    httpClient.post<User>(`${authApiUrl}//users/register`, adminRegUser, httpOptions)
      .subscribe((data) => {
          const status: number = data.status;
          expect(status).toBe(404);
          const user: User = data.body;
          done();
        },
        (err) => {
          // console.log('mockTestResponse', err);
          const status: number = err.status;
          expect(status).toBe(404);
          done();
        }, () => {
          done();
        });
  });

  it('should authenticate an admin', (done) => {
    httpClient.post<User>(`${authApiUrl}//users/authenticate`, loginAdminUser, httpOptions)
      .subscribe((data) => {
          const bearerToken = data.headers.get('Authorization');
          if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
            const jwtToken = bearerToken.slice(7, bearerToken.length);
            const token = decodeToken(jwtToken);
            if (token instanceof Jwt) {
              expect(token.body.sub).toEqual(loginAdminUser.username);
              httpOptionsJwtToken.headers = new HttpHeaders({
                'Content-Type': 'application/json',
                Authorization: 'Bearer ' + jwtToken
              });
            } else { fail(); }
          } else { fail(); }
          const user: User = data.body;
          const status: number = data.status;
          expect(status).toBe(200);
          expect(user.username).toEqual(loginAdminUser.username);
          done();
        },
        (err) => {
          fail();
          done();
        }, () => {
          done();
        });
  });

  it('should not authenticate an unknown user', (done) => {
    httpClient.post<User>(`${authApiUrl}//users/authenticate`, unknowLoginUser, httpOptions)
      .subscribe(data => {
          expect(data.status).toBe(401);

          done();
        },
        (err) => {
          expect(err.status).toBe(401);
          if (err.headers) {
            const bearerToken = err.headers.get('Authorization');
            if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
              fail();
            }
          }
          done();
        }, () => {
          done();
        });
  });

  it('should not authenticate a user with wrong password', (done) => {
    httpClient.post<User>(`${authApiUrl}//users/authenticate`, wrongPasswordLoginUser, httpOptions)
      .subscribe((data) => {
          const status: number = data.status;
          expect(status).toBe(401);
          done();
        },
        (err) => {
          expect(err.status).toBe(401);
          if (err.headers) {
            const bearerToken = err.headers.get('Authorization');
            if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
              fail();
            }
          }
          done();
        }, () => {
          done();
        });
  });

  it('should get all users with valid token and role admin', (done) => {
    httpClient.get<User>(`${authApiUrl}/users`, httpOptionsJwtToken)
      .subscribe((data) => {
          expect(data.status).toBe(200);
          expect(data.body[0].id).toBeGreaterThan(0);
          done();
        },
        (err) => {
          fail();
          done();
        }, () => {
          done();
        });
  });

  it('should NOT get all users with an EXPIRED token and role admin with return 401', (done) => {
    httpOptionsExpiredJwtToken.headers = new HttpHeaders({
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + expiredRsa256Token
    });

    httpClient.get<User>(`${authApiUrl}/users`, httpOptionsExpiredJwtToken)
      .subscribe((data) => {
          expect(data.status).toBe(401);
          done();
        },
        (err) => {
          expect(err.status).toBe(401);
          done();
        }, () => {
          done();
        });
  });

  it('should NOT get all users with an INVALID token and role admin with return 401', (done) => {
    let bearerToken = httpOptionsJwtToken.headers.get('Authorization');
    bearerToken = bearerToken.slice(0, -1) + '0';
    httpOptionsInvalidJwtToken.headers = new HttpHeaders({
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + bearerToken
    });

    httpClient.get<User>(`${authApiUrl}/users`, httpOptionsInvalidJwtToken)
      .subscribe((data) => {
          expect(data.status).toBe(401);
          done();
        },
        (err) => {
          expect(err.status).toBe(401);
          done();
        }, () => {
          done();
        });
  });

  it('should create a postUser with valid token and role admin', (done) => {
    httpClient.post<User>(`${authApiUrl}/users`, postUser, httpOptionsJwtToken)
      .subscribe((data) => {
          expect(data.status).toBe(200);
          expect(data.body.id).toBeGreaterThan(0);
          postUserId = data.body.id;
          done();
        },
        (err) => {
          done();
        }, () => {
          done();
        });
  });

  it('should change a postUser with valid token and role admin', (done) => {
    postUser.firstName = 'changed';
    httpClient.put<User>(`${authApiUrl}/users/` + postUserId, postUser, httpOptionsJwtToken)
      .subscribe((data) => {
          expect(data.status).toBe(200);
          expect(data.body.id).toBeGreaterThan(0);
          expect(data.body.id).toBe(postUser.id);
          expect(data.body.firstName).toEqual('changed');
          done();
        },
        (err) => {
          done();
        }, () => {
          done();
        });
  });


  it('should delete the postUser with valid token and role admin', (done) => {
    httpClient.delete<User>(`${authApiUrl}/users/` + postUserId, httpOptionsJwtToken)
      .subscribe((data) => {
          expect(data.status).toBe(200);
          done();
        },
        (err) => {
          done();
          fail();
        }, () => {
          done();
        });
  });


  it('should authenticate a user', (done) => {
    httpClient.post<User>(`${authApiUrl}/users/authenticate`, loginUser, httpOptions)
      .subscribe((data) => {
          const bearerToken = data.headers.get('Authorization');
          if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
            const jwtToken = bearerToken.slice(7, bearerToken.length);
            const token = decodeToken(jwtToken);
            if (token instanceof Jwt) {
              expect(token.body.sub).toEqual(loginUser.username);
              httpOptionsJwtToken.headers = new HttpHeaders({
                'Content-Type': 'application/json',
                Authorization: 'Bearer ' + jwtToken
              });
              loginUserRefresh.refreshToken = data.body.refreshToken;
            } else { fail(); }
          } else { fail(); }
          const user: User = data.body;
          const status: number = data.status;
          expect(status).toBe(200);
          expect(user.username).toEqual(loginUser.username);
          done();
        },
        (err) => {
          fail();
          done();
        }, () => {
          done();
        });

  });

  it('should NOT get all users with valid token and role user with return 403 forbidden', (done) => {
    httpClient.get<User>(`${authApiUrl}/users`, httpOptionsJwtToken)
      .subscribe((data) => {
          expect(data.status).toBe(403);
          done();
        },
        (err) => {
          expect(err.status).toBe(403);
          done();
        }, () => {
          done();
        });
  });

  it('should send a refresh token', (done) => {
    httpClient.post<User>(`${authApiUrl}/users/refresh_token`, loginUserRefresh, httpOptionsJwtToken)
      .subscribe((data) => {
          const bearerToken = data.headers.get('Authorization');
          if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') {
            const jwtToken = bearerToken.slice(7, bearerToken.length);
            const token = decodeToken(jwtToken);
            if (token instanceof Jwt) {
              expect(token.body.sub).toEqual(loginUser.username);
              httpOptionsJwtToken.headers = new HttpHeaders({
                'Content-Type': 'application/json',
                Authorization: 'Bearer ' + jwtToken
              });
              const refreshToken = decodeToken(data.body.refreshToken);
              if (refreshToken instanceof Jwt) {
                expect(refreshToken.body.sub).toEqual(loginUser.username);
              } else { fail(); }
              loginUserRefresh.refreshToken = data.body.refreshToken;

            } else { fail(); }
          } else { fail(); }
          const user: User = data.body;
          const status: number = data.status;
          expect(status).toBe(200);
          expect(user.username).toEqual(loginUser.username);
          done();
        },
        (err) => {
          expect(err.status).toBe(200);
          done();
        }, () => {
          done();
        });
  });

});

Refactor the Alert Component

The Alert component needs to be refactored in terms of unsubscribe only if there is a valid subscription. Refactor the file src/app/shard/component/alert.component.ts.

import {Component, OnInit, OnDestroy, Input} from '@angular/core';
import {Router, NavigationStart} from '@angular/router';
import {Subscription} from 'rxjs';

import {Alert, AlertType} from './alert.model';
import {AlertService} from './alert.service';

@Component(
  {
    selector: 'app-alert',
    templateUrl: 'alert.component.html',
    styleUrls: ['./alert.component.scss']
  }
)
export class AlertComponent implements OnInit, OnDestroy {
  @Input() id = 'default-alert';
  @Input() fade = true;

  alerts: Alert[] = [];
  alertSubscription: Subscription;
  routeSubscription: Subscription;

  constructor(private router: Router, private alertService: AlertService) {
  }

  ngOnInit(): void {
    // subscribe to new alert notifications
    this.alertSubscription = this.alertService.onAlert(this.id)
      .subscribe(alert => {
        // clear alerts when an empty alert is received
        if (!alert.message) {
          // filter out alerts without 'keepAfterRouteChange' flag
          this.alerts = this.alerts.filter(x => x.keepAfterRouteChange);

          // remove 'keepAfterRouteChange' flag on the rest
          this.alerts.forEach(x => delete x.keepAfterRouteChange);
          return;
        }

        // add alert to array
        this.alerts.push(alert);

        // auto close alert if required
        if (alert.autoClose) {
          setTimeout(() => this.removeAlert(alert), 3000);
        }
      });

    // clear alerts on location change
    this.routeSubscription = this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        this.alertService.clear(this.id);
      }
    });
  }

  ngOnDestroy(): void {
    // unsubscribe to avoid memory leaks
    if (this.alertSubscription) { this.alertSubscription.unsubscribe(); }
    if (this.routeSubscription)  { this.routeSubscription.unsubscribe(); }
  }

  removeAlert(alert: Alert): void {
    // check if already removed to prevent error on auto close
    if (!this.alerts.includes(alert)) {
      return;
    }

    if (this.fade) {
      // fade out alert
      this.alerts.find(x => x === alert).fade = true;

      // remove alert after faded out
      setTimeout(() => {
        this.alerts = this.alerts.filter(x => x !== alert);
      }, 250);
    } else {
      // remove alert
      this.alerts = this.alerts.filter(x => x !== alert);
    }
  }

  cssClass(alert: Alert): string {
    if (!alert) {
      return;
    }

    const classes = ['alert', 'alert-dismissable'];

    const alertTypeClass = {
      [AlertType.Success]: 'alert alert-success',
      [AlertType.Error]: 'alert alert-danger',
      [AlertType.Info]: 'alert alert-info',
      [AlertType.Warning]: 'alert alert-warning'
    };

    classes.push(alertTypeClass[alert.type]);

    if (alert.fade) {
      classes.push('fade');
    }

    return classes.join(' ');
  }
}

Add a line-height to the scss

Refactor the file src/app/shard/component/alert.component.scss.

a {
  cursor: pointer;
}

.close {
  line-height: 0.5;
}

Refactor the auction-data.service.ts

Refactor the file auction-data.service.ts and change the api to the environment api.

import { Injectable } from '@angular/core';
import {Auction} from './auction';
import {AUCTION_DATA} from './auction-data';
import {Observable, of} from 'rxjs';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../../environments/environment';



@Injectable({
  providedIn: 'root'
})
export class AuctionDataService {

  private auctions: Auction[] = AUCTION_DATA;
  private backendApiUrl;

  constructor(private httpClient: HttpClient) {
    this.backendApiUrl = environment.endpoints.backendAuthUrl;
  }

  public getAuctions(): Auction[] {
    return this.auctions;
  }

  getObservableAuctions(): Observable<Auction[]> {
    return of(this.auctions);
  }

  public getHttpAuctions(): Observable<Array<Auction>> {
    return this.httpClient.get<Array<Auction>>(this.backendApiUrl + '/auctions');
  }

  public getHttpAuction(id: number): Observable<Auction> {
    return this.httpClient.get<Auction>(this.backendApiUrl + '/auctions/' + id);
  }

  public create(auction: Auction): Observable<Auction> {
    return this.httpClient.post<Auction>(this.backendApiUrl + '/auctions', auction);
  }

  public delete(auction: Auction): Observable<Auction> {
    return this.httpClient.delete<Auction>(`${this.backendApiUrl}/auctions/${auction.id}`);
  }

  public get(id: number): Observable<Auction> {
    return this.httpClient.get<Auction>(this.backendApiUrl + '/auctions/' + id);
  }

  public list(): Observable<Array<Auction>> {
    return this.httpClient.get<Array<Auction>>(this.backendApiUrl + '/auctions');
  }

  public update(auction: Auction): Observable<Auction> {
    return this.httpClient.put<Auction>(`${this.backendApiUrl}/auctions/${auction.id}`, auction);
  }

}

Refactor the file auction-data.service.spec.ts and configure the MockBackend Provider.

import {TestBed} from '@angular/core/testing';

import {AuctionDataService} from './auction-data.service';
import {RouterTestingModule} from '@angular/router/testing';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {HTTP_INTERCEPTORS} from '@angular/common/http';
import {MockBackendInterceptor} from '../../shared/helper/mock/mock-backend-interceptor.service';

describe('AuctionDataService', () => {
  let service: AuctionDataService;

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([
          {path: 'login', redirectTo: ''}]),
        HttpClientTestingModule,

      ],
      providers: [
        AuctionDataService,
        {
          provide: HTTP_INTERCEPTORS,
          useClass: MockBackendInterceptor,
          multi: true
        }
      ]
    });
    service = TestBed.inject(AuctionDataService);

  });


  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should load an auction', (done) => {
    service.getHttpAuction(1).subscribe(
      (data) => {
        console.log('auctionData=' + data.id);
        expect(data.id).toBe(1);
        done();
      },
      (err) => {
        console.log('auctionResponse', err);
        fail();
        done();
      }, () => {
        // console.log('Test complete');
        done();
      });
  });

});

Refactor the Auction Comonent Spec

Refactor the src/app/auction/auction/auction.componen.spec.ts file.
Add the CommonModule and the RouterTestingModule.

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { AuctionComponent } from './auction.component';
import {RouterTestingModule} from '@angular/router/testing';
import {CommonModule} from '@angular/common';

describe('AuctionComponent', () => {
  let component: AuctionComponent;
  let fixture: ComponentFixture<AuctionComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        CommonModule,
        RouterTestingModule,
      ],
      declarations: [
        AuctionComponent,
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(AuctionComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Clone this wiki locally