Commit 0be738e7 authored by Guillaume Perréal's avatar Guillaume Perréal
Browse files

Split le cache.

parent f2718fac
import { HttpHeaderResponse } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import * as _ from 'lodash';
import { forkJoin, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { MarbleTestScheduler } from '../../../testing/marbles';
import { Collection, IRI, IRI_PROPERTY, Resource } from '../../shared/models';
import { safeForkJoin } from '../../shared/rxjs';
import { IRIMismatchError, MissingIRIError, ResourceCache, ValueHolder } from './cache.service';
describe('cache.service', () => {
interface MyResource extends Resource {
readonly '@id': IRI<MyResource>;
readonly '@type': 'MyResource';
value: string;
value2?: string;
}
function iri(x: string): IRI<MyResource> {
return x as any;
}
const MY_IRI = iri('/bla/a');
const OTHER_IRI = iri('/bla/B');
const VALUES: { [name: string]: MyResource } = {
a: { '@id': MY_IRI, '@type': 'MyResource', value: 'foo' },
b: { '@id': MY_IRI, '@type': 'MyResource', value: 'bar' },
c: { '@id': MY_IRI, '@type': 'MyResource', value: 'bar', value2: 'quz' },
d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' },
};
let scheduler: MarbleTestScheduler<any>;
beforeEach(() => {
scheduler = MarbleTestScheduler.create(VALUES, 'error');
});
import { ResourceCache } from './cache.service';
import { Collection, IRI, Resource } from '../types';
interface MyResource extends Resource {
readonly '@id': IRI<MyResource>;
readonly '@type': 'MyResource';
value: string;
value2?: string;
}
function iri(x: string): IRI<MyResource> {
return x as any;
}
const MY_IRI = iri('/bla/a');
const OTHER_IRI = iri('/bla/B');
const VALUES: { [name: string]: MyResource } = {
a: { '@id': MY_IRI, '@type': 'MyResource', value: 'foo' },
b: { '@id': MY_IRI, '@type': 'MyResource', value: 'bar' },
c: {
'@id': MY_IRI,
'@type': 'MyResource',
value: 'bar',
value2: 'quz',
},
d: { '@id': OTHER_IRI, '@type': 'MyResource', value: 'zig' },
};
let scheduler: MarbleTestScheduler<any>;
beforeEach(() => {
scheduler = MarbleTestScheduler.create(VALUES, 'error');
});
describe('ValueHolder', () => {
let holder: ValueHolder<any>;
describe('ResourceCache', () => {
beforeEach(() =>
TestBed.configureTestingModule({
providers: [ResourceCache],
}),
);
beforeEach(() => {
holder = new ValueHolder<MyResource>(iri('/bla/a'));
});
it('should be created', inject([ResourceCache], (service: ResourceCache) => {
expect(service).toBeDefined();
}));
it('should be created', () => {
expect(holder).toBeTruthy();
});
describe('.get()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const getQuery$ = cold('a|');
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({
value: VALUES.a,
SET_M: '(a|)',
}));
it(`should refuse value without ${IRI_PROPERTY}`, () =>
testSet({
value: {},
error: new MissingIRIError(),
SET_M: '#',
}));
it('should refuse value with different @id', () =>
testSet({
value: { '@id': iri('bar') },
error: new IRIMismatchError(MY_IRI, iri('bar')),
SET_M: '#',
}));
it('should always points to the same instance', () => {
forkJoin(holder.set(VALUES.a), holder.set(VALUES.b)).subscribe(([a, b]: MyResource[]) => {
expect(a).toBe(b);
});
});
});
describe('.update()', () => {
it('should provide the value from the server', () =>
scheduler.run(({ cold, expectObservable }) => {
const REQ_M = '---a|';
const UPD_M = '--(a|) ';
const request$ = cold(REQ_M);
expectObservable(holder.update(request$)).toBe(UPD_M);
}));
it('should cancel pending requests', () => {
const LOCAL_VALUES = {
a: VALUES.a,
b: VALUES.b,
j: [VALUES.b, VALUES.b],
};
const REQ1_M = '---a|';
const REQ2_M = 'b| ';
const UPDA_M = '(j|) ';
const REQ1_S = '(^!) ';
const REQ2_S = '(^!) ';
scheduler.withValues(LOCAL_VALUES).run(({ cold, expectObservable, expectSubscriptions }) => {
const request1$ = cold(REQ1_M);
const request2$ = cold(REQ2_M);
expectObservable(
safeForkJoin([
//
holder.update(request1$),
holder.update(request2$),
]),
).toBe(UPDA_M);
expectSubscriptions(request1$.subscriptions).toBe(REQ1_S);
expectSubscriptions(request2$.subscriptions).toBe(REQ2_S);
expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('(a|)');
}),
));
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$,
});
});
it('should propagate errors', () =>
scheduler.run(({ cold, expectObservable }) => {
const REQ_M = '#';
const UPD_M = '#';
const request$ = cold(REQ_M);
expectObservable(holder.update(request$)).toBe(UPD_M);
}));
it('should restart on errors', () =>
scheduler.run(({ cold, expectObservable }) => {
const REQ1_M = '#';
const UPD1_M = '#';
const obs$ = holder.update(cold(REQ1_M));
expectObservable(obs$).toBe(UPD1_M);
const REQ2_M = '-a|';
const UPD2_M = '-a|';
const obs2$ = holder.update(cold(REQ2_M));
expectObservable(obs2$).toBe(UPD2_M);
}));
});
describe('.listen()', () => {
function testListen({ REQUEST_M, LISTEN_M, initial }: any) {
scheduler.run(({ cold, expectObservable }) => {
if (initial) {
holder.set(initial);
}
expectObservable(holder.listen(() => cold(REQUEST_M))).toBe(LISTEN_M);
expectObservable(queries$.pipe(mergeMap((query$) => service.get(MY_IRI, () => query$)))).toBe('aa|');
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const getQuery$ = cold('#');
expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('#');
}),
));
});
describe('.put()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) => {
const putRequest$ = scheduler.createColdObservable('a|');
scheduler.expectObservable(service.put(MY_IRI, putRequest$)).toBe('a|');
}));
it('should not cache the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const putRequest$ = cold('a|');
const putRequest2$ = cold('b|');
// tslint:disable-next-line:rxjs-finnish
const requests$ = cold('ab|', {
a: putRequest$,
b: putRequest2$,
});
}
it('should provide the value', () =>
testListen({
initial: VALUES.a,
REQUEST_M: /**/ ' ',
LISTEN_M: /***/ '(a|)',
}));
it('should cache the value', () =>
testListen({
initial: VALUES.a,
REQUEST_M: /**/ 'b| ',
LISTEN_M: /***/ '(a|)',
}));
it('should propagate errors', () =>
testListen({
REQUEST_M: /**/ '#',
LISTEN_M: /***/ '#',
}));
});
it('.invalidate() should cause the value to be requested again', () =>
expectObservable(
requests$.pipe(
mergeMap((request$: Observable<MyResource>) => service.put(MY_IRI, request$)),
map((x) => _.clone(x)),
),
).toBe('ab|');
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const REQUEST_M = /**/ '(a|)';
const LISTEN_M = /***/ '(a|)';
const requestFactory = jasmine.createSpy('requestFactory');
requestFactory.and.returnValue(cold(REQUEST_M));
return holder
.set(VALUES.a)
.toPromise()
.then(() => holder.invalidate())
.then(() => expectObservable(holder.listen(requestFactory)).toBe(LISTEN_M))
.then(() => expect(requestFactory).toHaveBeenCalled());
}));
const putRequest$ = cold('#');
expectObservable(service.put(MY_IRI, putRequest$)).toBe('#');
}),
));
});
describe('ResourceCache', () => {
beforeEach(() =>
TestBed.configureTestingModule({
providers: [ResourceCache],
describe('.post()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const postRequest$ = cold('a|');
expectObservable(service.post(postRequest$)).toBe('a|');
}),
);
));
it('should be created', inject([ResourceCache], (service: ResourceCache) => {
expect(service).toBeDefined();
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const postRequest$ = cold('#');
expectObservable(service.post(postRequest$)).toBe('#');
}),
));
});
describe('.delete()', () => {
it('should clear the cache on successful fetch', inject([ResourceCache], (service: ResourceCache) => {
const response = new HttpHeaderResponse({ status: 200 });
const values = { r: response, a: VALUES.a, b: VALUES.b };
const sched = scheduler.withValues(values);
sched.run(async ({ cold, expectObservable }) => {
await service.get(MY_IRI, () => cold('a|')).toPromise();
await service.delete(MY_IRI, cold('r|')).toPromise();
return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(b|)');
});
}));
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 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$ });
expectObservable(queries$.pipe(mergeMap((query$) => service.get(MY_IRI, () => query$)))).toBe(
'aa|',
);
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const getQuery$ = cold('#');
expectObservable(service.get(MY_IRI, () => getQuery$)).toBe('#');
}),
));
});
describe('.put()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) => {
const putRequest$ = scheduler.createColdObservable('a|');
scheduler.expectObservable(service.put(MY_IRI, putRequest$)).toBe('a|');
}));
it('should not cache the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const putRequest$ = cold('a|');
const putRequest2$ = cold('b|');
// 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)),
),
).toBe('ab|');
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const putRequest$ = cold('#');
expectObservable(service.put(MY_IRI, putRequest$)).toBe('#');
}),
));
});
describe('.post()', () => {
it('should provide the value', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const postRequest$ = cold('a|');
expectObservable(service.post(postRequest$)).toBe('a|');
}),
));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const postRequest$ = cold('#');
expectObservable(service.post(postRequest$)).toBe('#');
}),
));
});
describe('.delete()', () => {
it('should clear the cache on successful fetch', inject([ResourceCache], (service: ResourceCache) => {
const response = new HttpHeaderResponse({ status: 200 });
const values = { r: response, a: VALUES.a, b: VALUES.b };
const sched = scheduler.withValues(values);
sched.run(async ({ cold, expectObservable }) => {
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const deleteRequest$ = cold('#');
expectObservable(service.delete(MY_IRI, deleteRequest$)).toBe('#');
}),
));
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('r|')).toPromise();
await service
.delete(MY_IRI, cold('#'))
.pipe(catchError((e) => e))
.toPromise();
return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(b|)');
return expectObservable(service.get(MY_IRI, () => cold('b|'))).toBe('(a|)');
});
}));
it('should propagate errors', inject([ResourceCache], (service: ResourceCache) =>
scheduler.run(({ cold, expectObservable }) => {
const deleteRequest$ = cold('#');
expectObservable(service.delete(MY_IRI, deleteRequest$)).toBe('#');
}),
));
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|)');
});
}));
});
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 }) => {
const getAllRequest$ = cold<Collection<MyResource>>('#');
expectObservable(service.getAll(getAllRequest$)).toBe('#');
}),
));
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|');
}));
});
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 }) => {
const getAllRequest$ = cold<Collection<MyResource>>('#');
expectObservable(service.getAll(getAllRequest$)).toBe('#');
}),
));
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|',