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 = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'jest'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'prettier/@typescript-eslint',
// Doit rester en dernier :
'plugin:prettier/recommended',
......
import { HttpHeaderResponse } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import * as _ from 'lodash';
import { Observable } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { from, Observable } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { marbles } from 'rxjs-marbles';
import { ResourceCache } from './cache.service';
......@@ -32,10 +33,6 @@ const VALUES: { [name: string]: MyResource } = {
},
d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' },
};
let scheduler: MarbleTestScheduler<any>;
beforeEach(() => {
scheduler = MarbleTestScheduler.create(VALUES, 'error');
});
describe('ResourceCache', () => {
beforeEach(() =>
......@@ -44,187 +41,243 @@ describe('ResourceCache', () => {
}),
);
it('should be created', inject([ResourceCache], (service: ResourceCache) => {
it('should be created', inject(
[ResourceCache],
(service: ResourceCache) => {
expect(service).toBeDefined();
}));
},
));
describe('.get()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const getQuery$ = cold('a|');
expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('(a|)');
}),
));
it('should provide the value', inject(
[ResourceCache],
(service: ResourceCache) =>
marbles(({ cold, expect }) => {
const getQuery$ = cold('a|', VALUES);
it('should cache the value', inject([ResourceCache], (service: ResourceCache) =>
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$,
});
const response$ = service.get(MY_IRI, () => getQuery$);
expectObservable(queries$.pipe(mergeMap((query$) => service.get(MY_IRI, () => query$)))).toBe('aa|');
expect(response$).toBeObservable('(a|)', VALUES);
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
// it('should cache the value', inject([ResourceCache], (service: ResourceCache) =>
// marbles(({ cold, expect }) => {
// const getQuery$ = cold('a|', VALUES);
// const getQuery2$ = cold('b|', VALUES);
// // 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('#');
expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('#');
const response$ = service.get(MY_IRI, () => getQuery$);
expect(response$).toBeObservable('#');
}),
));
});
describe('.put()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) => {
const putRequest$ = scheduler.createColdObservable('a|');
it('should provide the value', inject(
[ResourceCache],
(service: ResourceCache) =>
marbles(({ cold, expect }) => {
const putRequest$ = cold('a|', VALUES);
const response$ = service.put(MY_IRI, putRequest$);
scheduler.expectObservable(service.put(MY_IRI, putRequest$)).toBe('a|');
}));
expect(response$).toBeObservable('a|', VALUES);
}),
));
it('should not cache the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const putRequest$ = cold('a|');
const putRequest2$ = cold('b|');
it('should not cache the value', inject(
[ResourceCache],
(service: ResourceCache) =>
marbles(({ cold, expect }) => {
const putRequest$ = cold('a|', VALUES);
const putRequest2$ = cold('b|', VALUES);
// tslint:disable-next-line:rxjs-finnish
const requests$ = cold('ab|', {
a: putRequest$,
b: putRequest2$,
});
expectObservable(
requests$.pipe(
mergeMap((request$: Observable<MyResource>) => service.put(MY_IRI, request$)),
map((x) => _.clone(x)),
const response$ = requests$.pipe(
mergeMap((request$: Observable<MyResource>) =>
service.put(MY_IRI, request$),
),
).toBe('ab|');
map((x) => _.clone(x)),
);
expect(response$).toBeObservable('ab|', VALUES);
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
it('should propagate errors', inject(
[ResourceCache],
(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()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const postRequest$ = cold('a|');
it('should provide the value', inject(
[ResourceCache],
(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) =>
scheduler.run(({ cold, expectObservable }) => {
const postRequest$ = cold('#');
it('should propagate errors', inject(
[ResourceCache],
(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()', () => {
it('should clear the cache on successful fetch', inject([ResourceCache], (service: ResourceCache) => {
it('should clear the cache on successful fetch', inject(
[ResourceCache],
(service: ResourceCache) =>
marbles(({ cold, expect }) => {
const response = new HttpHeaderResponse({ status: 200 });
const values = { r: response, a: VALUES.a, b: VALUES.b };
const sched = scheduler.withValues(values);
const values = { r: response };
sched.run(async ({ cold, expectObservable }) => {
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|'),
);
return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(b|)');
});
}));
const all$ = from([
getResponse$,
deleteResponse$,
secondGetResponse$,
]).pipe(mergeMap((x) => x));
expect(all$).toBeObservable('(b|)', VALUES);
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
it('should propagate errors', inject(
[ResourceCache],
(service: ResourceCache) =>
marbles(({ cold, expect }) => {
const deleteRequest$ = cold('#');
expectObservable(service.delete(MY_IRI, deleteRequest$)).toBe('#');
const response$ = service.delete(MY_IRI, deleteRequest$);
expect(response$).toBeObservable('#');
}),
));
it('should not clear the cache on error', inject([ResourceCache], (service: ResourceCache) => {
const error = new HttpHeaderResponse({ status: 500 });
const values = { a: VALUES.a, b: VALUES.b, e: error };
scheduler
.withValues(values)
.withError(error)
.run(async ({ cold, expectObservable }) => {
await service.get(MY_IRI, () => cold('a|')).toPromise();
await service
.delete(MY_IRI, cold('#'))
.pipe(catchError((e) => e))
.toPromise();
return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(a|)');
});
}));
//
// it('should not clear the cache on error', inject([ResourceCache], (service: ResourceCache) => {
// const error = new HttpHeaderResponse({ status: 500 });
// const values = { a: VALUES.a, b: VALUES.b, e: error };
// scheduler
// .withValues(values)
// .withError(error)
// .run(async ({ cold, expect }) => {
// await service.get(MY_IRI, () => cold('a|')).toPromise();
// await service
// .delete(MY_IRI, cold('#'))
// .pipe(catchError((e) => e))
// .toPromise();
//
// return expect(service.get(MY_IRI, () => cold('b|'))).toBeObservable('(a|)');
// });
// }));
});
describe('.getAll()', () => {
it('should provide the returned value', inject([ResourceCache], (service: ResourceCache) => {
const values = {
a: {
'hydra:member': [VALUES.a, VALUES.d],
'hydra:totalItems': 2,
},
};
scheduler.withValues(values).run(({ cold, expectObservable }) => {
const getAllRequest$ = cold('a|');
expectObservable(service.getAll(getAllRequest$)).toBe('a|');
});
}));
it('should nicely handle empty collections', inject([ResourceCache], (service: ResourceCache) => {
const values = {
a: { 'hydra:member': [] as any, 'hydra:totalItems': 0 },
};
scheduler.withValues(values).run(({ cold, expectObservable }) => {
const getAllRequest$ = cold('a|');
expectObservable(service.getAll(getAllRequest$)).toBe('a|');
});
}));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
// it('should provide the returned value', inject([ResourceCache], (service: ResourceCache) => {
// const values = {
// a: {
// 'hydra:member': [VALUES.a, VALUES.d],
// 'hydra:totalItems': 2,
// },
// };
// scheduler.withValues(values).run(({ cold, expect }) => {
// const getAllRequest$ = cold('a|');
// expect(service.getAll(getAllRequest$)).toBeObservable('a|');
// });
// }));
//
// it('should nicely handle empty collections', inject([ResourceCache], (service: ResourceCache) => {
// const values = {
// a: { 'hydra:member': [] as any, 'hydra:totalItems': 0 },
// };
// scheduler.withValues(values).run(({ cold, expect }) => {
// const getAllRequest$ = cold('a|');
// expect(service.getAll(getAllRequest$)).toBeObservable('a|');
// });
// }));
it('should propagate errors', inject(
[ResourceCache],
(service: ResourceCache) =>
marbles(({ cold, expect }) => {
const getAllRequest$ = cold<Collection<MyResource>>('#');
expectObservable(service.getAll(getAllRequest$)).toBe('#');
expect(service.getAll(getAllRequest$)).toBeObservable('#');
}),
));
it('should populate the cache', inject([ResourceCache], (service: ResourceCache) => {
const values = {
a: VALUES.a,
b: VALUES.d,
h: {
'hydra:member': [VALUES.a, VALUES.d],
'hydra:totalItems': 2,
},
};
scheduler.withValues(values).run(({ cold, expectObservable }) => {
const getAllRequest$ = cold('h|');
const getRequest$ = cold('b|');
const requests$ = cold<() => Observable<any>>('ab|', {
a: () => service.getAll(getAllRequest$),
b: () => service.get(MY_IRI, () => getRequest$),
});
expectObservable(requests$.pipe(mergeMap((sendRequest: () => Observable<any>) => sendRequest()))).toBe(
'ha|',
);
});
}));
//
// it('should populate the cache', inject([ResourceCache], (service: ResourceCache) => {
// const values = {
// a: VALUES.a,
// b: VALUES.d,
// h: {
// 'hydra:member': [VALUES.a, VALUES.d],
// 'hydra:totalItems': 2,
// },
// };
// scheduler.withValues(values).run(({ cold, expect }) => {
// const getAllRequest$ = cold('h|');
// const getRequest$ = cold('b|');
// const requests$ = cold<() => Observable<any>>('ab|', {
// a: () => service.getAll(getAllRequest$),
// b: () => service.get(MY_IRI, () => getRequest$),
// });
//
// expect(requests$.pipe(mergeMap((sendRequest: () => Observable<any>) => sendRequest()))).toBeObservable(
// 'ha|',
// );
// });
// }));
});
});
import { HttpResponseBase } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
import { forkJoinArray } from 'rxjs-etc';
import { ValueHolder } from './value-holder';
import {
AbstractResourceCache,
......@@ -12,7 +15,6 @@ import {
Resource,
} from '../types';
import { ValueHolder } from './value-holder';
/**
* Implémentation d'un cache de resource.
*
......@@ -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.
*/
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é.
*
* 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);
}
/**
* 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$);
}
......@@ -53,7 +61,10 @@ export class ResourceCache extends AbstractResourceCache {
/**
* 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(
tap(() => {
const holder = this.holders.get(iri);
......@@ -69,12 +80,16 @@ export class ResourceCache extends AbstractResourceCache {
/**
* Fait une requête pour plusieurs ressources puis les mets en cache.
*/
public getAll<R extends Resource>(query$: Observable<Collection<R>>): Observable<Collection<R>> {
public getAll<R extends Resource>(
query$: Observable<Collection<R>>,
): Observable<Collection<R>> {
return query$.pipe(
switchMap((coll: Collection<R>) => {
const members = getCollectionMembers(coll);
const memberObservables$ = members.map((item) => this.received(item));
return safeForkJoin(memberObservables$).pipe(
const memberObservables$ = members.map((item) =>
this.received(item),
);
return forkJoinArray(memberObservables$).pipe(
map((items) =>
Object.assign({} as Collection<R>, coll, {
[COLLECTION_MEMBERS]: items,
......
import { forkJoin } from 'rxjs';
import { IRI, IRI_PROPERTY, Resource } from '../types';
import { ValueHolder } from './value-holder';
import { IRIMismatchError, MissingIRIError } from './errors';
import { forkJoinArray } from 'rxjs-etc';
import { marbles } from 'rxjs-marbles';
import { cases } from 'rxjs-marbles/jest';
interface MyResource extends Resource {
readonly '@id': IRI<MyResource>;
......@@ -25,10 +30,6 @@ const VALUES: { [name: string]: MyResource } = {
},
d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' },
};
let scheduler: MarbleTestScheduler<any>;
beforeEach(() => {
scheduler = MarbleTestScheduler.create(VALUES, 'error');
});
describe('ValueHolder', () => {
let holder: ValueHolder<any>;
......@@ -42,51 +43,59 @@ describe('ValueHolder', () => {
});
describe('.set()', () => {
function testSet({ value, error, SET_M }: any) {
scheduler.withError(error).run(({ expectObservable }) => {
expectObservable(holder.set(value)).toBe(SET_M);
});
}
it('should provide the value', () =>
testSet({
cases(
'foo',
({ expect }, { value, error, SET_M }) =>
// eslint-disable-next-line jest/no-standalone-expect
expect(holder.set(value)).toBeObservable(SET_M, VALUES, error),
[
{
name: 'should provide the value',
value: VALUES.a,
SET_M: '(a|)',
}));
it(`should refuse value without ${IRI_PROPERTY}`, () =>
testSet({
},
{
name: `should refuse value without ${IRI_PROPERTY}`,
value: {},
error: new MissingIRIError(),
SET_M: '#',
}));
it('should refuse value with different @id', () =>
testSet({
},
{
name: 'should refuse value with different @id',
value: { '@id': iri('bar') },
error: new IRIMismatchError(MY_IRI, iri('bar')),
error: new IRIMismatchError(
MY_IRI.toString(),
iri('bar').toString(),
),
SET_M: '#',
}));
},
],
);