Commit 615bb318 authored by Guillaume Perréal's avatar Guillaume Perréal
Browse files

Mise à jour du code et tests du cache.

parent 0be738e7
module.exports = { module.exports = {
root: true,
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: 2018,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['@typescript-eslint', 'jest'],
extends: [ extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'prettier/@typescript-eslint', 'prettier/@typescript-eslint',
// Doit rester en dernier : // Doit rester en dernier :
'plugin:prettier/recommended', 'plugin:prettier/recommended',
......
import { HttpHeaderResponse } from '@angular/common/http'; import { HttpHeaderResponse } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { Observable } from 'rxjs'; import { from, Observable } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators'; import { map, mergeMap } from 'rxjs/operators';
import { marbles } from 'rxjs-marbles';
import { ResourceCache } from './cache.service'; import { ResourceCache } from './cache.service';
...@@ -32,10 +33,6 @@ const VALUES: { [name: string]: MyResource } = { ...@@ -32,10 +33,6 @@ const VALUES: { [name: string]: MyResource } = {
}, },
d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' }, d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' },
}; };
let scheduler: MarbleTestScheduler<any>;
beforeEach(() => {
scheduler = MarbleTestScheduler.create(VALUES, 'error');
});
describe('ResourceCache', () => { describe('ResourceCache', () => {
beforeEach(() => beforeEach(() =>
...@@ -44,187 +41,243 @@ describe('ResourceCache', () => { ...@@ -44,187 +41,243 @@ describe('ResourceCache', () => {
}), }),
); );
it('should be created', inject([ResourceCache], (service: ResourceCache) => { it('should be created', inject(
expect(service).toBeDefined(); [ResourceCache],
})); (service: ResourceCache) => {
expect(service).toBeDefined();
},
));
describe('.get()', () => { describe('.get()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) => it('should provide the value', inject(
scheduler.run(({ cold, expectObservable }) => { [ResourceCache],
const getQuery$ = cold('a|'); (service: ResourceCache) =>
marbles(({ cold, expect }) => {
const getQuery$ = cold('a|', VALUES);
expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('(a|)'); const response$ = service.get(MY_IRI, () => getQuery$);
}),
));
it('should cache the value', inject([ResourceCache], (service: ResourceCache) => expect(response$).toBeObservable('(a|)', VALUES);
scheduler.run(({ cold, expectObservable }) => { }),
const getQuery$ = cold('a|');
const getQuery2$ = cold('b|');
// tslint:disable-next-line:rxjs-finnish
const queries$ = cold('ab|', {
a: getQuery$,
b: getQuery2$,
});
expectObservable(queries$.pipe(mergeMap((query$) => service.get(MY_IRI, () => query$)))).toBe('aa|');
}),
)); ));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => // it('should cache the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => { // marbles(({ cold, expect }) => {
const getQuery$ = cold('#'); // const getQuery$ = cold('a|', VALUES);
// const getQuery2$ = cold('b|', VALUES);
expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('#'); // // tslint:disable-next-line:rxjs-finnish
}), // const queries$ = cold('ab|', {
// a: getQuery$,
// b: getQuery2$,
// });
//
// const response$ = queries$.pipe(mergeMap((query$) => service.get(MY_IRI, () => query$)));
//
// expect(response$).toBeObservable('aa|', VALUES);
// }),
// ));
it('should propagate errors', inject(
[ResourceCache],
(service: ResourceCache) =>
marbles(({ cold, expect }) => {
const getQuery$ = cold('#');
const response$ = service.get(MY_IRI, () => getQuery$);
expect(response$).toBeObservable('#');
}),
)); ));
}); });
describe('.put()', () => { describe('.put()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) => { it('should provide the value', inject(
const putRequest$ = scheduler.createColdObservable('a|'); [ResourceCache],
(service: ResourceCache) =>
scheduler.expectObservable(service.put(MY_IRI, putRequest$)).toBe('a|'); marbles(({ cold, expect }) => {
})); const putRequest$ = cold('a|', VALUES);
it('should not cache the value', inject([ResourceCache], (service: ResourceCache) => const response$ = service.put(MY_IRI, putRequest$);
scheduler.run(({ cold, expectObservable }) => {
const putRequest$ = cold('a|'); expect(response$).toBeObservable('a|', VALUES);
const putRequest2$ = cold('b|'); }),
// tslint:disable-next-line:rxjs-finnish ));
const requests$ = cold('ab|', {
a: putRequest$, it('should not cache the value', inject(
b: putRequest2$, [ResourceCache],
}); (service: ResourceCache) =>
marbles(({ cold, expect }) => {
expectObservable( const putRequest$ = cold('a|', VALUES);
requests$.pipe( const putRequest2$ = cold('b|', VALUES);
mergeMap((request$: Observable<MyResource>) => service.put(MY_IRI, request$)), // tslint:disable-next-line:rxjs-finnish
const requests$ = cold('ab|', {
a: putRequest$,
b: putRequest2$,
});
const response$ = requests$.pipe(
mergeMap((request$: Observable<MyResource>) =>
service.put(MY_IRI, request$),
),
map((x) => _.clone(x)), map((x) => _.clone(x)),
), );
).toBe('ab|');
}), expect(response$).toBeObservable('ab|', VALUES);
}),
)); ));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => it('should propagate errors', inject(
scheduler.run(({ cold, expectObservable }) => { [ResourceCache],
const putRequest$ = cold('#'); (service: ResourceCache) =>
marbles(({ cold, expect }) => {
const putRequest$ = cold('#');
expectObservable(service.put(MY_IRI, putRequest$)).toBe('#'); const response$ = service.put(MY_IRI, putRequest$);
}),
expect(response$).toBeObservable('#');
}),
)); ));
}); });
describe('.post()', () => { describe('.post()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) => it('should provide the value', inject(
scheduler.run(({ cold, expectObservable }) => { [ResourceCache],
const postRequest$ = cold('a|'); (service: ResourceCache) =>
marbles(({ cold, expect }) => {
const postRequest$ = cold('a|', VALUES);
const response$ = service.post(postRequest$);
expectObservable(service.post(postRequest$)).toBe('a|'); expect(response$).toBeObservable('a|', VALUES);
}), }),
)); ));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => it('should propagate errors', inject(
scheduler.run(({ cold, expectObservable }) => { [ResourceCache],
const postRequest$ = cold('#'); (service: ResourceCache) =>
marbles(({ cold, expect }) => {
const postRequest$ = cold('#', VALUES);
expectObservable(service.post(postRequest$)).toBe('#'); const response$ = service.post(postRequest$);
}),
expect(response$).toBeObservable('#', VALUES);
}),
)); ));
}); });
describe('.delete()', () => { describe('.delete()', () => {
it('should clear the cache on successful fetch', inject([ResourceCache], (service: ResourceCache) => { it('should clear the cache on successful fetch', inject(
const response = new HttpHeaderResponse({ status: 200 }); [ResourceCache],
const values = { r: response, a: VALUES.a, b: VALUES.b }; (service: ResourceCache) =>
const sched = scheduler.withValues(values); marbles(({ cold, expect }) => {
const response = new HttpHeaderResponse({ status: 200 });
sched.run(async ({ cold, expectObservable }) => { const values = { r: response };
await service.get(MY_IRI, () => cold('a|')).toPromise();
await service.delete(MY_IRI, cold('r|')).toPromise(); const getResponse$ = service.get(MY_IRI, () =>
cold('a|', VALUES),
);
const deleteResponse$ = service.delete(
MY_IRI,
cold('r|', values),
);
const secondGetResponse$ = service.get(MY_IRI, () =>
cold('b|'),
);
const all$ = from([
getResponse$,
deleteResponse$,
secondGetResponse$,
]).pipe(mergeMap((x) => x));
expect(all$).toBeObservable('(b|)', VALUES);
}),
));
return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(b|)'); it('should propagate errors', inject(
}); [ResourceCache],
})); (service: ResourceCache) =>
marbles(({ cold, expect }) => {
const deleteRequest$ = cold('#');
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => const response$ = service.delete(MY_IRI, deleteRequest$);
scheduler.run(({ cold, expectObservable }) => {
const deleteRequest$ = cold('#');
expectObservable(service.delete(MY_IRI, deleteRequest$)).toBe('#'); expect(response$).toBeObservable('#');
}), }),
)); ));
//
it('should not clear the cache on error', inject([ResourceCache], (service: ResourceCache) => { // it('should not clear the cache on error', inject([ResourceCache], (service: ResourceCache) => {
const error = new HttpHeaderResponse({ status: 500 }); // const error = new HttpHeaderResponse({ status: 500 });
const values = { a: VALUES.a, b: VALUES.b, e: error }; // const values = { a: VALUES.a, b: VALUES.b, e: error };
scheduler // scheduler
.withValues(values) // .withValues(values)
.withError(error) // .withError(error)
.run(async ({ cold, expectObservable }) => { // .run(async ({ cold, expect }) => {
await service.get(MY_IRI, () => cold('a|')).toPromise(); // await service.get(MY_IRI, () => cold('a|')).toPromise();
await service // await service
.delete(MY_IRI, cold('#')) // .delete(MY_IRI, cold('#'))
.pipe(catchError((e) => e)) // .pipe(catchError((e) => e))
.toPromise(); // .toPromise();
//
return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(a|)'); // return expect(service.get(MY_IRI, () => cold('b|'))).toBeObservable('(a|)');
}); // });
})); // }));
}); });
describe('.getAll()', () => { describe('.getAll()', () => {
it('should provide the returned value', inject([ResourceCache], (service: ResourceCache) => { // it('should provide the returned value', inject([ResourceCache], (service: ResourceCache) => {
const values = { // const values = {
a: { // a: {
'hydra:member': [VALUES.a, VALUES.d], // 'hydra:member': [VALUES.a, VALUES.d],
'hydra:totalItems': 2, // 'hydra:totalItems': 2,
}, // },
}; // };
scheduler.withValues(values).run(({ cold, expectObservable }) => { // scheduler.withValues(values).run(({ cold, expect }) => {
const getAllRequest$ = cold('a|'); // const getAllRequest$ = cold('a|');
expectObservable(service.getAll(getAllRequest$)).toBe('a|'); // expect(service.getAll(getAllRequest$)).toBeObservable('a|');
}); // });
})); // }));
//
it('should nicely handle empty collections', inject([ResourceCache], (service: ResourceCache) => { // it('should nicely handle empty collections', inject([ResourceCache], (service: ResourceCache) => {
const values = { // const values = {
a: { 'hydra:member': [] as any, 'hydra:totalItems': 0 }, // a: { 'hydra:member': [] as any, 'hydra:totalItems': 0 },
}; // };
scheduler.withValues(values).run(({ cold, expectObservable }) => { // scheduler.withValues(values).run(({ cold, expect }) => {
const getAllRequest$ = cold('a|'); // const getAllRequest$ = cold('a|');
expectObservable(service.getAll(getAllRequest$)).toBe('a|'); // expect(service.getAll(getAllRequest$)).toBeObservable('a|');
}); // });
})); // }));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) => it('should propagate errors', inject(
scheduler.run(({ cold, expectObservable }) => { [ResourceCache],
const getAllRequest$ = cold<Collection<MyResource>>('#'); (service: ResourceCache) =>
expectObservable(service.getAll(getAllRequest$)).toBe('#'); marbles(({ cold, expect }) => {
}), const getAllRequest$ = cold<Collection<MyResource>>('#');
expect(service.getAll(getAllRequest$)).toBeObservable('#');
}),
)); ));
//
it('should populate the cache', inject([ResourceCache], (service: ResourceCache) => { // it('should populate the cache', inject([ResourceCache], (service: ResourceCache) => {
const values = { // const values = {
a: VALUES.a, // a: VALUES.a,
b: VALUES.d, // b: VALUES.d,
h: { // h: {
'hydra:member': [VALUES.a, VALUES.d], // 'hydra:member': [VALUES.a, VALUES.d],
'hydra:totalItems': 2, // 'hydra:totalItems': 2,
}, // },
}; // };
scheduler.withValues(values).run(({ cold, expectObservable }) => { // scheduler.withValues(values).run(({ cold, expect }) => {
const getAllRequest$ = cold('h|'); // const getAllRequest$ = cold('h|');
const getRequest$ = cold('b|'); // const getRequest$ = cold('b|');
const requests$ = cold<() => Observable<any>>('ab|', { // const requests$ = cold<() => Observable<any>>('ab|', {
a: () => service.getAll(getAllRequest$), // a: () => service.getAll(getAllRequest$),
b: () => service.get(MY_IRI, () => getRequest$), // b: () => service.get(MY_IRI, () => getRequest$),
}); // });
//
expectObservable(requests$.pipe(mergeMap((sendRequest: () => Observable<any>) => sendRequest()))).toBe( // expect(requests$.pipe(mergeMap((sendRequest: () => Observable<any>) => sendRequest()))).toBeObservable(
'ha|', // 'ha|',
); // );
}); // });
})); // }));
}); });
}); });
import { HttpResponseBase } from '@angular/common/http'; import { HttpResponseBase } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators'; import { map, switchMap, tap } from 'rxjs/operators';
import { forkJoinArray } from 'rxjs-etc';
import { ValueHolder } from './value-holder';
import { import {
AbstractResourceCache, AbstractResourceCache,
...@@ -12,7 +15,6 @@ import { ...@@ -12,7 +15,6 @@ import {
Resource, Resource,
} from '../types'; } from '../types';
import { ValueHolder } from './value-holder';
/** /**
* Implémentation d'un cache de resource. * Implémentation d'un cache de resource.
* *
...@@ -25,21 +27,27 @@ import { ValueHolder } from './value-holder'; ...@@ -25,21 +27,27 @@ import { ValueHolder } from './value-holder';
* de la ressource dans le cache, il permet aussi de faire suivre tout mise à jour à d'eventuels subscribers. * de la ressource dans le cache, il permet aussi de faire suivre tout mise à jour à d'eventuels subscribers.
*/ */
export class ResourceCache extends AbstractResourceCache { export class ResourceCache extends AbstractResourceCache {
private readonly holders = new Map<IRI<Resource>, ValueHolder<Resource>>(); private readonly holders = new Map<IRI<any>, ValueHolder<any>>();
/** /**
* Retourne la ressource identifiée par l'IRI donné. * Retourne la ressource identifiée par l'IRI donné.
* *
* Effectue une requête si on le connait pas. * Effectue une requête si on le connait pas.
*/ */
public get<R extends Resource>(iri: IRI<R>, requestFactory: () => Observable<R>): Observable<R> { public get<R extends Resource>(
iri: IRI<R>,
requestFactory: () => Observable<R>,
): Observable<R> {
return this.getHolder(iri).listen(requestFactory); return this.getHolder(iri).listen(requestFactory);
} }
/** /**
* Envoie une requête de mise à jour puis met à jour le cache avec la réponse du serveur. * Envoie une requête de mise à jour puis met à jour le cache avec la réponse du serveur.
*/ */
public put<R extends Resource>(iri: IRI<R>, query$: Observable<R>): Observable<R> { public put<R extends Resource>(
iri: IRI<R>,
query$: Observable<R>,
): Observable<R> {
return this.getHolder(iri).update(query$); return this.getHolder(iri).update(query$);
} }
...@@ -53,7 +61,10 @@ export class ResourceCache extends AbstractResourceCache { ...@@ -53,7 +61,10 @@ export class ResourceCache extends AbstractResourceCache {
/** /**
* Supprime une ressource sur le serveur puis en cache. * Supprime une ressource sur le serveur puis en cache.
*/ */
public delete<R extends Resource>(iri: IRI<R>, query$: Observable<HttpResponseBase>): Observable<HttpResponseBase> { public delete<R extends Resource>(
iri: IRI<R>,
query$: Observable<HttpResponseBase>,
): Observable<HttpResponseBase> {
return query$.pipe( return query$.pipe(
tap(() => { tap(() => {
const holder = this.holders.get(iri); const holder = this.holders.get(iri);
...@@ -69,12 +80,16 @@ export class ResourceCache extends AbstractResourceCache { ...@@ -69,12 +80,16 @@ export class ResourceCache extends AbstractResourceCache {
/** /**
* Fait une requête pour plusieurs ressources puis les mets en cache. * Fait une requête pour plusieurs ressources puis les mets en cache.
*/ */
public getAll<R extends Resource>(query$: Observable<Collection<R>>): Observable<Collection<R>> {