2

I'm using Angular 14. I'm trying to test this service:

import { EnvironmentService } from './environment.service';
import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
import { InteractionStatus, RedirectRequest, EventMessage, EventType } from '@azure/msal-browser';
import { Subject, filter, takeUntil } from 'rxjs';
import { Inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

const GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me';

/**
 * Every method is disabled if the environment is for development
 */
@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {

  public isIframe: boolean = false;
  public loginDisplay: boolean = !this._environmentService.getEnvironment().production;
  public userLogged$: Subject<boolean> = new Subject<boolean>();
  private readonly _destroying$ = new Subject<void>();
  public profile: any = {
    displayName: "Dev Mode User"
  };

  constructor(
    @Inject(MSAL_GUARD_CONFIG) private _msalGuardConfig: MsalGuardConfiguration,
    private _msalService: MsalService,
    private _msalBroadcastService: MsalBroadcastService,
    private _httpClient: HttpClient,
    private _environmentService: EnvironmentService
  ) { }

  /**
   * Send thorugh observable the information if the user logged or not
   */
  setLoginDisplay() {
    console.log("setLoginDisplay called");
  }

  /**
   * Redirec to login display
   */
  loginRedirect(): void {
    if (this._environmentService.getEnvironment().production) {
      this._msalService.loginRedirect().subscribe(
        () => {
          this.setLoginDisplay();
        }
      );
    }
  }

}

In other cases where the observable was a property of a dependency it worked, but not here for some reason because I keep getting this error: Expected spy setLoginDisplay to have been called.. By checking the coverage I noticed that the test wasn't considering the content of the subscription: enter image description here

This is the corresponding spec.ts file I tried:

import { EnvironmentService } from './environment.service';
import { Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG } from '@azure/msal-angular';
import { MockService } from 'ng-mocks';
import { TestBed, tick } from '@angular/core/testing';

import { AuthenticationService } from './authentication.service';
import { AccountInfo, EventMessage, InteractionStatus } from '@azure/msal-browser';

const mockInstance: any = {
  enableAccountStorageEvents(): void { },
  getAllAccounts(): AccountInfo[] { return [] },
  getActiveAccount(): AccountInfo | null { return null },
  setActiveAccount(account: AccountInfo | null): void { }
}


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

  const mockMsalService = MockService(MsalService, {
    instance: mockInstance
  });
  const mockMsalBroadcastService = MockService(MsalBroadcastService, {
    msalSubject$: new Observable<EventMessage>(),
    inProgress$: new Observable<InteractionStatus>(),
  });
  const mockHttpClient = MockService(HttpClient);
  const mockEnvironmentService = MockService(EnvironmentService, {
    getEnvironment() { return { production: true } }
  });

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: EnvironmentService,
          useValue: mockEnvironmentService
        },
        {
          provide: MsalService,
          useValue: mockMsalService
        },
        {
          provide: MsalBroadcastService,
          useValue: mockMsalBroadcastService
        },
        {
          provide: HttpClient,
          useValue: mockHttpClient
        },
        {
          provide: MSAL_GUARD_CONFIG,
          useValue: {}
        }
      ]
    });

    mockMsalBroadcastService.msalSubject$ = new Observable<EventMessage>();
    mockMsalBroadcastService.inProgress$ = new Observable<InteractionStatus>();
  });

  it('should call setLoginDisplay', fakeAsync(() => {
    service = TestBed.inject(AuthenticationService);
    const setLoginDisplaySpy = spyOn(service, "setLoginDisplay").and.callFake(() => { });
    const loginRedirectSpy = spyOn(mockMsalService, "loginRedirect").and.returnValue(new Observable<void>());
    service.loginRedirect();
    tick();
    expect(setLoginDisplaySpy).toHaveBeenCalled();

  }));


});

Can someone help me and maybe explain to me why this happens? Thank you in advance! ^^

SteDeus
  • 47
  • 4

2 Answers2

4

I think the new Observable() in your spied mockMsalService does not emit any value. Your subscription is therefore never executed even when you wait one tick. You might want to use of(undefined) or of({}) instead:

const loginRedirectSpy = spyOn(mockMsalService, "loginRedirect").and.returnValue(of(undefined));

Since of works synchronously you might even remove the tick() from your test.

Patrick
  • 126
  • 6
  • Thank you @Patrick S! You're absolutely right, works fine with `of(undefined)`. One last thing: the output value type of `loginRedirect` is "Observable", while the value you suggest is an "Observable". It doesn't display any type error and, as I said, it works, but shouldn't it be the wrong type? – SteDeus Jan 18 '23 at 16:09
  • 1
    @SteDeus i had no answer to that question so I asked SO. Others have asked the same question. I would recommend this https://stackoverflow.com/a/69732504. In short: It's part of the typescript definition/documentation: `In strict null checking mode, the null and undefined values are not in the domain of every type and are only assignable to themselves and any (the one exception being that undefined is also assignable to void).` – Patrick Jan 19 '23 at 07:27
0

You are making a fake call, see the line:

const setLoginDisplaySpy = 
      spyOn(service, "setLoginDisplay").and.callFake(() => { });

It's used to test the functionality and not the actual data!

Ali Adravi
  • 21,707
  • 9
  • 87
  • 85