From 525105b4dfda4b6ff5d7a5a16ba99e3189e04cf6 Mon Sep 17 00:00:00 2001 From: Jerome Simeon Date: Wed, 11 Dec 2019 17:46:29 -0500 Subject: [PATCH 1/4] feature(core) Stricter validation for atomic values Signed-off-by: Jerome Simeon --- .../lib/serializer/jsonpopulator.js | 38 +++++++++-- .../test/serializer/jsonpopulator.js | 64 ++++++++++++------- 2 files changed, 72 insertions(+), 30 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index a5f0488483..aad2ca281e 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -222,17 +222,41 @@ class JSONPopulator { } break; case 'Integer': - case 'Long': - result = this.ergo ? parseInt(json.nat) : parseInt(json); + case 'Long': { + const num = this.ergo ? json.nat : json; + if (typeof num === 'number') { + if (Math.trunc(num) !== num) { + throw new ValidationException(`Expected value ${JSON.stringify(json)} to be of type ${field.getType()}`); + } else { + result = num; + } + } else { + throw new ValidationException(`Expected value ${JSON.stringify(json)} to be of type ${field.getType()}`); + } + } break; - case 'Double': - result = parseFloat(json); + case 'Double': { + if (typeof json === 'number') { + result = parseFloat(json); + } else { + throw new ValidationException(`Expected value ${JSON.stringify(json)} to be of type ${field.getType()}`); + } + } break; - case 'Boolean': - result = (json === true || json === 'true'); + case 'Boolean': { + if (typeof json === 'boolean') { + result = json; + } else { + throw new ValidationException(`Expected value ${JSON.stringify(json)} to be of type ${field.getType()}`); + } + } break; case 'String': - result = json.toString(); + if (typeof json === 'string') { + result = json; + } else { + throw new ValidationException(`Expected value ${JSON.stringify(json)} to be of type ${field.getType()}`); + } break; default: { // everything else should be an enumerated value... diff --git a/packages/concerto-core/test/serializer/jsonpopulator.js b/packages/concerto-core/test/serializer/jsonpopulator.js index 2bf035bb57..ee38ba00f8 100644 --- a/packages/concerto-core/test/serializer/jsonpopulator.js +++ b/packages/concerto-core/test/serializer/jsonpopulator.js @@ -21,6 +21,7 @@ const ModelManager = require('../../lib/modelmanager'); const Relationship = require('../../lib/model/relationship'); const Resource = require('../../lib/model/resource'); const TypedStack = require('../../lib/serializer/typedstack'); +const ValidationException = require('../../lib/serializer/validationexception'); const TypeNotFoundException = require('../../lib/typenotfoundexception'); const Util = require('../composer/systemmodelutility'); const Moment = require('moment-mini'); @@ -114,13 +115,12 @@ describe('JSONPopulator', () => { value.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]').should.equal(Moment.parseZone('2016-10-20T05:34:03.000Z').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); }); - it('should convert to integers from strings', () => { + it('should not convert to integers from strings', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Integer'); - let value = jsonPopulator.convertToObject(field, '32768'); - value.should.equal(32768); - value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); - value.should.equal(32768); + (() => { + jsonPopulator.convertToObject(field, '32768'); + }).should.throw(ValidationException, /Expected value "32768" to be of type Integer/); }); it('should convert to integers from numbers', () => { @@ -128,17 +128,16 @@ describe('JSONPopulator', () => { field.getType.returns('Integer'); let value = jsonPopulator.convertToObject(field, 32768); value.should.equal(32768); - value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); + value = ergoJsonPopulator.convertToObject(field, {'nat':32768}); value.should.equal(32768); }); - it('should convert to longs from strings', () => { + it('should not convert to longs from strings', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Long'); - let value = jsonPopulator.convertToObject(field, '32768'); - value.should.equal(32768); - value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); - value.should.equal(32768); + (() => { + jsonPopulator.convertToObject(field, '32768'); + }).should.throw(ValidationException, /Expected value "32768" to be of type Long/); }); it('should convert to longs from numbers', () => { @@ -146,15 +145,24 @@ describe('JSONPopulator', () => { field.getType.returns('Long'); let value = jsonPopulator.convertToObject(field, 32768); value.should.equal(32768); - value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); + value = ergoJsonPopulator.convertToObject(field, {'nat':32768}); value.should.equal(32768); }); - it('should convert to doubles from strings', () => { + it('should convert to longs from numbers that are not integers', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Long'); + (() => { + jsonPopulator.convertToObject(field, 32.768); + }).should.throw(ValidationException, /Expected value 32.768 to be of type Long/); + }); + + it('should not convert to doubles from strings', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Double'); - let value = jsonPopulator.convertToObject(field, '32.768'); - value.should.equal(32.768); + (() => { + jsonPopulator.convertToObject(field, '32.768'); + }).should.throw(ValidationException, /Expected value "32.768" to be of type Double/); }); it('should convert to doubles from numbers', () => { @@ -164,18 +172,27 @@ describe('JSONPopulator', () => { value.should.equal(32.768); }); - it('should convert to booleans from strings', () => { + it('should convert to booleans from true', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Boolean'); - let value = jsonPopulator.convertToObject(field, 'true'); + let value = jsonPopulator.convertToObject(field, true); value.should.equal(true); }); - it('should convert to booleans from numbers', () => { + it('should not convert to booleans from strings', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Boolean'); + (() => { + jsonPopulator.convertToObject(field, 'true'); + }).should.throw(ValidationException, /Expected value "true" to be of type Boolean/); + }); + + it('should not convert to booleans from numbers', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Boolean'); - let value = jsonPopulator.convertToObject(field, false); - value.should.equal(false); + (() => { + jsonPopulator.convertToObject(field, 32.768); + }).should.throw(ValidationException, /Expected value 32.768 to be of type Boolean/); }); it('should convert to strings from strings', () => { @@ -185,11 +202,12 @@ describe('JSONPopulator', () => { value.should.equal('hello world'); }); - it('should convert to strings from numbers', () => { + it('should not convert to strings from numbers', () => { let field = sinon.createStubInstance(Field); field.getType.returns('String'); - let value = jsonPopulator.convertToObject(field, 32768); - value.should.equal('32768'); + (() => { + jsonPopulator.convertToObject(field, 32.768); + }).should.throw(ValidationException, /Expected value 32.768 to be of type String/); }); }); From 8d22d34747c99dd63bc178c2a2b127088742a521 Mon Sep 17 00:00:00 2001 From: Jerome Simeon Date: Thu, 12 Dec 2019 12:36:42 -0500 Subject: [PATCH 2/4] feature(core) Validation rejects dates that are not in ISO 8601 format Signed-off-by: Jerome Simeon --- packages/concerto-core/lib/serializer/jsonpopulator.js | 9 +++++++-- packages/concerto-core/test/serializer/jsonpopulator.js | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index aad2ca281e..35029d4afe 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -214,12 +214,17 @@ class JSONPopulator { let result = null; switch(field.getType()) { - case 'DateTime': + case 'DateTime': { if (Moment.isMoment(json)) { result = json; } else { - result = new Moment.parseZone(json); + // Uses strict mode + result = new Moment.parseZone(json, Moment.ISO_8601, true); } + if (!result.isValid()) { + throw new ValidationException(`Expected value ${JSON.stringify(json)} to be of type ${field.getType()}`); + } + } break; case 'Integer': case 'Long': { diff --git a/packages/concerto-core/test/serializer/jsonpopulator.js b/packages/concerto-core/test/serializer/jsonpopulator.js index ee38ba00f8..4b75af9b63 100644 --- a/packages/concerto-core/test/serializer/jsonpopulator.js +++ b/packages/concerto-core/test/serializer/jsonpopulator.js @@ -115,6 +115,14 @@ describe('JSONPopulator', () => { value.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]').should.equal(Moment.parseZone('2016-10-20T05:34:03.000Z').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]')); }); + it('should not convert to dates from invalid moments', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('DateTime'); + (() => { + jsonPopulator.convertToObject(field, 'foo'); + }).should.throw(ValidationException, /Expected value "foo" to be of type DateTime/); + }); + it('should not convert to integers from strings', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Integer'); From 480a3ba75dd279f776a5d1256b821cf9c9b329db Mon Sep 17 00:00:00 2001 From: Jerome Simeon Date: Thu, 12 Dec 2019 12:56:19 -0500 Subject: [PATCH 3/4] test(validation) More atomic value validation tests Signed-off-by: Jerome Simeon --- .../test/serializer/jsonpopulator.js | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/concerto-core/test/serializer/jsonpopulator.js b/packages/concerto-core/test/serializer/jsonpopulator.js index 4b75af9b63..d0203bd4ab 100644 --- a/packages/concerto-core/test/serializer/jsonpopulator.js +++ b/packages/concerto-core/test/serializer/jsonpopulator.js @@ -123,6 +123,30 @@ describe('JSONPopulator', () => { }).should.throw(ValidationException, /Expected value "foo" to be of type DateTime/); }); + it('should not convert to dates from null', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('DateTime'); + (() => { + jsonPopulator.convertToObject(field, null); + }).should.throw(ValidationException, /Expected value null to be of type DateTime/); + }); + + it('should not convert to dates from undefined', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('DateTime'); + (() => { + jsonPopulator.convertToObject(field, undefined); + }).should.throw(ValidationException, /Expected value undefined to be of type DateTime/); + }); + + it('should not convert to dates when not in ISO 8601 format', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('DateTime'); + (() => { + jsonPopulator.convertToObject(field, 'December 17, 1995 03:24:00'); + }).should.throw(ValidationException, /Expected value "December 17, 1995 03:24:00" to be of type DateTime/); + }); + it('should not convert to integers from strings', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Integer'); @@ -131,6 +155,22 @@ describe('JSONPopulator', () => { }).should.throw(ValidationException, /Expected value "32768" to be of type Integer/); }); + it('should not convert to integer from null', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Integer'); + (() => { + jsonPopulator.convertToObject(field, null); + }).should.throw(ValidationException, /Expected value null to be of type Integer/); + }); + + it('should not convert to integer from undefined', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Integer'); + (() => { + jsonPopulator.convertToObject(field, undefined); + }).should.throw(ValidationException, /Expected value undefined to be of type Integer/); + }); + it('should convert to integers from numbers', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Integer'); @@ -148,6 +188,22 @@ describe('JSONPopulator', () => { }).should.throw(ValidationException, /Expected value "32768" to be of type Long/); }); + it('should not convert to long from null', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Long'); + (() => { + jsonPopulator.convertToObject(field, null); + }).should.throw(ValidationException, /Expected value null to be of type Long/); + }); + + it('should not convert to long from undefined', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Long'); + (() => { + jsonPopulator.convertToObject(field, undefined); + }).should.throw(ValidationException, /Expected value undefined to be of type Long/); + }); + it('should convert to longs from numbers', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Long'); @@ -157,7 +213,7 @@ describe('JSONPopulator', () => { value.should.equal(32768); }); - it('should convert to longs from numbers that are not integers', () => { + it('should not convert to longs from numbers that are not integers', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Long'); (() => { @@ -173,6 +229,22 @@ describe('JSONPopulator', () => { }).should.throw(ValidationException, /Expected value "32.768" to be of type Double/); }); + it('should not convert to double from null', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Double'); + (() => { + jsonPopulator.convertToObject(field, null); + }).should.throw(ValidationException, /Expected value null to be of type Double/); + }); + + it('should not convert to double from undefined', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Double'); + (() => { + jsonPopulator.convertToObject(field, undefined); + }).should.throw(ValidationException, /Expected value undefined to be of type Double/); + }); + it('should convert to doubles from numbers', () => { let field = sinon.createStubInstance(Field); field.getType.returns('Double'); @@ -203,6 +275,22 @@ describe('JSONPopulator', () => { }).should.throw(ValidationException, /Expected value 32.768 to be of type Boolean/); }); + it('should not convert to boolean from null', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Boolean'); + (() => { + jsonPopulator.convertToObject(field, null); + }).should.throw(ValidationException, /Expected value null to be of type Boolean/); + }); + + it('should not convert to boolean from undefined', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('Boolean'); + (() => { + jsonPopulator.convertToObject(field, undefined); + }).should.throw(ValidationException, /Expected value undefined to be of type Boolean/); + }); + it('should convert to strings from strings', () => { let field = sinon.createStubInstance(Field); field.getType.returns('String'); @@ -218,6 +306,22 @@ describe('JSONPopulator', () => { }).should.throw(ValidationException, /Expected value 32.768 to be of type String/); }); + it('should not convert to string from null', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('String'); + (() => { + jsonPopulator.convertToObject(field, null); + }).should.throw(ValidationException, /Expected value null to be of type String/); + }); + + it('should not convert to string from undefined', () => { + let field = sinon.createStubInstance(Field); + field.getType.returns('String'); + (() => { + jsonPopulator.convertToObject(field, undefined); + }).should.throw(ValidationException, /Expected value undefined to be of type String/); + }); + }); describe('#convertItem', () => { From 2e2bfc41724f4c3775d563e71d373b4f7ed94344 Mon Sep 17 00:00:00 2001 From: Jerome Simeon Date: Fri, 13 Dec 2019 09:34:11 -0500 Subject: [PATCH 4/4] fix(validation) Serializing a not finite Double throws a validation error #158 Signed-off-by: Jerome Simeon --- .../lib/serializer/resourcevalidator.js | 12 ++++- packages/concerto-core/test/serializer.js | 50 +++++++++++++++++-- .../test/serializer/jsongenerator.js | 2 +- .../test/serializer/resourcevalidator.js | 17 +++++++ 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/packages/concerto-core/lib/serializer/resourcevalidator.js b/packages/concerto-core/lib/serializer/resourcevalidator.js index 85cff11fe6..5f8f925894 100644 --- a/packages/concerto-core/lib/serializer/resourcevalidator.js +++ b/packages/concerto-core/lib/serializer/resourcevalidator.js @@ -299,12 +299,16 @@ class ResourceValidator { invalid = true; } break; - case 'Double': case 'Long': case 'Integer': + case 'Double': { if(dataType !== 'number') { invalid = true; } + if (!isFinite(obj)) { + invalid = true; + } + } break; case 'Boolean': if(dataType !== 'boolean') { @@ -421,7 +425,11 @@ class ResourceValidator { else { if(value) { try { - value = JSON.stringify(value); + if (typeof value === 'number' && !isFinite(value)) { + value = value.toString(); + } else { + value = JSON.stringify(value); + } } catch(err) { value = value.toString(); diff --git a/packages/concerto-core/test/serializer.js b/packages/concerto-core/test/serializer.js index 5fdf5685f0..6368af3db2 100644 --- a/packages/concerto-core/test/serializer.js +++ b/packages/concerto-core/test/serializer.js @@ -45,6 +45,7 @@ describe('Serializer', () => { o String assetId --> SampleParticipant owner o String stringValue + o Double doubleValue } participant SampleParticipant identified by participantId { @@ -61,6 +62,7 @@ describe('Serializer', () => { concept Address { o String city o String country + o Double elevation } event SampleEvent{ @@ -113,6 +115,7 @@ describe('Serializer', () => { let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); resource.owner = factory.newRelationship('org.acme.sample', 'SampleParticipant', 'alice@email.com'); resource.stringValue = 'the value'; + resource.doubleValue = 3.14; let json = serializer.toJSON(resource, { validate: true }); @@ -120,10 +123,41 @@ describe('Serializer', () => { $class: 'org.acme.sample.SampleAsset', assetId: '1', owner: 'resource:org.acme.sample.SampleParticipant#alice@email.com', - stringValue: 'the value' + stringValue: 'the value', + doubleValue: 3.14 }); }); + it('should throw validation errors during JSON object generation if Double is NaN', () => { + let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); + resource.owner = factory.newRelationship('org.acme.sample', 'SampleParticipant', 'alice@email.com'); + resource.stringValue = 'the value'; + resource.doubleValue = NaN; + (() => { + serializer.toJSON(resource); + }).should.throw(/Model violation in instance org.acme.sample.SampleAsset#1 field doubleValue has value NaN/); + }); + + it('should throw validation errors during JSON object generation if Double is Infinity', () => { + let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); + resource.owner = factory.newRelationship('org.acme.sample', 'SampleParticipant', 'alice@email.com'); + resource.stringValue = 'the value'; + resource.doubleValue = Infinity; + (() => { + serializer.toJSON(resource); + }).should.throw(/Model violation in instance org.acme.sample.SampleAsset#1 field doubleValue has value Infinity/); + }); + + it('should throw validation errors during JSON object generation if Double is -Infinity', () => { + let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); + resource.owner = factory.newRelationship('org.acme.sample', 'SampleParticipant', 'alice@email.com'); + resource.stringValue = 'the value'; + resource.doubleValue = -Infinity; + (() => { + serializer.toJSON(resource); + }).should.throw(/Model violation in instance org.acme.sample.SampleAsset#1 field doubleValue has value -Infinity/); + }); + it('should throw validation errors during JSON object generation if the validate flag is not specified and errors are present', () => { let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); (() => { @@ -175,10 +209,12 @@ describe('Serializer', () => { let address = factory.newConcept('org.acme.sample', 'Address'); address.city = 'Winchester'; address.country = 'UK'; + address.elevation = 3.14; const json = serializer.toJSON(address); json.should.deep.equal({ $class: 'org.acme.sample.Address', country: 'UK', + elevation: 3.14, city: 'Winchester' }); }); @@ -187,6 +223,7 @@ describe('Serializer', () => { let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); resource.owner = factory.newRelationship('org.acme.sample', 'SampleParticipant', 'alice@email.com'); resource.stringValue = ''; + resource.doubleValue = 3.14; let json = serializer.toJSON(resource, { validate: true }); @@ -194,7 +231,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.SampleAsset', assetId: '1', owner: 'resource:org.acme.sample.SampleParticipant#alice@email.com', - stringValue: '' + stringValue: '', + doubleValue: 3.14 }); }); }); @@ -222,13 +260,15 @@ describe('Serializer', () => { $class: 'org.acme.sample.SampleAsset', assetId: '1', owner: 'resource:org.acme.sample.SampleParticipant#alice@email.com', - stringValue: 'the value' + stringValue: 'the value', + doubleValue: 3.14 }; let resource = serializer.fromJSON(json); resource.should.be.an.instanceOf(Resource); resource.assetId.should.equal('1'); resource.owner.should.be.an.instanceOf(Relationship); resource.stringValue.should.equal('the value'); + resource.doubleValue.should.equal(3.14); }); it('should deserialize a valid transaction', () => { @@ -263,12 +303,14 @@ describe('Serializer', () => { let json = { $class: 'org.acme.sample.Address', city: 'Winchester', - country: 'UK' + country: 'UK', + elevation: 3.14 }; let resource = serializer.fromJSON(json); resource.should.be.an.instanceOf(Concept); resource.city.should.equal('Winchester'); resource.country.should.equal('UK'); + resource.elevation.should.equal(3.14); }); it('should throw validation errors if the validate flag is not specified', () => { diff --git a/packages/concerto-core/test/serializer/jsongenerator.js b/packages/concerto-core/test/serializer/jsongenerator.js index 84b8e569de..9630084c90 100644 --- a/packages/concerto-core/test/serializer/jsongenerator.js +++ b/packages/concerto-core/test/serializer/jsongenerator.js @@ -135,7 +135,7 @@ describe('JSONGenerator', () => { ergoJsonGenerator.convertToJSON({ getType: () => { return 'Integer'; } }, 123456).nat.should.equal(123456); }); - it('should pass through a double object', () => { + it('should pass through a finite double object', () => { jsonGenerator.convertToJSON({ getType: () => { return 'Double'; } }, 3.142).should.equal(3.142); }); diff --git a/packages/concerto-core/test/serializer/resourcevalidator.js b/packages/concerto-core/test/serializer/resourcevalidator.js index 1a5464fc75..6752f0b29c 100644 --- a/packages/concerto-core/test/serializer/resourcevalidator.js +++ b/packages/concerto-core/test/serializer/resourcevalidator.js @@ -74,6 +74,7 @@ describe('ResourceValidator', function () { import org.acme.l1.Person asset Vehicle extends Base { o Integer numberOfWheels + o Double milage } participant PrivateOwner identified by employeeId extends Person { o String employeeId @@ -414,6 +415,7 @@ describe('ResourceValidator', function () { vehicle.$identifier = ''; // empty the identifier vehicle.model = 'Ford'; vehicle.numberOfWheels = 4; + vehicle.milage = 3.14; const typedStack = new TypedStack(vehicle); const assetDeclaration = modelManager.getType('org.acme.l3.Car'); const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'ABC' }; @@ -423,6 +425,21 @@ describe('ResourceValidator', function () { }).should.throw(/has an empty identifier/); }); + it('should reject a Double which is not finite', function () { + const vehicle = factory.newResource('org.acme.l3', 'Car', 'foo'); + vehicle.$identifier = '42'; + vehicle.model = 'Ford'; + vehicle.numberOfWheels = 4; + vehicle.milage = NaN; // NaN + const typedStack = new TypedStack(vehicle); + const assetDeclaration = modelManager.getType('org.acme.l3.Car'); + const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'ABC' }; + + (() => { + assetDeclaration.accept(resourceValidator,parameters); + }).should.throw(/Model violation in instance org.acme.l3.Car#42 field milage has value NaN/); + }); + it('should report undeclared field if not identifiable', () => { const data = factory.newConcept('org.acme.l1', 'Data'); data.name = 'name';