Skip to content

Commit

Permalink
Added an HTTP interceptor [#328]
Browse files Browse the repository at this point in the history
 * Attaches the authentication token if one is currently held.
  • Loading branch information
mcpierce committed Mar 1, 2021
1 parent fcb46bf commit f6e8b19
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 1 deletion.
21 changes: 21 additions & 0 deletions comixed-web/src/app/app.constants.ts
@@ -0,0 +1,21 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

export const HTTP_AUTHORIZATION_HEADER = 'Authorization';
export const HTTP_REQUESTED_WITH_HEADER = 'X-Requested-With';
export const HTTP_XML_REQUEST = 'XMLHttpRequest';
6 changes: 5 additions & 1 deletion comixed-web/src/app/app.module.ts
Expand Up @@ -21,11 +21,15 @@ import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpInterceptor } from '@app/interceptors/http.interceptor';

@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule, BrowserAnimationsModule],
providers: [],
providers: [
[{ provide: HTTP_INTERCEPTORS, useClass: HttpInterceptor, multi: true }]
],
bootstrap: [AppComponent]
})
export class AppModule {}
119 changes: 119 additions & 0 deletions comixed-web/src/app/interceptors/http.interceptor.spec.ts
@@ -0,0 +1,119 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

import { TestBed } from '@angular/core/testing';
import { HttpInterceptor } from './http.interceptor';
import { LoggerModule } from '@angular-ru/logger';
import { TokenService } from '@app/core';
import { CoreModule } from '@app/core/core.module';
import { AUTHENTICATION_TOKEN } from '@app/core/core.fixtures';
import {
HttpClientTestingModule,
HttpTestingController,
TestRequest
} from '@angular/common/http/testing';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import {
HTTP_AUTHORIZATION_HEADER,
HTTP_REQUESTED_WITH_HEADER,
HTTP_XML_REQUEST
} from '@app/app.constants';

const TEST_REQUEST_URL = 'http://localhost';

@Injectable()
class TestService {
constructor(private http: HttpClient) {}

request(): Observable<any> {
return this.http.get(TEST_REQUEST_URL);
}
}

describe('HttpInterceptor', () => {
let interceptor: HttpInterceptor;
let tokenService: jasmine.SpyObj<TokenService>;
let httpMock: HttpTestingController;
let testService: TestService;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreModule, HttpClientTestingModule, LoggerModule.forRoot()],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: HttpInterceptor, multi: true },
TestService,
{
provide: TokenService,
useValue: {
hasAuthToken: jasmine.createSpy('TokenService.hasAuthToken()'),
getAuthToken: jasmine.createSpy('TokenService.getAuthToken()')
}
}
]
});

tokenService = TestBed.inject(TokenService) as jasmine.SpyObj<TokenService>;
httpMock = TestBed.inject(HttpTestingController);
testService = TestBed.inject(TestService);
});

describe('when no authentication token is present', () => {
let req: TestRequest;

beforeEach(() => {
tokenService.hasAuthToken.and.returnValue(false);
testService.request().subscribe(() => {});
req = httpMock.expectOne(TEST_REQUEST_URL);
});

it('does not contain an authorization header', () => {
expect(req.request.headers.get(HTTP_AUTHORIZATION_HEADER)).toBeNull();
});

it('attaches a requested with header', () => {
expect(req.request.headers.get(HTTP_REQUESTED_WITH_HEADER)).toEqual(
HTTP_XML_REQUEST
);
});
});

describe('when an authentication token is present', () => {
let req: TestRequest;

beforeEach(() => {
tokenService.hasAuthToken.and.returnValue(true);
tokenService.getAuthToken.and.returnValue(AUTHENTICATION_TOKEN);
testService.request().subscribe(() => {});
req = httpMock.expectOne(TEST_REQUEST_URL);
});

it('attaches the token as a request header', () => {
expect(req.request.headers.get(HTTP_AUTHORIZATION_HEADER)).toEqual(
`Bearer ${AUTHENTICATION_TOKEN}`
);
});

it('attaches a requested with header', () => {
expect(req.request.headers.get(HTTP_REQUESTED_WITH_HEADER)).toEqual(
HTTP_XML_REQUEST
);
});
});
});
64 changes: 64 additions & 0 deletions comixed-web/src/app/interceptors/http.interceptor.ts
@@ -0,0 +1,64 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { LoggerService } from '@angular-ru/logger';
import { TokenService } from '@app/core';
import {
HTTP_AUTHORIZATION_HEADER,
HTTP_REQUESTED_WITH_HEADER,
HTTP_XML_REQUEST
} from '@app/app.constants';
import { retry, tap } from 'rxjs/operators';

@Injectable()
export class HttpInterceptor implements HttpInterceptor {
constructor(
private logger: LoggerService,
private tokenService: TokenService
) {}

intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
let requestClone = request.clone({
headers: request.headers.set(HTTP_REQUESTED_WITH_HEADER, HTTP_XML_REQUEST)
});

this.logger.trace('Processing outgoing HTTP request');
if (this.tokenService.hasAuthToken()) {
this.logger.trace('Adding authentication token to request');
const token = this.tokenService.getAuthToken();
requestClone = requestClone.clone({
headers: requestClone.headers.set(
HTTP_AUTHORIZATION_HEADER,
`Bearer ${token}`
)
});
}
return next.handle(requestClone).pipe(
retry(3),
tap(response => {
this.logger.trace('Response received:', response);
})
);
}
}

0 comments on commit f6e8b19

Please sign in to comment.