From c32e4ad7c794ce76b7441411abcf6139f1898b90 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Mon, 12 Sep 2022 14:15:28 +0200 Subject: [PATCH] 93803: Add tests for create, search & put --- src/app/core/data/base/create-data.spec.ts | 119 ++++++++++++++--- src/app/core/data/base/put-data.spec.ts | 70 +++++++--- src/app/core/data/base/search-data.spec.ts | 141 +++++++++++---------- src/app/core/data/base/search-data.ts | 2 +- 4 files changed, 229 insertions(+), 103 deletions(-) diff --git a/src/app/core/data/base/create-data.spec.ts b/src/app/core/data/base/create-data.spec.ts index ceefd3c51d..0ae31e37ab 100644 --- a/src/app/core/data/base/create-data.spec.ts +++ b/src/app/core/data/base/create-data.spec.ts @@ -16,10 +16,11 @@ import { NotificationsService } from '../../../shared/notifications/notification import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; import { RemoteData } from '../remote-data'; import { RequestEntryState } from '../request-entry-state.model'; +import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { RestRequestMethod } from '../rest-request-method'; const endpoint = 'https://rest.api/core'; @@ -31,10 +32,10 @@ class TestService extends CreateDataImpl { protected halService: HALEndpointService, protected notificationsService: NotificationsService, ) { - super(undefined, requestService, rdbService, objectCache, halService, notificationsService, undefined); + super('test', requestService, rdbService, objectCache, halService, notificationsService, undefined); } - public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { + public getEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { return observableOf(endpoint); } } @@ -46,10 +47,14 @@ describe('CreateDataImpl', () => { let rdbService; let objectCache; let notificationsService; - let selfLink; - let linksToFollow; - let testScheduler; let remoteDataMocks; + let obj; + + let MOCK_SUCCEEDED_RD; + let MOCK_FAILED_RD; + + let buildFromRequestUUIDSpy: jasmine.Spy; + let createOnEndpointSpy: jasmine.Spy; function initTestService(): TestService { requestService = getMockRequestService(); @@ -67,19 +72,14 @@ describe('CreateDataImpl', () => { /* empty */ }, } as any; - notificationsService = {} as NotificationsService; - selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; - linksToFollow = [ - followLink('a'), - followLink('b'), - ]; - - testScheduler = new TestScheduler((actual, expected) => { - // asserting the two objects are equal - // e.g. using chai. - expect(actual).toEqual(expected); + notificationsService = jasmine.createSpyObj('notificationsService', { + error: undefined, }); + obj = { + uuid: '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7', + }; + const timeStamp = new Date().getTime(); const msToLive = 15 * 60 * 1000; const payload = { foo: 'bar' }; @@ -106,7 +106,88 @@ describe('CreateDataImpl', () => { beforeEach(() => { service = initTestService(); + + buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough(); + createOnEndpointSpy = spyOn(service, 'createOnEndpoint').and.callThrough(); + + MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({}); + MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong'); }); - // todo: add specs (there were no ceate specs in original DataService suite!) + describe('create', () => { + it('should POST the object to the root endpoint with the given parameters and return the remote data', (done) => { + const params = [ + new RequestParam('abc', 123), new RequestParam('def', 456) + ]; + buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Success)); + + service.create(obj, ...params).subscribe(out => { + expect(createOnEndpointSpy).toHaveBeenCalledWith(obj, jasmine.anything()); + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.POST, + uuid: requestService.generateRequestId(), + href: 'https://rest.api/core?abc=123&def=456', + body: JSON.stringify(obj), + })); + expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId()); + expect(out).toEqual(remoteDataMocks.Success); + done(); + }); + }); + }); + + describe('createOnEndpoint', () => { + beforeEach(() => { + buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Success)); + }); + + it('should send a POST request with the object as JSON', (done) => { + service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.POST, + body: JSON.stringify(obj), + })); + done(); + }); + }); + + it('should send the POST request to the given endpoint', (done) => { + + service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.POST, + href: 'https://rest.api/core/custom?search', + })); + done(); + }); + }); + + it('should return the remote data for the sent request', (done) => { + service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.POST, + uuid: requestService.generateRequestId(), + })); + expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId()); + expect(notificationsService.error).not.toHaveBeenCalled(); + expect(out).toEqual(remoteDataMocks.Success); + done(); + }); + }); + + it('should show an error notification if the request fails', (done) => { + buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Error)); + + service.createOnEndpoint(obj, observableOf('https://rest.api/core/custom?search')).subscribe(out => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.POST, + uuid: requestService.generateRequestId(), + })); + expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId()); + expect(notificationsService.error).toHaveBeenCalled(); + expect(out).toEqual(remoteDataMocks.Error); + done(); + }); + }); + }); }); diff --git a/src/app/core/data/base/put-data.spec.ts b/src/app/core/data/base/put-data.spec.ts index 01b5caea5b..5e2f836776 100644 --- a/src/app/core/data/base/put-data.spec.ts +++ b/src/app/core/data/base/put-data.spec.ts @@ -15,11 +15,11 @@ import { Observable, of as observableOf } from 'rxjs'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; import { RemoteData } from '../remote-data'; import { RequestEntryState } from '../request-entry-state.model'; import { PutDataImpl } from './put-data'; +import { RestRequestMethod } from '../rest-request-method'; +import { DSpaceObject } from '../../shared/dspace-object.model'; const endpoint = 'https://rest.api/core'; @@ -45,10 +45,11 @@ describe('PutDataImpl', () => { let rdbService; let objectCache; let selfLink; - let linksToFollow; - let testScheduler; let remoteDataMocks; + let obj; + let buildFromRequestUUIDSpy: jasmine.Spy; + function initTestService(): TestService { requestService = getMockRequestService(); halService = new HALEndpointServiceStub('url') as any; @@ -66,16 +67,6 @@ describe('PutDataImpl', () => { }, } as any; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; - linksToFollow = [ - followLink('a'), - followLink('b'), - ]; - - testScheduler = new TestScheduler((actual, expected) => { - // asserting the two objects are equal - // e.g. using chai. - expect(actual).toEqual(expected); - }); const timeStamp = new Date().getTime(); const msToLive = 15 * 60 * 1000; @@ -102,7 +93,56 @@ describe('PutDataImpl', () => { beforeEach(() => { service = initTestService(); + + obj = Object.assign(new DSpaceObject(), { + uuid: '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7', + metadata: { // recognized properties will be serialized + ['dc.title']: [ + { language: 'en', value: 'some object' }, + ] + }, + data: [ 1, 2, 3, 4 ], // unrecognized properties won't be serialized + _links: { self: { href: selfLink } }, + }); + + + buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.returnValue(observableOf(remoteDataMocks.Success)); }); - // todo: add specs (there were no put specs in original DataService suite!) + describe('put', () => { + it('should send a PUT request with the serialized object', (done) => { + service.put(obj).subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.PUT, + body: { // _links are not serialized + uuid: obj.uuid, + metadata: obj.metadata + }, + })); + done(); + }); + }); + + it('should send the PUT request to the object\'s self link', (done) => { + service.put(obj).subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.PUT, + href: selfLink, + })); + done(); + }); + }); + + it('should return the remote data for the sent request', (done) => { + service.put(obj).subscribe(out => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ + method: RestRequestMethod.PUT, + uuid: requestService.generateRequestId(), + })); + expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith(requestService.generateRequestId()); + expect(out).toEqual(remoteDataMocks.Success); + done(); + }); + }); + }); }); diff --git a/src/app/core/data/base/search-data.spec.ts b/src/app/core/data/base/search-data.spec.ts index 7abf26b5b8..37ad589740 100644 --- a/src/app/core/data/base/search-data.spec.ts +++ b/src/app/core/data/base/search-data.spec.ts @@ -5,21 +5,13 @@ * * http://www.dspace.org/license/ */ -import { SearchData, SearchDataImpl } from './search-data'; -import createSpyObj = jasmine.createSpyObj; +import { constructSearchEndpointDefault, SearchData, SearchDataImpl } from './search-data'; import { FindListOptions } from '../find-list-options.model'; import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../../cache/object-cache.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { TestScheduler } from 'rxjs/testing'; -import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; +import createSpyObj = jasmine.createSpyObj; /** * Tests whether calls to `SearchData` methods are correctly patched through in a concrete data service that implements it @@ -61,80 +53,42 @@ export function testSearchDataImplementation(service: SearchData, methods = const endpoint = 'https://rest.api/core'; -class TestService extends SearchDataImpl { - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - ) { - super(undefined, requestService, rdbService, objectCache, halService, undefined); - } - - public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return observableOf(endpoint); - } -} - describe('SearchDataImpl', () => { - let service: TestService; + let service: SearchDataImpl; let requestService; let halService; let rdbService; - let objectCache; - let selfLink; let linksToFollow; - let testScheduler; - let remoteDataMocks; - function initTestService(): TestService { + let constructSearchEndpointSpy; + let options; + + function initTestService(): SearchDataImpl { requestService = getMockRequestService(); - halService = new HALEndpointServiceStub('url') as any; + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf(endpoint), + }); rdbService = getMockRemoteDataBuildService(); - objectCache = { - - addPatch: () => { - /* empty */ - }, - getObjectBySelfLink: () => { - /* empty */ - }, - getByHref: () => { - /* empty */ - }, - } as any; - selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), followLink('b'), ]; - testScheduler = new TestScheduler((actual, expected) => { - // asserting the two objects are equal - // e.g. using chai. - expect(actual).toEqual(expected); + constructSearchEndpointSpy = jasmine.createSpy('constructSearchEndpointSpy').and.callFake(constructSearchEndpointDefault); + + options = Object.assign(new FindListOptions(), { + elementsPerPage: 5, + currentPage: 3, }); - const timeStamp = new Date().getTime(); - const msToLive = 15 * 60 * 1000; - const payload = { foo: 'bar' }; - const statusCodeSuccess = 200; - const statusCodeError = 404; - const errorMessage = 'not found'; - remoteDataMocks = { - RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), - ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), - Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), - SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), - Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), - ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), - }; - - return new TestService( + return new SearchDataImpl( + 'test', requestService, rdbService, - objectCache, + undefined, halService, + undefined, + constructSearchEndpointSpy, ); } @@ -142,5 +96,56 @@ describe('SearchDataImpl', () => { service = initTestService(); }); - // todo: add specs (there were no search specs in original DataService suite!) + describe('getSearchEndpoint', () => { + it('should return the search endpoint for the given method', (done) => { + (service as any).getSearchEndpoint('testMethod').subscribe(searchEndpoint => { + expect(halService.getEndpoint).toHaveBeenCalledWith('test'); + expect(searchEndpoint).toBe('https://rest.api/core/search/testMethod'); + done(); + }); + }); + + it('should use constructSearchEndpoint to construct the search endpoint', (done) => { + (service as any).getSearchEndpoint('testMethod').subscribe(() => { + expect(constructSearchEndpointSpy).toHaveBeenCalledWith('https://rest.api/core', 'testMethod'); + done(); + }); + }); + }); + + describe('getSearchByHref', () => { + beforeEach(() => { + spyOn(service as any, 'getSearchEndpoint').and.callThrough(); + spyOn(service, 'buildHrefFromFindOptions').and.callThrough(); + }); + + it('should return the search endpoint with additional query parameters', (done) => { + service.getSearchByHref('testMethod', options, ...linksToFollow).subscribe(href => { + expect((service as any).getSearchEndpoint).toHaveBeenCalledWith('testMethod'); + expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith( + 'https://rest.api/core/search/testMethod', + options, + [], + ...linksToFollow, + ); + + expect(href).toBe('https://rest.api/core/search/testMethod?page=2&size=5&embed=a&embed=b'); + + done(); + }); + }); + }); + + describe('searchBy', () => { + it('should patch getSearchEndpoint into findListByHref and return the result', () => { + spyOn(service, 'getSearchByHref').and.returnValue('endpoint' as any); + spyOn(service, 'findListByHref').and.returnValue('resulting remote data' as any); + + const out: any = service.searchBy('testMethod', options, false, true, ...linksToFollow); + + expect(service.getSearchByHref).toHaveBeenCalledWith('testMethod', options, ...linksToFollow); + expect(service.findListByHref).toHaveBeenCalledWith('endpoint', undefined, false, true); + expect(out).toBe('resulting remote data'); + }); + }); }); diff --git a/src/app/core/data/base/search-data.ts b/src/app/core/data/base/search-data.ts index 12cae77c3a..ba8fac9888 100644 --- a/src/app/core/data/base/search-data.ts +++ b/src/app/core/data/base/search-data.ts @@ -110,7 +110,7 @@ export class SearchDataImpl extends BaseDataService[]): Observable>> { const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); - return this.findListByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + return this.findListByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale); } /**