diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 0dc8abf860..8c2b4026e0 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -22,7 +22,7 @@ import { } from './auth.actions'; import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; import { AuthService } from './auth.service'; -import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; +import { AuthState } from './auth.reducer'; import { EPersonMock } from '../../shared/testing/eperson-mock'; @@ -30,7 +30,7 @@ describe('AuthEffects', () => { let authEffects: AuthEffects; let actions: Observable; let authServiceStub; - const store: Store = jasmine.createSpyObj('store', { + const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ dispatch: {}, /* tslint:enable:no-empty */ diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index d39c0a4590..c461148eea 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -73,7 +73,7 @@ describe('AuthService test', () => { { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, { provide: ActivatedRoute, useValue: routeStub }, - {provide: Store, useValue: mockStore}, + { provide: Store, useValue: mockStore }, { provide: RemoteDataBuildService, useValue: rdbService }, CookieService, AuthService diff --git a/src/app/core/json-patch/json-patch-operations.effects.spec.ts b/src/app/core/json-patch/json-patch-operations.effects.spec.ts new file mode 100644 index 0000000000..a21f8ec46a --- /dev/null +++ b/src/app/core/json-patch/json-patch-operations.effects.spec.ts @@ -0,0 +1,58 @@ +import { TestBed } from '@angular/core/testing'; + +import { cold, hot } from 'jasmine-marbles'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Store } from '@ngrx/store'; +import { Observable, of as observableOf } from 'rxjs'; + +import { JsonPatchOperationsEffects } from './json-patch-operations.effects'; +import { JsonPatchOperationsState } from './json-patch-operations.reducer'; + +import { FlushPatchOperationsAction, JsonPatchOperationsActionTypes } from './json-patch-operations.actions'; + +describe('JsonPatchOperationsEffects test suite', () => { + let jsonPatchOperationsEffects: JsonPatchOperationsEffects; + let actions: Observable; + const store: Store = jasmine.createSpyObj('store', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: observableOf(true) + }); + const testJsonPatchResourceType = 'testResourceType'; + const testJsonPatchResourceId = 'testResourceId'; + + function init() { + } + + beforeEach(() => { + init(); + TestBed.configureTestingModule({ + providers: [ + JsonPatchOperationsEffects, + {provide: Store, useValue: store}, + provideMockActions(() => actions), + // other providers + ], + }); + + jsonPatchOperationsEffects = TestBed.get(JsonPatchOperationsEffects); + }); + + describe('commit$', () => { + it('should return a FLUSH_JSON_PATCH_OPERATIONS action in response to a COMMIT_JSON_PATCH_OPERATIONS action', () => { + actions = hot('--a-', { + a: { + type: JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS, + payload: {resourceType: testJsonPatchResourceType, resourceId: testJsonPatchResourceId} + } + }); + + const expected = cold('--b-', { + b: new FlushPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId) + }); + + expect(jsonPatchOperationsEffects.commit$).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts new file mode 100644 index 0000000000..b2d65975f2 --- /dev/null +++ b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts @@ -0,0 +1,328 @@ +import * as deepFreeze from 'deep-freeze'; + +import { + CommitPatchOperationsAction, + FlushPatchOperationsAction, + NewPatchAddOperationAction, + NewPatchRemoveOperationAction, + RollbacktPatchOperationsAction, + StartTransactionPatchOperationsAction +} from './json-patch-operations.actions'; +import { + JsonPatchOperationsEntry, + jsonPatchOperationsReducer, + JsonPatchOperationsResourceEntry, + JsonPatchOperationsState +} from './json-patch-operations.reducer'; + +class NullAction extends NewPatchAddOperationAction { + resourceType: string; + resourceId: string; + path: string; + value: any; + + constructor() { + super(null, null, null, null); + this.type = null; + } +} + +describe('jsonPatchOperationsReducer test suite', () => { + const testJsonPatchResourceType = 'testResourceType'; + const testJsonPatchResourceId = 'testResourceId'; + const testJsonPatchResourceAnotherId = 'testResourceAnotherId'; + const testJsonPatchResourcePath = '/testResourceType/testResourceId/testField'; + const testJsonPatchResourceValue = ['test']; + const patchOpBody = [{ + op: 'add', + path: '/testResourceType/testResourceId/testField', + value: ['test'] + }]; + const timestampBeforeStart = 1545994811991; + const timestampAfterStart = 1545994837492; + const startTimestamp = 1545994827492; + const testState: JsonPatchOperationsState = { + testResourceType: { + children: { + testResourceId: { + body: [ + { + operation: { + op: 'add', + path: '/testResourceType/testResourceId/testField', + value: ['test'] + }, + timeAdded: timestampBeforeStart + }, + ] + } as JsonPatchOperationsEntry + }, + transactionStartTime: null, + commitPending: false + } as JsonPatchOperationsResourceEntry + }; + + let initState: JsonPatchOperationsState; + + const anotherTestState: JsonPatchOperationsState = { + testResourceType: { + children: { + testResourceId: { + body: [ + { + operation: { + op: 'add', + path: '/testResourceType/testResourceId/testField', + value: ['test'] + }, + timeAdded: timestampBeforeStart + }, + { + operation: { + op: 'remove', + path: '/testResourceType/testResourceId/testField' + }, + timeAdded: timestampBeforeStart + }, + ] + } as JsonPatchOperationsEntry + }, + transactionStartTime: null, + commitPending: false + } as JsonPatchOperationsResourceEntry + }; + deepFreeze(testState); + + beforeEach(() => { + spyOn(Date.prototype, 'getTime').and.callFake(() => { + return timestampBeforeStart; + }); + }); + + it('should start with an empty state', () => { + const action = new NullAction(); + const initialState = jsonPatchOperationsReducer(undefined, action); + + expect(initialState).toEqual(Object.create(null)); + }); + + it('should return the current state when no valid actions have been made', () => { + const action = new NullAction(); + const newState = jsonPatchOperationsReducer(testState, action); + + expect(newState).toEqual(testState); + }); + + describe('When a new patch operation actions have been dispatched', () => { + + it('should return the properly state when it is empty', () => { + const action = new NewPatchAddOperationAction( + testJsonPatchResourceType, + testJsonPatchResourceId, + testJsonPatchResourcePath, + testJsonPatchResourceValue); + const newState = jsonPatchOperationsReducer(undefined, action); + + expect(newState).toEqual(testState); + }); + + it('should return the properly state when it is not empty', () => { + const action = new NewPatchRemoveOperationAction( + testJsonPatchResourceType, + testJsonPatchResourceId, + testJsonPatchResourcePath); + const newState = jsonPatchOperationsReducer(testState, action); + + expect(newState).toEqual(anotherTestState); + }); + }); + + describe('When StartTransactionPatchOperationsAction has been dispatched', () => { + it('should set \'transactionStartTime\' and \'commitPending\' to true', () => { + const action = new StartTransactionPatchOperationsAction( + testJsonPatchResourceType, + testJsonPatchResourceId, + startTimestamp); + const newState = jsonPatchOperationsReducer(testState, action); + + expect(newState[testJsonPatchResourceType].transactionStartTime).toEqual(startTimestamp); + expect(newState[testJsonPatchResourceType].commitPending).toBeTruthy(); + }); + }); + + describe('When CommitPatchOperationsAction has been dispatched', () => { + it('should set \'commitPending\' to false ', () => { + const action = new CommitPatchOperationsAction( + testJsonPatchResourceType, + testJsonPatchResourceId); + initState = Object.assign({}, testState, { + [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], { + transactionStartTime: startTimestamp, + commitPending: true + }) + }); + const newState = jsonPatchOperationsReducer(initState, action); + + expect(newState[testJsonPatchResourceType].transactionStartTime).toEqual(startTimestamp); + expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy(); + }); + }); + + describe('When RollbacktPatchOperationsAction has been dispatched', () => { + it('should set \'transactionStartTime\' to null and \'commitPending\' to false ', () => { + const action = new RollbacktPatchOperationsAction( + testJsonPatchResourceType, + testJsonPatchResourceId); + initState = Object.assign({}, testState, { + [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], { + transactionStartTime: startTimestamp, + commitPending: true + }) + }); + const newState = jsonPatchOperationsReducer(initState, action); + + expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull(); + expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy(); + }); + }); + + describe('When FlushPatchOperationsAction has been dispatched', () => { + + it('should flush only committed operations', () => { + const action = new FlushPatchOperationsAction( + testJsonPatchResourceType, + testJsonPatchResourceId); + initState = Object.assign({}, testState, { + [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], { + children: { + testResourceId: { + body: [ + { + operation: { + op: 'add', + path: '/testResourceType/testResourceId/testField', + value: ['test'] + }, + timeAdded: timestampBeforeStart + }, + { + operation: { + op: 'remove', + path: '/testResourceType/testResourceId/testField' + }, + timeAdded: timestampAfterStart + }, + ] + } as JsonPatchOperationsEntry + }, + transactionStartTime: startTimestamp, + commitPending: false + }) + }); + const newState = jsonPatchOperationsReducer(initState, action); + const expectedBody: any = [ + { + operation: { + op: 'remove', + path: '/testResourceType/testResourceId/testField' + }, + timeAdded: timestampAfterStart + }, + ]; + expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull(); + expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy(); + expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual(expectedBody); + }); + + beforeEach(() => { + initState = Object.assign({}, testState, { + [testJsonPatchResourceType]: Object.assign({}, testState[testJsonPatchResourceType], { + children: { + testResourceId: { + body: [ + { + operation: { + op: 'add', + path: '/testResourceType/testResourceId/testField', + value: ['test'] + }, + timeAdded: timestampBeforeStart + }, + { + operation: { + op: 'remove', + path: '/testResourceType/testResourceId/testField' + }, + timeAdded: timestampBeforeStart + }, + ] + } as JsonPatchOperationsEntry, + testResourceAnotherId: { + body: [ + { + operation: { + op: 'add', + path: '/testResourceType/testResourceAnotherId/testField', + value: ['test'] + }, + timeAdded: timestampBeforeStart + }, + { + operation: { + op: 'remove', + path: '/testResourceType/testResourceAnotherId/testField' + }, + timeAdded: timestampBeforeStart + }, + ] + } as JsonPatchOperationsEntry + }, + transactionStartTime: startTimestamp, + commitPending: false + }) + }); + }); + + it('should flush committed operations for specified resource id', () => { + const action = new FlushPatchOperationsAction( + testJsonPatchResourceType, + testJsonPatchResourceId); + const newState = jsonPatchOperationsReducer(initState, action); + const expectedBody: any = [ + { + operation: { + op: 'add', + path: '/testResourceType/testResourceAnotherId/testField', + value: ['test'] + }, + timeAdded: timestampBeforeStart + }, + { + operation: { + op: 'remove', + path: '/testResourceType/testResourceAnotherId/testField' + }, + timeAdded: timestampBeforeStart + }, + ]; + expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull(); + expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy(); + expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual([]); + expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceAnotherId].body).toEqual(expectedBody); + }); + + it('should flush operation list', () => { + const action = new FlushPatchOperationsAction(testJsonPatchResourceType, undefined); + const newState = jsonPatchOperationsReducer(initState, action); + + console.log(initState); + console.log(newState); + expect(newState[testJsonPatchResourceType].transactionStartTime).toBeNull(); + expect(newState[testJsonPatchResourceType].commitPending).toBeFalsy(); + expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceId].body).toEqual([]); + expect(newState[testJsonPatchResourceType].children[testJsonPatchResourceAnotherId].body).toEqual([]); + }); + + }); + +}); diff --git a/src/app/core/json-patch/json-patch-operations.service.spec.ts b/src/app/core/json-patch/json-patch-operations.service.spec.ts new file mode 100644 index 0000000000..4666a34134 --- /dev/null +++ b/src/app/core/json-patch/json-patch-operations.service.spec.ts @@ -0,0 +1,245 @@ +import { cold, getTestScheduler } from 'jasmine-marbles'; + +import { TestScheduler } from 'rxjs/testing'; +import { of as observableOf } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { SubmissionPatchRequest } from '../data/request.models'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { JsonPatchOperationsService } from './json-patch-operations.service'; +import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer'; +import { MockStore } from '../../shared/testing/mock-store'; +import { + CommitPatchOperationsAction, + RollbacktPatchOperationsAction, + StartTransactionPatchOperationsAction +} from './json-patch-operations.actions'; + +class TestService extends JsonPatchOperationsService { + protected linkPath = ''; + protected patchRequestConstructor = SubmissionPatchRequest; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store, + protected halService: HALEndpointService) { + + super(); + } +} + +describe('JsonPatchOperationsService test suite', () => { + let scheduler: TestScheduler; + let service: TestService; + let responseCache: ResponseCacheService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let halService: any; + let store: any; + + const timestamp = 1545994811991; + const timestampResponse = 1545994811992; + const mockState = { + 'json/patch': { + testResourceType: { + children: { + testResourceId: { + body: [ + { + operation: { + op: 'add', + path: '/testResourceType/testResourceId/testField', + value: ['test'] + }, + timeAdded: timestamp + }, + ] + } as JsonPatchOperationsEntry + }, + transactionStartTime: null, + commitPending: false + } as JsonPatchOperationsResourceEntry + } + }; + const resourceEndpointURL = 'https://rest.api/endpoint'; + const resourceEndpoint = 'resource'; + const resourceScope = '260'; + const resourceHref = resourceEndpointURL + '/' + resourceEndpoint + '/' + resourceScope; + + const testJsonPatchResourceType = 'testResourceType'; + const testJsonPatchResourceId = 'testResourceId'; + const patchOpBody = [{ + op: 'add', + path: '/testResourceType/testResourceId/testField', + value: ['test'] + }]; + + function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { + return jasmine.createSpyObj('responseCache', { + get: cold('c-', { + c: {response: {isSuccessful}, + timeAdded: timestampResponse} + }) + }); + } + + function initTestService(): TestService { + return new TestService( + responseCache, + requestService, + store, + halService + ); + + } + + beforeEach(() => { + store = new MockStore({} as CoreState); + responseCache = initMockResponseCacheService(true); + requestService = getMockRequestService(); + rdbService = getMockRemoteDataBuildService(); + scheduler = getTestScheduler(); + halService = new HALEndpointServiceStub(resourceEndpointURL); + service = initTestService(); + + spyOn((service as any).store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); + spyOn((service as any).store, 'dispatch').and.callThrough(); + spyOn(Date.prototype, 'getTime').and.callFake(() => { + return timestamp; + }); + }); + + describe('jsonPatchByResourceType', () => { + + it('should call submitJsonPatchOperations method', () => { + spyOn((service as any), 'submitJsonPatchOperations').and.callThrough(); + + scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpointURL, resourceScope, testJsonPatchResourceType).subscribe()); + scheduler.flush(); + + expect((service as any).submitJsonPatchOperations).toHaveBeenCalled(); + }); + + it('should configure a new SubmissionPatchRequest', () => { + const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody); + scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected, true); + }); + + it('should dispatch a new StartTransactionPatchOperationsAction', () => { + const expectedAction = new StartTransactionPatchOperationsAction(testJsonPatchResourceType, undefined, timestamp) + scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); + scheduler.flush(); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + }); + + describe('when request is successful', () => { + it('should dispatch a new CommitPatchOperationsAction', () => { + const expectedAction = new CommitPatchOperationsAction(testJsonPatchResourceType, undefined) + scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); + scheduler.flush(); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + }); + }); + + describe('when request is not successful', () => { + beforeEach(() => { + store = new MockStore({} as CoreState); + responseCache = initMockResponseCacheService(false); + requestService = getMockRequestService(); + rdbService = getMockRemoteDataBuildService(); + scheduler = getTestScheduler(); + halService = new HALEndpointServiceStub(resourceEndpointURL); + service = initTestService(); + + spyOn((service as any).store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); + spyOn((service as any).store, 'dispatch').and.callThrough(); + }); + + it('should dispatch a new RollbacktPatchOperationsAction', () => { + + const expectedAction = new RollbacktPatchOperationsAction(testJsonPatchResourceType, undefined) + scheduler.schedule(() => service.jsonPatchByResourceType(resourceEndpoint, resourceScope, testJsonPatchResourceType).subscribe()); + scheduler.flush(); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + }); + }); + }); + + describe('jsonPatchByResourceID', () => { + + it('should call submitJsonPatchOperations method', () => { + spyOn((service as any), 'submitJsonPatchOperations').and.callThrough(); + + scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpointURL, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); + scheduler.flush(); + + expect((service as any).submitJsonPatchOperations).toHaveBeenCalled(); + }); + + it('should configure a new SubmissionPatchRequest', () => { + const expected = new SubmissionPatchRequest(requestService.generateRequestId(), resourceHref, patchOpBody); + scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected, true); + }); + + it('should dispatch a new StartTransactionPatchOperationsAction', () => { + const expectedAction = new StartTransactionPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId, timestamp) + scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); + scheduler.flush(); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + }); + + describe('when request is successful', () => { + it('should dispatch a new CommitPatchOperationsAction', () => { + const expectedAction = new CommitPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId) + scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); + scheduler.flush(); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + }); + }); + + describe('when request is not successful', () => { + beforeEach(() => { + store = new MockStore({} as CoreState); + responseCache = initMockResponseCacheService(false); + requestService = getMockRequestService(); + rdbService = getMockRemoteDataBuildService(); + scheduler = getTestScheduler(); + halService = new HALEndpointServiceStub(resourceEndpointURL); + service = initTestService(); + + spyOn((service as any).store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); + spyOn((service as any).store, 'dispatch').and.callThrough(); + }); + + it('should dispatch a new RollbacktPatchOperationsAction', () => { + + const expectedAction = new RollbacktPatchOperationsAction(testJsonPatchResourceType, testJsonPatchResourceId) + scheduler.schedule(() => service.jsonPatchByResourceID(resourceEndpoint, resourceScope, testJsonPatchResourceType, testJsonPatchResourceId).subscribe()); + scheduler.flush(); + + expect((service as any).store.dispatch).toHaveBeenCalledWith(expectedAction); + }); + }); + }); + +}); diff --git a/src/app/shared/testing/mock-submission-config.ts b/src/app/shared/testing/mock-submission-config.ts index 212a2fc5f3..3be82f65ee 100644 --- a/src/app/shared/testing/mock-submission-config.ts +++ b/src/app/shared/testing/mock-submission-config.ts @@ -13,15 +13,15 @@ export const MOCK_SUBMISSION_CONFIG = { metadata: [ { name: 'mainField', - style: 'fa-user' + style: 'fas fa-user' }, { name: 'relatedField', - style: 'fa-university' + style: 'fas fa-university' }, { name: 'otherRelatedField', - style: 'fa-circle' + style: 'fas fa-circle' }, { name: 'default',