diff --git a/src/app/components/components/http/http.component.html b/src/app/components/components/http/http.component.html index aa02c6caba..a4f005fcef 100644 --- a/src/app/components/components/http/http.component.html +++ b/src/app/components/components/http/http.component.html @@ -70,17 +70,31 @@

Setup:

} ]]> -

Then, import the [CovalentHttpModule] using the forRoot() method with the desired interceptors in your NgModule:

+

Then, import the [CovalentHttpModule] using the forRoot() method with the desired interceptors and paths to intercept in your NgModule:

[] = [ + CustomInterceptor, + ... + ]; + @NgModule({ imports: [ HttpModule, /* or CovalentCoreModule.forRoot() */ - CovalentHttpModule.forRoot([CustomInterceptor]), + CovalentHttpModule.forRoot({ + interceptors: [{ + interceptor: CustomInterceptor, paths: ['**'], + }], + }), + ... + ], + providers: [ + httpInterceptorProviders, ... ], ... @@ -89,6 +103,63 @@

Setup:

]]>

After that, just inject [HttpInterceptorService] and use it for your requests.

+

Paths:

+

The following characters are accepted as a path to intercept:

+ +

+- `**` is a wildcard for `[a-zA-Z0-9-_]` (including `/`)
+- `*` is a wildcard for `[a-zA-Z0-9-_]` (excluding `/`)
+- `[a-zA-Z0-9-_]`
+      
+
+

Examples:

+ +

+Example 1
+
+`/users/*/groups` intercepts:
+- `www.url.com/users/id-of-user/groups`
+- `www.url.com/users/id/groups`
+
+`/users/*/groups` DOES NOT intercept:
+- `www.url.com/users/id-of-user/groups/path`
+- `www.url.com/users/id-of-user/path/groups`
+- `www.url.com/users/groups`
+
+Example 2
+
+`/users/**/groups` intercepts:
+- `www.url.com/users/id-of-user/groups`
+- `www.url.com/users/id/groups`
+- `www.url.com/users/id-of-user/path/groups`
+
+`/users/**/groups` DOES NOT intercept:
+- `www.url.com/users/id-of-user/groups/path`
+- `www.url.com/users/groups`
+
+Example 3
+
+`/users/**` intercepts:
+- `www.url.com/users/id-of-user/groups`
+- `www.url.com/users/id/groups`
+- `www.url.com/users/id-of-user/path/groups`
+- `www.url.com/users/id-of-user/groups/path`
+- `www.url.com/users/groups`
+
+`/users/**` DOES NOT intercept:
+- `www.url.com/users`
+
+Example 4
+
+`/users**` intercepts:
+- `www.url.com/users/id-of-user/groups`
+- `www.url.com/users/id/groups`
+- `www.url.com/users/id-of-user/path/groups`
+- `www.url.com/users/id-of-user/groups/path`
+- `www.url.com/users/groups`
+- `www.url.com/users`
+      
+
diff --git a/src/platform/http/README.md b/src/platform/http/README.md index e6cfe7665a..4b154e691d 100644 --- a/src/platform/http/README.md +++ b/src/platform/http/README.md @@ -73,16 +73,31 @@ export class CustomInterceptor implements IHttpInterceptor { ``` -Then, import the [CovalentHttpModule] using the forRoot() method with the desired interceptors in your NgModule: +Then, import the [CovalentHttpModule] using the forRoot() method with the desired interceptors and paths to intercept in your NgModule: ```typescript +import { NgModule, Type } from '@angular/core'; import { HttpModule } from '@angular/http'; -import { CovalentHttpModule } from '@covalent/http'; +import { CovalentHttpModule, IHttpInterceptor } from '@covalent/http'; import { CustomInterceptor } from 'dir/to/interceptor'; + +const httpInterceptorProviders: Type[] = [ + CustomInterceptor, + ... +]; + @NgModule({ imports: [ HttpModule, /* or CovalentCoreModule.forRoot() */ - CovalentHttpModule.forRoot([CustomInterceptor]), + CovalentHttpModule.forRoot({ + interceptors: [{ + interceptor: CustomInterceptor, paths: ['**'], + }], + }), + ... + ], + providers: [ + httpInterceptorProviders, ... ], ... @@ -92,6 +107,59 @@ export class MyModule {} After that, just inject [HttpInterceptorService] and use it for your requests. +## Paths + +The following characters are accepted as a path to intercept +- `**` is a wildcard for `[a-zA-Z0-9-_]` (including `/`) +- `*` is a wildcard for `[a-zA-Z0-9-_]` (excluding `/`) +- `[a-zA-Z0-9-_]` + +#### Examples + +Example 1 + +`/users/*/groups` intercepts: +- `www.url.com/users/id-of-user/groups` +- `www.url.com/users/id/groups` + +`/users/*/groups` DOES NOT intercept: +- `www.url.com/users/id-of-user/groups/path` +- `www.url.com/users/id-of-user/path/groups` +- `www.url.com/users/groups` + +Example 2 + +`/users/**/groups` intercepts: +- `www.url.com/users/id-of-user/groups` +- `www.url.com/users/id/groups` +- `www.url.com/users/id-of-user/path/groups` + +`/users/**/groups` DOES NOT intercept: +- `www.url.com/users/id-of-user/groups/path` +- `www.url.com/users/groups` + +Example 3 + +`/users/**` intercepts: +- `www.url.com/users/id-of-user/groups` +- `www.url.com/users/id/groups` +- `www.url.com/users/id-of-user/path/groups` +- `www.url.com/users/id-of-user/groups/path` +- `www.url.com/users/groups` + +`/users/**` DOES NOT intercept: +- `www.url.com/users` + +Example 4 + +`/users**` intercepts: +- `www.url.com/users/id-of-user/groups` +- `www.url.com/users/id/groups` +- `www.url.com/users/id-of-user/path/groups` +- `www.url.com/users/id-of-user/groups/path` +- `www.url.com/users/groups` +- `www.url.com/users` + # RESTService diff --git a/src/platform/http/http-interceptor.service.spec.ts b/src/platform/http/http-interceptor.service.spec.ts deleted file mode 100644 index 914ff7a67e..0000000000 --- a/src/platform/http/http-interceptor.service.spec.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { - TestBed, - inject, - async, -} from '@angular/core/testing'; -import { Injector } from '@angular/core'; -import { XHRBackend, Response, ResponseOptions } from '@angular/http'; -import { Observable } from 'rxjs/Observable'; -import { MockBackend } from '@angular/http/testing'; -import { HttpModule, Http } from '@angular/http'; -import { HttpInterceptorService } from './http-interceptor.service'; -import 'rxjs/Rx'; - -describe('Service: HttpInterceptor', () => { - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - HttpModule, - ], - providers: [ - MockBackend, { - provide: XHRBackend, - useExisting: MockBackend, - }, { - provide: HttpInterceptorService, - useFactory: (http: Http, injector: Injector): HttpInterceptorService => { - return new HttpInterceptorService(http, injector, []); - }, - deps: [Http, Injector], - }, - ], - }); - })); - - it('expect to do a forkJoin get succesfully with observables', - async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { - mockBackend.connections.subscribe((connection: any) => { - connection.mockRespond(new Response(new ResponseOptions({ - status: 200, - body: JSON.stringify('success')} - ))); - }); - let success: boolean = false; - let error: boolean = false; - let complete: boolean = false; - - Observable.forkJoin( - service.get('testurl'), - service.get('testurl')) - .subscribe((response: Response[]) => { - success = true; - }, () => { - error = true; - }, () => { - complete = true; - }); - - expect(success).toBe(true, 'on success didnt execute with observables'); - expect(error).toBe(false, 'on error executed when it shouldnt have with observables'); - expect(complete).toBe(true, 'on complete didnt execute with observables'); - }) - )); - - it('expect to do a post succesfully with observables', - async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { - mockBackend.connections.subscribe((connection: any) => { - connection.mockRespond(new Response(new ResponseOptions({ - status: 200, - body: JSON.stringify('success')} - ))); - }); - let success: boolean = false; - let error: boolean = false; - let complete: boolean = false; - service.post('testurl', {}).map((res: Response) => res.json()).subscribe((data: string) => { - expect(data).toBe('success'); - success = true; - }, () => { - error = true; - }, () => { - complete = true; - }); - expect(success).toBe(true, 'on success didnt execute with observables'); - expect(error).toBe(false, 'on error executed when it shouldnt have with observables'); - expect(complete).toBe(true, 'on complete didnt execute with observables'); - }) - )); - - it('expect to do a post failure with observables', - async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { - mockBackend.connections.subscribe((connection: any) => { - connection.mockError(new Error('error')); - }); - let success: boolean = false; - let error: boolean = false; - let complete: boolean = false; - service.post('testurl', {}).map((res: Response) => res.json()).subscribe(() => { - success = true; - }, (err: Error) => { - expect(err.message).toBe('error'); - error = true; - }, () => { - complete = true; - }); - expect(success).toBe(false, 'on success execute when it shouldnt have with observables'); - expect(error).toBe(true, 'on error didnt execute with observables'); - expect(complete).toBe(false, 'on complete execute when it shouldnt have with observables'); - }) - )); - - it('expect to do a post succesfully with promises', - async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { - mockBackend.connections.subscribe((connection: any) => { - connection.mockRespond(new Response(new ResponseOptions({ - status: 200, - body: JSON.stringify('success')} - ))); - }); - let success: boolean = false; - let error: boolean = false; - service.post('testurl', {}).map((res: Response) => res.json()).toPromise().then((data: string) => { - expect(data).toBe('success'); - success = true; - }, () => { - error = true; - }); - setTimeout(() => { - expect(success).toBe(true, 'on success didnt execute with promises'); - expect(error).toBe(false, 'on error executed when it shouldnt have with promises'); - }); - }) - )); - - it('expect to do a post failure with promises', - async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { - mockBackend.connections.subscribe((connection: any) => { - connection.mockError(new Error('error')); - }); - let success: boolean = false; - let error: boolean = false; - service.post('testurl', {}).map((res: Response) => res.json()).toPromise().then(() => { - success = true; - }, (err: Error) => { - expect(err.message).toBe('error'); - error = true; - }); - setTimeout(() => { - expect(success).toBe(false, 'on success execute when it shouldnt have with promises'); - expect(error).toBe(true, 'on error didnt execute with promises'); - }); - }) - )); -}); diff --git a/src/platform/http/http-interceptor.service.ts b/src/platform/http/http-interceptor.service.ts deleted file mode 100644 index cce6825abb..0000000000 --- a/src/platform/http/http-interceptor.service.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Injectable, Type, Injector } from '@angular/core'; -import { Http, RequestOptionsArgs, Response, Request } from '@angular/http'; - -import { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; - -export interface IHttpInterceptor { - onRequest?: (requestOptions: RequestOptionsArgs) => RequestOptionsArgs; - onRequestError?: (requestOptions: RequestOptionsArgs) => RequestOptionsArgs; - onResponse?: (response: Response) => Response; - onResponseError?: (error: Response) => Response; -} - -@Injectable() -export class HttpInterceptorService { - - private _requestInterceptors: IHttpInterceptor[] = []; - - constructor(private _http: Http, private _injector: Injector, requestInterceptors: Type[]) { - requestInterceptors.forEach((interceptor: Type) => { - this._requestInterceptors.push(_injector.get(interceptor)); - }); - } - - request(url: string | Request, options: RequestOptionsArgs = {}): Observable { - let requestOptionsArgs: RequestOptionsArgs; - try { - requestOptionsArgs = this._requestResolve(options); - } catch (e) { - return new Observable((subscriber: Subscriber) => { - subscriber.error(e); - }); - } - return this._setupRequest(this._http.request(url, requestOptionsArgs)); - } - - delete(url: string, options: RequestOptionsArgs = {}): Observable { - let requestOptionsArgs: RequestOptionsArgs; - try { - requestOptionsArgs = this._requestResolve(options); - } catch (e) { - return new Observable((subscriber: Subscriber) => { - subscriber.error(e); - }); - } - return this._setupRequest(this._http.delete(url, requestOptionsArgs)); - } - - get(url: string, options: RequestOptionsArgs = {}): Observable { - let requestOptionsArgs: RequestOptionsArgs; - try { - requestOptionsArgs = this._requestResolve(options); - } catch (e) { - return new Observable((subscriber: Subscriber) => { - subscriber.error(e); - }); - } - return this._setupRequest(this._http.get(url, requestOptionsArgs)); - } - - head(url: string, options: RequestOptionsArgs = {}): Observable { - let requestOptionsArgs: RequestOptionsArgs; - try { - requestOptionsArgs = this._requestResolve(options); - } catch (e) { - return new Observable((subscriber: Subscriber) => { - subscriber.error(e); - }); - } - return this._setupRequest(this._http.head(url, requestOptionsArgs)); - } - - patch(url: string, data: any, options: RequestOptionsArgs = {}): Observable { - let requestOptionsArgs: RequestOptionsArgs; - try { - requestOptionsArgs = this._requestResolve(options); - } catch (e) { - return new Observable((subscriber: Subscriber) => { - subscriber.error(e); - }); - } - return this._setupRequest(this._http.patch(url, data, requestOptionsArgs)); - } - - post(url: string, data: any, options: RequestOptionsArgs = {}): Observable { - let requestOptionsArgs: RequestOptionsArgs; - try { - requestOptionsArgs = this._requestResolve(options); - } catch (e) { - return new Observable((subscriber: Subscriber) => { - subscriber.error(e); - }); - } - return this._setupRequest(this._http.post(url, data, requestOptionsArgs)); - } - - put(url: string, data: any, options: RequestOptionsArgs = {}): Observable { - let requestOptionsArgs: RequestOptionsArgs; - try { - requestOptionsArgs = this._requestResolve(options); - } catch (e) { - return new Observable((subscriber: Subscriber) => { - subscriber.error(e); - }); - } - return this._setupRequest(this._http.put(url, data, requestOptionsArgs)); - } - - private _setupRequest(responseObservable: Observable): Observable { - return new Observable((subscriber: Subscriber) => { - responseObservable.do((response: Response) => { - subscriber.next(this._responseResolve(response)); - subscriber.complete(); - }).catch((error: Response) => { - return new Observable(() => { - subscriber.error(this._responseErrorResolve(error)); - }); - }).subscribe(); - }); - } - - private _requestResolve(requestOptions: RequestOptionsArgs): RequestOptionsArgs { - this._requestInterceptors.forEach((interceptor: IHttpInterceptor) => { - if (interceptor.onRequest) { - try { - requestOptions = interceptor.onRequest(requestOptions); - } catch (e) { - if (interceptor.onRequestError) { - requestOptions = interceptor.onRequestError(requestOptions); - if (!requestOptions) { - throw e; - } - } else { - throw e; - } - } - } - }); - return requestOptions; - } - - private _responseResolve(response: Response): Response { - this._requestInterceptors.forEach((interceptor: IHttpInterceptor) => { - if (interceptor.onResponse) { - response = interceptor.onResponse(response); - } - }); - return response; - } - - private _responseErrorResolve(error: Response): Response { - this._requestInterceptors.forEach((interceptor: IHttpInterceptor) => { - if (interceptor.onResponseError) { - error = interceptor.onResponseError(error); - } - }); - return error; - } - -} diff --git a/src/platform/http/http.module.ts b/src/platform/http/http.module.ts index 85f02379ed..063e094bd4 100644 --- a/src/platform/http/http.module.ts +++ b/src/platform/http/http.module.ts @@ -1,32 +1,35 @@ -import { NgModule, ModuleWithProviders, Type, Injector } from '@angular/core'; +import { NgModule, ModuleWithProviders, Injector, OpaqueToken } from '@angular/core'; import { HttpModule, Http } from '@angular/http'; -import { HttpInterceptorService } from './http-interceptor.service'; +import { HttpInterceptorService, IHttpInterceptorConfig } from './interceptors/http-interceptor.service'; +import { URLRegExpInterceptorMatcher } from './interceptors/url-regexp-interceptor-matcher.class'; + +export const HTTP_CONFIG: OpaqueToken = new OpaqueToken('HTTP_CONFIG'); + +export type HttpConfig = {inteceptors: IHttpInterceptorConfig[]}; + +export function httpFactory(http: Http, injector: Injector, config: HttpConfig): HttpInterceptorService { + return new HttpInterceptorService(http, injector, new URLRegExpInterceptorMatcher(), config.inteceptors); +} @NgModule({ imports: [ HttpModule, ], - providers: [ - HttpInterceptorService, - ], }) export class CovalentHttpModule { - static forRoot(requestInterceptors: Type[] = []): ModuleWithProviders { - let providers: any[] = []; - requestInterceptors.forEach((interceptor: Type) => { - providers.push(interceptor); - }); - providers.push({ - provide: HttpInterceptorService, - useFactory: (http: Http, injector: Injector): HttpInterceptorService => { - return new HttpInterceptorService(http, injector, requestInterceptors); - }, - deps: [Http, Injector], - }); + static forRoot(config: HttpConfig = {inteceptors: []}): ModuleWithProviders { return { ngModule: CovalentHttpModule, - providers: providers, + providers: [{ + provide: HTTP_CONFIG, + useValue: config, + }, { + provide: HttpInterceptorService, + useFactory: httpFactory, + deps: [Http, Injector, HTTP_CONFIG], + }, + ], }; } } diff --git a/src/platform/http/index.ts b/src/platform/http/index.ts index 47d596a918..ed64edb854 100644 --- a/src/platform/http/index.ts +++ b/src/platform/http/index.ts @@ -1,3 +1,4 @@ export { RESTService, IRestTransform, IRestConfig, IRestQuery, IHttp } from './http-rest.service'; -export { IHttpInterceptor, HttpInterceptorService } from './http-interceptor.service'; -export { CovalentHttpModule } from './http.module'; +export { HttpInterceptorService, IHttpInterceptorConfig } from './interceptors/http-interceptor.service'; +export { IHttpInterceptor } from './interceptors/http-interceptor.interface'; +export { CovalentHttpModule, HttpConfig } from './http.module'; diff --git a/src/platform/http/interceptors/http-interceptor-mapping.interface.ts b/src/platform/http/interceptors/http-interceptor-mapping.interface.ts new file mode 100644 index 0000000000..0f2315edad --- /dev/null +++ b/src/platform/http/interceptors/http-interceptor-mapping.interface.ts @@ -0,0 +1,10 @@ +import { IHttpInterceptor } from './http-interceptor.interface'; + +/** + * Interface for http interceptor mappings. + * Maps the interceptor with the desired interception rule. + */ +export interface IHttpInterceptorMapping { + interceptor: IHttpInterceptor; + paths: string[]; +} diff --git a/src/platform/http/interceptors/http-interceptor-matcher.interface.ts b/src/platform/http/interceptors/http-interceptor-matcher.interface.ts new file mode 100644 index 0000000000..e638eab5a4 --- /dev/null +++ b/src/platform/http/interceptors/http-interceptor-matcher.interface.ts @@ -0,0 +1,13 @@ +import { RequestOptionsArgs } from '@angular/http'; + +import { IHttpInterceptorMapping } from './http-interceptor-mapping.interface'; + +/** + * Interface for http interceptor matchers. + * Implement a class to set the behavior of how the interceptors are matched with the requests. + */ +export interface IHttpInterceptorMatcher { + + matches(options: RequestOptionsArgs, mapping: IHttpInterceptorMapping): boolean; + +} diff --git a/src/platform/http/interceptors/http-interceptor.interface.ts b/src/platform/http/interceptors/http-interceptor.interface.ts new file mode 100644 index 0000000000..d47c2daddd --- /dev/null +++ b/src/platform/http/interceptors/http-interceptor.interface.ts @@ -0,0 +1,12 @@ +import { RequestOptionsArgs, Response } from '@angular/http'; + +/** + * Interface for http interceptors. + * Implement the methods you want to be executed in the request pipeline on interception. + */ +export interface IHttpInterceptor { + onRequest?: (requestOptions: RequestOptionsArgs) => RequestOptionsArgs; + onRequestError?: (requestOptions: RequestOptionsArgs) => RequestOptionsArgs; + onResponse?: (response: Response) => Response; + onResponseError?: (error: Response) => Response; +} diff --git a/src/platform/http/interceptors/http-interceptor.service.spec.ts b/src/platform/http/interceptors/http-interceptor.service.spec.ts new file mode 100644 index 0000000000..ca7c06c1a4 --- /dev/null +++ b/src/platform/http/interceptors/http-interceptor.service.spec.ts @@ -0,0 +1,386 @@ +import { + TestBed, + inject, + async, +} from '@angular/core/testing'; +import { Injector, Injectable, Type } from '@angular/core'; +import { Headers, XHRBackend, Response, ResponseOptions, RequestOptionsArgs } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +import { MockBackend, MockConnection } from '@angular/http/testing'; +import { HttpModule, Http } from '@angular/http'; +import { HttpInterceptorService, HttpConfig, CovalentHttpModule, IHttpInterceptor } from '../'; +import { URLRegExpInterceptorMatcher } from './url-regexp-interceptor-matcher.class'; +import 'rxjs/Rx'; + +@Injectable() +export class ResponseOverrideInterceptor { + + onResponse(response: Response): Response { + return new Response(new ResponseOptions({body: JSON.stringify('override'), status: 200})); + } +} + +@Injectable() +export class RequestAuthInterceptor { + onRequest(request: RequestOptionsArgs): RequestOptionsArgs { + if (!request.headers) { + request.headers = new Headers(); + } + request.headers.set('auth', 'test-auth'); + return request; + } +} + +@Injectable() +export class RequestFailureInterceptor { + onRequest(request: RequestOptionsArgs): RequestOptionsArgs { + throw 'error'; + } +} + +@Injectable() +export class RequestRecoveryInterceptor { + onRequest(request: RequestOptionsArgs): RequestOptionsArgs { + throw 'error'; + } + + onRequestError(request: RequestOptionsArgs): RequestOptionsArgs { + if (!request.headers) { + request.headers = new Headers(); + } + request.headers.set('recovered', 'yes'); + return request; + } + + onResponse(response: Response): Response { + return new Response(new ResponseOptions({body: JSON.stringify('recovered'), status: 200})); + } +} + +describe('Service: HttpInterceptor', () => { + + let config: HttpConfig = { + inteceptors: [{ + interceptor: ResponseOverrideInterceptor, paths: ['/url**'], + }, { + interceptor: RequestAuthInterceptor, paths: ['**'], + }, { + interceptor: RequestFailureInterceptor, paths: ['/error'], + }, { + interceptor: RequestRecoveryInterceptor, paths: ['/recovery/*/fromerror'], + }], + }; + + const httpInterceptorProviders: Type[] = [ + ResponseOverrideInterceptor, + RequestAuthInterceptor, + RequestFailureInterceptor, + RequestRecoveryInterceptor, + ]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CovalentHttpModule.forRoot(config), + ], + providers: [ + MockBackend, { + provide: XHRBackend, + useExisting: MockBackend, + }, + httpInterceptorProviders, + ], + }); + })); + + it('expect to intercept only the route with `/recovery/*/fromerror` and recover from a failure', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + if (connection.request.url === 'http://www.test.com/recovery/id/fromerror') { + expect(connection.request.headers.get('recovered')).toBe('yes', 'did not execute onRequestError when failed'); + } else { + expect(connection.request.headers.get('recovered')) + .toBeNull('did execute onRequestError when failed when it shouldnt'); + } + connection.mockRespond(new Response(new ResponseOptions({ + status: 200, + body: JSON.stringify('success')} + ))); + }); + + service.patch('http://www.test.com/recovery/id/id2/fromerror', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('success', '/error/*/fromerror was intercepted'); + }); + + service.patch('http://www.test.com/recovery/id/fromerror', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('recovered', '/error/*/fromerror was not intercepted'); + }); + }) + )); + + it('expect to intercept only the route with `/error` and fail', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockRespond(new Response(new ResponseOptions({ + status: 200, + body: JSON.stringify('success')} + ))); + }); + + service.patch('http://www.test.com/error', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeUndefined('/error was not intercepted so request didnt fail'); + }, (error: any) => { + expect(error).toBe('error'); + }); + }) + )); + + it('expect to intercept all routes and add an `auth=test-auth` header', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + expect(connection.request.headers.get('auth')).toBe('test-auth', 'didnt add `auth` header on all routes'); + connection.mockRespond(new Response(new ResponseOptions({ + status: 200, + body: JSON.stringify('success')} + ))); + }); + + service.post('http://www.test.com/url/with/any-path/another-path?query=1&query=2', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + + service.post('http://www.test.com/any_path?query=1&query=2', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + + service.get('http://www.test.com/url/any-path/111') + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + + service.get('http://www.test.com/anypath/url') + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + + service.delete('http://www.test.com/any_path?', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + + service.delete('http://www.test.com', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + + service.patch('http://www.test.com/any_path/111/url/another_path', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + + service.patch('http://www.test.com/any-path/111/another_path/', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBeTruthy(); + }); + }) + )); + + it('expect to intercept routes that contain `/url` in them and override responses body with `override`', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockRespond(new Response(new ResponseOptions({ + status: 200, + body: JSON.stringify('success')} + ))); + }); + + service.post('http://www.test.com/url/with/any-path/another-path?query=1&query=2', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('override', 'didnt intercept url with `/url`'); + }); + + service.post('http://www.test.com/any_path?query=1&query=2', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('success', 'intercepted url without `/url`'); + }); + + service.get('http://www.test.com/url/any-path/111') + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('override', 'didnt intercept url with `/url`'); + }); + + service.get('http://www.test.com/anypath/url') + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('override', 'didnt intercept url with `/url`'); + }); + + service.delete('http://www.test.com/any_path?', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('success', 'intercepted url without `/url`'); + }); + + service.delete('http://www.test.com', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('success', 'intercepted url without `/url`'); + }); + + service.patch('http://www.test.com/any_path/111/url/another_path', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('override', 'didnt intercept url with `/url`'); + }); + + service.patch('http://www.test.com/any-path/111/another_path/', {}) + .map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('success', 'intercepted url without `/url`'); + }); + }) + )); + +}); + +describe('Service: HttpInterceptor', () => { + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpModule, + ], + providers: [ + MockBackend, { + provide: XHRBackend, + useExisting: MockBackend, + }, { + provide: HttpInterceptorService, + useFactory: (http: Http, injector: Injector): HttpInterceptorService => { + return new HttpInterceptorService(http, injector, new URLRegExpInterceptorMatcher(), []); + }, + deps: [Http, Injector], + }, + ], + }); + })); + + it('expect to do a forkJoin get succesfully with observables', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockRespond(new Response(new ResponseOptions({ + status: 200, + body: JSON.stringify('success')} + ))); + }); + let success: boolean = false; + let error: boolean = false; + let complete: boolean = false; + + Observable.forkJoin( + service.get('testurl'), + service.get('testurl')) + .subscribe((response: Response[]) => { + success = true; + }, () => { + error = true; + }, () => { + complete = true; + }); + + expect(success).toBe(true, 'on success didnt execute with observables'); + expect(error).toBe(false, 'on error executed when it shouldnt have with observables'); + expect(complete).toBe(true, 'on complete didnt execute with observables'); + }) + )); + + it('expect to do a post succesfully with observables', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockRespond(new Response(new ResponseOptions({ + status: 200, + body: JSON.stringify('success')} + ))); + }); + let success: boolean = false; + let error: boolean = false; + let complete: boolean = false; + service.post('testurl', {}).map((res: Response) => res.json()).subscribe((data: string) => { + expect(data).toBe('success'); + success = true; + }, () => { + error = true; + }, () => { + complete = true; + }); + expect(success).toBe(true, 'on success didnt execute with observables'); + expect(error).toBe(false, 'on error executed when it shouldnt have with observables'); + expect(complete).toBe(true, 'on complete didnt execute with observables'); + }) + )); + + it('expect to do a post failure with observables', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockError(new Error('error')); + }); + let success: boolean = false; + let error: boolean = false; + let complete: boolean = false; + service.post('testurl', {}).map((res: Response) => res.json()).subscribe(() => { + success = true; + }, (err: Error) => { + expect(err.message).toBe('error'); + error = true; + }, () => { + complete = true; + }); + expect(success).toBe(false, 'on success execute when it shouldnt have with observables'); + expect(error).toBe(true, 'on error didnt execute with observables'); + expect(complete).toBe(false, 'on complete execute when it shouldnt have with observables'); + }) + )); + + it('expect to do a post succesfully with promises', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockRespond(new Response(new ResponseOptions({ + status: 200, + body: JSON.stringify('success')} + ))); + }); + let success: boolean = false; + let error: boolean = false; + service.post('testurl', {}).map((res: Response) => res.json()).toPromise().then((data: string) => { + expect(data).toBe('success'); + success = true; + }, () => { + error = true; + }); + setTimeout(() => { + expect(success).toBe(true, 'on success didnt execute with promises'); + expect(error).toBe(false, 'on error executed when it shouldnt have with promises'); + }); + }) + )); + + it('expect to do a post failure with promises', + async(inject([HttpInterceptorService, MockBackend], (service: HttpInterceptorService, mockBackend: MockBackend) => { + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockError(new Error('error')); + }); + let success: boolean = false; + let error: boolean = false; + service.post('testurl', {}).map((res: Response) => res.json()).toPromise().then(() => { + success = true; + }, (err: Error) => { + expect(err.message).toBe('error'); + error = true; + }); + setTimeout(() => { + expect(success).toBe(false, 'on success execute when it shouldnt have with promises'); + expect(error).toBe(true, 'on error didnt execute with promises'); + }); + }) + )); +}); diff --git a/src/platform/http/interceptors/http-interceptor.service.ts b/src/platform/http/interceptors/http-interceptor.service.ts new file mode 100644 index 0000000000..3cd4670260 --- /dev/null +++ b/src/platform/http/interceptors/http-interceptor.service.ts @@ -0,0 +1,151 @@ +import { Injectable, Type, Injector } from '@angular/core'; +import { Http, RequestOptionsArgs, Response, Request, RequestMethod } from '@angular/http'; + +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; + +import { IHttpInterceptor } from './http-interceptor.interface'; +import { IHttpInterceptorMatcher } from './http-interceptor-matcher.interface'; +import { IHttpInterceptorMapping } from './http-interceptor-mapping.interface'; + +export interface IHttpInterceptorConfig { + interceptor: Type; + paths: string[]; +} + +@Injectable() +export class HttpInterceptorService { + + private _requestInterceptors: IHttpInterceptorMapping[] = []; + + constructor(private _http: Http, + private _injector: Injector, + private _httpInterceptorMatcher: IHttpInterceptorMatcher, + requestInterceptorConfigs: IHttpInterceptorConfig[]) { + requestInterceptorConfigs.forEach((config: IHttpInterceptorConfig) => { + this._requestInterceptors.push({ + interceptor: _injector.get(config.interceptor), + paths: config.paths, + }); + }); + } + + public delete(url: string, requestOptions: RequestOptionsArgs = {}): Observable { + requestOptions.url = url; + requestOptions.method = RequestMethod.Delete; + return this.request(url, requestOptions); + } + + public get(url: string, requestOptions: RequestOptionsArgs = {}): Observable { + requestOptions.url = url; + requestOptions.method = RequestMethod.Get; + return this.request(url, requestOptions); + } + + public head(url: string, requestOptions: RequestOptionsArgs = {}): Observable { + requestOptions.url = url; + requestOptions.method = RequestMethod.Head; + return this.request(url, requestOptions); + } + + public patch(url: string, data: any, requestOptions: RequestOptionsArgs = {}): Observable { + requestOptions.url = url; + requestOptions.method = RequestMethod.Patch; + requestOptions.body = data; + return this.request(url, requestOptions); + } + + public post(url: string, data: any, requestOptions: RequestOptionsArgs = {}): Observable { + requestOptions.url = url; + requestOptions.method = RequestMethod.Post; + requestOptions.body = data; + return this.request(url, requestOptions); + } + + public put(url: string, data: any, requestOptions: RequestOptionsArgs = {}): Observable { + requestOptions.url = url; + requestOptions.method = RequestMethod.Put; + requestOptions.body = data; + return this.request(url, requestOptions); + } + + public request(url: string | Request, requestOptions: RequestOptionsArgs = {}): Observable { + let requestUrl: string; + if (url instanceof Request) { + requestUrl = url.url ? url.url : requestOptions.url; + } else { + requestUrl = url; + } + if (!requestOptions.url) { + requestOptions.url = requestUrl; + } + let interceptors: IHttpInterceptor[] = this._requestInterceptors.filter((mapping: IHttpInterceptorMapping) => { + return this._httpInterceptorMatcher.matches(requestOptions, mapping); + }).map((mapping: IHttpInterceptorMapping) => { + return mapping.interceptor; + }); + return this._setupRequest(url, requestOptions, interceptors); + } + + private _setupRequest(url: string | Request, + requestOptions: RequestOptionsArgs, + interceptors: IHttpInterceptor[]): Observable { + try { + requestOptions = this._requestResolve(requestOptions, interceptors); + } catch (e) { + return new Observable((subscriber: Subscriber) => { + subscriber.error(e); + }); + } + return new Observable((subscriber: Subscriber) => { + this._http.request(url, requestOptions) + .do((response: Response) => { + subscriber.next(this._responseResolve(response, interceptors)); + subscriber.complete(); + }).catch((error: Response) => { + return new Observable(() => { + subscriber.error(this._responseErrorResolve(error, interceptors)); + }); + }).subscribe(); + }); + } + + private _requestResolve(requestOptions: RequestOptionsArgs, interceptors: IHttpInterceptor[]): RequestOptionsArgs { + interceptors.forEach((interceptor: IHttpInterceptor) => { + if (interceptor.onRequest) { + try { + requestOptions = interceptor.onRequest(requestOptions); + } catch (e) { + if (interceptor.onRequestError) { + requestOptions = interceptor.onRequestError(requestOptions); + if (!requestOptions) { + throw e; + } + } else { + throw e; + } + } + } + }); + return requestOptions; + } + + private _responseResolve(response: Response, interceptors: IHttpInterceptor[]): Response { + interceptors.forEach((interceptor: IHttpInterceptor) => { + if (interceptor.onResponse) { + response = interceptor.onResponse(response); + } + }); + return response; + } + + private _responseErrorResolve(error: Response, interceptors: IHttpInterceptor[]): Response { + interceptors.forEach((interceptor: IHttpInterceptor) => { + if (interceptor.onResponseError) { + error = interceptor.onResponseError(error); + } + }); + return error; + } + +} diff --git a/src/platform/http/interceptors/url-regexp-interceptor-matcher.class.spec.ts b/src/platform/http/interceptors/url-regexp-interceptor-matcher.class.spec.ts new file mode 100644 index 0000000000..4487d44923 --- /dev/null +++ b/src/platform/http/interceptors/url-regexp-interceptor-matcher.class.spec.ts @@ -0,0 +1,147 @@ +import { URLRegExpInterceptorMatcher } from './url-regexp-interceptor-matcher.class'; + +describe('Http: URLRegExpInterceptorMatcher', () => { + let matcher: URLRegExpInterceptorMatcher; + + beforeEach(() => { + matcher = new URLRegExpInterceptorMatcher(); + }); + + it('should match all URLs', () => { + expect(matcher.matches( + {url: 'www.google.com/path/1_1_-_1/ano111_ther/11-22?query=1&query=2'}, + {interceptor: undefined, paths: ['**']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/path/111/another/11-22'}, + {interceptor: undefined, paths: ['**']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/path'}, + {interceptor: undefined, paths: ['**']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com?'}, + {interceptor: undefined, paths: ['**']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/path/1_1_-_1/?query=1&query=2'}, + {interceptor: undefined, paths: ['**']} + )).toBeTruthy(); + }); + + it('should match routes that contain with /apps or /sys', () => { + expect(matcher.matches( + {url: 'www.google.com/apps/1_1_-_1/ano111_ther/11-22?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps**', '/sys**']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/path/111/sys/11-22'}, + {interceptor: undefined, paths: ['/apps**', '/sys**']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/apps'}, + {interceptor: undefined, paths: ['/apps**', '/sys**']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com?'}, + {interceptor: undefined, paths: ['/apps**', '/sys**']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/path/1_1_-_1/?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps**', '/sys**']} + )).toBeFalsy(); + }); + + it('should match routes that end with /apps', () => { + expect(matcher.matches( + {url: 'www.google.com/apps/1_1_-_1/ano111_ther/11-22?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/path/111/sys/11-22'}, + {interceptor: undefined, paths: ['/apps']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/apps'}, + {interceptor: undefined, paths: ['/apps']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com?'}, + {interceptor: undefined, paths: ['/apps']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/path/1_1_-_1/?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps']} + )).toBeFalsy(); + }); + + it('should match routes that end with /apps/* (only with extra path)', () => { + expect(matcher.matches( + {url: 'www.google.com/apps/1_1_-_1/ano111_ther/11-22?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps/*']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/path/111/apps/11-22'}, + {interceptor: undefined, paths: ['/apps/*']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/apps'}, + {interceptor: undefined, paths: ['/apps/*']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com?'}, + {interceptor: undefined, paths: ['/apps/*']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/path/1_1_-_1/?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps/*']} + )).toBeFalsy(); + }); + + it('should match routes that end with /apps/*/path (only one path in the middle)', () => { + expect(matcher.matches( + {url: 'www.google.com/apps/1_1_-_1/path?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps/*/path']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/apps/111/path/11-22'}, + {interceptor: undefined, paths: ['/apps/*/path']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/apps'}, + {interceptor: undefined, paths: ['/apps/*/path']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com?'}, + {interceptor: undefined, paths: ['/apps/*/path']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/path/1_1_-_1/?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps/*/path']} + )).toBeFalsy(); + }); + + it('should match routes that end with /apps/**/path (any number of paths in the middle)', () => { + expect(matcher.matches( + {url: 'www.google.com/apps/1_1_-_1/path?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps/**/path']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/apps/111/122/path'}, + {interceptor: undefined, paths: ['/apps/**/path']} + )).toBeTruthy(); + expect(matcher.matches( + {url: 'www.google.com/apps'}, + {interceptor: undefined, paths: ['/apps/**/path']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com?'}, + {interceptor: undefined, paths: ['/apps/**/path']} + )).toBeFalsy(); + expect(matcher.matches( + {url: 'www.google.com/path/1_1_-_1/?query=1&query=2'}, + {interceptor: undefined, paths: ['/apps/**/path']} + )).toBeFalsy(); + }); +}); diff --git a/src/platform/http/interceptors/url-regexp-interceptor-matcher.class.ts b/src/platform/http/interceptors/url-regexp-interceptor-matcher.class.ts new file mode 100644 index 0000000000..1ef857a98e --- /dev/null +++ b/src/platform/http/interceptors/url-regexp-interceptor-matcher.class.ts @@ -0,0 +1,25 @@ +import { RequestOptionsArgs } from '@angular/http'; + +import { IHttpInterceptorMapping } from './http-interceptor-mapping.interface'; +import { IHttpInterceptorMatcher } from './http-interceptor-matcher.interface'; + +/** + * Concrete implementation for http interceptor matchers. + * This implementation uses regex to check mapping paths vs request url. + */ +export class URLRegExpInterceptorMatcher implements IHttpInterceptorMatcher { + + matches(options: RequestOptionsArgs, mapping: IHttpInterceptorMapping): boolean { + return mapping.paths.filter((path: string) => { + path = path.replace(/\*\*/gi, '<>') + .replace(/\*/gi, '[a-zA-Z0-9\\-_]+') + .replace(/<>/gi, '[a-zA-Z0-9\\-_\/]*'); + if (path) { + path += '(\\?{1}.*)?$'; + return new RegExp(path).test(options.url); + } + return false; + }).length > 0; + } + +}