#Trouble with HttpTestingController

9 messages · Page 1 of 1 (latest)

river reef
#

Hello,

I'm trying to test an API call however I can't seem to figure out what is going wrong.

I keep getting errors like:

DashboardComponent > should create
Error: Expected one matching request for criteria "Match URL: https://localhost:5000/api/Release/client-counts", found none.
    at HttpClientTestingBackend.expectOne (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/common/fesm2022/http/testing.mjs:274:19)
    at mockInitialRequests (http://localhost:9876/_karma_webpack_/webpack:/src/app/components/dashboard/dashboard.component.spec.ts:109:14)
    at UserContext.apply (http://localhost:9876/_karma_webpack_/webpack:/src/app/components/dashboard/dashboard.component.spec.ts:114:5)
    at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:369:28)
    at ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:2082:39)
    at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:368:34)
    at ZoneImpl.run (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:111:43)
    at runInTestZone (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:216:38)
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:234:32)
    at <Jasmine>```

and 

```c#
DashboardComponent > should create
Error: Expected one matching request for criteria "Match by function: ", found none.
    at HttpClientTestingBackend.expectOne (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/common/fesm2022/http/testing.mjs:274:19)
    at mockInitialRequests (http://localhost:9876/_karma_webpack_/webpack:/src/app/components/dashboard/dashboard.component.spec.ts:106:30)
    at UserContext.apply (http://localhost:9876/_karma_webpack_/webpack:/src/app/components/dashboard/dashboard.component.spec.ts:114:5)
    at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:369:28)
    at ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:2082:39)
    at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:368:34)
    at ZoneImpl.run (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:111:43)
    at runInTestZone (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:216:38)
    at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:234:32)
    at <Jasmine>```
#

It happened when I changed my HeaderService from

import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MsalService } from '@azure/msal-angular';

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

  constructor(private authService: MsalService) { }

  getHeaders(): HttpHeaders{
    var headers = new HttpHeaders();
    const token = this.authService.instance.getActiveAccount()?.idToken;
    if(token !== null) {
      headers = headers.append("Authorization", "Bearer " + token);
    }
    return headers;
  }
}```

to

```c#
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MsalService } from '@azure/msal-angular';
import { AuthenticationResult } from '@azure/msal-browser';

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

  constructor(private authService: MsalService) { }

  async getHeaders(): Promise<HttpHeaders> {
    let headers = new HttpHeaders();
    let token = this.authService.instance.getActiveAccount()?.idToken;

    if (token) {
      const tokenExpiration = this.authService.instance.getActiveAccount()?.idTokenClaims?.exp;
      const currentTime = Math.floor(Date.now() / 1000);

      if (tokenExpiration && tokenExpiration < currentTime) {
        try {
          const response: AuthenticationResult = await this.authService.instance.acquireTokenSilent({
            scopes: ["user.read"],
            account: this.authService.instance.getActiveAccount() || undefined
          });
          token = response.idToken;
        } catch (error) {
          console.error("Token acquisition failed", error);
        }
      }

      if (token) {
        headers = headers.append("Authorization", "Bearer " + token);
      }
    }

    return headers;
  }
}```

This change was to handle automatically refreshing expired tokens.
#

I had to change my regular API calls to support this change, there I changed this:

    getAllClientCounts() {
        this.http.get<IClientCount[]>(`${environment.apiUrl}/Release/client-counts`, { headers: this.headerService.getHeaders() }).subscribe({
            next: (res) => {
                this.clientCountList = res;
                this.dataSource = this.clientCountList;
            },
            error: () => {
                this.openSnackBar("Something went wrong", "Close", 10, "error");
            }
        });
    }```

to

```c#
    getAllClientCounts() {
        this.headerService.getHeaders().then(headers => {
            this.http.get<IClientCount[]>(`${environment.apiUrl}/Release/client-counts`, { headers }).subscribe({
                next: (res) => {
                    this.clientCountList = res;
                    this.dataSource = this.clientCountList;
                },
                error: () => {
                    this.openSnackBar("Something went wrong", "Close", 10, "error");
                }
            });
        });
    }```
#

My test looks like this:

import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSelectModule } from '@angular/material/select';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDialogModule } from '@angular/material/dialog';
import { HeaderService } from '../../services/header.service';
import { MsalService } from '@azure/msal-angular';
import { environment } from '../../../environments/environment';
import { MatSnackBar } from '@angular/material/snack-bar';
import { AccountInfo, IPublicClientApplication } from '@azure/msal-browser';
import { DashboardComponent } from './dashboard.component';
import { MatTableModule } from '@angular/material/table';

describe('DashboardComponent', () => {
  let httpMock: HttpTestingController;
  let headerServiceSpy: jasmine.SpyObj<HeaderService>;
  let snackBarSpy: jasmine.SpyObj<MatSnackBar>;
  let msalServiceStub: Partial<MsalService>;
  let component: DashboardComponent;
  let fixture: ComponentFixture<DashboardComponent>;

  beforeEach(waitForAsync(() => {
    snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']);

    const mockAccount: AccountInfo = {
      homeAccountId: 'home-account-id',
      environment: 'environment',
      tenantId: 'tenant-id',
      username: 'username',
      localAccountId: 'local-account-id',
      idToken: 'test-token',
      idTokenClaims: {
        roles: ['']
      }
    };

    const mockMsalInstance: Partial<IPublicClientApplication> = {
      getActiveAccount: () => mockAccount
    };

    msalServiceStub = {
      instance: mockMsalInstance as IPublicClientApplication
    };
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule,
        ReactiveFormsModule,
        FormsModule,
        MatSnackBarModule,
        MatPaginatorModule,
        MatExpansionModule,
        MatButtonModule,
        MatProgressSpinnerModule,
        MatCardModule,
        MatCheckboxModule,
        MatFormFieldModule,
        MatInputModule,
        MatSlideToggleModule,
        MatSelectModule,
        BrowserAnimationsModule,
        MatDialogModule,
        DashboardComponent,
        MatTableModule,
      ],
      declarations: [],
      providers: [
        { provide: MsalService, useValue: msalServiceStub },
        { provide: MatSnackBar, useValue: snackBarSpy }
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(DashboardComponent);
    component = fixture.componentInstance;
    httpMock = TestBed.inject(HttpTestingController);
    fixture.detectChanges();
  });

  afterEach(() => {
    httpMock.verify();
  });

  function mockInitialRequests() {
    const request = httpMock.expectOne(`${environment.apiUrl}/Release/client-counts`);
    request.flush([{ majorVersion: '1.0', clientCount: 100 }]);
  }

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

I have also tried this:

function mockInitialRequests() {
    const authToken = 'Bearer test-token';
    const request = httpMock.expectOne((request) =>
      request.url === `${environment.apiUrl}/Release/client-counts` && request.headers.has('Authorization') && request.headers.get('Authorization') === authToken
    );
    request.request.headers.set('Authorization', authToken);
    request.flush([{ majorVersion: '1.0', clientCount: 100 }]);
  }```
#

Any help is welcome :)

hexed iron
#

Okay, it's a big piece of code, but I can see you're making an effort on your own—great job!

  1. DashboardComponent be in:
    declarations: [DashboardComponent], // Add this

  2. I guess you should mock environment.apiUrl, something like (not sure):
    jest.mock('../../../environments/environment', () => ({ environment: { apiUrl: 'http://mock-api-url' } }));

Do you probably have others issues but starting by that... and then, take a look on: HeaderService and MatSnackBar

#

have a good day bro, keep going!

river reef
# hexed iron Okay, it's a big piece of code, but I can see you're making an effort on your ow...

DashboardComponent can't be in the declarations since its a standalone component.

In my angular.json I define that the test configuration uses the dev api url, which it gets correctly as you can see by the first error I posted.

I think the issue is related to the async getHeaders() method in my HeaderService. This method returns a Promise<HttpHeaders> however I think this is too slow for the test because when I log it it says "Error: NG0205: Injector has already been destroyed."