diff --git a/platform/commonUI/browse/src/creation/CreationService.js b/platform/commonUI/browse/src/creation/CreationService.js index 3dd96d289e..baac8488c3 100644 --- a/platform/commonUI/browse/src/creation/CreationService.js +++ b/platform/commonUI/browse/src/creation/CreationService.js @@ -86,37 +86,18 @@ define( // composition, so that it will subsequently appear // as a child contained by that parent. function addToComposition(id, parent, parentPersistence) { - var mutatationResult = parent.useCapability("mutation", function (model) { - if (Array.isArray(model.composition)) { - // Don't add if the id is already there - if (model.composition.indexOf(id) === -1) { - model.composition.push(id); - } - } else { - // This is abnormal; composition should be an array - self.$log.warn(NO_COMPOSITION_WARNING + parent.getId()); - return false; // Cancel mutation - } - }); + var compositionCapability = parent.getCapability('composition'), + addResult = compositionCapability && + compositionCapability.add(id); - return self.$q.when(mutatationResult).then(function (result) { + return self.$q.when(addResult).then(function (result) { if (!result) { - self.$log.error("Could not mutate " + parent.getId()); + self.$log.error("Could not modify " + parent.getId()); return undefined; } return parentPersistence.persist().then(function () { - // Locate and return new Object in context of parent. - return parent - .useCapability('composition') - .then(function (children) { - var i; - for (i = 0; i < children.length; i += 1) { - if (children[i].getId() === id) { - return children[i]; - } - } - }); + return result; }); }); } diff --git a/platform/commonUI/browse/test/creation/CreationServiceSpec.js b/platform/commonUI/browse/test/creation/CreationServiceSpec.js index bdcd752c6c..bc26c8be23 100644 --- a/platform/commonUI/browse/test/creation/CreationServiceSpec.js +++ b/platform/commonUI/browse/test/creation/CreationServiceSpec.js @@ -86,7 +86,7 @@ define( ); mockCompositionCapability = jasmine.createSpyObj( "composition", - ["invoke"] + ["invoke", "add"] ); mockContextCapability = jasmine.createSpyObj( "context", @@ -120,6 +120,7 @@ define( mockCompositionCapability.invoke.andReturn( mockPromise([mockNewObject]) ); + mockCompositionCapability.add.andReturn(mockPromise(true)); creationService = new CreationService( mockPersistenceService, @@ -143,33 +144,34 @@ define( parentModel = { composition: ["notAnyUUID"] }; creationService.createObject(model, mockParentObject); - // Invoke the mutation callback - expect(mockMutationCapability.invoke).toHaveBeenCalled(); - mockMutationCapability.invoke.mostRecentCall.args[0](parentModel); - - // Should have a longer composition now, with the new UUID - expect(parentModel.composition.length).toEqual(2); + // Verify that a new ID was added + expect(mockCompositionCapability.add) + .toHaveBeenCalledWith(jasmine.any(String)); }); - it("warns if parent has no composition", function () { - var model = { someKey: "some value" }, - parentModel = { }; - creationService.createObject(model, mockParentObject); + it("provides the newly-created object", function () { + var mockDomainObject = jasmine.createSpyObj( + 'newDomainObject', + ['getId', 'getModel', 'getCapability'] + ), + mockCallback = jasmine.createSpy('callback'); - // Verify precondition; no prior warnings - expect(mockLog.warn).not.toHaveBeenCalled(); + // Act as if the object had been created + mockCompositionCapability.add.andCallFake(function (id) { + mockDomainObject.getId.andReturn(id); + mockCompositionCapability.invoke + .andReturn(mockPromise([mockDomainObject])); + return mockPromise(mockDomainObject); + }); - // Invoke the mutation callback - expect(mockMutationCapability.invoke).toHaveBeenCalled(); - mockMutationCapability.invoke.mostRecentCall.args[0](parentModel); + // Should find it in the composition + creationService.createObject({}, mockParentObject) + .then(mockCallback); + + expect(mockCallback).toHaveBeenCalledWith(mockDomainObject); - // Should have a longer composition now, with the new UUID - expect(mockLog.warn).toHaveBeenCalled(); - // Composition should still be undefined - expect(parentModel.composition).toBeUndefined(); }); - it("warns if parent has no persistence capability", function () { // Callbacks var success = jasmine.createSpy("success"), @@ -185,7 +187,6 @@ define( expect(mockLog.warn).toHaveBeenCalled(); expect(success).not.toHaveBeenCalled(); expect(failure).toHaveBeenCalled(); - }); it("logs an error when mutaton fails", function () { @@ -194,7 +195,7 @@ define( var model = { someKey: "some value" }, parentModel = { composition: ["notAnyUUID"] }; - mockMutationCapability.invoke.andReturn(mockPromise(false)); + mockCompositionCapability.add.andReturn(mockPromise(false)); creationService.createObject(model, mockParentObject); diff --git a/platform/commonUI/edit/src/actions/LinkAction.js b/platform/commonUI/edit/src/actions/LinkAction.js index 74abd2a93c..95ed9a8082 100644 --- a/platform/commonUI/edit/src/actions/LinkAction.js +++ b/platform/commonUI/edit/src/actions/LinkAction.js @@ -36,20 +36,11 @@ define( function LinkAction(context) { this.domainObject = (context || {}).domainObject; this.selectedObject = (context || {}).selectedObject; - this.selectedId = this.selectedObject && this.selectedObject.getId(); } LinkAction.prototype.perform = function () { var self = this; - // Add this domain object's identifier - function addId(model) { - if (Array.isArray(model.composition) && - model.composition.indexOf(self.selectedId) < 0) { - model.composition.push(self.selectedId); - } - } - // Persist changes to the domain object function doPersist() { var persistence = @@ -59,11 +50,13 @@ define( // Link these objects function doLink() { - return self.domainObject.useCapability("mutation", addId) - .then(doPersist); + var composition = self.domainObject && + self.domainObject.getCapability('composition'); + return composition && composition.add(self.selectedObject) + .then(doPersist); } - return this.selectedId && doLink(); + return this.selectedObject && doLink(); }; return LinkAction; diff --git a/platform/commonUI/edit/src/actions/PropertiesDialog.js b/platform/commonUI/edit/src/actions/PropertiesDialog.js index 97ee1f5c0a..f461c0b8e1 100644 --- a/platform/commonUI/edit/src/actions/PropertiesDialog.js +++ b/platform/commonUI/edit/src/actions/PropertiesDialog.js @@ -54,6 +54,9 @@ define( var row = Object.create(property.getDefinition()); row.key = index; return row; + }).filter(function (row) { + // Only show properties which are editable + return row.control; }) }] }; diff --git a/platform/commonUI/edit/test/actions/LinkActionSpec.js b/platform/commonUI/edit/test/actions/LinkActionSpec.js index 835f93740a..96ea30e2b3 100644 --- a/platform/commonUI/edit/test/actions/LinkActionSpec.js +++ b/platform/commonUI/edit/test/actions/LinkActionSpec.js @@ -31,7 +31,7 @@ define( mockDomainObject, mockParent, mockContext, - mockMutation, + mockComposition, mockPersistence, mockType, actionContext, @@ -67,7 +67,7 @@ define( } }; mockContext = jasmine.createSpyObj("context", [ "getParent" ]); - mockMutation = jasmine.createSpyObj("mutation", [ "invoke" ]); + mockComposition = jasmine.createSpyObj("composition", [ "invoke", "add" ]); mockPersistence = jasmine.createSpyObj("persistence", [ "persist" ]); mockType = jasmine.createSpyObj("type", [ "hasFeature" ]); @@ -75,11 +75,11 @@ define( mockDomainObject.getCapability.andReturn(mockContext); mockContext.getParent.andReturn(mockParent); mockType.hasFeature.andReturn(true); - mockMutation.invoke.andReturn(mockPromise(true)); - + mockComposition.invoke.andReturn(mockPromise(true)); + mockComposition.add.andReturn(mockPromise(true)); capabilities = { - mutation: mockMutation, + composition: mockComposition, persistence: mockPersistence, type: mockType }; @@ -96,33 +96,17 @@ define( }); - it("mutates the parent when performed", function () { + it("adds to the parent's composition when performed", function () { action.perform(); - expect(mockMutation.invoke) - .toHaveBeenCalledWith(jasmine.any(Function)); + expect(mockComposition.add) + .toHaveBeenCalledWith(mockDomainObject); }); - it("changes composition from its mutation function", function () { - var mutator, result; + it("persists changes afterward", function () { action.perform(); - mutator = mockMutation.invoke.mostRecentCall.args[0]; - result = mutator(model); - - // Should not have cancelled the mutation - expect(result).not.toBe(false); - - // Simulate mutate's behavior (remove can either return a - // new model or modify this one in-place) - result = result || model; - - // Should have removed "test" - that was our - // mock domain object's id. - expect(result.composition).toEqual(["a", "b", "c", "test"]); - - // Finally, should have persisted expect(mockPersistence.persist).toHaveBeenCalled(); }); }); } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/test/actions/PropertiesDialogSpec.js b/platform/commonUI/edit/test/actions/PropertiesDialogSpec.js index 7269ae64a3..a9077e8ec6 100644 --- a/platform/commonUI/edit/test/actions/PropertiesDialogSpec.js +++ b/platform/commonUI/edit/test/actions/PropertiesDialogSpec.js @@ -39,7 +39,7 @@ define( return { getValue: function (model) { return model[k]; }, setValue: function (model, v) { model[k] = v; }, - getDefinition: function () { return {}; } + getDefinition: function () { return { control: 'textfield '}; } }; }); diff --git a/platform/core/bundle.json b/platform/core/bundle.json index c42ebbd844..952daf5570 100644 --- a/platform/core/bundle.json +++ b/platform/core/bundle.json @@ -63,7 +63,12 @@ "provides": "modelService", "type": "provider", "implementation": "models/PersistedModelProvider.js", - "depends": [ "persistenceService", "$q", "PERSISTENCE_SPACE" ] + "depends": [ + "persistenceService", + "$q", + "PERSISTENCE_SPACE", + "ADDITIONAL_PERSISTENCE_SPACES" + ] }, { "provides": "modelService", @@ -218,6 +223,17 @@ "composition": [] } } + ], + "constants": [ + { + "key": "PERSISTENCE_SPACE", + "value": "mct" + }, + { + "key": "ADDITIONAL_PERSISTENCE_SPACES", + "value": [], + "description": "An array of additional persistence spaces to load models from." + } ] } } diff --git a/platform/core/src/capabilities/CompositionCapability.js b/platform/core/src/capabilities/CompositionCapability.js index 4204eddd39..977d8cb418 100644 --- a/platform/core/src/capabilities/CompositionCapability.js +++ b/platform/core/src/capabilities/CompositionCapability.js @@ -50,6 +50,66 @@ define( this.domainObject = domainObject; } + /** + * Add a domain object to the composition of the field. + * This mutates but does not persist the modified object. + * + * If no index is given, this is added to the end of the composition. + * + * @param {DomainObject|string} domainObject the domain object to add, + * or simply its identifier + * @param {number} [index] the index at which to add the object + * @returns {Promise.} a promise for the added object + * in its new context + */ + CompositionCapability.prototype.add = function (domainObject, index) { + var self = this, + id = typeof domainObject === 'string' ? + domainObject : domainObject.getId(), + model = self.domainObject.getModel(), + composition = model.composition, + oldIndex = composition.indexOf(id); + + // Find the object with the above id, used to contextualize + function findObject(objects) { + var i; + for (i = 0; i < objects.length; i += 1) { + if (objects[i].getId() === id) { + return objects[i]; + } + } + } + + function contextualize(mutationResult) { + return mutationResult && self.invoke().then(findObject); + } + + function addIdToModel(model) { + // Pick a specific index if needed. + index = isNaN(index) ? composition.length : index; + // Also, don't put past the end of the array + index = Math.min(composition.length, index); + + // Remove the existing instance of the id + if (oldIndex !== -1) { + model.composition.splice(oldIndex, 1); + } + + // ...and add it back at the appropriate index. + model.composition.splice(index, 0, id); + } + + // If no index has been specified already and the id is already + // present, nothing to do. If the id is already at that index, + // also nothing to do, so cancel mutation. + if ((isNaN(index) && oldIndex !== -1) || (index === oldIndex)) { + return contextualize(true); + } + + return this.domainObject.useCapability('mutation', addIdToModel) + .then(contextualize); + }; + /** * Request the composition of this object. * @returns {Promise.} a list of all domain diff --git a/platform/core/src/models/PersistedModelProvider.js b/platform/core/src/models/PersistedModelProvider.js index 59ab020b14..a10f818179 100644 --- a/platform/core/src/models/PersistedModelProvider.js +++ b/platform/core/src/models/PersistedModelProvider.js @@ -39,23 +39,37 @@ define( * @param {PersistenceService} persistenceService the service in which * domain object models are persisted. * @param $q Angular's $q service, for working with promises - * @param {string} SPACE the name of the persistence space from which - * models should be retrieved. + * @param {string} space the name of the persistence space(s) + * from which models should be retrieved. + * @param {string} spaces additional persistence spaces to use */ - function PersistedModelProvider(persistenceService, $q, space) { + function PersistedModelProvider(persistenceService, $q, space, spaces) { this.persistenceService = persistenceService; this.$q = $q; - this.space = space; + this.spaces = [space].concat(spaces || []); + } + + // Take the most recently modified model, for cases where + // multiple persistence spaces return models. + function takeMostRecent(modelA, modelB) { + return (!modelB || modelB.modified === undefined) ? modelA : + (!modelA || modelA.modified === undefined) ? modelB : + modelB.modified > modelA.modified ? modelB : + modelA; } PersistedModelProvider.prototype.getModels = function (ids) { var persistenceService = this.persistenceService, $q = this.$q, - space = this.space; + spaces = this.spaces; - // Load a single object model from persistence + // Load a single object model from any persistence spaces function loadModel(id) { - return persistenceService.readObject(space, id); + return $q.all(spaces.map(function (space) { + return persistenceService.readObject(space, id); + })).then(function (models) { + return models.reduce(takeMostRecent); + }); } // Package the result as id->model diff --git a/platform/core/test/capabilities/CompositionCapabilitySpec.js b/platform/core/test/capabilities/CompositionCapabilitySpec.js index c6f6b76aba..98cc1916d2 100644 --- a/platform/core/test/capabilities/CompositionCapabilitySpec.js +++ b/platform/core/test/capabilities/CompositionCapabilitySpec.js @@ -51,7 +51,7 @@ define( // so support that, but don't introduce complication of // native promises. function mockPromise(value) { - return { + return (value || {}).then ? value : { then: function (callback) { return mockPromise(callback(value)); } @@ -123,6 +123,98 @@ define( }); + it("allows domain objects to be added", function () { + var result, + testModel = { composition: [] }, + mockChild = jasmine.createSpyObj("child", DOMAIN_OBJECT_METHODS); + + mockDomainObject.getModel.andReturn(testModel); + mockObjectService.getObjects.andReturn(mockPromise({a: mockChild})); + mockChild.getCapability.andReturn(undefined); + mockChild.getId.andReturn('a'); + + mockDomainObject.useCapability.andCallFake(function (key, mutator) { + if (key === 'mutation') { + mutator(testModel); + return mockPromise(true); + } + }); + + composition.add(mockChild).then(function (domainObject) { + result = domainObject; + }); + + expect(testModel.composition).toEqual(['a']); + + // Should have returned the added object in its new context + expect(result.getId()).toEqual('a'); + expect(result.getCapability('context')).toBeDefined(); + expect(result.getCapability('context').getParent()) + .toEqual(mockDomainObject); + }); + + it("does not re-add IDs which are already present", function () { + var result, + testModel = { composition: [ 'a' ] }, + mockChild = jasmine.createSpyObj("child", DOMAIN_OBJECT_METHODS); + + mockDomainObject.getModel.andReturn(testModel); + mockObjectService.getObjects.andReturn(mockPromise({a: mockChild})); + mockChild.getCapability.andReturn(undefined); + mockChild.getId.andReturn('a'); + + mockDomainObject.useCapability.andCallFake(function (key, mutator) { + if (key === 'mutation') { + mutator(testModel); + return mockPromise(true); + } + }); + + composition.add(mockChild).then(function (domainObject) { + result = domainObject; + }); + + // Still just 'a' + expect(testModel.composition).toEqual(['a']); + + // Should have returned the added object in its new context + expect(result.getId()).toEqual('a'); + expect(result.getCapability('context')).toBeDefined(); + expect(result.getCapability('context').getParent()) + .toEqual(mockDomainObject); + }); + + it("can add objects at a specified index", function () { + var result, + testModel = { composition: [ 'a', 'b', 'c' ] }, + mockChild = jasmine.createSpyObj("child", DOMAIN_OBJECT_METHODS); + + mockDomainObject.getModel.andReturn(testModel); + mockObjectService.getObjects.andReturn(mockPromise({a: mockChild})); + mockChild.getCapability.andReturn(undefined); + mockChild.getId.andReturn('a'); + + mockDomainObject.useCapability.andCallFake(function (key, mutator) { + if (key === 'mutation') { + mutator(testModel); + return mockPromise(true); + } + }); + + composition.add(mockChild, 1).then(function (domainObject) { + result = domainObject; + }); + + // Still just 'a' + expect(testModel.composition).toEqual(['b', 'a', 'c']); + + // Should have returned the added object in its new context + expect(result.getId()).toEqual('a'); + expect(result.getCapability('context')).toBeDefined(); + expect(result.getCapability('context').getParent()) + .toEqual(mockDomainObject); + }); + }); } ); diff --git a/platform/core/test/models/PersistedModelProviderSpec.js b/platform/core/test/models/PersistedModelProviderSpec.js index 20f0e89649..8dcb58a400 100644 --- a/platform/core/test/models/PersistedModelProviderSpec.js +++ b/platform/core/test/models/PersistedModelProviderSpec.js @@ -32,7 +32,9 @@ define( describe("The persisted model provider", function () { var mockQ, mockPersistenceService, - SPACE = "some space", + SPACE = "space0", + spaces = [ "space1" ], + modTimes, provider; function mockPromise(value) { @@ -51,12 +53,14 @@ define( } beforeEach(function () { + modTimes = {}; mockQ = { when: mockPromise, all: mockAll }; mockPersistenceService = { readObject: function (space, id) { return mockPromise({ space: space, - id: id + id: id, + modified: (modTimes[space] || {})[id] }); } }; @@ -64,7 +68,8 @@ define( provider = new PersistedModelProvider( mockPersistenceService, mockQ, - SPACE + SPACE, + spaces ); }); @@ -82,6 +87,24 @@ define( }); }); + it("reads object models from multiple spaces", function () { + var models; + + modTimes.space1 = { + 'x': 12321 + }; + + provider.getModels(["a", "x", "zz"]).then(function (m) { + models = m; + }); + + expect(models).toEqual({ + a: { space: SPACE, id: "a" }, + x: { space: 'space1', id: "x", modified: 12321 }, + zz: { space: SPACE, id: "zz" } + }); + }); + }); } -); \ No newline at end of file +); diff --git a/platform/entanglement/src/actions/AbstractComposeAction.js b/platform/entanglement/src/actions/AbstractComposeAction.js index cc78eb1888..f68391adc9 100644 --- a/platform/entanglement/src/actions/AbstractComposeAction.js +++ b/platform/entanglement/src/actions/AbstractComposeAction.js @@ -32,7 +32,8 @@ define( * @private */ /** - * Change the composition of the specified objects. + * Change the composition of the specified objects. Note that this + * should only be invoked after successfully validating. * * @param {DomainObject} domainObject the domain object to * move, copy, or link. @@ -43,7 +44,8 @@ define( * @method platform/entanglement.AbstractComposeService#perform */ /** - * Check if one object can be composed into another. + * Check if this composition change is valid for these objects. + * * @param {DomainObject} domainObject the domain object to * move, copy, or link. * @param {DomainObject} parent the domain object whose composition diff --git a/platform/entanglement/src/services/CopyService.js b/platform/entanglement/src/services/CopyService.js index d62eb2a0ed..48ba3e7ce5 100644 --- a/platform/entanglement/src/services/CopyService.js +++ b/platform/entanglement/src/services/CopyService.js @@ -64,6 +64,12 @@ define( return self.perform(domainObject, parent); } + if (!this.validate(domainObject, parent)) { + throw new Error( + "Tried to copy objects without validating first." + ); + } + if (domainObject.hasCapability('composition')) { model.composition = []; } diff --git a/platform/entanglement/src/services/LinkService.js b/platform/entanglement/src/services/LinkService.js index 9fb38dd273..5989c3cf06 100644 --- a/platform/entanglement/src/services/LinkService.js +++ b/platform/entanglement/src/services/LinkService.js @@ -45,6 +45,9 @@ define( if (parentCandidate.getId() === object.getId()) { return false; } + if (!parentCandidate.hasCapability('composition')) { + return false; + } if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { return false; } @@ -56,26 +59,18 @@ define( }; LinkService.prototype.perform = function (object, parentObject) { - function findChild(children) { - var i; - for (i = 0; i < children.length; i += 1) { - if (children[i].getId() === object.getId()) { - return children[i]; - } - } + if (!this.validate(object, parentObject)) { + throw new Error( + "Tried to link objects without validating first." + ); } - return parentObject.useCapability('mutation', function (model) { - if (model.composition.indexOf(object.getId()) === -1) { - model.composition.push(object.getId()); - } - }).then(function () { - return parentObject.getCapability('persistence').persist(); - }).then(function getObjectWithNewContext() { - return parentObject - .useCapability('composition') - .then(findChild); - }); + return parentObject.getCapability('composition').add(object) + .then(function (objectInNewContext) { + return parentObject.getCapability('persistence') + .persist() + .then(function () { return objectInNewContext; }); + }); }; return LinkService; diff --git a/platform/entanglement/src/services/MoveService.js b/platform/entanglement/src/services/MoveService.js index 30c341ef22..608163310f 100644 --- a/platform/entanglement/src/services/MoveService.js +++ b/platform/entanglement/src/services/MoveService.js @@ -82,6 +82,12 @@ define( } } + if (!this.validate(object, parentObject)) { + throw new Error( + "Tried to move objects without validating first." + ); + } + return this.linkService .perform(object, parentObject) .then(relocate) diff --git a/platform/entanglement/test/services/CopyServiceSpec.js b/platform/entanglement/test/services/CopyServiceSpec.js index 6d7b1d5069..2788fcefa8 100644 --- a/platform/entanglement/test/services/CopyServiceSpec.js +++ b/platform/entanglement/test/services/CopyServiceSpec.js @@ -41,19 +41,23 @@ define( } describe("CopyService", function () { + var policyService; + + beforeEach(function () { + policyService = jasmine.createSpyObj( + 'policyService', + ['allow'] + ); + }); + describe("validate", function () { - var policyService, - copyService, + var copyService, object, parentCandidate, validate; beforeEach(function () { - policyService = jasmine.createSpyObj( - 'policyService', - ['allow'] - ); copyService = new CopyService( null, null, @@ -126,6 +130,16 @@ define( copyResult, copyFinished; + beforeEach(function () { + creationService = jasmine.createSpyObj( + 'creationService', + ['createObject'] + ); + createObjectPromise = synchronousPromise(undefined); + creationService.createObject.andReturn(createObjectPromise); + policyService.allow.andReturn(true); + }); + describe("on domain object without composition", function () { beforeEach(function () { object = domainObjectFactory({ @@ -142,13 +156,7 @@ define( composition: [] } }); - creationService = jasmine.createSpyObj( - 'creationService', - ['createObject'] - ); - createObjectPromise = synchronousPromise(undefined); - creationService.createObject.andReturn(createObjectPromise); - copyService = new CopyService(null, creationService); + copyService = new CopyService(null, creationService, policyService); copyResult = copyService.perform(object, newParent); copyFinished = jasmine.createSpy('copyFinished'); copyResult.then(copyFinished); @@ -180,7 +188,8 @@ define( }); describe("on domainObject with composition", function () { - var childObject, + var newObject, + childObject, compositionCapability, compositionPromise; @@ -216,6 +225,17 @@ define( composition: compositionCapability } }); + newObject = domainObjectFactory({ + name: 'object', + id: 'abc2', + model: { + name: 'some object', + composition: [] + }, + capabilities: { + composition: compositionCapability + } + }); newParent = domainObjectFactory({ name: 'newParent', id: '456', @@ -223,13 +243,10 @@ define( composition: [] } }); - creationService = jasmine.createSpyObj( - 'creationService', - ['createObject'] - ); - createObjectPromise = synchronousPromise(undefined); + + createObjectPromise = synchronousPromise(newObject); creationService.createObject.andReturn(createObjectPromise); - copyService = new CopyService(mockQ, creationService); + copyService = new CopyService(mockQ, creationService, policyService); copyResult = copyService.perform(object, newParent); copyFinished = jasmine.createSpy('copyFinished'); copyResult.then(copyFinished); @@ -266,6 +283,38 @@ define( }); }); + describe("on invalid inputs", function () { + beforeEach(function () { + object = domainObjectFactory({ + name: 'object', + capabilities: { + type: { type: 'object' } + } + }); + newParent = domainObjectFactory({ + name: 'parentCandidate', + capabilities: { + type: { type: 'parentCandidate' } + } + }); + }); + + it("throws an error", function () { + var copyService = + new CopyService(mockQ, creationService, policyService); + + function perform() { + copyService.perform(object, newParent); + } + + spyOn(copyService, "validate"); + copyService.validate.andReturn(true); + expect(perform).not.toThrow(); + copyService.validate.andReturn(false); + expect(perform).toThrow(); + }); + }); + }); }); } diff --git a/platform/entanglement/test/services/LinkServiceSpec.js b/platform/entanglement/test/services/LinkServiceSpec.js index b9bbe62c58..3d63c2c030 100644 --- a/platform/entanglement/test/services/LinkServiceSpec.js +++ b/platform/entanglement/test/services/LinkServiceSpec.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -/*global define,describe,beforeEach,it,jasmine,expect */ +/*global define,describe,beforeEach,it,jasmine,expect,spyOn */ define( [ @@ -41,6 +41,7 @@ define( 'policyService', ['allow'] ); + mockPolicyService.allow.andReturn(true); linkService = new LinkService(mockPolicyService); }); @@ -55,7 +56,13 @@ define( name: 'object' }); parentCandidate = domainObjectFactory({ - name: 'parentCandidate' + name: 'parentCandidate', + capabilities: { + composition: jasmine.createSpyObj( + 'composition', + ['invoke', 'add'] + ) + } }); validate = function () { return linkService.validate(object, parentCandidate); @@ -81,6 +88,18 @@ define( expect(validate()).toBe(false); }); + it("does not allow parents without composition", function () { + parentCandidate = domainObjectFactory({ + name: 'parentCandidate' + }); + object.id = 'abc'; + parentCandidate.id = 'xyz'; + parentCandidate.hasCapability.andCallFake(function (c) { + return c !== 'composition'; + }); + expect(validate()).toBe(false); + }); + describe("defers to policyService", function () { beforeEach(function () { object.id = 'abc'; @@ -121,16 +140,16 @@ define( linkedObject, parentModel, parentObject, - mutationPromise, compositionPromise, persistencePromise, + addPromise, compositionCapability, persistenceCapability; beforeEach(function () { - mutationPromise = new ControlledPromise(); compositionPromise = new ControlledPromise(); persistencePromise = new ControlledPromise(); + addPromise = new ControlledPromise(); persistenceCapability = jasmine.createSpyObj( 'persistenceCapability', ['persist'] @@ -138,9 +157,10 @@ define( persistenceCapability.persist.andReturn(persistencePromise); compositionCapability = jasmine.createSpyObj( 'compositionCapability', - ['invoke'] + ['invoke', 'add'] ); compositionCapability.invoke.andReturn(compositionPromise); + compositionCapability.add.andReturn(addPromise); parentModel = { composition: [] }; @@ -151,7 +171,7 @@ define( mutation: { invoke: function (mutator) { mutator(parentModel); - return mutationPromise; + return new ControlledPromise(); } }, persistence: persistenceCapability, @@ -172,20 +192,17 @@ define( }); - it("modifies parent model composition", function () { - expect(parentModel.composition.length).toBe(0); + it("adds to the parent's composition", function () { + expect(compositionCapability.add).not.toHaveBeenCalled(); linkService.perform(object, parentObject); - expect(parentObject.useCapability).toHaveBeenCalledWith( - 'mutation', - jasmine.any(Function) - ); - expect(parentModel.composition).toContain('xyz'); + expect(compositionCapability.add) + .toHaveBeenCalledWith(object); }); it("persists parent", function () { linkService.perform(object, parentObject); - expect(mutationPromise.then).toHaveBeenCalled(); - mutationPromise.resolve(); + expect(addPromise.then).toHaveBeenCalled(); + addPromise.resolve(linkedObject); expect(parentObject.getCapability) .toHaveBeenCalledWith('persistence'); expect(persistenceCapability.persist).toHaveBeenCalled(); @@ -197,11 +214,23 @@ define( whenComplete = jasmine.createSpy('whenComplete'); returnPromise.then(whenComplete); - mutationPromise.resolve(); + addPromise.resolve(linkedObject); persistencePromise.resolve(); compositionPromise.resolve([linkedObject]); expect(whenComplete).toHaveBeenCalledWith(linkedObject); }); + + it("throws an error when performed on invalid inputs", function () { + function perform() { + linkService.perform(object, parentObject); + } + + spyOn(linkService, 'validate'); + linkService.validate.andReturn(true); + expect(perform).not.toThrow(); + linkService.validate.andReturn(false); + expect(perform).toThrow(); + }); }); }); } diff --git a/platform/entanglement/test/services/MoveServiceSpec.js b/platform/entanglement/test/services/MoveServiceSpec.js index 02494a0afb..0eb9943dd8 100644 --- a/platform/entanglement/test/services/MoveServiceSpec.js +++ b/platform/entanglement/test/services/MoveServiceSpec.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -/*global define,describe,beforeEach,it,jasmine,expect */ +/*global define,describe,beforeEach,it,jasmine,expect,spyOn */ define( [ '../../src/services/MoveService', @@ -40,58 +40,57 @@ define( var moveService, policyService, + object, + objectContextCapability, + currentParent, + parentCandidate, linkService; beforeEach(function () { + objectContextCapability = jasmine.createSpyObj( + 'objectContextCapability', + [ + 'getParent' + ] + ); + + object = domainObjectFactory({ + name: 'object', + id: 'a', + capabilities: { + context: objectContextCapability, + type: { type: 'object' } + } + }); + + currentParent = domainObjectFactory({ + name: 'currentParent', + id: 'b' + }); + + objectContextCapability.getParent.andReturn(currentParent); + + parentCandidate = domainObjectFactory({ + name: 'parentCandidate', + model: { composition: [] }, + id: 'c', + capabilities: { + type: { type: 'parentCandidate' } + } + }); policyService = jasmine.createSpyObj( 'policyService', ['allow'] ); linkService = new MockLinkService(); + policyService.allow.andReturn(true); moveService = new MoveService(policyService, linkService); }); describe("validate", function () { - var object, - objectContextCapability, - currentParent, - parentCandidate, - validate; + var validate; beforeEach(function () { - - objectContextCapability = jasmine.createSpyObj( - 'objectContextCapability', - [ - 'getParent' - ] - ); - - object = domainObjectFactory({ - name: 'object', - id: 'a', - capabilities: { - context: objectContextCapability, - type: { type: 'object' } - } - }); - - currentParent = domainObjectFactory({ - name: 'currentParent', - id: 'b' - }); - - objectContextCapability.getParent.andReturn(currentParent); - - parentCandidate = domainObjectFactory({ - name: 'parentCandidate', - model: { composition: [] }, - id: 'c', - capabilities: { - type: { type: 'parentCandidate' } - } - }); - validate = function () { return moveService.validate(object, parentCandidate); }; @@ -145,14 +144,15 @@ define( describe("perform", function () { - var object, - newParent, - actionCapability, + var actionCapability, locationCapability, locationPromise, + newParent, moveResult; beforeEach(function () { + newParent = parentCandidate; + actionCapability = jasmine.createSpyObj( 'actionCapability', ['perform'] @@ -175,7 +175,9 @@ define( name: 'object', capabilities: { action: actionCapability, - location: locationCapability + location: locationCapability, + context: objectContextCapability, + type: { type: 'object' } } }); @@ -194,6 +196,18 @@ define( .toHaveBeenCalledWith(jasmine.any(Function)); }); + it("throws an error when performed on invalid inputs", function () { + function perform() { + moveService.perform(object, newParent); + } + + spyOn(moveService, "validate"); + moveService.validate.andReturn(true); + expect(perform).not.toThrow(); + moveService.validate.andReturn(false); + expect(perform).toThrow(); + }); + describe("when moving an original", function () { beforeEach(function () { locationCapability.getContextualLocation