forked from hazza/dspace-angular
Cache redesign part 1, and add support for alternative links
This commit is contained in:
200
src/app/core/cache/object-cache.service.spec.ts
vendored
200
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -1,54 +1,73 @@
|
||||
import * as ngrx from '@ngrx/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { empty, of as observableOf } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { Item } from '../shared/item.model';
|
||||
import {
|
||||
AddPatchObjectCacheAction,
|
||||
AddToObjectCacheAction,
|
||||
ApplyPatchObjectCacheAction,
|
||||
RemoveFromObjectCacheAction
|
||||
} from './object-cache.actions';
|
||||
import { AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||
import { Patch } from './object-cache.reducer';
|
||||
|
||||
import { ObjectCacheService } from './object-cache.service';
|
||||
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||
import { RemoveFromIndexBySubstringAction } from '../index/index.actions';
|
||||
import { IndexName } from '../index/index.reducer';
|
||||
import { HALLink } from '../shared/hal-link.model';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
describe('ObjectCacheService', () => {
|
||||
let service: ObjectCacheService;
|
||||
let store: Store<CoreState>;
|
||||
let linkServiceStub;
|
||||
|
||||
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
|
||||
const timestamp = new Date().getTime();
|
||||
const msToLive = 900000;
|
||||
let objectToCache = {
|
||||
type: Item.type,
|
||||
_links: {
|
||||
self: { href: selfLink }
|
||||
}
|
||||
};
|
||||
let selfLink;
|
||||
let anotherLink;
|
||||
let altLink1;
|
||||
let altLink2;
|
||||
let requestUUID;
|
||||
let alternativeLink;
|
||||
let timestamp;
|
||||
let timestamp2;
|
||||
let msToLive;
|
||||
let msToLive2
|
||||
let objectToCache;
|
||||
let cacheEntry;
|
||||
let cacheEntry2;
|
||||
let invalidCacheEntry;
|
||||
const operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
||||
let operations;
|
||||
|
||||
function init() {
|
||||
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
anotherLink = 'https://another.link/endpoint/1234';
|
||||
altLink1 = 'https://alternative.link/endpoint/1234';
|
||||
altLink2 = 'https://alternative.link/endpoint/5678';
|
||||
requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
|
||||
alternativeLink = 'https://rest.api/endpoint/5e4f8a5-be98-4c51-9fd8-6bfedcbd59b7/item';
|
||||
timestamp = new Date().getTime();
|
||||
timestamp2 = new Date().getTime() - 200;
|
||||
msToLive = 900000;
|
||||
msToLive2 = 120000;
|
||||
objectToCache = {
|
||||
type: Item.type,
|
||||
_links: {
|
||||
self: { href: selfLink }
|
||||
self: { href: selfLink },
|
||||
anotherLink: { href: anotherLink }
|
||||
}
|
||||
};
|
||||
cacheEntry = {
|
||||
data: objectToCache,
|
||||
timeAdded: timestamp,
|
||||
msToLive: msToLive
|
||||
timeCompleted: timestamp,
|
||||
msToLive: msToLive,
|
||||
alternativeLinks: [altLink1, altLink2]
|
||||
};
|
||||
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 })
|
||||
cacheEntry2 = {
|
||||
data: objectToCache,
|
||||
timeCompleted: timestamp2,
|
||||
msToLive: msToLive2,
|
||||
alternativeLinks: [altLink2]
|
||||
};
|
||||
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
|
||||
operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -68,47 +87,73 @@ describe('ObjectCacheService', () => {
|
||||
|
||||
describe('add', () => {
|
||||
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
|
||||
service.add(objectToCache, msToLive, requestUUID);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID));
|
||||
service.add(objectToCache, msToLive, requestUUID, alternativeLink);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID, alternativeLink));
|
||||
expect(linkServiceStub.removeResolvedLinks).toHaveBeenCalledWith(objectToCache);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service as any, 'getByHref').and.returnValue(observableOf(cacheEntry));
|
||||
});
|
||||
|
||||
it('should dispatch a REMOVE action with the self link of the object to remove', () => {
|
||||
service.remove(selfLink);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromObjectCacheAction(selfLink));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBySelfLink', () => {
|
||||
it('should return an observable of the cached object with the specified self link and type', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(cacheEntry);
|
||||
};
|
||||
});
|
||||
|
||||
// due to the implementation of spyOn above, this subscribe will be synchronous
|
||||
service.getObjectBySelfLink(selfLink).pipe(first()).subscribe((o) => {
|
||||
expect(o._links.self.href).toBe(selfLink);
|
||||
// this only works if testObj is an instance of TestClass
|
||||
expect(o instanceof Item).toBeTruthy();
|
||||
}
|
||||
);
|
||||
it('should dispatch a REMOVE_BY_SUBSTRING action on the index state for each alternativeLink in the object', () => {
|
||||
service.remove(selfLink);
|
||||
cacheEntry.alternativeLinks.forEach(
|
||||
(link: string) => expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, link)))
|
||||
});
|
||||
|
||||
it('should not return a cached object that has exceeded its time to live', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(invalidCacheEntry);
|
||||
};
|
||||
it('should dispatch a REMOVE_BY_SUBSTRING action on the index state for each _links in the object, except the self link', () => {
|
||||
service.remove(selfLink);
|
||||
Object.entries(objectToCache._links).forEach(([key, value]: [string, HALLink]) => {
|
||||
if (key !== 'self') {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, value.href))
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByHref', () => {
|
||||
describe('if getBySelfLink emits a valid object and getByAlternativeLink emits undefined', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(cacheEntry));
|
||||
spyOn(service as any, 'getByAlternativeLink').and.returnValue(observableOf(undefined));
|
||||
});
|
||||
|
||||
let getObsHasFired = false;
|
||||
const subscription = service.getObjectBySelfLink(selfLink).subscribe((o) => getObsHasFired = true);
|
||||
expect(getObsHasFired).toBe(false);
|
||||
subscription.unsubscribe();
|
||||
it('should return the object emitted by getBySelfLink', () => {
|
||||
const result = service.getByHref(selfLink);
|
||||
getTestScheduler().expectObservable(result).toBe('(a|)', { a: cacheEntry })
|
||||
});
|
||||
});
|
||||
|
||||
describe('if getBySelfLink emits undefined and getByAlternativeLink a valid object', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(undefined));
|
||||
spyOn(service as any, 'getByAlternativeLink').and.returnValue(observableOf(cacheEntry));
|
||||
});
|
||||
|
||||
it('should return the object emitted by getByAlternativeLink', () => {
|
||||
const result = service.getByHref(selfLink);
|
||||
getTestScheduler().expectObservable(result).toBe('(a|)', { a: cacheEntry })
|
||||
});
|
||||
});
|
||||
|
||||
describe('if getBySelfLink emits an invalid and getByAlternativeLink a valid object', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(cacheEntry));
|
||||
spyOn(service as any, 'getByAlternativeLink').and.returnValue(observableOf(cacheEntry2));
|
||||
});
|
||||
|
||||
it('should return the object emitted by getByAlternativeLink', () => {
|
||||
const result = service.getByHref(selfLink);
|
||||
getTestScheduler().expectObservable(result).toBe('(a|)', { a: cacheEntry2 })
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,7 +162,7 @@ describe('ObjectCacheService', () => {
|
||||
const item = Object.assign(new Item(), {
|
||||
_links: { self: { href: selfLink } }
|
||||
});
|
||||
spyOn(service, 'getObjectBySelfLink').and.returnValue(observableOf(item));
|
||||
spyOn(service, 'getObjectByHref').and.returnValue(observableOf(item));
|
||||
|
||||
service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => {
|
||||
expect(arr[0]._links.self.href).toBe(selfLink);
|
||||
@@ -127,34 +172,52 @@ describe('ObjectCacheService', () => {
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('should return true if the object with the supplied self link is cached and still valid', () => {
|
||||
|
||||
describe('getByHref emits an object', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'getByHref').and.returnValue(observableOf(cacheEntry))
|
||||
});
|
||||
|
||||
it('should return true', () => {
|
||||
expect(service.hasByHref(selfLink)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByHref emits nothing', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'getByHref').and.returnValue(empty())
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
expect(service.hasByHref(selfLink)).toBe(false);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('getBySelfLink', () => {
|
||||
it('should return the entry returned by the select method', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(cacheEntry);
|
||||
};
|
||||
});
|
||||
|
||||
expect(service.hasBySelfLink(selfLink)).toBe(true);
|
||||
getTestScheduler().expectObservable((service as any).getBySelfLink(selfLink)).toBe('(a|)', { a: cacheEntry });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if the object with the supplied self link isn\'t cached', () => {
|
||||
describe('getByAlternativeLink', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service as any, 'getBySelfLink').and.returnValue(observableOf(cacheEntry));
|
||||
});
|
||||
it('should call getBySelfLink with the value returned by the select method', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(undefined);
|
||||
return () => observableOf(anotherLink);
|
||||
};
|
||||
});
|
||||
|
||||
expect(service.hasBySelfLink(selfLink)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the object with the supplied self link is cached but has exceeded its time to live', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(invalidCacheEntry);
|
||||
};
|
||||
});
|
||||
|
||||
expect(service.hasBySelfLink(selfLink)).toBe(false);
|
||||
(service as any).getByAlternativeLink(selfLink).subscribe();
|
||||
expect((service as any).getBySelfLink).toHaveBeenCalledWith(anotherLink);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,7 +232,8 @@ describe('ObjectCacheService', () => {
|
||||
cacheEntry.patches = [
|
||||
{
|
||||
operations: operations
|
||||
} as Patch];
|
||||
} as Patch
|
||||
];
|
||||
const result = (service as any).isDirty(cacheEntry);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
Reference in New Issue
Block a user