diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index 2b50a974ee..ed9ab4a891 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -22,13 +22,14 @@ import { FieldChangeType } from '../../../core/data/object-updates/object-update import { MetadatumViewModel } from '../../../core/shared/metadata.models'; import { RegistryService } from '../../../core/registry/registry.service'; import { PaginatedList } from '../../../core/data/paginated-list'; -import { Metadata } from '../../../core/shared/metadata.utils'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { DSOSuccessResponse } from '../../../core/cache/response.models'; let comp: any; let fixture: ComponentFixture; @@ -43,6 +44,7 @@ const router = new RouterStub(); let metadataFieldService; let paginatedMetadataFields; let routeStub; +let objectCacheService; const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' }); const mdField1 = Object.assign(new MetadataField(), { @@ -101,6 +103,8 @@ const fieldUpdate3 = { changeType: undefined }; +const operation1 = { op: 'remove', path: '/metadata/dc.title/1' }; + let scheduler: TestScheduler; let item; describe('ItemMetadataComponent', () => { @@ -119,7 +123,9 @@ describe('ItemMetadataComponent', () => { ; itemService = jasmine.createSpyObj('itemService', { update: createSuccessfulRemoteDataObject$(item), - commitUpdates: {} + commitUpdates: {}, + patch: observableOf(new DSOSuccessResponse(['item-selflink'], 200, 'OK')), + findByHref: createSuccessfulRemoteDataObject$(item) }); routeStub = { data: observableOf({}), @@ -148,9 +154,13 @@ describe('ItemMetadataComponent', () => { getLastModified: observableOf(date), hasUpdates: observableOf(true), isReinstatable: observableOf(false), // should always return something --> its in ngOnInit - isValidPage: observableOf(true) + isValidPage: observableOf(true), + createPatch: observableOf([ + operation1 + ]) } ); + objectCacheService = jasmine.createSpyObj('objectCacheService', ['addPatch']); TestBed.configureTestingModule({ imports: [SharedModule, TranslateModule.forRoot()], @@ -162,6 +172,7 @@ describe('ItemMetadataComponent', () => { { provide: ActivatedRoute, useValue: routeStub }, { provide: NotificationsService, useValue: notificationsService }, { provide: RegistryService, useValue: metadataFieldService }, + { provide: ObjectCacheService, useValue: objectCacheService }, ], schemas: [ NO_ERRORS_SCHEMA ] @@ -215,8 +226,8 @@ describe('ItemMetadataComponent', () => { }); it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url and metadata', () => { - expect(objectUpdatesService.getUpdatedFields).toHaveBeenCalledWith(url, comp.item.metadataAsList); - expect(itemService.update).toHaveBeenCalledWith(Object.assign(comp.item, { metadata: Metadata.toMetadataMap(comp.item.metadataAsList) })); + expect(objectUpdatesService.createPatch).toHaveBeenCalledWith(url); + expect(itemService.patch).toHaveBeenCalledWith(comp.item, [ operation1 ]); expect(objectUpdatesService.getFieldUpdates).toHaveBeenCalledWith(url, comp.item.metadataAsList); }); }); diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index eccfc42c57..75bbc32700 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -5,15 +5,13 @@ import { ObjectUpdatesService } from '../../../core/data/object-updates/object-u import { ActivatedRoute, Router } from '@angular/router'; import { cloneDeep } from 'lodash'; import { Observable } from 'rxjs'; -import { Identifiable } from '../../../core/data/object-updates/object-updates.reducer'; -import { first, map, switchMap, take, tap } from 'rxjs/operators'; +import { first, map, switchMap, take } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { RegistryService } from '../../../core/registry/registry.service'; import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models'; -import { Metadata } from '../../../core/shared/metadata.utils'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { UpdateDataService } from '../../../core/data/update-data.service'; diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts index cb7f44039c..4a14e2e874 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts @@ -231,7 +231,8 @@ describe('objectUpdatesReducer', () => { }, fieldUpdates: {}, virtualMetadataSources: {}, - lastModified: modDate + lastModified: modDate, + patchOperationServiceToken: undefined } }; const newState = objectUpdatesReducer(testState, action); diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index 04018b8de2..ae73dc851f 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -12,6 +12,7 @@ import { Notification } from '../../../shared/notifications/models/notification. import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; import {Relationship} from '../../shared/item-relationships/relationship.model'; +import { Injector } from '@angular/core'; describe('ObjectUpdatesService', () => { let service: ObjectUpdatesService; @@ -31,6 +32,9 @@ describe('ObjectUpdatesService', () => { }; const modDate = new Date(2010, 2, 11); + const injectionToken = 'fake-injection-token'; + let patchOperationService; + let injector: Injector; beforeEach(() => { const fieldStates = { @@ -40,11 +44,17 @@ describe('ObjectUpdatesService', () => { }; const objectEntry = { - fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {} + fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationServiceToken: injectionToken }; store = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); - service = new ObjectUpdatesService(store); + patchOperationService = jasmine.createSpyObj('patchOperationService', { + fieldUpdatesToPatchOperations: [] + }); + injector = jasmine.createSpyObj('injector', { + get: patchOperationService + }); + service = new ObjectUpdatesService(store, injector); spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry)); spyOn(service as any, 'getFieldState').and.callFake((uuid) => { @@ -277,4 +287,26 @@ describe('ObjectUpdatesService', () => { }); }); + describe('createPatch', () => { + let result$; + + beforeEach(() => { + result$ = service.createPatch(url); + }); + + it('should inject the service using the token stored in the entry', (done) => { + result$.subscribe(() => { + expect(injector.get).toHaveBeenCalledWith(injectionToken); + done(); + }); + }); + + it('should create a patch from the fieldUpdates using the injected service', (done) => { + result$.subscribe(() => { + expect(patchOperationService.fieldUpdatesToPatchOperations).toHaveBeenCalledWith(fieldUpdates); + done(); + }); + }); + }); + }); diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts new file mode 100644 index 0000000000..007dc9ba8a --- /dev/null +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts @@ -0,0 +1,252 @@ +import { MetadataPatchOperationService } from './metadata-patch-operation.service'; +import { FieldUpdates } from '../object-updates.reducer'; +import { Operation } from 'fast-json-patch'; +import { FieldChangeType } from '../object-updates.actions'; +import { MetadatumViewModel } from '../../../shared/metadata.models'; + +describe('MetadataPatchOperationService', () => { + let service: MetadataPatchOperationService; + + beforeEach(() => { + service = new MetadataPatchOperationService(); + }); + + describe('fieldUpdatesToPatchOperations', () => { + let fieldUpdates: FieldUpdates; + let expected: Operation[]; + let result: Operation[]; + + describe('when fieldUpdates contains a single remove', () => { + beforeEach(() => { + fieldUpdates = Object.assign({ + update1: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Deleted title', + place: 0 + }), + changeType: FieldChangeType.REMOVE + } + }); + expected = [ + { op: 'remove', path: '/metadata/dc.title/0', value: undefined } + ] as any[]; + result = service.fieldUpdatesToPatchOperations(fieldUpdates); + }); + + it('should contain a single remove operation with the correct path', () => { + expect(result).toEqual(expected); + }); + }); + + describe('when fieldUpdates contains a single add', () => { + beforeEach(() => { + fieldUpdates = Object.assign({ + update1: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Added title', + place: 0 + }), + changeType: FieldChangeType.ADD + } + }); + expected = [ + { op: 'add', path: '/metadata/dc.title', value: [ { value: 'Added title', language: undefined } ] } + ] as any[]; + result = service.fieldUpdatesToPatchOperations(fieldUpdates); + }); + + it('should contain a single add operation with the correct path', () => { + expect(result).toEqual(expected); + }); + }); + + describe('when fieldUpdates contains a single update', () => { + beforeEach(() => { + fieldUpdates = Object.assign({ + update1: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Changed title', + place: 0 + }), + changeType: FieldChangeType.UPDATE + } + }); + expected = [ + { op: 'replace', path: '/metadata/dc.title/0', value: { value: 'Changed title', language: undefined } } + ] as any[]; + result = service.fieldUpdatesToPatchOperations(fieldUpdates); + }); + + it('should contain a single replace operation with the correct path', () => { + expect(result).toEqual(expected); + }); + }); + + describe('when fieldUpdates contains multiple removes with incrementing indexes', () => { + beforeEach(() => { + fieldUpdates = Object.assign({ + update1: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'First deleted title', + place: 0 + }), + changeType: FieldChangeType.REMOVE + }, + update2: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Second deleted title', + place: 1 + }), + changeType: FieldChangeType.REMOVE + }, + update3: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Third deleted title', + place: 2 + }), + changeType: FieldChangeType.REMOVE + } + }); + expected = [ + { op: 'remove', path: '/metadata/dc.title/0', value: undefined }, + { op: 'remove', path: '/metadata/dc.title/0', value: undefined }, + { op: 'remove', path: '/metadata/dc.title/0', value: undefined } + ] as any[]; + result = service.fieldUpdatesToPatchOperations(fieldUpdates); + }); + + it('should contain all the remove operations on the same index', () => { + expect(result).toEqual(expected); + }); + }); + + describe('when fieldUpdates contains multiple removes with decreasing indexes', () => { + beforeEach(() => { + fieldUpdates = Object.assign({ + update1: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Third deleted title', + place: 2 + }), + changeType: FieldChangeType.REMOVE + }, + update2: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Second deleted title', + place: 1 + }), + changeType: FieldChangeType.REMOVE + }, + update3: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'First deleted title', + place: 0 + }), + changeType: FieldChangeType.REMOVE + } + }); + expected = [ + { op: 'remove', path: '/metadata/dc.title/2', value: undefined }, + { op: 'remove', path: '/metadata/dc.title/1', value: undefined }, + { op: 'remove', path: '/metadata/dc.title/0', value: undefined } + ] as any[]; + result = service.fieldUpdatesToPatchOperations(fieldUpdates); + }); + + it('should contain all the remove operations with their corresponding indexes', () => { + expect(result).toEqual(expected); + }); + }); + + describe('when fieldUpdates contains multiple removes with random indexes', () => { + beforeEach(() => { + fieldUpdates = Object.assign({ + update1: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Second deleted title', + place: 1 + }), + changeType: FieldChangeType.REMOVE + }, + update2: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Third deleted title', + place: 2 + }), + changeType: FieldChangeType.REMOVE + }, + update3: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'First deleted title', + place: 0 + }), + changeType: FieldChangeType.REMOVE + } + }); + expected = [ + { op: 'remove', path: '/metadata/dc.title/1', value: undefined }, + { op: 'remove', path: '/metadata/dc.title/1', value: undefined }, + { op: 'remove', path: '/metadata/dc.title/0', value: undefined } + ] as any[]; + result = service.fieldUpdatesToPatchOperations(fieldUpdates); + }); + + it('should contain all the remove operations with the correct indexes taking previous operations into account', () => { + expect(result).toEqual(expected); + }); + }); + + describe('when fieldUpdates contains multiple removes and updates with random indexes', () => { + beforeEach(() => { + fieldUpdates = Object.assign({ + update1: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Second deleted title', + place: 1 + }), + changeType: FieldChangeType.REMOVE + }, + update2: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'Third changed title', + place: 2 + }), + changeType: FieldChangeType.UPDATE + }, + update3: { + field: Object.assign(new MetadatumViewModel(), { + key: 'dc.title', + value: 'First deleted title', + place: 0 + }), + changeType: FieldChangeType.REMOVE + } + }); + expected = [ + { op: 'remove', path: '/metadata/dc.title/1', value: undefined }, + { op: 'replace', path: '/metadata/dc.title/1', value: { value: 'Third changed title', language: undefined } }, + { op: 'remove', path: '/metadata/dc.title/0', value: undefined } + ] as any[]; + result = service.fieldUpdatesToPatchOperations(fieldUpdates); + }); + + it('should contain all the remove and replace operations with the correct indexes taking previous remove operations into account', () => { + expect(result).toEqual(expected); + }); + }); + }); +});