From d4709e0646951d6866900c313ace2528e28a8f60 Mon Sep 17 00:00:00 2001 From: Joel Andrews Date: Wed, 4 Jul 2018 16:08:04 -0700 Subject: [PATCH] Issue #33: Allow validation to be skipped when an item has a semantically equal value Introduces the `skipValidationWhenValueUnchanged` constraint for all validation types. When enabled, validation is not performed on the property or element when it semantically equals the value from the previous revision (e.g. a `datetime` of "2018-06-13T18:01-07:00" is semantically equal to "2018-06-13T21:01-04:00"). The result is that, even if the property or element no longer satisfies the item constraint(s), its value will be allowed if it has not changed. --- CHANGELOG.md | 3 +- README.md | 1 + .../document-definitions-validator.spec.js | 4 +- src/validation/property-validator-schema.js | 1 + .../validation-function/comparison-module.js | 1 + .../document-properties-validation-module.js | 8 + ...on-when-value-unchanged-doc-definitions.js | 114 ++++++ ...ip-validation-when-value-unchanged.spec.js | 383 ++++++++++++++++++ 8 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 test/resources/skip-validation-when-value-unchanged-doc-definitions.js create mode 100644 test/skip-validation-when-value-unchanged.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 24cf081..61259eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). All notable changes will be documented in this file. ## [Unreleased] -Nothing yet. +### Added +- [#33](https://github.com/OldSneerJaw/couchster/issues/33): Option to ignore item validation errors when value is unchanged ## [1.1.0] - 2018-06-04 ### Added diff --git a/README.md b/README.md index 737ecf0..50e586c 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,7 @@ Validation for all simple and complex data types support the following additiona * `immutableWhenSetStrict`: As with the `immutableWhenSet` constraint, the item cannot be changed if it already has a value. However, it differs in that specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`) are not compared semantically; for example, the two `date` values of "2018" and "2018-01-01" are _not_ considered equal because the strings are not strictly equal. Defaults to `false`. * `mustEqual`: The value of the item must be equal to the specified value. Useful in cases where the item's value should be computed from other properties of the document (e.g. a reference ID that is encoded into the document's ID or a number that is the result of some calculation performed on other properties in the document). For that reason, this constraint is perhaps most useful when specified as a dynamic constraint (e.g. `mustEqual: function(newDoc, oldDoc, value, oldValue) { ... }`) rather than as a static value (e.g. `mustEqual: 'foobar'`). If this constraint is set to `null`, then only values of `null` or missing/`undefined` will be accepted for the corresponding property or element. Differs from `mustEqualStrict` in that it checks for semantic equality of specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`); for example, the two `datetime` values of "2018-02-12T11:02:00.000-08:00" and "2018-02-12T11:02-0800" are considered equal with this constraint since they represent the same point in time. No constraint by default. * `mustEqualStrict`: The value of the property or element must be strictly equal to the specified value. Differs from `mustEqual` in that specialized string validation types (e.g. `date`, `datetime`, `time`, `timezone`, `uuid`) are not compared semantically; for example, the two `timezone` values of "Z" and "+00:00" are _not_ considered equal because the strings are not strictly equal. No constraint by default. +* `skipValidationWhenValueUnchanged`: When set to `true`, the property or element is not validated if the document is being replaced and its value is equal to the same property or element value from the previous document revision. Useful if a change that is not backward compatible must be introduced to a property/element validator and existing values from documents that are already stored in the database should be preserved as they are. Defaults to `false`. * `customValidation`: A function that accepts as parameters (1) the new document, (2) the old document that is being replaced/deleted (if any), (3) an object that contains metadata about the current item to validate, (4) a stack of the items (e.g. object properties, array elements, hashtable element values) that have gone through validation, where the last/top element contains metadata for the direct parent of the item currently being validated and the first/bottom element is metadata for the root (i.e. the document itself), (5) the CouchDB [user context](http://docs.couchdb.org/en/latest/json-structure.html#userctx-object) of the authenticated user (or `null` if the request is not authenticated), and (6) the CouchDB [security object](http://docs.couchdb.org/en/latest/json-structure.html#security-object) for the database. In cases where the document is in the process of being deleted, the first parameter's `_deleted` property will be `true` and, if the document does not yet exist or it was deleted, the second parameter will be `null`. Generally, custom validation should not throw exceptions; it's recommended to return an array/list of error descriptions so the validation function can compile a list of all validation errors that were encountered once full validation is complete. A return value of `null`, `undefined` or an empty array indicate there were no validation errors. An example: ``` diff --git a/src/validation/document-definitions-validator.spec.js b/src/validation/document-definitions-validator.spec.js index 71c652f..ac0b450 100644 --- a/src/validation/document-definitions-validator.spec.js +++ b/src/validation/document-definitions-validator.spec.js @@ -89,7 +89,8 @@ describe('Document definitions validator:', () => { unrecognizedConstraint: true, // Invalid property constraint propertyValidators: { _validName: { - type: 'boolean' + type: 'boolean', + skipValidationWhenValueUnchanged: 1 // Must be a boolean }, dateProperty: { type: 'date', @@ -233,6 +234,7 @@ describe('Document definitions validator:', () => { 'myDoc1.propertyValidators.timezoneProperty.maximumValueExclusive: \"maximumValueExclusive\" conflict with forbidden peer \"mustEqual\"', 'myDoc1.propertyValidators._invalidName: "_invalidName" is not allowed', 'myDoc1.propertyValidators.nestedObject.unrecognizedConstraint: "unrecognizedConstraint" is not allowed', + 'myDoc1.propertyValidators.nestedObject.propertyValidators._validName.skipValidationWhenValueUnchanged: \"skipValidationWhenValueUnchanged\" must be a boolean', 'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.immutableWhenSet: \"immutableWhenSet\" conflict with forbidden peer \"immutable\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.immutable: \"immutable\" conflict with forbidden peer \"immutableWhenSet\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.maximumValue: "maximumValue" with value "2018-01-31T17:31:27.283-08:00" fails to match the required pattern: /^([+-]\\d{6}|\\d{4})(-(0[1-9]|1[0-2])(-(0[1-9]|[12]\\d|3[01]))?)?$/', diff --git a/src/validation/property-validator-schema.js b/src/validation/property-validator-schema.js index 9486f48..76eb014 100644 --- a/src/validation/property-validator-schema.js +++ b/src/validation/property-validator-schema.js @@ -240,6 +240,7 @@ function universalConstraintSchemas(typeEqualitySchema) { immutableWhenSetStrict: dynamicConstraintSchema(joi.boolean()), mustEqual: dynamicConstraintSchema(mustEqualConstraintSchema(typeEqualitySchema)), mustEqualStrict: dynamicConstraintSchema(mustEqualConstraintSchema(typeEqualitySchema)), + skipValidationWhenValueUnchanged: dynamicConstraintSchema(joi.boolean()), customValidation: joi.func().maxArity(6) // Function parameters: doc, oldDoc, currentItemEntry, validationItemStack, userContext, securityInfo }; } diff --git a/templates/validation-function/comparison-module.js b/templates/validation-function/comparison-module.js index c123daa..6df2693 100644 --- a/templates/validation-function/comparison-module.js +++ b/templates/validation-function/comparison-module.js @@ -1,5 +1,6 @@ function comparisonModule(utils, buildItemPath, timeModule) { return { + checkItemEquality: checkItemEquality, validateMinValueInclusiveConstraint: validateMinValueInclusiveConstraint, validateMaxValueInclusiveConstraint: validateMaxValueInclusiveConstraint, validateMinValueExclusiveConstraint: validateMinValueExclusiveConstraint, diff --git a/templates/validation-function/document-properties-validation-module.js b/templates/validation-function/document-properties-validation-module.js index f2610f1..307c77d 100644 --- a/templates/validation-function/document-properties-validation-module.js +++ b/templates/validation-function/document-properties-validation-module.js @@ -93,6 +93,14 @@ function documentPropertiesValidationModule(utils, simpleTypeFilter, typeIdValid var itemValue = currentItemEntry.itemValue; var validatorType = resolveItemConstraint(validator.type); + if (!utils.isDocumentMissingOrDeleted(oldDoc) && + resolveItemConstraint(validator.skipValidationWhenValueUnchanged) && + comparisonModule.checkItemEquality(itemValue, currentItemEntry.oldItemValue, validatorType)) { + // No need to perform further validation since the validator is configured to skip validation when the current + // value and old value are semantically equal to each other + return; + } + if (validator.customValidation) { performCustomValidation(validator); } diff --git a/test/resources/skip-validation-when-value-unchanged-doc-definitions.js b/test/resources/skip-validation-when-value-unchanged-doc-definitions.js new file mode 100644 index 0000000..60f1f34 --- /dev/null +++ b/test/resources/skip-validation-when-value-unchanged-doc-definitions.js @@ -0,0 +1,114 @@ +function() { + return { + staticSkipValidationWhenValueUnchangedDoc: { + typeFilter: simpleTypeFilter, + authorizedRoles: { write: 'write' }, + propertyValidators: { + integerProp: { + type: 'integer', + skipValidationWhenValueUnchanged: true, + minimumValue: 0 + }, + floatProp: { + type: 'float', + skipValidationWhenValueUnchanged: true, + maximumValue: 0 + }, + stringProp: { + type: 'string', + skipValidationWhenValueUnchanged: true, + minimumLength: 4 + }, + booleanProp: { + type: 'boolean', + skipValidationWhenValueUnchanged: true, + customValidation: function(doc, oldDoc, currentItemEntry) { + if (isValueNullOrUndefined(currentItemEntry.itemValue) || currentItemEntry.itemValue) { + return [ ]; + } else { + return [ currentItemEntry.itemName + ' must be true' ]; + } + } + }, + dateProp: { + type: 'date', + skipValidationWhenValueUnchanged: true, + maximumValue: '1953-01-14' + }, + datetimeProp: { + type: 'datetime', + skipValidationWhenValueUnchanged: true, + minimumValue: '2018-06-13T23:33+00:00', + maximumValue: '2018-06-13T23:33Z' + }, + timeProp: { + type: 'time', + skipValidationWhenValueUnchanged: true, + minimumValueExclusive: '17:45:53.911' + }, + timezoneProp: { + type: 'timezone', + skipValidationWhenValueUnchanged: true, + maximumValueExclusive: '+15:30' + }, + enumProp: { + type: 'enum', + predefinedValues: [ 1, 2, 3 ], + skipValidationWhenValueUnchanged: true + }, + uuidProp: { + type: 'uuid', + skipValidationWhenValueUnchanged: true, + maximumValueExclusive: '10000000-0000-0000-0000-000000000000' + }, + attachmentReferenceProp: { + type: 'attachmentReference', + skipValidationWhenValueUnchanged: true, + regexPattern: /^[a-z]+\.txt$/ + }, + arrayProp: { + type: 'array', + skipValidationWhenValueUnchanged: true, + maximumLength: 3 + }, + objectProp: { + type: 'object', + skipValidationWhenValueUnchanged: true, + propertyValidators: { + nestedProp: { + type: 'string' + } + } + }, + hashtableProp: { + type: 'hashtable', + skipValidationWhenValueUnchanged: true, + hashtableValuesValidator: { + type: 'integer' + } + } + } + }, + dynamicSkipValidationWhenValueUnchangedDoc: { + typeFilter: simpleTypeFilter, + authorizedRoles: { write: 'write' }, + propertyValidators: { + allowValidationSkip: { + type: 'boolean' + }, + minimumUuidValue: { + type: 'uuid' + }, + uuidProp: { + type: 'uuid', + skipValidationWhenValueUnchanged: function(doc, oldDoc, value, oldValue) { + return !isDocumentMissingOrDeleted(oldDoc) ? oldDoc.allowValidationSkip : doc.allowValidationSkip; + }, + minimumValue: function(doc, oldDoc, value, oldValue) { + return doc.minimumUuidValue; + } + } + } + } + }; +} diff --git a/test/skip-validation-when-value-unchanged.spec.js b/test/skip-validation-when-value-unchanged.spec.js new file mode 100644 index 0000000..06151c3 --- /dev/null +++ b/test/skip-validation-when-value-unchanged.spec.js @@ -0,0 +1,383 @@ +const testFixtureMaker = require('../src/testing/test-fixture-maker'); +const errorFormatter = require('../src/testing/validation-error-formatter'); + +describe('Skip validation for unchanged value constraint:', () => { + const testFixture = testFixtureMaker.initFromValidationFunction( + 'build/validation-functions/test-skip-validation-when-value-unchanged-validation-function.js'); + + afterEach(() => { + testFixture.resetTestEnvironment(); + }); + + describe('with static validation', () => { + it('allows document creation with valid values', () => { + const doc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: 49, + floatProp: -153.9, + stringProp: 'a-string', + booleanProp: true, + dateProp: '1953-01-14', + datetimeProp: '2018-06-13T16:33-07:00', + timeProp: '17:45:53.912', + timezoneProp: '+15:15', + enumProp: 3, + uuidProp: '0d852732-81f0-4501-bb13-2c1ebc98d8f6', + attachmentReferenceProp: 'foobar.txt', + arrayProp: [ 'a', 'b', 'c' ], + objectProp: { nestedProp: 'foo' }, + hashtableProp: { bar: -7 } + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('allows document replacement with invalid values that semantically equal the original values', () => { + const oldDoc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: -1, + floatProp: 14.36, + stringProp: 'foo', + booleanProp: false, + dateProp: '1953-01-15', + datetimeProp: '2018-06-13T16:33-08:00', + timeProp: '17:45:53.911', + timezoneProp: '+15:30', + enumProp: 'a', + uuidProp: '34f0d4a2-bc77-4aea-b3fd-d579b66ee4bc', + attachmentReferenceProp: 'foobar.baz', + arrayProp: [ 'a', 'b', 'c', 'd' ], + objectProp: { foo: 'bar' }, + hashtableProp: { bar: 'baz' } + }; + + const doc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: -1.0, + floatProp: 14.360, + stringProp: 'foo', + booleanProp: false, + dateProp: '1953-01-15', + datetimeProp: '2018-06-14T01:33:00+01:00', + timeProp: '17:45:53.911', + timezoneProp: '+15:30', + enumProp: 'a', + uuidProp: '34F0D4A2-BC77-4AEA-B3FD-D579B66EE4BC', + attachmentReferenceProp: 'foobar.baz', + arrayProp: [ 'a', 'b', 'c', 'd' ], + objectProp: { foo: 'bar' }, + hashtableProp: { bar: 'baz' } + }; + + testFixture.verifyDocumentReplaced(doc, oldDoc); + }); + + it('allows document replacement of invalid values with valid values', () => { + const oldDoc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: -1, + floatProp: 14.36, + stringProp: 'foo', + booleanProp: false, + dateProp: '1953-01-15', + datetimeProp: '2018-06-13T16:33-08:00', + timeProp: '17:45:53.911', + timezoneProp: '+15:30', + enumProp: 'a', + uuidProp: '34f0d4a2-bc77-4aea-b3fd-d579b66ee4bc', + attachmentReferenceProp: 'foobar.baz', + arrayProp: [ 'a', 'b', 'c', 'd' ], + objectProp: { foo: 'bar' }, + hashtableProp: { bar: 'baz' } + }; + + const doc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: 0, + floatProp: 0, + stringProp: 'foobar', + booleanProp: true, + dateProp: '1952-12-31', + datetimeProp: '2018-06-13T16:03-07:30', + timeProp: '18:21:35', + timezoneProp: 'Z', + enumProp: 1, + uuidProp: '094b0d17-f266-4a10-af04-8b6ae0312131', + attachmentReferenceProp: 'barbaz.txt', + arrayProp: [ ], + objectProp: { }, + hashtableProp: { foo: 5564 } + }; + + testFixture.verifyDocumentReplaced(doc, oldDoc); + }); + + it('allows document replacement of invalid values with null values', () => { + const oldDoc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: -1, + floatProp: 14.36, + stringProp: 'foo', + booleanProp: false, + dateProp: '1953-01-15', + datetimeProp: '2018-06-13T16:33-08:00', + timeProp: '17:45:53.911', + timezoneProp: '+15:30', + enumProp: 'a', + uuidProp: '34f0d4a2-bc77-4aea-b3fd-d579b66ee4bc', + attachmentReferenceProp: 'foobar.baz', + arrayProp: [ 'a', 'b', 'c', 'd' ], + objectProp: { foo: 'bar' }, + hashtableProp: { bar: 'baz' } + }; + + const doc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: null, + floatProp: null, + stringProp: null, + booleanProp: null, + dateProp: null, + datetimeProp: null, + timeProp: null, + timezoneProp: null, + enumProp: null, + uuidProp: null, + attachmentReferenceProp: null, + arrayProp: null, + objectProp: null, + hashtableProp: null + }; + + testFixture.verifyDocumentReplaced(doc, oldDoc); + }); + + it('allows document replacement of invalid values with undefined values', () => { + const oldDoc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: -1, + floatProp: 14.36, + stringProp: 'foo', + booleanProp: false, + dateProp: '1953-01-15', + datetimeProp: '2018-06-13T16:33-08:00', + timeProp: '17:45:53.911', + timezoneProp: '+15:30', + enumProp: 'a', + uuidProp: '34f0d4a2-bc77-4aea-b3fd-d579b66ee4bc', + attachmentReferenceProp: 'foobar.baz', + arrayProp: [ 'a', 'b', 'c', 'd' ], + objectProp: { foo: 'bar' }, + hashtableProp: { bar: 'baz' } + }; + + const doc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc' + }; + + testFixture.verifyDocumentReplaced(doc, oldDoc); + }); + + it('rejects document creation with invalid values', () => { + const doc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: -1, + floatProp: 14.36, + stringProp: 'foo', + booleanProp: false, + dateProp: '1953-01-15', + datetimeProp: '2018-06-13T16:33-08:00', + timeProp: '17:45:53.911', + timezoneProp: '+15:30', + enumProp: 'a', + uuidProp: '34f0d4a2-bc77-4aea-b3fd-d579b66ee4bc', + attachmentReferenceProp: 'foobar.baz', + arrayProp: [ 'a', 'b', 'c', 'd' ], + objectProp: { foo: 'bar' }, + hashtableProp: { bar: 'baz' } + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'staticSkipValidationWhenValueUnchangedDoc', + [ + errorFormatter.minimumValueViolation('integerProp', 0), + errorFormatter.maximumValueViolation('floatProp', 0), + errorFormatter.minimumLengthViolation('stringProp', 4), + 'booleanProp must be true', + errorFormatter.maximumValueViolation('dateProp', '1953-01-14'), + errorFormatter.maximumValueViolation('datetimeProp', '2018-06-13T23:33Z'), + errorFormatter.minimumValueExclusiveViolation('timeProp', '17:45:53.911'), + errorFormatter.maximumValueExclusiveViolation('timezoneProp', '+15:30'), + errorFormatter.enumPredefinedValueViolation('enumProp', [ 1, 2, 3 ]), + errorFormatter.maximumValueExclusiveViolation('uuidProp', '10000000-0000-0000-0000-000000000000'), + errorFormatter.attachmentReferenceRegexPatternViolation('attachmentReferenceProp', /^[a-z]+\.txt$/), + errorFormatter.maximumLengthViolation('arrayProp', 3), + errorFormatter.unsupportedProperty('objectProp.foo'), + errorFormatter.typeConstraintViolation('hashtableProp[bar]', 'integer') + ]); + }); + + it('rejects document replacement with invalid values', () => { + const oldDoc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: 49, + floatProp: -153.9, + stringProp: 'a-string', + booleanProp: true, + dateProp: '1953-01-14', + datetimeProp: '2018-06-13T16:33-07:00', + timeProp: '17:45:53.912', + timezoneProp: '+15:15', + enumProp: 3, + uuidProp: '0d852732-81f0-4501-bb13-2c1ebc98d8f6', + attachmentReferenceProp: 'foobar.txt', + arrayProp: [ 'a', 'b', 'c' ], + objectProp: { nestedProp: 'foo' }, + hashtableProp: { bar: -7 } + }; + + const doc = { + _id: 'my-doc', + type: 'staticSkipValidationWhenValueUnchangedDoc', + integerProp: -1, + floatProp: 14.36, + stringProp: 'foo', + booleanProp: false, + dateProp: '1953-01-15', + datetimeProp: '2018-06-13T16:33-08:00', + timeProp: '17:45:53.911', + timezoneProp: '+15:30', + enumProp: 'a', + uuidProp: '34f0d4a2-bc77-4aea-b3fd-d579b66ee4bc', + attachmentReferenceProp: 'foobar.baz', + arrayProp: [ 'a', 'b', 'c', 'd' ], + objectProp: { foo: 'bar' }, + hashtableProp: { bar: 'baz' } + }; + + testFixture.verifyDocumentNotReplaced( + doc, + oldDoc, + 'staticSkipValidationWhenValueUnchangedDoc', + [ + errorFormatter.minimumValueViolation('integerProp', 0), + errorFormatter.maximumValueViolation('floatProp', 0), + errorFormatter.minimumLengthViolation('stringProp', 4), + 'booleanProp must be true', + errorFormatter.maximumValueViolation('dateProp', '1953-01-14'), + errorFormatter.maximumValueViolation('datetimeProp', '2018-06-13T23:33Z'), + errorFormatter.minimumValueExclusiveViolation('timeProp', '17:45:53.911'), + errorFormatter.maximumValueExclusiveViolation('timezoneProp', '+15:30'), + errorFormatter.enumPredefinedValueViolation('enumProp', [ 1, 2, 3 ]), + errorFormatter.maximumValueExclusiveViolation('uuidProp', '10000000-0000-0000-0000-000000000000'), + errorFormatter.attachmentReferenceRegexPatternViolation('attachmentReferenceProp', /^[a-z]+\.txt$/), + errorFormatter.maximumLengthViolation('arrayProp', 3), + errorFormatter.unsupportedProperty('objectProp.foo'), + errorFormatter.typeConstraintViolation('hashtableProp[bar]', 'integer') + ]); + }); + }); + + describe('with dynamic validation', () => { + it('allows document creation with valid values', () => { + const doc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + allowValidationSkip: true, + uuidProp: '7ed6ad77-b18f-4a1a-bf72-6e74a22b88c1', + minimumUuidValue: '70000000-0000-0000-0000-000000000000' + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('allows document replacement with equal invalid values when validation is allowed to be skipped', () => { + const oldDoc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + allowValidationSkip: true, + uuidProp: '189331f4-62dd-4036-90e5-291648b05e38' + }; + + const doc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + uuidProp: '189331F4-62DD-4036-90E5-291648B05E38', // Same value; just converted to uppercase + minimumUuidValue: '20000000-0000-0000-0000-000000000000' + }; + + testFixture.verifyDocumentReplaced(doc, oldDoc); + }); + + it('rejects document creation with invalid values', () => { + const doc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + allowValidationSkip: true, + uuidProp: '0b352e9b-1c1f-433a-9c98-dd0242127185', + minimumUuidValue: '10000000-0000-0000-0000-000000000000' + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'dynamicSkipValidationWhenValueUnchangedDoc', + [ errorFormatter.minimumValueViolation('uuidProp', '10000000-0000-0000-0000-000000000000') ]); + }); + + it('rejects document replacement with unequal invalid values when validation is allowed to be skipped', () => { + const oldDoc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + allowValidationSkip: true, + uuidProp: '4b424579-81c9-4740-9511-eca9c8a89f95' + }; + + const doc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + uuidProp: '1d9d9668-249e-47fe-95e0-06cdac93f0f1', + minimumUuidValue: '50000000-0000-0000-0000-000000000000' + }; + + testFixture.verifyDocumentNotReplaced( + doc, + oldDoc, + 'dynamicSkipValidationWhenValueUnchangedDoc', + [ errorFormatter.minimumValueViolation('uuidProp', '50000000-0000-0000-0000-000000000000') ]); + }); + + it('rejects document replacement with equal invalid values when validation is NOT allowed to be skipped', () => { + const oldDoc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + allowValidationSkip: false, + uuidProp: '22632a18-4045-4dfc-a5f0-23d2aa6af894' + }; + + const doc = { + _id: 'my-doc', + type: 'dynamicSkipValidationWhenValueUnchangedDoc', + uuidProp: '22632a18-4045-4dfc-a5f0-23d2aa6af894', + minimumUuidValue: 'd0000000-0000-0000-0000-000000000000' + }; + + testFixture.verifyDocumentNotReplaced( + doc, + oldDoc, + 'dynamicSkipValidationWhenValueUnchangedDoc', + [ errorFormatter.minimumValueViolation('uuidProp', 'd0000000-0000-0000-0000-000000000000') ]); + }); + }); +});