build: import in-memory-web-api project (#37182)
Moves the `angular-in-memory-web-api` project into the main repository in order to make it easier to maintain and release. PR Close #37182
This commit is contained in:
34
packages/misc/angular-in-memory-web-api/test/BUILD.bazel
Normal file
34
packages/misc/angular-in-memory-web-api/test/BUILD.bazel
Normal file
@ -0,0 +1,34 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*.ts"]),
|
||||
# Visible to //:saucelabs_unit_tests_poc target
|
||||
visibility = ["//:__pkg__"],
|
||||
deps = [
|
||||
"//packages/common",
|
||||
"//packages/common/http",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/misc/angular-in-memory-web-api",
|
||||
"@npm//@types/jasmine-ajax",
|
||||
"@npm//jasmine-ajax",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test_web",
|
||||
# do not sort
|
||||
bootstrap = [
|
||||
"@npm//:node_modules/core-js/client/core.js",
|
||||
"@npm//:node_modules/reflect-metadata/Reflect.js",
|
||||
"@npm//:node_modules/jasmine-ajax/lib/mock-ajax.js",
|
||||
"//packages/zone.js/bundles:zone.umd.js",
|
||||
"//packages/zone.js/bundles:zone-testing.umd.js",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
)
|
89
packages/misc/angular-in-memory-web-api/test/fixtures/hero-in-mem-data-override-service.ts
vendored
Normal file
89
packages/misc/angular-in-memory-web-api/test/fixtures/hero-in-mem-data-override-service.ts
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is an example of a Hero-oriented InMemoryDbService with method overrides.
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {getStatusText, ParsedRequestUrl, RequestInfo, RequestInfoUtilities, ResponseOptions, STATUS} from 'angular-in-memory-web-api';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
import {HeroInMemDataService} from './hero-in-mem-data-service';
|
||||
|
||||
const villains = [
|
||||
// deliberately using string ids that look numeric
|
||||
{id: 100, name: 'Snidley Wipsnatch'}, {id: 101, name: 'Boris Badenov'},
|
||||
{id: 103, name: 'Natasha Fatale'}
|
||||
];
|
||||
|
||||
// Pseudo guid generator
|
||||
function guid() {
|
||||
const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HeroInMemDataOverrideService extends HeroInMemDataService {
|
||||
// Overrides id generator and delivers next available `id`, starting with 1001.
|
||||
genId<T extends {id: any}>(collection: T[], collectionName: string): any {
|
||||
if (collectionName === 'nobodies') {
|
||||
return guid();
|
||||
} else if (collection) {
|
||||
return 1 + collection.reduce((prev, curr) => Math.max(prev, curr.id || 0), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP GET interceptor
|
||||
get(reqInfo: RequestInfo): Observable<any>|undefined {
|
||||
const collectionName = reqInfo.collectionName;
|
||||
if (collectionName === 'villains') {
|
||||
return this.getVillains(reqInfo);
|
||||
}
|
||||
return undefined; // let the default GET handle all others
|
||||
}
|
||||
|
||||
// HTTP GET interceptor handles requests for villains
|
||||
private getVillains(reqInfo: RequestInfo) {
|
||||
return reqInfo.utils.createResponse$(() => {
|
||||
const collection = villains.slice();
|
||||
const dataEncapsulation = reqInfo.utils.getConfig().dataEncapsulation;
|
||||
const id = reqInfo.id;
|
||||
const data = id == null ? collection : reqInfo.utils.findById(collection, id);
|
||||
|
||||
const options: ResponseOptions = data ?
|
||||
{body: dataEncapsulation ? {data} : data, status: STATUS.OK} :
|
||||
{body: {error: `'Villains' with id='${id}' not found`}, status: STATUS.NOT_FOUND};
|
||||
return this.finishOptions(options, reqInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// parseRequestUrl override
|
||||
// Do this to manipulate the request URL or the parsed result
|
||||
// into something your data store can handle.
|
||||
// This example turns a request for `/foo/heroes` into just `/heroes`.
|
||||
// It leaves other URLs untouched and forwards to the default parser.
|
||||
// It also logs the result of the default parser.
|
||||
parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl {
|
||||
const newUrl = url.replace(/\/foo\/heroes/, '/heroes');
|
||||
return utils.parseRequestUrl(newUrl);
|
||||
}
|
||||
|
||||
responseInterceptor(resOptions: ResponseOptions, reqInfo: RequestInfo) {
|
||||
if (resOptions.headers) {
|
||||
resOptions.headers = resOptions.headers.set('x-test', 'test-header');
|
||||
}
|
||||
return resOptions;
|
||||
}
|
||||
|
||||
private finishOptions(options: ResponseOptions, {headers, url}: RequestInfo) {
|
||||
options.statusText = options.status == null ? undefined : getStatusText(options.status);
|
||||
options.headers = headers;
|
||||
options.url = url;
|
||||
return options;
|
||||
}
|
||||
}
|
79
packages/misc/angular-in-memory-web-api/test/fixtures/hero-in-mem-data-service.ts
vendored
Normal file
79
packages/misc/angular-in-memory-web-api/test/fixtures/hero-in-mem-data-service.ts
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* This is an example of a Hero-oriented InMemoryDbService.
|
||||
*
|
||||
* For demonstration purposes, it can return the database
|
||||
* synchronously as an object (default),
|
||||
* as an observable, or as a promise.
|
||||
*
|
||||
* Add the following line to `AppModule.imports`
|
||||
* InMemoryWebApiModule.forRoot(HeroInMemDataService) // or HeroInMemDataOverrideService
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {InMemoryDbService, RequestInfo} from 'angular-in-memory-web-api';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {delay} from 'rxjs/operators';
|
||||
|
||||
interface Person {
|
||||
id: string|number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface PersonResponse {
|
||||
heroes: Person[];
|
||||
stringers: Person[];
|
||||
nobodies: Person[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HeroInMemDataService implements InMemoryDbService {
|
||||
createDb(reqInfo?: RequestInfo):
|
||||
Observable<PersonResponse>|Promise<PersonResponse>|PersonResponse {
|
||||
const heroes = [
|
||||
{id: 1, name: 'Windstorm'}, {id: 2, name: 'Bombasto'}, {id: 3, name: 'Magneta'},
|
||||
{id: 4, name: 'Tornado'}
|
||||
];
|
||||
|
||||
const nobodies: any[] = [];
|
||||
|
||||
// entities with string ids that look like numbers
|
||||
const stringers = [{id: '10', name: 'Bob String'}, {id: '20', name: 'Jill String'}];
|
||||
|
||||
// default returnType
|
||||
let returnType = 'object';
|
||||
// let returnType = 'observable';
|
||||
// let returnType = 'promise';
|
||||
|
||||
// demonstrate POST commands/resetDb
|
||||
// this example clears the collections if the request body tells it to do so
|
||||
if (reqInfo) {
|
||||
const body = reqInfo.utils.getJsonBody(reqInfo.req) || {};
|
||||
if (body.clear === true) {
|
||||
heroes.length = 0;
|
||||
nobodies.length = 0;
|
||||
stringers.length = 0;
|
||||
}
|
||||
|
||||
// 'returnType` can be 'object' | 'observable' | 'promise'
|
||||
returnType = body.returnType || 'object';
|
||||
}
|
||||
const db = {heroes, nobodies, stringers};
|
||||
|
||||
switch (returnType) {
|
||||
case 'observable':
|
||||
return of(db).pipe(delay(10));
|
||||
case 'promise':
|
||||
return new Promise(resolve => setTimeout(() => resolve(db), 10));
|
||||
default:
|
||||
return db;
|
||||
}
|
||||
}
|
||||
}
|
21
packages/misc/angular-in-memory-web-api/test/fixtures/hero-service.ts
vendored
Normal file
21
packages/misc/angular-in-memory-web-api/test/fixtures/hero-service.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Observable} from 'rxjs';
|
||||
import {Hero} from './hero';
|
||||
|
||||
export abstract class HeroService {
|
||||
heroesUrl = 'api/heroes'; // URL to web api
|
||||
|
||||
abstract getHeroes(): Observable<Hero[]>;
|
||||
abstract getHero(id: number): Observable<Hero>;
|
||||
abstract addHero(name: string): Observable<Hero>;
|
||||
abstract deleteHero(hero: Hero|number): Observable<Hero>;
|
||||
abstract searchHeroes(term: string): Observable<Hero[]>;
|
||||
abstract updateHero(hero: Hero): Observable<Hero>;
|
||||
}
|
14
packages/misc/angular-in-memory-web-api/test/fixtures/hero.ts
vendored
Normal file
14
packages/misc/angular-in-memory-web-api/test/fixtures/hero.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export class Hero {
|
||||
constructor(public id = 0, public name = '') {}
|
||||
clone() {
|
||||
return new Hero(this.id, this.name);
|
||||
}
|
||||
}
|
76
packages/misc/angular-in-memory-web-api/test/fixtures/http-client-hero-service.ts
vendored
Normal file
76
packages/misc/angular-in-memory-web-api/test/fixtures/http-client-hero-service.ts
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Observable, throwError} from 'rxjs';
|
||||
import {catchError} from 'rxjs/operators';
|
||||
|
||||
import {Hero} from './hero';
|
||||
import {HeroService} from './hero-service';
|
||||
|
||||
const cudOptions = {
|
||||
headers: new HttpHeaders({'Content-Type': 'application/json'})
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class HttpClientHeroService extends HeroService {
|
||||
constructor(private http: HttpClient) {
|
||||
super();
|
||||
}
|
||||
|
||||
getHeroes(): Observable<Hero[]> {
|
||||
return this.http.get<Hero[]>(this.heroesUrl).pipe(catchError(this.handleError));
|
||||
}
|
||||
|
||||
// This get-by-id will 404 when id not found
|
||||
getHero(id: number): Observable<Hero> {
|
||||
const url = `${this.heroesUrl}/${id}`;
|
||||
return this.http.get<Hero>(url).pipe(catchError(this.handleError));
|
||||
}
|
||||
|
||||
// This get-by-id does not 404; returns undefined when id not found
|
||||
// getHero<Data>(id: number): Observable<Hero> {
|
||||
// const url = `${this._heroesUrl}/?id=${id}`;
|
||||
// return this.http.get<Hero[]>(url)
|
||||
// .map(heroes => heroes[0] as Hero)
|
||||
// .catch(this.handleError);
|
||||
// }
|
||||
|
||||
addHero(name: string): Observable<Hero> {
|
||||
const hero = {name};
|
||||
|
||||
return this.http.post<Hero>(this.heroesUrl, hero, cudOptions)
|
||||
.pipe(catchError(this.handleError));
|
||||
}
|
||||
|
||||
deleteHero(hero: Hero|number): Observable<Hero> {
|
||||
const id = typeof hero === 'number' ? hero : hero.id;
|
||||
const url = `${this.heroesUrl}/${id}`;
|
||||
|
||||
return this.http.delete<Hero>(url, cudOptions).pipe(catchError(this.handleError));
|
||||
}
|
||||
|
||||
searchHeroes(term: string): Observable<Hero[]> {
|
||||
term = term.trim();
|
||||
// add safe, encoded search parameter if term is present
|
||||
const options = term ? {params: new HttpParams().set('name', term)} : {};
|
||||
|
||||
return this.http.get<Hero[]>(this.heroesUrl, options).pipe(catchError(this.handleError));
|
||||
}
|
||||
|
||||
updateHero(hero: Hero): Observable<Hero> {
|
||||
return this.http.put<Hero>(this.heroesUrl, hero, cudOptions).pipe(catchError(this.handleError));
|
||||
}
|
||||
|
||||
private handleError(error: any) {
|
||||
// In a real world app, we might send the error to remote logging infrastructure
|
||||
// and reformat for user consumption
|
||||
return throwError(error);
|
||||
}
|
||||
}
|
@ -0,0 +1,574 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/// <reference types="jasmine-ajax" />
|
||||
|
||||
import {HTTP_INTERCEPTORS, HttpBackend, HttpClient, HttpClientModule, HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {async, TestBed} from '@angular/core/testing';
|
||||
import {HttpClientBackendService, HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
|
||||
import {Observable, zip} from 'rxjs';
|
||||
import {concatMap, map, tap} from 'rxjs/operators';
|
||||
|
||||
import {Hero} from './fixtures/hero';
|
||||
import {HeroInMemDataOverrideService} from './fixtures/hero-in-mem-data-override-service';
|
||||
import {HeroInMemDataService} from './fixtures/hero-in-mem-data-service';
|
||||
import {HeroService} from './fixtures/hero-service';
|
||||
import {HttpClientHeroService} from './fixtures/http-client-hero-service';
|
||||
|
||||
describe('HttpClient Backend Service', () => {
|
||||
const delay = 1; // some minimal simulated latency delay
|
||||
|
||||
describe('raw Angular HttpClient', () => {
|
||||
let http: HttpClient;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule, HttpClientInMemoryWebApiModule.forRoot(HeroInMemDataService, {delay})
|
||||
]
|
||||
});
|
||||
|
||||
http = TestBed.get(HttpClient);
|
||||
});
|
||||
|
||||
it('can get heroes', async(() => {
|
||||
http.get<Hero[]>('api/heroes')
|
||||
.subscribe(
|
||||
heroes => expect(heroes.length).toBeGreaterThan(0, 'should have heroes'),
|
||||
failRequest);
|
||||
}));
|
||||
|
||||
it('GET should be a "cold" observable', async(() => {
|
||||
const httpBackend = TestBed.get(HttpBackend);
|
||||
const spy = spyOn(httpBackend, 'collectionHandler').and.callThrough();
|
||||
const get$ = http.get<Hero[]>('api/heroes');
|
||||
|
||||
// spy on `collectionHandler` should not be called before subscribe
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
get$.subscribe(heroes => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(heroes.length).toBeGreaterThan(0, 'should have heroes');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('GET should wait until after delay to respond', async(() => {
|
||||
// to make test fail, set `delay=0` above
|
||||
let gotResponse = false;
|
||||
|
||||
http.get<Hero[]>('api/heroes').subscribe(heroes => {
|
||||
gotResponse = true;
|
||||
expect(heroes.length).toBeGreaterThan(0, 'should have heroes');
|
||||
}, failRequest);
|
||||
|
||||
expect(gotResponse).toBe(false, 'should delay before response');
|
||||
}));
|
||||
|
||||
it('Should only initialize the db once', async(() => {
|
||||
const httpBackend = TestBed.get(HttpBackend);
|
||||
const spy = spyOn(httpBackend, 'resetDb').and.callThrough();
|
||||
|
||||
// Simultaneous backend.handler calls
|
||||
// Only the first should initialize by calling `resetDb`
|
||||
// All should wait until the db is "ready"
|
||||
// then they share the same db instance.
|
||||
http.get<Hero[]>('api/heroes').subscribe();
|
||||
http.get<Hero[]>('api/heroes').subscribe();
|
||||
http.get<Hero[]>('api/heroes').subscribe();
|
||||
http.get<Hero[]>('api/heroes').subscribe();
|
||||
|
||||
expect(spy.calls.count()).toBe(1);
|
||||
}));
|
||||
|
||||
it('can get heroes (w/ a different base path)', async(() => {
|
||||
http.get<Hero[]>('some-base-path/heroes').subscribe(heroes => {
|
||||
expect(heroes.length).toBeGreaterThan(0, 'should have heroes');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('should 404 when GET unknown collection (after delay)', async(() => {
|
||||
let gotError = false;
|
||||
const url = 'api/unknown-collection';
|
||||
http.get<Hero[]>(url).subscribe(
|
||||
() => fail(`should not have found data for '${url}'`), err => {
|
||||
gotError = true;
|
||||
expect(err.status).toBe(404, 'should have 404 status');
|
||||
});
|
||||
|
||||
expect(gotError).toBe(false, 'should not get error until after delay');
|
||||
}));
|
||||
|
||||
it('should return the hero w/id=1 for GET app/heroes/1', async(() => {
|
||||
http.get<Hero>('api/heroes/1')
|
||||
.subscribe(
|
||||
hero => expect(hero).toBeDefined('should find hero with id=1'), failRequest);
|
||||
}));
|
||||
|
||||
// test where id is string that looks like a number
|
||||
it('should return the stringer w/id="10" for GET app/stringers/10', async(() => {
|
||||
http.get<Hero>('api/stringers/10')
|
||||
.subscribe(
|
||||
hero => expect(hero).toBeDefined('should find string with id="10"'), failRequest);
|
||||
}));
|
||||
|
||||
it('should return 1-item array for GET app/heroes/?id=1', async(() => {
|
||||
http.get<Hero[]>('api/heroes/?id=1')
|
||||
.subscribe(
|
||||
heroes => expect(heroes.length).toBe(1, 'should find one hero w/id=1'),
|
||||
failRequest);
|
||||
}));
|
||||
|
||||
it('should return 1-item array for GET app/heroes?id=1', async(() => {
|
||||
http.get<Hero[]>('api/heroes?id=1')
|
||||
.subscribe(
|
||||
heroes => expect(heroes.length).toBe(1, 'should find one hero w/id=1'),
|
||||
failRequest);
|
||||
}));
|
||||
|
||||
it('should return undefined for GET app/heroes?id=not-found-id', async(() => {
|
||||
http.get<Hero[]>('api/heroes?id=123456')
|
||||
.subscribe(heroes => expect(heroes.length).toBe(0), failRequest);
|
||||
}));
|
||||
|
||||
it('should return 404 for GET app/heroes/not-found-id', async(() => {
|
||||
const url = 'api/heroes/123456';
|
||||
http.get<Hero[]>(url).subscribe(
|
||||
() => fail(`should not have found data for '${url}'`),
|
||||
err => expect(err.status).toBe(404, 'should have 404 status'));
|
||||
}));
|
||||
|
||||
it('can generate the id when add a hero with no id', async(() => {
|
||||
const hero = new Hero(undefined, 'SuperDooper');
|
||||
http.post<Hero>('api/heroes', hero).subscribe(replyHero => {
|
||||
expect(replyHero.id).toBeDefined('added hero should have an id');
|
||||
expect(replyHero).not.toBe(hero, 'reply hero should not be the request hero');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('can get nobodies (empty collection)', async(() => {
|
||||
http.get<Hero[]>('api/nobodies').subscribe(nobodies => {
|
||||
expect(nobodies.length).toBe(0, 'should have no nobodies');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('can add a nobody with an id to empty nobodies collection', async(() => {
|
||||
const id = 'g-u-i-d';
|
||||
|
||||
http.post('api/nobodies', {id, name: 'Noman'})
|
||||
.pipe(concatMap(() => http.get<{id: string; name: string;}[]>('api/nobodies')))
|
||||
.subscribe(nobodies => {
|
||||
expect(nobodies.length).toBe(1, 'should a nobody');
|
||||
expect(nobodies[0].name).toBe('Noman', 'should be "Noman"');
|
||||
expect(nobodies[0].id).toBe(id, 'should preserve the submitted, ' + id);
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('should fail when add a nobody without an id to empty nobodies collection', async(() => {
|
||||
http.post('api/nobodies', {name: 'Noman'})
|
||||
.subscribe(
|
||||
() => fail(`should not have been able to add 'Norman' to 'nobodies'`), err => {
|
||||
expect(err.status).toBe(422, 'should have 422 status');
|
||||
expect(err.body.error).toContain('id type is non-numeric');
|
||||
});
|
||||
}));
|
||||
|
||||
describe('can reset the database', () => {
|
||||
it('to empty (object db)', async(() => resetDatabaseTest('object')));
|
||||
|
||||
it('to empty (observable db)', async(() => resetDatabaseTest('observable')));
|
||||
|
||||
it('to empty (promise db)', async(() => resetDatabaseTest('promise')));
|
||||
|
||||
function resetDatabaseTest(returnType: string) {
|
||||
// Observable of the number of heroes and nobodies
|
||||
const sizes$ =
|
||||
zip(http.get<Hero[]>('api/heroes'), http.get<Hero[]>('api/nobodies'),
|
||||
http.get<Hero[]>('api/stringers'))
|
||||
.pipe(map(
|
||||
([h, n, s]) => ({heroes: h.length, nobodies: n.length, stringers: s.length})));
|
||||
|
||||
// Add a nobody so that we have one
|
||||
http.post('api/nobodies', {id: 42, name: 'Noman'})
|
||||
.pipe(
|
||||
// Reset database with "clear" option
|
||||
concatMap(() => http.post('commands/resetDb', {clear: true, returnType})),
|
||||
// get the number of heroes and nobodies
|
||||
concatMap(() => sizes$))
|
||||
.subscribe(sizes => {
|
||||
expect(sizes.heroes).toBe(0, 'reset should have cleared the heroes');
|
||||
expect(sizes.nobodies).toBe(0, 'reset should have cleared the nobodies');
|
||||
expect(sizes.stringers).toBe(0, 'reset should have cleared the stringers');
|
||||
}, failRequest);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('raw Angular HttpClient w/ override service', () => {
|
||||
let http: HttpClient;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
HttpClientInMemoryWebApiModule.forRoot(HeroInMemDataOverrideService, {delay})
|
||||
]
|
||||
});
|
||||
|
||||
http = TestBed.get(HttpClient);
|
||||
});
|
||||
|
||||
it('can get heroes', async(() => {
|
||||
http.get<Hero[]>('api/heroes')
|
||||
.subscribe(
|
||||
heroes => expect(heroes.length).toBeGreaterThan(0, 'should have heroes'),
|
||||
failRequest);
|
||||
}));
|
||||
|
||||
it('can translate `foo/heroes` to `heroes` via `parsedRequestUrl` override', async(() => {
|
||||
http.get<Hero[]>('api/foo/heroes')
|
||||
.subscribe(
|
||||
heroes => expect(heroes.length).toBeGreaterThan(0, 'should have heroes'),
|
||||
failRequest);
|
||||
}));
|
||||
|
||||
it('can get villains', async(() => {
|
||||
http.get<Hero[]>('api/villains')
|
||||
.subscribe(
|
||||
villains => expect(villains.length).toBeGreaterThan(0, 'should have villains'),
|
||||
failRequest);
|
||||
}));
|
||||
|
||||
it('should 404 when POST to villains', async(() => {
|
||||
const url = 'api/villains';
|
||||
http.post<Hero[]>(url, {id: 42, name: 'Dr. Evil'})
|
||||
.subscribe(
|
||||
() => fail(`should not have POSTed data for '${url}'`),
|
||||
err => expect(err.status).toBe(404, 'should have 404 status'));
|
||||
}));
|
||||
|
||||
it('should 404 when GET unknown collection', async(() => {
|
||||
const url = 'api/unknown-collection';
|
||||
http.get<Hero[]>(url).subscribe(
|
||||
() => fail(`should not have found data for '${url}'`),
|
||||
err => expect(err.status).toBe(404, 'should have 404 status'));
|
||||
}));
|
||||
|
||||
it('should use genId override to add new hero, "Maxinius"', async(() => {
|
||||
http.post('api/heroes', {name: 'Maxinius'})
|
||||
.pipe(concatMap(() => http.get<Hero[]>('api/heroes?name=Maxi')))
|
||||
.subscribe(heroes => {
|
||||
expect(heroes.length).toBe(1, 'should have found "Maxinius"');
|
||||
expect(heroes[0].name).toBe('Maxinius');
|
||||
expect(heroes[0].id).toBeGreaterThan(1000);
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('should use genId override guid generator for a new nobody without an id', async(() => {
|
||||
http.post('api/nobodies', {name: 'Noman'})
|
||||
.pipe(concatMap(() => http.get<{id: string; name: string}[]>('api/nobodies')))
|
||||
.subscribe(nobodies => {
|
||||
expect(nobodies.length).toBe(1, 'should a nobody');
|
||||
expect(nobodies[0].name).toBe('Noman', 'should be "Noman"');
|
||||
expect(typeof nobodies[0].id).toBe('string', 'should create a string (guid) id');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
describe('can reset the database', () => {
|
||||
it('to empty (object db)', async(() => resetDatabaseTest('object')));
|
||||
|
||||
it('to empty (observable db)', async(() => resetDatabaseTest('observable')));
|
||||
|
||||
it('to empty (promise db)', async(() => resetDatabaseTest('promise')));
|
||||
|
||||
function resetDatabaseTest(returnType: string) {
|
||||
// Observable of the number of heroes, nobodies and villains
|
||||
const sizes$ = zip(http.get<Hero[]>('api/heroes'), http.get<Hero[]>('api/nobodies'),
|
||||
http.get<Hero[]>('api/stringers'), http.get<Hero[]>('api/villains'))
|
||||
.pipe(map(([h, n, s, v]) => ({
|
||||
heroes: h.length,
|
||||
nobodies: n.length,
|
||||
stringers: s.length,
|
||||
villains: v.length
|
||||
})));
|
||||
|
||||
// Add a nobody so that we have one
|
||||
http.post('api/nobodies', {id: 42, name: 'Noman'})
|
||||
.pipe(
|
||||
// Reset database with "clear" option
|
||||
concatMap(() => http.post('commands/resetDb', {clear: true, returnType})),
|
||||
// count all the collections
|
||||
concatMap(() => sizes$))
|
||||
.subscribe(sizes => {
|
||||
expect(sizes.heroes).toBe(0, 'reset should have cleared the heroes');
|
||||
expect(sizes.nobodies).toBe(0, 'reset should have cleared the nobodies');
|
||||
expect(sizes.stringers).toBe(0, 'reset should have cleared the stringers');
|
||||
expect(sizes.villains).toBeGreaterThan(0, 'reset should NOT clear villains');
|
||||
}, failRequest);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('HttpClient HeroService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule, HttpClientInMemoryWebApiModule.forRoot(HeroInMemDataService, {delay})
|
||||
],
|
||||
providers: [{provide: HeroService, useClass: HttpClientHeroService}]
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeroService core', () => {
|
||||
let heroService: HeroService;
|
||||
|
||||
beforeEach(() => {
|
||||
heroService = TestBed.get(HeroService);
|
||||
});
|
||||
|
||||
it('can get heroes', async(() => {
|
||||
heroService.getHeroes().subscribe(heroes => {
|
||||
expect(heroes.length).toBeGreaterThan(0, 'should have heroes');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('can get hero w/ id=1', async(() => {
|
||||
heroService.getHero(1).subscribe(hero => {
|
||||
expect(hero.name).toBe('Windstorm');
|
||||
}, () => fail('getHero failed'));
|
||||
}));
|
||||
|
||||
it('should 404 when hero id not found', async(() => {
|
||||
const id = 123456;
|
||||
heroService.getHero(id).subscribe(
|
||||
() => fail(`should not have found hero for id='${id}'`), err => {
|
||||
expect(err.status).toBe(404, 'should have 404 status');
|
||||
});
|
||||
}));
|
||||
|
||||
it('can add a hero', async(() => {
|
||||
heroService.addHero('FunkyBob')
|
||||
.pipe(
|
||||
tap(hero => expect(hero.name).toBe('FunkyBob')),
|
||||
// Get the new hero by its generated id
|
||||
concatMap(hero => heroService.getHero(hero.id)))
|
||||
.subscribe(hero => {
|
||||
expect(hero.name).toBe('FunkyBob');
|
||||
}, () => failRequest('re-fetch of new hero failed'));
|
||||
}),
|
||||
10000);
|
||||
|
||||
it('can delete a hero', async(() => {
|
||||
const id = 1;
|
||||
heroService.deleteHero(id).subscribe((_: {}) => expect(_).toBeDefined(), failRequest);
|
||||
}));
|
||||
|
||||
it('should allow delete of non-existent hero', async(() => {
|
||||
const id = 123456;
|
||||
heroService.deleteHero(id).subscribe((_: {}) => expect(_).toBeDefined(), failRequest);
|
||||
}));
|
||||
|
||||
it('can search for heroes by name containing "a"', async(() => {
|
||||
heroService.searchHeroes('a').subscribe((heroes: Hero[]) => {
|
||||
expect(heroes.length).toBe(3, 'should find 3 heroes with letter "a"');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('can update existing hero', async(() => {
|
||||
const id = 1;
|
||||
heroService.getHero(id)
|
||||
.pipe(
|
||||
concatMap(hero => {
|
||||
hero.name = 'Thunderstorm';
|
||||
return heroService.updateHero(hero);
|
||||
}),
|
||||
concatMap(() => heroService.getHero(id)))
|
||||
.subscribe(
|
||||
hero => expect(hero.name).toBe('Thunderstorm'),
|
||||
() => fail('re-fetch of updated hero failed'));
|
||||
}),
|
||||
10000);
|
||||
|
||||
it('should create new hero when try to update non-existent hero', async(() => {
|
||||
const falseHero = new Hero(12321, 'DryMan');
|
||||
heroService.updateHero(falseHero).subscribe(
|
||||
hero => expect(hero.name).toBe(falseHero.name), failRequest);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('HttpClient interceptor', () => {
|
||||
let http: HttpClient;
|
||||
let interceptors: HttpInterceptor[];
|
||||
let httpBackend: HttpClientBackendService;
|
||||
|
||||
/**
|
||||
* Test interceptor adds a request header and a response header
|
||||
*/
|
||||
@Injectable()
|
||||
class TestHeaderInterceptor implements HttpInterceptor {
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
const reqClone = req.clone({setHeaders: {'x-test-req': 'req-test-header'}});
|
||||
|
||||
return next.handle(reqClone).pipe(map(event => {
|
||||
if (event instanceof HttpResponse) {
|
||||
event = event.clone({headers: event.headers.set('x-test-res', 'res-test-header')});
|
||||
}
|
||||
return event;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule, HttpClientInMemoryWebApiModule.forRoot(HeroInMemDataService, {delay})
|
||||
],
|
||||
providers: [
|
||||
// Add test interceptor just for this test suite
|
||||
{provide: HTTP_INTERCEPTORS, useClass: TestHeaderInterceptor, multi: true}
|
||||
]
|
||||
});
|
||||
|
||||
http = TestBed.get(HttpClient);
|
||||
httpBackend = TestBed.get(HttpBackend);
|
||||
interceptors = TestBed.get(HTTP_INTERCEPTORS);
|
||||
});
|
||||
|
||||
// sanity test
|
||||
it('TestingModule should provide the test interceptor', () => {
|
||||
const ti = interceptors.find(i => i instanceof TestHeaderInterceptor);
|
||||
expect(ti).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have GET request header from test interceptor', async(() => {
|
||||
const handle = spyOn(httpBackend, 'handle').and.callThrough();
|
||||
|
||||
http.get<Hero[]>('api/heroes').subscribe(heroes => {
|
||||
// HttpRequest is first arg of the first call to in-mem backend `handle`
|
||||
const req: HttpRequest<Hero[]> = handle.calls.argsFor(0)[0];
|
||||
const reqHeader = req.headers.get('x-test-req');
|
||||
expect(reqHeader).toBe('req-test-header');
|
||||
|
||||
expect(heroes.length).toBeGreaterThan(0, 'should have heroes');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('should have GET response header from test interceptor', async(() => {
|
||||
let gotResponse = false;
|
||||
const req = new HttpRequest<any>('GET', 'api/heroes');
|
||||
http.request<Hero[]>(req).subscribe(event => {
|
||||
if (event.type === HttpEventType.Response) {
|
||||
gotResponse = true;
|
||||
|
||||
const resHeader = event.headers.get('x-test-res');
|
||||
expect(resHeader).toBe('res-test-header');
|
||||
|
||||
const heroes = event.body as Hero[];
|
||||
expect(heroes.length).toBeGreaterThan(0, 'should have heroes');
|
||||
}
|
||||
}, failRequest, () => expect(gotResponse).toBe(true, 'should have seen Response event'));
|
||||
}));
|
||||
});
|
||||
|
||||
describe('HttpClient passThru', () => {
|
||||
let http: HttpClient;
|
||||
let httpBackend: HttpClientBackendService;
|
||||
let createPassThruBackend: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
HttpClientInMemoryWebApiModule.forRoot(
|
||||
HeroInMemDataService, {delay, passThruUnknownUrl: true})
|
||||
]
|
||||
});
|
||||
|
||||
http = TestBed.get(HttpClient);
|
||||
httpBackend = TestBed.get(HttpBackend);
|
||||
createPassThruBackend = spyOn(<any>httpBackend, 'createPassThruBackend').and.callThrough();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.Ajax.install();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.Ajax.uninstall();
|
||||
});
|
||||
|
||||
it('can get heroes (no passthru)', async(() => {
|
||||
http.get<Hero[]>('api/heroes').subscribe(heroes => {
|
||||
expect(createPassThruBackend).not.toHaveBeenCalled();
|
||||
expect(heroes.length).toBeGreaterThan(0, 'should have heroes');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
// `passthru` is NOT a collection in the data store
|
||||
// so requests for it should pass thru to the "real" server
|
||||
|
||||
it('can GET passthru', async(() => {
|
||||
jasmine.Ajax.stubRequest('api/passthru').andReturn({
|
||||
'status': 200,
|
||||
'contentType': 'application/json',
|
||||
'response': JSON.stringify([{id: 42, name: 'Dude'}])
|
||||
});
|
||||
|
||||
http.get<any[]>('api/passthru').subscribe(passthru => {
|
||||
expect(passthru.length).toBeGreaterThan(0, 'should have passthru data');
|
||||
}, failRequest);
|
||||
}));
|
||||
|
||||
it('can ADD to passthru', async(() => {
|
||||
jasmine.Ajax.stubRequest('api/passthru').andReturn({
|
||||
'status': 200,
|
||||
'contentType': 'application/json',
|
||||
'response': JSON.stringify({id: 42, name: 'Dude'})
|
||||
});
|
||||
|
||||
http.post<any>('api/passthru', {name: 'Dude'}).subscribe(passthru => {
|
||||
expect(passthru).toBeDefined('should have passthru data');
|
||||
expect(passthru.id).toBe(42, 'passthru object should have id 42');
|
||||
}, failRequest);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Http dataEncapsulation = true', () => {
|
||||
let http: HttpClient;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
HttpClientInMemoryWebApiModule.forRoot(
|
||||
HeroInMemDataService, {delay, dataEncapsulation: true})
|
||||
]
|
||||
});
|
||||
|
||||
http = TestBed.get(HttpClient);
|
||||
});
|
||||
|
||||
it('can get heroes (encapsulated)', async(() => {
|
||||
http.get<{data: any}>('api/heroes')
|
||||
.pipe(map(data => data.data as Hero[]))
|
||||
.subscribe(
|
||||
heroes => expect(heroes.length).toBeGreaterThan(0, 'should have data.heroes'),
|
||||
failRequest);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Fail a Jasmine test such that it displays the error object,
|
||||
* typically passed in the error path of an Observable.subscribe()
|
||||
*/
|
||||
function failRequest(err: any) {
|
||||
fail(JSON.stringify(err));
|
||||
}
|
Reference in New Issue
Block a user