Reputation: 483
I have these two service files where one is included in another.
app.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { throwError, Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Router } from '@angular/router';
import { environment } from '../../environments/environment';
import { AuthService } from '../_services/auth.service';
@Injectable({
providedIn: 'root'
})
export class AppService {
protected _apiURL = environment.apiURL;
// Define URLs here
_overviewURL: string;
_deleteUserUrl: string;
constructor(private http: HttpClient,
private _router: Router,
private _authService: AuthService) {
this.setApiUrl();
}
/* Begin: Misc services */
/**
* @description Sets the header for each request
* @param authorize Flag to whether include authorization token in headers or not
* @returns - Header consisting of Authorization token & Content-type
*/
setHeaders(authorize: boolean = false) {
const headers: any = {};
headers['Content-Type'] = 'application/json';
if (authorize && this._authService.isAuthenticated()) {
const authenticated = this._authService.getAuthenticatedUser();
headers.Authorization = `Bearer ${authenticated.idToken}`;
}
return {
headers: new HttpHeaders(headers)
};
}
/**
* @description Sets all the service URLs with the respective endpoints
*/
setApiUrl() {
this._overviewURL = this._apiURL + 'overview';
this._deleteUserUrl = this._apiURL + 'user/delete';
}
/**
* @description Gets the user overview page details based on BGtOccName & BGtID
* @param params - consists of BGtOccName & BGtId (BG Occupation Name & BG Occupation ID).
* Refer BG Docs: https://dev.burning-glass.com/docs/versions/3.3/getting-started
*/
getOverviewPageInfo(params: any) {
return this.http.post(this._overviewURL, params, this.setHeaders())
.pipe(
map(this.handleResponse),
catchError(this.handleError)
);
}
/**
* @description Delete an authenticated user
* @param user User object from localStorage
*/
deleteUser(user: any) {
return this.http.post(this._deleteUserUrl, user, this.setHeaders(true))
.pipe(
map(this.handleResponse),
catchError(this.handleError)
);
}
/**
* @description processes observable response
* @param res - takes in the response object
* @returns - data object
*/
private handleResponse = (res: any) => {
return res.data || {};
}
/**
* @description processes observable error
* @param error - takes in the error object
* @returns - error object
*/
private handleError = (error: Response | any) => {
console.error(error.error.message || error.message);
const errorMsg = error.error.message || error.message;
if (errorMsg === 'Invalid token') { this._router.navigate(['/login']); localStorage.removeItem('loggedUser'); }
return throwError(error.error.error);
}
}
auth.service.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { environment } from '../../environments/environment';
import * as auth0 from 'auth0-js';
import { ToastrService } from 'ngx-toastr';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private _idToken: string;
private _accessToken: string;
private _expiresAt: number;
private auth0User: any;
auth0 = new auth0.WebAuth({
clientID: environment.AUTH0_CLIENTID,
domain: environment.AUTH0_DOMAIN,
responseType: 'token id_token',
redirectUri: environment.AUTH0_REDIRECT_URI
});
constructor( public router: Router,
private _toastr: ToastrService) {
this.auth0User = JSON.parse(localStorage.getItem('auth0User'));
this._idToken = (this.auth0User && this.auth0User.idToken) ? this.auth0User.idToken : '';
this._accessToken = (this.auth0User && this.auth0User.accessToken) ? this.auth0User.accessToken : '';
this._expiresAt = (this.auth0User && this.auth0User.expiresAt) ? this.auth0User.expiresAt : 0;
}
get accessToken(): string {
return this._accessToken;
}
get idToken(): string {
return this._idToken;
}
public login(): void {
this.auth0.authorize();
}
public handleAuthentication(): void {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.localLogin(authResult);
this.router.navigate(['/home']);
this._toastr.success(`You have been logged in!`, `Success`);
} else if (err) {
this.router.navigate(['/home']);
this._toastr.error(`Invalid login`, `Failed`);
}
});
}
private localLogin(authResult): void {
// Set the time that the access token will expire at
const expiresAt = (authResult.expiresIn * 1000) + Date.now();
this._accessToken = authResult.accessToken;
this._idToken = authResult.idToken;
this._expiresAt = expiresAt;
let auth0User: any = localStorage.getItem('auth0User');
if (auth0User) {
auth0User = JSON.parse(auth0User);
auth0User.idToken = authResult.idToken;
auth0User.expiresAt = expiresAt;
auth0User.accessToken = authResult.accessToken;
localStorage.setItem('auth0User', JSON.stringify(auth0User));
} else {
localStorage.setItem('auth0User', JSON.stringify({
idToken: authResult.idToken,
expiresAt: expiresAt,
accessToken: authResult.accessToken,
idTokenPayload: authResult.idTokenPayload
}));
}
}
public renewTokens(): void {
this.auth0.checkSession({}, (err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.localLogin(authResult);
} else if (err) {
this._toastr.error(`Could not get a new token (${err.error}: ${err.error_description}).`, `Failed`);
this.logout();
}
});
}
public logout(): void {
// Remove tokens and expiry time
this._accessToken = '';
this._idToken = '';
this._expiresAt = 0;
localStorage.removeItem('auth0User');
this.auth0.logout({
returnTo: window.location.origin
});
this._toastr.success(`You have been logged out!`, `Success`);
}
public isAuthenticated(): boolean {
// Check whether the current time is past the
// access token's expiry time
return this._accessToken && Date.now() < this._expiresAt;
}
public getAuthenticatedUser() {
if (localStorage.getItem('auth0User')) {
return JSON.parse(localStorage.getItem('auth0User'));
} else {
return null;
}
}
}
Whenever I run my app.service.spec.ts test file it throws error which is not relevent to any code.
app.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppService } from './app.service';
describe('AppService', () => {
let service: AppService;
let httpMock: HttpTestingController;
beforeEach(() => TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule
],
providers: [AppService]
}));
beforeEach(() => {
service = TestBed.get(AppService);
httpMock = TestBed.get(HttpTestingController);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
afterEach(() => {
httpMock.verify();
});
it('should retrieve overview details', () => {
const postParams = {
'designation': 'Fire Chief / Marshal',
'id': '378'
};
const overviewDetails = {
'data': {
'highSalary': 115511.77,
'estimatedSalary': 98935,
'nationalSalary': 98498.34,
'skills': [
{
'name': 'JavaScript Object Notation (JSON)',
'description': 'In computing, JavaScript Object Notation or JSON ( JAY-sn), is an open-standard ' +
'file format that uses human-readable text to transmit data objects consisting of' +
'attributevalue pairs and array data types (or any other serializable value).',
'count': 45084
},
{
'name': 'Software Architecture',
'description': 'Software architecture refers to the high level structures of a software system,' +
'the discipline of creating such structures, and the documentation of these structures.',
'count': 42676
}
],
'careers': [
{
'name': 'Chief Executive Officer',
'meanSalaryDiff': 11347.74
},
{
'name': 'Database Architect',
'meanSalaryDiff': 7699.84
}
]
}
};
service.getOverviewPageInfo(postParams).subscribe(overview => {
expect(overview).toEqual(overviewDetails.data);
});
const req = httpMock.expectOne(service._overviewURL);
expect(req.request.method).toBe('POST');
req.flush(overviewDetails);
});
});
However if remove the lines
if (authorize && this._authService.isAuthenticated()) {
const authenticated = this._authService.getAuthenticatedUser();
headers.Authorization = `Bearer ${authenticated.idToken}`;
}
from app.service.ts file, then all the test works fine(as you can see it is calling authService functions).
I have tried to include authService into providers of app.service.spec.ts like below, but no luck. :(
beforeEach(() => TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule
],
providers: [AppService, AuthService]
}));
My Question: how to include/test one injectable(service) in another injectable file?
Upvotes: 3
Views: 4668
Reputation: 1397
I think you need to provide a mock for it unless you want to do an integration test. For example, the way you tried to fix it by just including AuthService
in your providers: [...]
list won't work unless you also add of the necessary files to construct your AuthService
(ie. you would need to include ToastrService
to your list of providers.
My recommendation is to just mock out the AuthService
in your test by telling the TestBed
to use your mock instead of the actual AuthService
. Here is what I have and it passed the tests:
// in app.service.spec.ts
//imports
describe('AppService', () => {
let service: AppService;
let httpMock: HttpTestingController;
// Add your mock and all the methods/values you need.
// You can Spy on this object if you need to change the return values
// for different tests
let mockAuthSerice: any = {
isAuthenticated: () => true,
getAuthenticatedUser: () => {
return { user: 'bob', idToken: 'token' }
},
};
beforeEach(() => TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule
],
// Provide AuthServide but tell Angular to use the mock instead
providers: [
AppService,
{ provide: AuthService, useValue: mockAuthSerice }
]
}));
// ...rest of test file
});
Upvotes: 6