import { fakeAsync, flush, TestBed, waitForAsync, } from '@angular/core/testing'; import { Store, StoreModule, } from '@ngrx/store'; import { MockStore, provideMockStore, } from '@ngrx/store/testing'; import { cold, getTestScheduler, } from 'jasmine-marbles'; import { EMPTY, Observable, of as observableOf, } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { storeModuleConfig } from '../../app.reducer'; import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; import { defaultUUID, getMockUUIDService, } from '../../shared/mocks/uuid.service.mock'; import { ObjectCacheService } from '../cache/object-cache.service'; import { coreReducers } from '../core.reducers'; import { CoreState } from '../core-state.model'; import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction, RequestStaleAction, } from './request.actions'; import { DeleteRequest, GetRequest, HeadRequest, OptionsRequest, PatchRequest, PostRequest, PutRequest, } from './request.models'; import { RequestService } from './request.service'; import { RequestEntry } from './request-entry.model'; import { RequestEntryState } from './request-entry-state.model'; import { RestRequest } from './rest-request.model'; describe('RequestService', () => { let scheduler: TestScheduler; let service: RequestService; let serviceAsAny: any; let objectCache: ObjectCacheService; let uuidService: UUIDService; let store: Store; let mockStore: MockStore; const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'; const testHref = 'https://rest.api/endpoint/selfLink'; const testGetRequest = new GetRequest(testUUID, testHref); const testPostRequest = new PostRequest(testUUID, testHref); const testPutRequest = new PutRequest(testUUID, testHref); const testDeleteRequest = new DeleteRequest(testUUID, testHref); const testOptionsRequest = new OptionsRequest(testUUID, testHref); const testHeadRequest = new HeadRequest(testUUID, testHref); const testPatchRequest = new PatchRequest(testUUID, testHref); const initialState: any = { core: { 'cache/object': {}, 'cache/syncbuffer': {}, 'cache/object-updates': {}, 'data/request': {}, 'index': {}, }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ StoreModule.forRoot(coreReducers, storeModuleConfig), ], providers: [ provideMockStore({ initialState }), { provide: RequestService, useValue: service }, ], }).compileComponents(); })); beforeEach(() => { scheduler = getTestScheduler(); objectCache = getMockObjectCacheService(); (objectCache.hasByHref as any).and.returnValue(false); uuidService = getMockUUIDService(); store = TestBed.inject(Store); mockStore = store as MockStore; mockStore.setState(initialState); service = new RequestService( objectCache, uuidService, store, ); serviceAsAny = service as any; }); describe('generateRequestId', () => { it('should generate a new request ID', () => { const result = service.generateRequestId(); const expected = `client/${defaultUUID}`; expect(result).toBe(expected); }); }); describe('isPending', () => { describe('before the request is configured', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValue(observableOf(undefined)); }); it('should return false', () => { const result = service.isPending(testGetRequest); const expected = false; expect(result).toBe(expected); }); }); describe('when the request has been configured but hasn\'t reached the store yet', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValue(observableOf(undefined)); serviceAsAny.requestsOnTheirWayToTheStore = [testHref]; }); it('should return true', () => { const result = service.isPending(testGetRequest); const expected = true; expect(result).toBe(expected); }); }); describe('when the request has reached the store, before the server responds', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValue(observableOf({ state: RequestEntryState.ResponsePending, } as RequestEntry)); }); it('should return true', () => { const result = service.isPending(testGetRequest); const expected = true; expect(result).toBe(expected); }); }); describe('after the server responds', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValues(observableOf({ state: RequestEntryState.Success, } as RequestEntry)); }); it('should return false', () => { const result = service.isPending(testGetRequest); const expected = false; expect(result).toBe(expected); }); }); }); describe('getByUUID', () => { describe('if the request with the specified UUID exists in the store', () => { let entry; beforeEach(() => { entry = { state: RequestEntryState.Success, response: { timeCompleted: new Date().getTime(), }, request: new GetRequest('request-uuid', 'request-href'), }; const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'data/request': { '5f2a0d2a-effa-4d54-bd54-5663b960f9eb': entry, }, 'index': { 'get-request/configured-to-cache-uuid': { '5f2a0d2a-effa-4d54-bd54-5663b960f9eb': '5f2a0d2a-effa-4d54-bd54-5663b960f9eb', }, }, }), }); mockStore.setState(state); }); it('should return an Observable of the RequestEntry', () => { const result = service.getByUUID(testUUID); const expected = cold('b', { b: entry, }); expect(result).toBeObservable(expected); }); }); describe(`if the request with the specified UUID doesn't exist in the store `, () => { beforeEach(() => { // No direct hit in the request cache // No hit in the index // So no mapped hit in the request cache mockStore.setState(initialState); }); it('should return an Observable of undefined', () => { const result = service.getByUUID(testUUID); const expected = cold('a', { a: undefined }); expect(result).toBeObservable(expected); }); }); }); describe('getByHref', () => { describe('when the request with the specified href exists in the store', () => { let entry; beforeEach(() => { entry = { state: RequestEntryState.Success, response: { timeCompleted: new Date().getTime(), }, request: new GetRequest('request-uuid', 'request-href'), }; const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'data/request': { '5f2a0d2a-effa-4d54-bd54-5663b960f9eb': entry, }, 'index': { 'get-request/configured-to-cache-uuid': { '5f2a0d2a-effa-4d54-bd54-5663b960f9eb': '5f2a0d2a-effa-4d54-bd54-5663b960f9eb', }, 'get-request/href-to-uuid': { 'https://rest.api/endpoint/selfLink': '5f2a0d2a-effa-4d54-bd54-5663b960f9eb', }, }, }), }); mockStore.setState(state); }); it('should return an Observable of the RequestEntry', () => { const result = service.getByHref(testHref); const expected = cold('c', { c: entry, }); expect(result).toBeObservable(expected); }); }); describe('when the request with the specified href doesn\'t exist in the store', () => { beforeEach(() => { // No direct hit in the request cache // No hit in the index // So no mapped hit in the request cache mockStore.setState(initialState); }); it('should return an Observable of undefined', () => { const result = service.getByHref(testHref); const expected = cold('c', { c: undefined, }); expect(result).toBeObservable(expected); }); }); }); describe('send', () => { beforeEach(() => { spyOn(serviceAsAny, 'dispatchRequest'); }); describe('when the request is a GET request', () => { let request: RestRequest; beforeEach(() => { request = testGetRequest; }); it('should track it on it\'s way to the store', () => { spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore'); spyOn(serviceAsAny, 'shouldDispatchRequest').and.returnValue(true); service.send(request); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).toHaveBeenCalledWith(request); }); describe('and it isn\'t cached or pending', () => { beforeEach(() => { spyOn(serviceAsAny, 'shouldDispatchRequest').and.returnValue(true); }); it('should dispatch the request', () => { scheduler.schedule(() => service.send(request, true)); scheduler.flush(); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request); }); }); describe('and it is already cached or pending', () => { beforeEach(() => { spyOn(serviceAsAny, 'shouldDispatchRequest').and.returnValue(false); }); it('shouldn\'t dispatch the request', () => { service.send(request, true); expect(serviceAsAny.dispatchRequest).not.toHaveBeenCalled(); }); }); }); describe('when the request isn\'t a GET request', () => { it('should dispatch the request', () => { service.send(testPostRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest); service.send(testPutRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest); service.send(testDeleteRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest); service.send(testOptionsRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest); service.send(testHeadRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest); service.send(testPatchRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest); }); }); }); const expectAllNonGetRequestsToBeTrue = () => { expect(serviceAsAny.shouldDispatchRequest(testPostRequest, true)).toBeTrue(); expect(serviceAsAny.shouldDispatchRequest(testPutRequest, true)).toBeTrue(); expect(serviceAsAny.shouldDispatchRequest(testDeleteRequest, true)).toBeTrue(); expect(serviceAsAny.shouldDispatchRequest(testOptionsRequest, true)).toBeTrue(); expect(serviceAsAny.shouldDispatchRequest(testHeadRequest, true)).toBeTrue(); expect(serviceAsAny.shouldDispatchRequest(testPatchRequest, true)).toBeTrue(); }; describe('shouldDispatchRequest', () => { describe(`when it's not a GET request`, () => { describe('and it is pending', () => { beforeEach(() => { spyOn(service, 'isPending').and.returnValue(true); }); it('should return true', expectAllNonGetRequestsToBeTrue); }); describe(`and it isn't pending`, () => { beforeEach(() => { spyOn(service, 'isPending').and.returnValue(false); }); describe(`and useCachedVersionIfAvailable is false`, () => { it('should return true', expectAllNonGetRequestsToBeTrue); }); describe(`and useCachedVersionIfAvailable is true`, () => { describe('and it is cached', () => { describe('in the ObjectCache', () => { beforeEach(() => { (objectCache.getByHref as any).and.returnValue(observableOf({ requestUUID: 'some-uuid' })); spyOn(serviceAsAny, 'hasByHref').and.returnValue(false); spyOn(serviceAsAny, 'hasByUUID').and.returnValue(true); }); it('should return true', expectAllNonGetRequestsToBeTrue); }); describe('in the request cache', () => { beforeEach(() => { (objectCache.getByHref as any).and.returnValue(observableOf(undefined)); spyOn(serviceAsAny, 'hasByHref').and.returnValues(true); spyOn(serviceAsAny, 'hasByUUID').and.returnValue(false); }); it('should return true', expectAllNonGetRequestsToBeTrue); }); }); describe(`and it isn't cached`, () => { beforeEach(() => { (objectCache.getByHref as any).and.returnValue(EMPTY); spyOn(serviceAsAny, 'hasByHref').and.returnValues(false); spyOn(serviceAsAny, 'hasByUUID').and.returnValue(false); }); it('should return true', expectAllNonGetRequestsToBeTrue); }); }); }); }); describe(`when it is a GET request`, () => { describe('and it is pending', () => { beforeEach(() => { spyOn(service, 'isPending').and.returnValue(true); }); it('should return false', () => { const result = serviceAsAny.shouldDispatchRequest(testGetRequest, false); const expected = false; expect(result).toEqual(expected); }); }); describe(`and it isn't pending`, () => { beforeEach(() => { spyOn(service, 'isPending').and.returnValue(false); }); describe(`and useCachedVersionIfAvailable is false`, () => { it(`should return true`, () => { const result = serviceAsAny.shouldDispatchRequest(testGetRequest, false); const expected = true; expect(result).toEqual(expected); }); }); describe(`and useCachedVersionIfAvailable is true`, () => { describe('and it is cached', () => { describe('in the ObjectCache', () => { beforeEach(() => { (objectCache.getByHref as any).and.returnValue(observableOf({ requestUUIDs: ['some-uuid'] })); spyOn(serviceAsAny, 'hasByHref').and.returnValue(false); spyOn(serviceAsAny, 'hasByUUID').and.returnValue(true); }); it('should return false', () => { const result = serviceAsAny.shouldDispatchRequest(testGetRequest, true); const expected = false; expect(result).toEqual(expected); }); }); describe('in the request cache', () => { beforeEach(() => { (objectCache.getByHref as any).and.returnValue(observableOf(undefined)); spyOn(serviceAsAny, 'hasByHref').and.returnValues(true); spyOn(serviceAsAny, 'hasByUUID').and.returnValue(false); }); it('should return false', () => { const result = serviceAsAny.shouldDispatchRequest(testGetRequest, true); const expected = false; expect(result).toEqual(expected); }); }); }); describe(`and it isn't cached`, () => { beforeEach(() => { (objectCache.getByHref as any).and.returnValue(EMPTY); spyOn(serviceAsAny, 'hasByHref').and.returnValues(false); spyOn(serviceAsAny, 'hasByUUID').and.returnValue(false); }); it('should return true', () => { const result = serviceAsAny.shouldDispatchRequest(testGetRequest, true); const expected = true; expect(result).toEqual(expected); }); }); }); }); }); }); describe('dispatchRequest', () => { let dispatchSpy: jasmine.Spy; beforeEach(() => { dispatchSpy = spyOn(store, 'dispatch'); }); it('should dispatch a RequestConfigureAction', fakeAsync(() => { const request = testGetRequest; serviceAsAny.dispatchRequest(request); flush(); const firstAction = dispatchSpy.calls.argsFor(0)[0]; expect(firstAction).toBeInstanceOf(RequestConfigureAction); expect(firstAction.payload).toEqual(request); })); it('should dispatch a RequestExecuteAction', fakeAsync(() => { const request = testGetRequest; serviceAsAny.dispatchRequest(request); flush(); const secondAction = dispatchSpy.calls.argsFor(1)[0]; expect(secondAction).toBeInstanceOf(RequestExecuteAction); expect(secondAction.payload).toEqual(request.uuid); })); describe('when it\'s not a GET request', () => { it('shouldn\'t track it', () => { spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore'); serviceAsAny.dispatchRequest(testPostRequest); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); serviceAsAny.dispatchRequest(testPutRequest); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); serviceAsAny.dispatchRequest(testDeleteRequest); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); serviceAsAny.dispatchRequest(testOptionsRequest); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); serviceAsAny.dispatchRequest(testHeadRequest); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); serviceAsAny.dispatchRequest(testPatchRequest); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); }); }); }); describe('trackRequestsOnTheirWayToTheStore', () => { let request: GetRequest; let entry; beforeEach(() => { request = testGetRequest; entry = { state: RequestEntryState.Success, response: { timeCompleted: new Date().getTime(), }, request: request, }; }); describe('when the method is called with a new request', () => { it('should start tracking the request', () => { expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy(); serviceAsAny.trackRequestsOnTheirWayToTheStore(request); expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeTruthy(); }); }); describe('when the request is added to the store', () => { it('should stop tracking the request', () => { spyOn(serviceAsAny, 'getByHref').and.returnValue(observableOf(entry)); serviceAsAny.trackRequestsOnTheirWayToTheStore(request); expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy(); }); }); }); describe('hasByHref', () => { describe('when nothing is returned by getByHref', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValue(EMPTY); }); it('hasByHref should return false', () => { const result = service.hasByHref(''); expect(result).toBe(false); }); }); describe('when the RequestEntry is undefined', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValue(observableOf(undefined)); }); it('hasByHref should return false', () => { const result = service.hasByHref('', false); expect(result).toBe(false); }); }); describe('when the RequestEntry is not undefined', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValue(observableOf({} as any)); }); it('hasByHref should return true', () => { const result = service.hasByHref('', false); expect(result).toBe(true); }); }); }); describe('uriEncodeBody', () => { it('should properly encode the body', () => { const body = { 'property1': 'multiple\nlines\nto\nsend', 'property2': 'sp&ci@l characters', 'sp&ci@l-chars in prop': 'test123', }; const queryParams = service.uriEncodeBody(body); expect(queryParams).toEqual( 'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123', ); }); it('should properly encode the body with an array', () => { const body = { 'property1': 'multiple\nlines\nto\nsend', 'property2': 'sp&ci@l characters', 'sp&ci@l-chars in prop': 'test123', 'arrayParam': ['arrayValue1', 'arrayValue2'], }; const queryParams = service.uriEncodeBody(body); expect(queryParams).toEqual( 'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123&arrayParam=arrayValue1&arrayParam=arrayValue2', ); }); }); describe('setStaleByUUID', () => { let dispatchSpy: jasmine.Spy; let getByUUIDSpy: jasmine.Spy; beforeEach(() => { dispatchSpy = spyOn(store, 'dispatch'); getByUUIDSpy = spyOn(service, 'getByUUID').and.callThrough(); }); it('should dispatch a RequestStaleAction', () => { service.setStaleByUUID('something'); const firstAction = dispatchSpy.calls.argsFor(0)[0]; expect(firstAction).toBeInstanceOf(RequestStaleAction); expect(firstAction.payload).toEqual({ uuid: 'something' }); }); it('should return an Observable that emits true as soon as the request is stale', fakeAsync(() => { dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache a: { state: RequestEntryState.ResponsePending }, b: { state: RequestEntryState.Success }, c: { state: RequestEntryState.SuccessStale }, d: { state: RequestEntryState.Error }, })); const done$ = service.setStaleByUUID('something'); expect(done$).toBeObservable(cold('-----(t|)', { t: true })); })); }); describe('setStaleByHref', () => { const uuid = 'c574a42c-4818-47ac-bbe1-6c3cd622c81f'; const href = 'https://rest.api/some/object'; const freshRE: any = { request: { uuid, href }, state: RequestEntryState.Success, }; const staleRE: any = { request: { uuid, href }, state: RequestEntryState.SuccessStale, }; it(`should call getByHref to retrieve the RequestEntry matching the href`, () => { spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE)); service.setStaleByHref(href); expect(service.getByHref).toHaveBeenCalledWith(href); }); it(`should dispatch a RequestStaleAction for the RequestEntry returned by getByHref`, (done: DoneFn) => { spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE)); spyOn(store, 'dispatch'); service.setStaleByHref(href).subscribe(() => { const requestStaleAction = new RequestStaleAction(uuid); requestStaleAction.lastUpdated = jasmine.any(Number) as any; expect(store.dispatch).toHaveBeenCalledWith(requestStaleAction); done(); }); }); it(`should emit true when the request in the store is stale`, () => { spyOn(service, 'getByHref').and.returnValue(cold('a-b', { a: freshRE, b: staleRE, })); const result$ = service.setStaleByHref(href); expect(result$).toBeObservable(cold('--(c|)', { c: true })); }); }); describe('setStaleByHrefSubstring', () => { let dispatchSpy: jasmine.Spy; let getByUUIDSpy: jasmine.Spy; beforeEach(() => { dispatchSpy = spyOn(store, 'dispatch'); getByUUIDSpy = spyOn(service, 'getByUUID').and.callThrough(); }); describe('with an empty/no matching requests in the state', () => { it('should return true', () => { const done$: Observable = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink'); expect(done$).toBeObservable(cold('(a|)', { a: true })); }); }); describe('with a matching request in the state', () => { beforeEach(() => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'index': { 'get-request/href-to-uuid': { 'https://rest.api/endpoint/selfLink': '5f2a0d2a-effa-4d54-bd54-5663b960f9eb', }, }, }), }); mockStore.setState(state); }); it('should return an Observable that emits true as soon as the request is stale', () => { dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache a: { state: RequestEntryState.ResponsePending }, b: { state: RequestEntryState.Success }, c: { state: RequestEntryState.SuccessStale }, d: { state: RequestEntryState.Error }, })); const done$: Observable = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink'); expect(done$).toBeObservable(cold('-----(a|)', { a: true })); }); }); }); });