Compare commits
9 Commits
style-guid
...
about-1924
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ae898427a | ||
|
|
12574a1333 | ||
|
|
dc91a94f0e | ||
|
|
0243aa6584 | ||
|
|
e5d869f01e | ||
|
|
d4e3e6689c | ||
|
|
0363d0e8ad | ||
|
|
3669e776a9 | ||
|
|
5d3adc6a7f |
121
docs/src/guide/security.md
Normal file
121
docs/src/guide/security.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Security Guide
|
||||
|
||||
Open MCT is a rich client with plugin support that executes as a single page
|
||||
web application in a browser environment. Security concerns and
|
||||
vulnerabilities associated with the web as a platform should be considered
|
||||
before deploying Open MCT (or any other web application) for mission or
|
||||
production usage.
|
||||
|
||||
This document describes several important points to consider when developing
|
||||
for or deploying Open MCT securely. Other resources such as
|
||||
[Open Web Application Security Project (OWASP)](https://www.owasp.org)
|
||||
provide a deeper and more general overview of security for web applications.
|
||||
|
||||
|
||||
## Security Model
|
||||
|
||||
Open MCT has been architected assuming the following deployment pattern:
|
||||
|
||||
* A tagged, tested Open MCT version will be used.
|
||||
* Externally authored plugins will be installed.
|
||||
* A server will provide persistent storage, telemetry, and other shared data.
|
||||
* Authorization, authentication, and auditing will be handled by a server.
|
||||
|
||||
|
||||
## Security Procedures
|
||||
|
||||
The Open MCT team secures our code base using a combination of code review,
|
||||
dependency review, and periodic security reviews. Static analysis performed
|
||||
during automated verification additionally safeguards against common
|
||||
coding errors which may result in vulnerabilities.
|
||||
|
||||
|
||||
### Code Review
|
||||
|
||||
All contributions are reviewed by internal team members. External
|
||||
contributors receive increased scrutiny for security and quality,
|
||||
and must sign a licensing agreement.
|
||||
|
||||
### Dependency Review
|
||||
|
||||
Before integrating third-party dependencies, they are reviewed for security
|
||||
and quality, with consideration given to authors and users of these
|
||||
dependencies, as well as review of open source code.
|
||||
|
||||
### Periodic Security Reviews
|
||||
|
||||
Open MCT's code, design, and architecture are periodically reviewed
|
||||
(approximately annually) for common security issues, such as the
|
||||
[OWASP Top Ten](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project).
|
||||
|
||||
|
||||
## Security Concerns
|
||||
|
||||
Certain security concerns deserve special attention when deploying Open MCT,
|
||||
or when authoring plugins.
|
||||
|
||||
### Identity Spoofing
|
||||
|
||||
Open MCT issues calls to web services with the privileges of a logged in user.
|
||||
Compromised sources (either for Open MCT itself or a plugin) could
|
||||
therefore allow malicious code to execute with those privileges.
|
||||
|
||||
To avoid this:
|
||||
|
||||
* Serve Open MCT and other scripts over SSL (https rather than http)
|
||||
to prevent man-in-the-middle attacks.
|
||||
* Exercise precautions such as security reviews for any plugins or
|
||||
applications built for or with Open MCT to reject malicious changes.
|
||||
|
||||
### Information Disclosure
|
||||
|
||||
If Open MCT is used to handle or display sensitive data, any components
|
||||
(such as adapter plugins) must take care to avoid leaking or disclosing
|
||||
this information. For example, avoid sending sensitive data to third-party
|
||||
servers or insecure APIs.
|
||||
|
||||
### Data Tampering
|
||||
|
||||
The web application architecture leaves open the possibility that direct
|
||||
calls will be made to back-end services, circumventing Open MCT entirely.
|
||||
As such, Open MCT assumes that server components will perform any necessary
|
||||
data validation during calls issues to the server.
|
||||
|
||||
Additionally, plugins which serialize and write data to the server must
|
||||
escape that data to avoid database injection attacks, and similar.
|
||||
|
||||
### Repudiation
|
||||
|
||||
Open MCT assumes that servers log any relevant interactions and associates
|
||||
these with a user identity; the specific user actions taken within the
|
||||
application are assumed not to be of concern for auditing.
|
||||
|
||||
In the absence of server-side logging, users may disclaim (maliciously,
|
||||
mistakenly, or otherwise) actions taken within the system without any
|
||||
way to prove otherwise.
|
||||
|
||||
If keeping client-level interactions is important, this will need to be
|
||||
implemented via a plugin.
|
||||
|
||||
### Denial-of-service
|
||||
|
||||
Open MCT assumes that server-side components will be insulated against
|
||||
denial-of-service attacks. Services should only permit resource-intensive
|
||||
tasks to be initiated by known or trusted users.
|
||||
|
||||
### Elevation of Privilege
|
||||
|
||||
Corollary to the assumption that servers guide against identity spoofing,
|
||||
Open MCT assumes that services do not allow a user to act with
|
||||
inappropriately escalated privileges. Open MCT cannot protect against
|
||||
such escalation; in the clearest case, a malicious actor could interact
|
||||
with web services directly to exploit such a vulnerability.
|
||||
|
||||
## Additional Reading
|
||||
|
||||
The following resources have been used as a basis for identifying potential
|
||||
security threats to Open MCT deployments in preparation of this document:
|
||||
|
||||
* [STRIDE model](https://www.owasp.org/index.php/Threat_Risk_Modeling#STRIDE)
|
||||
* [Attack Surface Analysis Cheat Sheet](https://www.owasp.org/index.php/Attack_Surface_Analysis_Cheat_Sheet)
|
||||
* [XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)
|
||||
@@ -52,7 +52,9 @@ var gulp = require('gulp'),
|
||||
var buildVariables = {
|
||||
version: project.version,
|
||||
timestamp: moment.utc(Date.now()).format(),
|
||||
revision: fs.existsSync('.git') ? git.long() : 'Unknown',
|
||||
revision: fs.existsSync('.git') ? git.long() :
|
||||
project._resolved ? project._resolved.replace(/^.*#/g, "") :
|
||||
'Unknown',
|
||||
branch: fs.existsSync('.git') ? git.branch() : 'Unknown'
|
||||
};
|
||||
return fs.readFileSync("src/start.frag", 'utf-8')
|
||||
|
||||
@@ -40,7 +40,7 @@ requirejs.config({
|
||||
"vue": "node_modules/vue/dist/vue.min",
|
||||
"zepto": "bower_components/zepto/zepto.min",
|
||||
"lodash": "bower_components/lodash/lodash",
|
||||
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
|
||||
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
|
||||
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
||||
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
||||
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
||||
|
||||
@@ -52,8 +52,22 @@ define(
|
||||
}
|
||||
|
||||
function setSelection(selection) {
|
||||
self.scope.selection = selection;
|
||||
self.refreshComposition(selection);
|
||||
if (!selection[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.mutationListener) {
|
||||
self.mutationListener();
|
||||
delete self.mutationListener;
|
||||
}
|
||||
|
||||
var domainObject = selection[0].context.oldItem;
|
||||
self.refreshComposition(domainObject);
|
||||
|
||||
if (domainObject) {
|
||||
self.mutationListener = domainObject.getCapability('mutation')
|
||||
.listen(self.refreshComposition.bind(self, domainObject));
|
||||
}
|
||||
}
|
||||
|
||||
$scope.filterBy = filterBy;
|
||||
@@ -70,16 +84,11 @@ define(
|
||||
/**
|
||||
* Gets the composition for the selected object and populates the scope with it.
|
||||
*
|
||||
* @param selection the selection object
|
||||
* @param domainObject the selected object
|
||||
* @private
|
||||
*/
|
||||
ElementsController.prototype.refreshComposition = function (selection) {
|
||||
if (!selection[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = selection[0].context.oldItem;
|
||||
var selectedObjectComposition = selected && selected.useCapability('composition');
|
||||
ElementsController.prototype.refreshComposition = function (domainObject) {
|
||||
var selectedObjectComposition = domainObject && domainObject.useCapability('composition');
|
||||
|
||||
if (selectedObjectComposition) {
|
||||
selectedObjectComposition.then(function (composition) {
|
||||
|
||||
@@ -29,9 +29,25 @@ define(
|
||||
var mockScope,
|
||||
mockOpenMCT,
|
||||
mockSelection,
|
||||
mockDomainObject,
|
||||
mockMutationCapability,
|
||||
mockUnlisten,
|
||||
selectable = [],
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockUnlisten = jasmine.createSpy('unlisten');
|
||||
mockMutationCapability = jasmine.createSpyObj("mutationCapability", [
|
||||
"listen"
|
||||
]);
|
||||
mockMutationCapability.listen.andReturn(mockUnlisten);
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", [
|
||||
"getCapability",
|
||||
"useCapability"
|
||||
]);
|
||||
mockDomainObject.useCapability.andCallThrough();
|
||||
mockDomainObject.getCapability.andReturn(mockMutationCapability);
|
||||
|
||||
mockScope = jasmine.createSpyObj("$scope", ['$on']);
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'on',
|
||||
@@ -43,6 +59,14 @@ define(
|
||||
selection: mockSelection
|
||||
};
|
||||
|
||||
selectable[0] = {
|
||||
context: {
|
||||
oldItem: mockDomainObject
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(ElementsController.prototype, 'refreshComposition');
|
||||
|
||||
controller = new ElementsController(mockScope, mockOpenMCT);
|
||||
});
|
||||
|
||||
@@ -75,6 +99,44 @@ define(
|
||||
expect(objects.filter(mockScope.searchElements).length).toBe(4);
|
||||
});
|
||||
|
||||
it("refreshes composition on selection", function () {
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(ElementsController.prototype.refreshComposition).toHaveBeenCalledWith(mockDomainObject);
|
||||
});
|
||||
|
||||
it("listens on mutation and refreshes composition", function () {
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('mutation');
|
||||
expect(mockMutationCapability.listen).toHaveBeenCalled();
|
||||
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(1);
|
||||
|
||||
mockMutationCapability.listen.mostRecentCall.args[0](mockDomainObject);
|
||||
|
||||
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it("cleans up mutation listener when selection changes", function () {
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockMutationCapability.listen).toHaveBeenCalled();
|
||||
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockUnlisten).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not listen on mutation for element proxy selectable", function () {
|
||||
selectable[0] = {
|
||||
context: {
|
||||
elementProxy: {}
|
||||
}
|
||||
};
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockDomainObject.getCapability).not.toHaveBeenCalledWith('mutation');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -272,7 +272,7 @@ define(
|
||||
self.resizeHandles = self.generateDragHandles(self.selectedElementProxy);
|
||||
} else {
|
||||
// Make fixed view selectable if it's not already.
|
||||
if (!self.fixedViewSelectable) {
|
||||
if (!self.fixedViewSelectable && selectable.length === 1) {
|
||||
self.fixedViewSelectable = true;
|
||||
selection.context.viewProxy = new FixedProxy(addElement, $q, dialogService);
|
||||
self.openmct.selection.select(selection);
|
||||
|
||||
@@ -415,7 +415,7 @@ define(
|
||||
PlotController.prototype.exportPNG = function () {
|
||||
var self = this;
|
||||
self.hideExportButtons = true;
|
||||
self.exportImageService.exportPNG(self.$element[0], "plot.png").finally(function () {
|
||||
self.exportImageService.exportPNG(self.$element[0], "plot.png", 'white').finally(function () {
|
||||
self.hideExportButtons = false;
|
||||
});
|
||||
};
|
||||
@@ -426,7 +426,7 @@ define(
|
||||
PlotController.prototype.exportJPG = function () {
|
||||
var self = this;
|
||||
self.hideExportButtons = true;
|
||||
self.exportImageService.exportJPG(self.$element[0], "plot.jpg").finally(function () {
|
||||
self.exportImageService.exportJPG(self.$element[0], "plot.jpg", 'white').finally(function () {
|
||||
self.hideExportButtons = false;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ define(
|
||||
* @param {string} type of image to convert the element to
|
||||
* @returns {promise}
|
||||
*/
|
||||
function renderElement(element, type) {
|
||||
function renderElement(element, type, color) {
|
||||
var defer = self.$q.defer(),
|
||||
validTypes = ["png", "jpg", "jpeg"],
|
||||
renderTimeout,
|
||||
@@ -72,11 +72,13 @@ define(
|
||||
return;
|
||||
}
|
||||
|
||||
// Save color to be restored later
|
||||
originalColor = element.style.backgroundColor || '';
|
||||
if (color) {
|
||||
// Save color to be restored later
|
||||
originalColor = element.style.backgroundColor || '';
|
||||
|
||||
// Defaulting to white so we can see the chart when printed
|
||||
self.changeBackgroundColor(element, 'white');
|
||||
// Defaulting to white so we can see the chart when printed
|
||||
self.changeBackgroundColor(element, color);
|
||||
}
|
||||
|
||||
renderTimeout = self.$timeout(function () {
|
||||
defer.reject("html2canvas timed out");
|
||||
@@ -86,7 +88,9 @@ define(
|
||||
try {
|
||||
self.html2canvas(element, {
|
||||
onrendered: function (canvas) {
|
||||
self.changeBackgroundColor(element, originalColor);
|
||||
if (color) {
|
||||
self.changeBackgroundColor(element, originalColor);
|
||||
}
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case "png":
|
||||
@@ -106,7 +110,10 @@ define(
|
||||
|
||||
defer.promise.finally(function () {
|
||||
renderTimeout.cancel();
|
||||
self.changeBackgroundColor(element, originalColor);
|
||||
|
||||
if (color) {
|
||||
self.changeBackgroundColor(element, originalColor);
|
||||
}
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
@@ -149,8 +156,8 @@ define(
|
||||
* @param {string} filename the exported image
|
||||
* @returns {promise}
|
||||
*/
|
||||
ExportImageService.prototype.exportJPG = function (element, filename) {
|
||||
return renderElement(element, "jpeg").then(function (img) {
|
||||
ExportImageService.prototype.exportJPG = function (element, filename, color) {
|
||||
return renderElement(element, "jpeg", color).then(function (img) {
|
||||
self.saveAs(img, filename);
|
||||
});
|
||||
};
|
||||
@@ -161,8 +168,8 @@ define(
|
||||
* @param {string} filename the exported image
|
||||
* @returns {promise}
|
||||
*/
|
||||
ExportImageService.prototype.exportPNG = function (element, filename) {
|
||||
return renderElement(element, "png").then(function (img) {
|
||||
ExportImageService.prototype.exportPNG = function (element, filename, color) {
|
||||
return renderElement(element, "png", color).then(function (img) {
|
||||
self.saveAs(img, filename);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -121,16 +121,26 @@ define(
|
||||
});
|
||||
|
||||
it("changes background color to white and returns color back to original after snapshot, for better visibility of plot lines on print", function () {
|
||||
exportImageService.exportPNG(testElement, "plot.png");
|
||||
exportImageService.exportPNG(testElement, "plot.png", 'white');
|
||||
|
||||
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
|
||||
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
|
||||
|
||||
exportImageService.exportJPG(testElement, "plot.jpg");
|
||||
exportImageService.exportJPG(testElement, "plot.jpg", 'white');
|
||||
|
||||
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
|
||||
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
|
||||
});
|
||||
|
||||
it("does not change background color when color is not specified in parameters", function () {
|
||||
exportImageService.exportPNG(testElement, "plot.png");
|
||||
|
||||
expect(mockChangeBackgroundColor).not.toHaveBeenCalled();
|
||||
|
||||
exportImageService.exportJPG(testElement, "plot.jpg");
|
||||
|
||||
expect(mockChangeBackgroundColor).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
269
src/api/composition/CompositionAPISpec.js
Normal file
269
src/api/composition/CompositionAPISpec.js
Normal file
@@ -0,0 +1,269 @@
|
||||
define([
|
||||
'./CompositionAPI',
|
||||
'./CompositionCollection'
|
||||
], function (
|
||||
CompositionAPI,
|
||||
CompositionCollection
|
||||
) {
|
||||
|
||||
describe('The Composition API', function () {
|
||||
var publicAPI;
|
||||
var compositionAPI;
|
||||
var topicService;
|
||||
var mutationTopic;
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
mutationTopic = jasmine.createSpyObj('mutationTopic', [
|
||||
'listen'
|
||||
]);
|
||||
topicService = jasmine.createSpy('topicService');
|
||||
topicService.andReturn(mutationTopic);
|
||||
publicAPI = {};
|
||||
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
|
||||
'get'
|
||||
]);
|
||||
publicAPI.objects.get.andCallFake(function (identifier) {
|
||||
return Promise.resolve({identifier: identifier});
|
||||
});
|
||||
publicAPI.$injector = jasmine.createSpyObj('$injector', [
|
||||
'get'
|
||||
]);
|
||||
publicAPI.$injector.get.andReturn(topicService);
|
||||
compositionAPI = new CompositionAPI(publicAPI);
|
||||
});
|
||||
|
||||
it('returns falsy if an object does not support composition', function () {
|
||||
expect(compositionAPI.get({})).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('default composition', function () {
|
||||
var domainObject;
|
||||
var composition;
|
||||
|
||||
beforeEach(function () {
|
||||
domainObject = {
|
||||
name: 'test folder',
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test',
|
||||
key: 'a'
|
||||
}
|
||||
]
|
||||
};
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('returns composition collection', function () {
|
||||
expect(composition).toBeDefined();
|
||||
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
||||
});
|
||||
|
||||
it('loads composition from domain object', function () {
|
||||
var listener = jasmine.createSpy('addListener');
|
||||
var loaded = false;
|
||||
composition.on('add', listener);
|
||||
composition.load()
|
||||
.then(function () {
|
||||
loaded = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return loaded;
|
||||
});
|
||||
runs(function () {
|
||||
expect(listener.calls.length).toBe(1);
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
identifier: {namespace: 'test', key: 'a'}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Implement add/removal in new default provider.
|
||||
xit('synchronizes changes between instances', function () {
|
||||
var otherComposition = compositionAPI.get(domainObject);
|
||||
var addListener = jasmine.createSpy('addListener');
|
||||
var removeListener = jasmine.createSpy('removeListener');
|
||||
var otherAddListener = jasmine.createSpy('otherAddListener');
|
||||
var otherRemoveListener = jasmine.createSpy('otherRemoveListener');
|
||||
composition.on('add', addListener);
|
||||
composition.on('remove', removeListener);
|
||||
otherComposition.on('add', otherAddListener);
|
||||
otherComposition.on('remove', otherRemoveListener);
|
||||
var loaded = false;
|
||||
Promise.all([composition.load(), otherComposition.load()])
|
||||
.then(function () {
|
||||
loaded = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return loaded;
|
||||
});
|
||||
runs(function () {
|
||||
expect(addListener).toHaveBeenCalled();
|
||||
expect(otherAddListener).toHaveBeenCalled();
|
||||
expect(removeListener).not.toHaveBeenCalled();
|
||||
expect(otherRemoveListener).not.toHaveBeenCalled();
|
||||
|
||||
var object = addListener.mostRecentCall.args[0];
|
||||
composition.remove(object);
|
||||
expect(removeListener).toHaveBeenCalled();
|
||||
expect(otherRemoveListener).toHaveBeenCalled();
|
||||
|
||||
addListener.reset();
|
||||
otherAddListener.reset();
|
||||
composition.add(object);
|
||||
expect(addListener).toHaveBeenCalled();
|
||||
expect(otherAddListener).toHaveBeenCalled();
|
||||
|
||||
removeListener.reset();
|
||||
otherRemoveListener.reset();
|
||||
otherComposition.remove(object);
|
||||
expect(removeListener).toHaveBeenCalled();
|
||||
expect(otherRemoveListener).toHaveBeenCalled();
|
||||
|
||||
addListener.reset();
|
||||
otherAddListener.reset();
|
||||
otherComposition.add(object);
|
||||
expect(addListener).toHaveBeenCalled();
|
||||
expect(otherAddListener).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('static custom composition', function () {
|
||||
var customProvider;
|
||||
var domainObject;
|
||||
var composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A simple custom provider, returns the same composition for
|
||||
// all objects of a given type.
|
||||
customProvider = {
|
||||
appliesTo: function (object) {
|
||||
return object.type === 'custom-object-type';
|
||||
},
|
||||
load: function (object) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
namespace: 'custom',
|
||||
key: 'thing'
|
||||
}
|
||||
]);
|
||||
}
|
||||
};
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
var listener = jasmine.createSpy('addListener');
|
||||
var loaded = false;
|
||||
composition.on('add', listener);
|
||||
composition.load()
|
||||
.then(function () {
|
||||
loaded = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return loaded;
|
||||
});
|
||||
runs(function () {
|
||||
expect(listener.calls.length).toBe(1);
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
identifier: {namespace: 'custom', key: 'thing'}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamic custom composition', function () {
|
||||
var customProvider;
|
||||
var domainObject;
|
||||
var composition;
|
||||
|
||||
beforeEach(function () {
|
||||
// A dynamic provider, loads an empty composition and exposes
|
||||
// listener functions.
|
||||
customProvider = jasmine.createSpyObj('dynamicProvider', [
|
||||
'appliesTo',
|
||||
'load',
|
||||
'on',
|
||||
'off'
|
||||
]);
|
||||
|
||||
customProvider.appliesTo.andReturn('true');
|
||||
customProvider.load.andReturn(Promise.resolve([]));
|
||||
|
||||
domainObject = {
|
||||
identifier: {
|
||||
namespace: 'test',
|
||||
key: '1'
|
||||
},
|
||||
type: 'custom-object-type'
|
||||
};
|
||||
compositionAPI.addProvider(customProvider);
|
||||
composition = compositionAPI.get(domainObject);
|
||||
});
|
||||
|
||||
it('supports listening and loading', function () {
|
||||
var addListener = jasmine.createSpy('addListener');
|
||||
var removeListener = jasmine.createSpy('removeListener');
|
||||
var loaded = false;
|
||||
composition.on('add', addListener);
|
||||
composition.on('remove', removeListener);
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'add',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
expect(customProvider.on).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
'remove',
|
||||
jasmine.any(Function),
|
||||
jasmine.any(CompositionCollection)
|
||||
);
|
||||
var add = customProvider.on.calls[0].args[2];
|
||||
var remove = customProvider.on.calls[1].args[2];
|
||||
composition.load()
|
||||
.then(function () {
|
||||
loaded = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return loaded;
|
||||
});
|
||||
runs(function () {
|
||||
expect(addListener).not.toHaveBeenCalled();
|
||||
expect(removeListener).not.toHaveBeenCalled();
|
||||
add({namespace: 'custom', key: 'thing'});
|
||||
});
|
||||
waitsFor(function () {
|
||||
return addListener.calls.length > 0;
|
||||
});
|
||||
runs(function () {
|
||||
expect(addListener).toHaveBeenCalledWith({
|
||||
identifier: {namespace: 'custom', key: 'thing'}
|
||||
});
|
||||
remove(addListener.mostRecentCall.args[0]);
|
||||
});
|
||||
waitsFor(function () {
|
||||
return removeListener.calls.length > 0;
|
||||
});
|
||||
runs(function () {
|
||||
expect(removeListener).toHaveBeenCalledWith({
|
||||
identifier: {namespace: 'custom', key: 'thing'}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -76,20 +76,22 @@ define([
|
||||
throw new Error('Event not supported by composition: ' + event);
|
||||
}
|
||||
|
||||
if (event === 'add') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} if (event === 'remove') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
if (this.provider.on && this.provider.off) {
|
||||
if (event === 'add') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} if (event === 'remove') {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners[event].push({
|
||||
@@ -124,20 +126,22 @@ define([
|
||||
if (this.listeners[event].length === 0) {
|
||||
// Remove provider listener if this is the last callback to
|
||||
// be removed.
|
||||
if (event === 'add') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} else if (event === 'remove') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
if (this.provider.off && this.provider.on) {
|
||||
if (event === 'add') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
} else if (event === 'remove') {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ requirejs.config({
|
||||
"vue": "node_modules/vue/dist/vue.min",
|
||||
"zepto": "bower_components/zepto/zepto.min",
|
||||
"lodash": "bower_components/lodash/lodash",
|
||||
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
|
||||
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
|
||||
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
||||
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
||||
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
||||
|
||||
Reference in New Issue
Block a user