diff --git a/packages/composer-common/lib/acl/aclfile.js b/packages/composer-common/lib/acl/aclfile.js index 49de341009..cfba84bb8f 100644 --- a/packages/composer-common/lib/acl/aclfile.js +++ b/packages/composer-common/lib/acl/aclfile.js @@ -32,7 +32,7 @@ class AclFile { * @param {ModelManager} modelManager - the ModelManager that manages this * ModelFile and that will be used to validate the rules in the AclFile * @param {string} definitions - The ACL rules as a string. - * @throws {IllegalModelException} + * @throws {IllegalAclException} */ constructor(id, modelManager, definitions) { this.modelManager = modelManager; @@ -62,8 +62,6 @@ class AclFile { // TODO (DCS) check that the id of the AclRule does not already exist this.rules.push(aclRule); } - - // console.log(JSON.stringify(this.ast)); } /** @@ -97,7 +95,7 @@ class AclFile { /** * Validates the ModelFile. * - * @throws {IllegalModelException} if the model is invalid + * @throws {IllegalAclException} if the model is invalid * @private */ validate() { diff --git a/packages/composer-common/lib/acl/aclrule.js b/packages/composer-common/lib/acl/aclrule.js index 2021b6c839..5d6de7ec51 100644 --- a/packages/composer-common/lib/acl/aclrule.js +++ b/packages/composer-common/lib/acl/aclrule.js @@ -14,9 +14,10 @@ 'use strict'; -const IllegalModelException = require('../introspect/illegalmodelexception'); +const IllegalAclException = require('./illegalaclexception'); const ModelBinding = require('./modelbinding'); const ParticipantDeclaration = require('../introspect/participantdeclaration'); +const Operation = require('./operation'); const Predicate = require('./predicate'); const TransactionDeclaration = require('../introspect/transactiondeclaration'); @@ -37,11 +38,11 @@ class AclRule { * * @param {AclFile} aclFile - the AclFile for this rule * @param {string} ast - the AST created by the parser - * @throws {IllegalModelException} + * @throws {IllegalAclException} */ constructor(aclFile, ast) { if(!aclFile || !ast) { - throw new IllegalModelException('Invalid AclFile or AST'); + throw new IllegalAclException('Invalid AclFile or AST'); } this.ast = ast; @@ -72,13 +73,13 @@ class AclRule { /** * Process the AST and build the model * - * @throws {IllegalModelException} + * @throws {IllegalAclException} * @private */ process() { this.name = this.ast.id.name; this.noun = new ModelBinding(this, this.ast.noun, this.ast.nounVariable); - this.verbs = this.ast.verbs; + this.operation = new Operation(this, this.ast.operation); this.participant = null; if(this.ast.participant && this.ast.participant !== 'ANY') { @@ -105,26 +106,19 @@ class AclRule { /** * Semantic validation of the structure of this AclRule. * - * @throws {IllegalModelException} + * @throws {IllegalAclException} * @private */ validate() { this.noun.validate(); - - const foundVerbs = {}; - this.verbs.forEach((verb) => { - if (foundVerbs[verb]) { - throw new IllegalModelException(`The verb '${verb}' has been specified more than once in the ACL rule '${this.name}'`); - } - foundVerbs[verb] = true; - }); + this.operation.validate(); if(this.participant) { this.participant.validate(); let participantClassDeclaration = this.participant.getClassDeclaration(); if (participantClassDeclaration && !(participantClassDeclaration instanceof ParticipantDeclaration)) { - throw new IllegalModelException(`The participant '${participantClassDeclaration.getName()}' must be a participant`); + throw new IllegalAclException(`Expected '${participantClassDeclaration.getName()}' to be a participant`, this.aclFile, this.participant.ast.location); } } @@ -133,7 +127,7 @@ class AclRule { let transactionClassDeclaration = this.transaction.getClassDeclaration(); if (transactionClassDeclaration && !(transactionClassDeclaration instanceof TransactionDeclaration)) { - throw new IllegalModelException(`The transaction '${transactionClassDeclaration.getName()}' must be a transaction`); + throw new IllegalAclException(`Expected '${transactionClassDeclaration.getName()}' to be a transaction`, this.aclFile, this.transaction.ast.location); } } @@ -166,7 +160,7 @@ class AclRule { * @return {string} the verb */ getVerbs() { - return this.verbs; + return this.operation.verbs; } /** diff --git a/packages/composer-common/lib/acl/illegalaclexception.js b/packages/composer-common/lib/acl/illegalaclexception.js new file mode 100644 index 0000000000..ce79b6d7e3 --- /dev/null +++ b/packages/composer-common/lib/acl/illegalaclexception.js @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const BaseFileException = require('../basefileexception'); + +/** + * Exception throws when a composer acl file is semantically invalid + * @extends BaseFileException + * @see See {@link BaseFileException} + * @class + * @memberof module:composer-common + * @private + */ +class IllegalAclException extends BaseFileException { + + /** + * Create an IllegalAclException. + * @param {String} message - the message for the exception + * @param {AclFile} [aclFile] - the optional aclFile associated with the exception + * @param {Object} [fileLocation] - location details of the error within the model file. + * @param {String} fileLocation.start.line - start line of the error location. + * @param {String} fileLocation.start.column - start column of the error location. + * @param {String} fileLocation.end.line - end line of the error location. + * @param {String} fileLocation.end.column - end column of the error location. + */ + constructor(message, aclFile, fileLocation) { + + let messageSuffix = ''; + if(aclFile && aclFile.getIdentifier()) { + messageSuffix = 'File \'' + aclFile.getIdentifier() + '\': ' ; + } + + if(fileLocation) { + messageSuffix = messageSuffix + 'line ' + fileLocation.start.line + ' column ' + + fileLocation.start.column + ', to line ' + fileLocation.end.line + ' column ' + + fileLocation.end.column + '. '; + } + + // First character to be uppercase, and prepended with a space + if (messageSuffix) { + messageSuffix = ' ' + messageSuffix.charAt(0).toUpperCase() + messageSuffix.slice(1); + } + + super(message, fileLocation, message + messageSuffix); + this.aclFile = aclFile; + } + + /** + * Returns the aclFile associated with the exception or null + * @return {AclFile} the optional acl file associated with the exception + */ + getAclFile() { + return this.aclFile; + } +} + +module.exports = IllegalAclException; diff --git a/packages/composer-common/lib/acl/modelbinding.js b/packages/composer-common/lib/acl/modelbinding.js index 4a61ad1b39..2344893348 100644 --- a/packages/composer-common/lib/acl/modelbinding.js +++ b/packages/composer-common/lib/acl/modelbinding.js @@ -14,7 +14,7 @@ 'use strict'; -const IllegalModelException = require('../introspect/illegalmodelexception'); +const IllegalAclException = require('./illegalaclexception'); const ModelUtil = require('../modelutil'); /** @@ -35,11 +35,11 @@ class ModelBinding { * @param {AclRule} aclRule - the AclRule for this ModelBinding * @param {Object} ast - the AST created by the parser * @param {Object} variableAst - the variable binding AST created by the parser - * @throws {IllegalModelException} + * @throws {IllegalAclException} */ constructor(aclRule, ast, variableAst) { if(!aclRule || !ast) { - throw new IllegalModelException('Invalid AclRule or AST'); + throw new IllegalAclException('Invalid AclRule or AST'); } this.ast = ast; @@ -78,7 +78,7 @@ class ModelBinding { /** * Process the AST and build the model * - * @throws {IllegalModelException} + * @throws {IllegalAclException} * @private */ process() { @@ -97,7 +97,7 @@ class ModelBinding { } /** - * Returns strind representation of this object + * Returns string representation of this object * * @return {string} the string version of the object */ @@ -153,7 +153,7 @@ class ModelBinding { /** * Semantic validation of the structure of this ModelBinding. * - * @throws {IllegalModelException} + * @throws {IllegalAclException} * @private */ validate() { @@ -171,26 +171,29 @@ class ModelBinding { if (namespaces.findIndex(function (element, index, array) { return (ns === element || element.startsWith(ns + '.')); })=== -1) { - throw new IllegalModelException('Failed to find namespace ' + this.qualifiedName); + throw new IllegalAclException(`Expected namespace \"${this.qualifiedName}\" to be defined`, this.aclRule.getAclFile(), this.ast.location); } } else if (ModelUtil.isWildcardName(this.qualifiedName)) { const modelFile = mm.getModelFile(ns); if(!modelFile) { - throw new IllegalModelException('Failed to find namespace ' + this.qualifiedName); + throw new IllegalAclException(`Expected namespace \"${this.qualifiedName}\" to be defined`, this.aclRule.getAclFile(), this.ast.location); } } else { + if (!ns) { + throw new IllegalAclException(`Expected class \"${this.qualifiedName}\" to include namespace`, this.aclRule.getAclFile(), this.ast.location); + } const modelFile = mm.getModelFile(ns); if(!modelFile) { - throw new IllegalModelException('Failed to find namespace ' + ns); + throw new IllegalAclException(`Expected class \"${this.qualifiedName}\" to be defined but namespace \"${ns}\" not found`, this.aclRule.getAclFile(), this.ast.location); } const className = ModelUtil.getShortName(this.qualifiedName); const classDeclaration = modelFile.getLocalType(className); if(!classDeclaration) { - throw new IllegalModelException('Failed to find class ' + this.qualifiedName); + throw new IllegalAclException(`Expected class \"${this.qualifiedName}\" to be defined`, this.aclRule.getAclFile(), this.ast.location); } this.classDeclaration = classDeclaration; diff --git a/packages/composer-common/lib/acl/operation.js b/packages/composer-common/lib/acl/operation.js new file mode 100644 index 0000000000..18046cade9 --- /dev/null +++ b/packages/composer-common/lib/acl/operation.js @@ -0,0 +1,104 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const IllegalAclException = require('./illegalaclexception'); + +/** + * Operation captures the array ofaction verbs that the ACL rule + * governs. + * + * @private + * @class + * @memberof module:composer-common + */ +class Operation { + + /** + * Create an Operation from an Abstract Syntax Tree. The AST is the + * result of parsing. + * + * @param {AclRule} aclRule - the AclRule for this Operation + * @param {Object} ast - the AST created by the parser + * @throws {IllegalAclException} + */ + constructor(aclRule, ast) { + if(!aclRule || !ast) { + throw new IllegalAclException('Invalid AclRule or AST'); + } + + this.ast = ast; + this.aclRule = aclRule; + this.process(); + } + + /** + * Visitor design pattern + * @param {Object} visitor - the visitor + * @param {Object} parameters - the parameter + * @return {Object} the result of visiting or null + * @private + */ + accept(visitor,parameters) { + return visitor.visit(this, parameters); + } + + /** + * Returns the AclRule that owns this Operation. + * + * @return {AclRule} the owning AclRule + */ + getAclRule() { + return this.aclRule; + } + + /** + * Returns the expression as a text string. + * + * @return {string} the verbs for the operation + */ + getVerbs() { + return this.verbs; + } + + /** + * Process the AST and build the model + * + * @throws {IllegalAclException} + * @private + */ + process() { + this.verbs = this.ast.verbs; + } + + /** + * Semantic validation of the structure of this Operation. + * + * @throws {IllegalAclException} + * @private + */ + validate() { + const foundVerbs = {}; + this.verbs.forEach((verb) => { + if (foundVerbs[verb]) { + throw new IllegalAclException(`The verb '${verb}' has been specified more than once in the ACL rule '${this.aclRule.getName()}'`, this.aclRule.getAclFile(), this.ast.location); + } + foundVerbs[verb] = true; + }); + } + +} + +module.exports = Operation; diff --git a/packages/composer-common/lib/acl/parser.pegjs b/packages/composer-common/lib/acl/parser.pegjs index 01f04288cc..3b47330f99 100644 --- a/packages/composer-common/lib/acl/parser.pegjs +++ b/packages/composer-common/lib/acl/parser.pegjs @@ -1386,7 +1386,7 @@ SimpleRule type: "SimpleRule", id: ruleId, noun: noun, - verbs: verbs, + operation: verbs, participant: participant, transaction: transaction, action: action, @@ -1417,7 +1417,7 @@ VariableBinding id: ruleId, noun: noun, nounVariable: nounVariable, - verbs: verbs, + operation: verbs, participant: participant, participantVariable: participantVariable, transaction: transaction, @@ -1495,7 +1495,10 @@ AdditionalBasicVerb = __ "," __ verb:BasicVerb */ BasicVerbList = first:BasicVerb others:(AdditionalBasicVerb)* { - return [first].concat(others); + return { + verbs: [first].concat(others), + location: location() + }; } /** @@ -1503,7 +1506,10 @@ BasicVerbList = first:BasicVerb others:(AdditionalBasicVerb)* */ AllVerb = 'ALL' { - return ['ALL'] + return { + verbs: ['ALL'], + location: location() + }; } /** diff --git a/packages/composer-common/lib/acl/predicate.js b/packages/composer-common/lib/acl/predicate.js index bc54788a29..bf8f74ac07 100644 --- a/packages/composer-common/lib/acl/predicate.js +++ b/packages/composer-common/lib/acl/predicate.js @@ -14,7 +14,7 @@ 'use strict'; -const IllegalModelException = require('../introspect/illegalmodelexception'); +const IllegalAclException = require('./illegalaclexception'); /** * Predicate captures a conditional Javascript expression: @@ -32,11 +32,11 @@ class Predicate { * * @param {AclRule} aclRule - the AclRule for this Predicate * @param {Object} ast - the AST created by the parser - * @throws {IllegalModelException} + * @throws {IllegalAclException} */ constructor(aclRule, ast) { if(!aclRule || !ast) { - throw new IllegalModelException('Invalid AclRule or AST'); + throw new IllegalAclException('Invalid AclRule or AST'); } this.expression = ast; @@ -76,7 +76,7 @@ class Predicate { /** * Process the AST and build the model * - * @throws {IllegalModelException} + * @throws {IllegalAclException} * @private */ process() { @@ -85,7 +85,7 @@ class Predicate { /** * Semantic validation of the structure of this ModelBinding. * - * @throws {IllegalModelException} + * @throws {IllegalAclException} * @private */ validate() { diff --git a/packages/composer-common/test/acl/aclfile.js b/packages/composer-common/test/acl/aclfile.js index 28d267bf05..fdf28613b7 100644 --- a/packages/composer-common/test/acl/aclfile.js +++ b/packages/composer-common/test/acl/aclfile.js @@ -17,7 +17,7 @@ const AclFile = require('../../lib/acl/aclfile'); const parser = require('../../lib/acl/parser'); const ModelManager = require('../../lib/modelmanager'); -const IllegalModelException = require('../../lib/introspect/illegalmodelexception'); +const IllegalAclException = require('../../lib/acl/illegalaclexception'); const ParseException = require('../../lib/introspect/parseexception'); const fs = require('fs'); const path = require('path'); @@ -60,7 +60,7 @@ describe('AclFile', () => { it('should call the parser with the definitions and save the abstract syntax tree', () => { const ast = { - rules: [ {id: {name: 'fake'}, noun: {qualifiedName: 'org.acme.*'}, verbs: 'UPDATE', participant: 'ANY', action: 'ALLOW'} ] + rules: [ {id: {name: 'fake'}, noun: {qualifiedName: 'org.acme.*'}, operation: {verbs: ['UPDATE']}, participant: 'ANY', action: 'ALLOW'} ] }; sandbox.stub(parser, 'parse').returns(ast); let mf = new AclFile( 'test', modelManager, 'fake definitions'); @@ -495,7 +495,7 @@ describe('AclFile', () => { const aclFile = new AclFile('test.acl', modelManager, aclContents); (() => { aclFile.validate(); - }).should.throw(IllegalModelException, /The participant.*must be a participant/); + }).should.throw(IllegalAclException, /Expected \'Car\' to be a participant File \'test.acl\': line 3 column 31, to line 3 column 43./); }); it('should fail to validate a rule when transaction is not a transaction', () => { @@ -510,7 +510,7 @@ describe('AclFile', () => { const aclFile = new AclFile('test.acl', modelManager, aclContents); (() => { aclFile.validate(); - }).should.throw(IllegalModelException, /The transaction.*must be a transaction/); + }).should.throw(IllegalAclException, /Expected \'Car\' to be a transaction File \'test.acl\': line 6 column 31, to line 6 column 43./); }); }); diff --git a/packages/composer-common/test/acl/aclrule.js b/packages/composer-common/test/acl/aclrule.js index 5a2298fe01..300f666992 100644 --- a/packages/composer-common/test/acl/aclrule.js +++ b/packages/composer-common/test/acl/aclrule.js @@ -49,7 +49,9 @@ describe('AclRule', () => { }, variableName: null }, - verbs: ['DELETE'], + operation: { + verbs: ['DELETE'] + }, participant: { type: 'Binding', qualifiedName: 'org.acme.Driver', @@ -138,7 +140,9 @@ describe('AclRule', () => { qualifiedName: 'org.acme.Car', variableName: null }, - verbs: ['ALL'], + operation: { + verbs: ['ALL'] + }, participant: 'ANY', transaction: null, predicate: 'true', @@ -161,7 +165,9 @@ describe('AclRule', () => { qualifiedName: 'org.acme.Car', variableName: null }, - verbs: ['ALL'], + operation: { + verbs: ['ALL'] + }, participant: 'ANY', transaction: { binding: { @@ -190,7 +196,9 @@ describe('AclRule', () => { qualifiedName: 'org.acme.Car', variableName: null }, - verbs: ['ALL'], + operation: { + verbs: ['ALL'] + }, participant: 'ANY', transaction: { variableBinding: { diff --git a/packages/composer-common/test/acl/illegalaclexception.js b/packages/composer-common/test/acl/illegalaclexception.js new file mode 100644 index 0000000000..74ea7e1df0 --- /dev/null +++ b/packages/composer-common/test/acl/illegalaclexception.js @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const BaseFileException = require('../../lib/basefileexception'); +const IllegalAclException = require('../../lib/acl/illegalaclexception'); +const AclFile = require('../../lib/acl/aclfile'); + +const should = require('chai').should(); +const sinon = require('sinon'); + + +describe('IllegalAclException', function () { + + let aclFile; + let fileLocation = {start: {column: 1, line: 1}, end: {column: 1, line: 1}}; + + beforeEach(function () { + aclFile = sinon.createStubInstance(AclFile); + aclFile.getIdentifier.returns('permissions.acl'); + }); + + describe('#constructor', function () { + + it('should return an instance of BaseFileException', function () { + let exc = new IllegalAclException('message', aclFile); + exc.should.be.an.instanceOf(BaseFileException); + }); + + it('should have a message', function () { + let exc = new IllegalAclException('message'); + exc.message.should.equal('message'); + }); + + it('should have a message including the file location', function () { + let exc = new IllegalAclException('message', aclFile, fileLocation); + exc.message.should.match(/message File \'permissions.acl\': line 1 column 1, to line 1 column 1./); + }); + + it('should have a aclFile', function () { + let exc = new IllegalAclException('message', aclFile); + exc.getAclFile().should.equal(aclFile); + }); + + it('should not have an aclFile', function () { + let exc = new IllegalAclException('message'); + should.not.exist(exc.getAclFile()); + }); + + it('should have a stack trace', function () { + let exc = new IllegalAclException('message', aclFile, fileLocation); + exc.stack.should.be.a('string'); + }); + + it('should handle a lack of support for stack traces', function () { + let captureStackTrace = Error.captureStackTrace; + Error.captureStackTrace = null; + try { + new IllegalAclException('message', aclFile, fileLocation); + } finally { + Error.captureStackTrace = captureStackTrace; + } + }); + + }); + +}); diff --git a/packages/composer-common/test/acl/modelbinding.js b/packages/composer-common/test/acl/modelbinding.js index 03df44d931..455fd90881 100644 --- a/packages/composer-common/test/acl/modelbinding.js +++ b/packages/composer-common/test/acl/modelbinding.js @@ -41,6 +41,7 @@ describe('ModelBinding', () => { const missingNamespace = {'type':'Binding','qualifiedName':'org.missing.Missing.*'}; const missingRecursiveNamespace = {'type':'Binding','qualifiedName':'org.missing.Missing.**'}; const missing = {'type':'Binding','qualifiedName':'org.missing.Missing.*','instanceId':'ABC123'}; + const noNS = {'type':'Binding','qualifiedName':'any'}; beforeEach(() => { aclFile = sinon.createStubInstance(AclFile); @@ -123,7 +124,7 @@ describe('ModelBinding', () => { (() => { modelBinding = new ModelBinding( aclRule, missingClass ); modelBinding.validate(); - }).should.throw(/Failed to find class org.acme.Missing/); + }).should.throw(/^Expected class \"org\.acme\.Missing\" to be defined$/); }); it('should detect reference to missing namespace in the modelmanager', () => { @@ -131,28 +132,35 @@ describe('ModelBinding', () => { modelBinding = new ModelBinding( aclRule, classAst ); sinon.stub(modelManager,'getModelFile').returns(false); modelBinding.validate(); - }).should.throw(/Failed to find namespace org.acme/); + }).should.throw(/Expected class \"org\.acme\.Car\" to be defined but namespace \"org\.acme\" not found/); }); it('should detect reference to missing namespace', () => { (() => { modelBinding = new ModelBinding( aclRule, missingNamespace ); modelBinding.validate(); - }).should.throw(/Failed to find namespace org.missing.Missing/); + }).should.throw(/Expected namespace \"org\.missing\.Missing\.\*\" to be defined/); }); it('should detect reference to missing recursive namespace', () => { (() => { modelBinding = new ModelBinding( aclRule, missingRecursiveNamespace ); modelBinding.validate(); - }).should.throw(/Failed to find namespace org.missing.Missing/); + }).should.throw(/Expected namespace \"org\.missing\.Missing\.\*\*" to be defined/); }); it('should detect reference to missing namespace with variable name', () => { (() => { modelBinding = new ModelBinding( aclRule, missing ); modelBinding.validate(); - }).should.throw(/Failed to find namespace org.missing.Missing/); + }).should.throw(/Expected namespace \"org\.missing\.Missing\.\*" to be defined/); + }); + + it('should detect reference to class with no namespace', () => { + (() => { + modelBinding = new ModelBinding( aclRule, noNS ); + modelBinding.validate(); + }).should.throw(/Expected class \"any\" to include namespace/); }); }); diff --git a/packages/composer-common/test/acl/operation.js b/packages/composer-common/test/acl/operation.js new file mode 100644 index 0000000000..b60a517b8b --- /dev/null +++ b/packages/composer-common/test/acl/operation.js @@ -0,0 +1,99 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const AclRule = require('../../lib/acl/aclrule'); +const AclFile = require('../../lib/acl/aclfile'); +const ModelManager = require('../../lib/modelmanager'); +const Operation = require('../../lib/acl/operation'); + +require('chai').should(); +const sinon = require('sinon'); + +describe('Operation', () => { + + let operation; + let aclRule; + let aclFile; + let modelManager; + let sandbox; + const ast = { + verbs: ['ALL'] + }; + + beforeEach(() => { + aclFile = sinon.createStubInstance(AclFile); + modelManager = new ModelManager(); + aclFile.getModelManager.returns(modelManager); + aclRule = sinon.createStubInstance(AclRule); + aclRule.getAclFile.returns(aclFile); + aclRule.getName.returns('TestRule'); + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#constructor', () => { + + it('should throw when null AclRule provided', () => { + (() => { + operation = new Operation( null, {} ); + }).should.throw(/Invalid AclRule or AST/); + }); + + it('should throw when invalid definitions provided', () => { + (() => { + operation = new Operation( aclRule, null ); + }).should.throw(/Invalid AclRule or AST/); + }); + }); + + describe('#validate', () => { + + it('should validate correct contents', () => { + operation = new Operation( aclRule, ast ); + operation.validate(); + operation.getVerbs().should.deep.equal(['ALL']); + operation.getAclRule().should.equal(aclRule); + }); + + it('should detect duplicated operation verbs', () => { + const duplicateVerbAst = { + verbs: ['READ', 'UPDATE', 'READ'] + }; + + (() => { + operation = new Operation( aclRule, duplicateVerbAst ); + operation.validate(); + }).should.throw(/The verb \'READ\' has been specified more than once in the ACL rule \'TestRule\'/); + }); + }); + + describe('#accept', () => { + + it('should call the visitor', () => { + operation = new Operation( aclRule, ast ); + let visitor = { + visit: sinon.stub() + }; + operation.accept(visitor, ['some', 'args']); + sinon.assert.calledOnce(visitor.visit); + sinon.assert.calledWith(visitor.visit, operation, ['some', 'args']); + }); + + }); +});