UNPKG

synctos

Version:

The Syncmaker. A tool to build comprehensive sync functions for Couchbase Sync Gateway.

361 lines (351 loc) 27.3 kB
const { expect } = require('chai'); const docDefinitionsLoader = require('../loading/document-definitions-loader'); const validator = require('./document-definitions-validator'); describe('Document definitions validator:', () => { it('performs validation on the sample document definitions file', () => { const filePath = 'samples/sample-sync-doc-definitions.js'; const sampleDocDefinitions = docDefinitionsLoader.load(filePath); const results = validator.validate(sampleDocDefinitions, filePath); expect(results.length).to.equal(0); }); it('performs validation on a document definitions object', () => { const fakeDocDefinitions = { myDoc1: { documentIdRegexPattern: { }, // Must be a RegExp object allowUnknownProperties: 1, // Must be a boolean immutable: true, cannotDelete: true, // Must not be defined if "immutable" is also defined attachmentConstraints: (a, b) => b, // "allowAttachments" must also be defined accessAssignments: [ { // Must include either a "roles" or "users" property channels: [ ] // Must not be empty }, { // Must include a "channels" property type: 'channel', roles: [ ], // Must not be empty }, { type: 'role', roles: [ 1 ], // Must only contain strings users: [ ], // Must not be empty channels: [ 'foo' ] // The "channels" property is not supported for the "role" type of assignment }, { type: 'invalid-type' // Must be either "role", "channel" or null } ], expiry: -25637, // Must be no less than zero customActions: { onTypeIdentificationSucceeded: (a, b, c, extraParam) => extraParam, // Too many parameters onAuthorizationSucceeded: 5, // Must be a function onExpiryAssignmentSucceeded: (a, b, c) => c, invalidEvent: (a, b, c) => c // Unsupported event type } } }; const results = validator.validate(fakeDocDefinitions); expect(results).to.have.members( [ 'myDoc1: "value" must contain at least one of [channels, authorizedRoles, authorizedUsers]', 'myDoc1.typeFilter: "typeFilter" is required', 'myDoc1.propertyValidators: "propertyValidators" is required', 'myDoc1.documentIdRegexPattern: \"documentIdRegexPattern\" must be an instance of \"RegExp\"', 'myDoc1.allowUnknownProperties: \"allowUnknownProperties\" must be a boolean', 'myDoc1.immutable: \"immutable\" conflict with forbidden peer \"cannotDelete\"', 'myDoc1.allowAttachments: \"allowAttachments\" is required', 'myDoc1.accessAssignments.0: \"value\" must contain at least one of [roles, users]', 'myDoc1.accessAssignments.0.channels: \"channels\" must contain at least 1 items', 'myDoc1.accessAssignments.1.channels: \"channels\" is required', 'myDoc1.accessAssignments.1.roles: \"roles\" must contain at least 1 items', 'myDoc1.accessAssignments.2.roles.0: \"0\" must be a string', 'myDoc1.accessAssignments.2.users: \"users\" must contain at least 1 items', 'myDoc1.accessAssignments.2.channels: \"channels\" is not allowed', 'myDoc1.accessAssignments.3.type: \"type\" must be one of [channel, role, null]', 'myDoc1.expiry: \"expiry\" must be larger than or equal to 0', 'myDoc1.customActions.onTypeIdentificationSucceeded: \"onTypeIdentificationSucceeded\" must have an arity lesser or equal to 3', 'myDoc1.customActions.onAuthorizationSucceeded: \"onAuthorizationSucceeded\" must be a Function', 'myDoc1.customActions.invalidEvent: \"invalidEvent\" is not allowed' ]); }); it('performs validation on a document definitions function', () => { const fakeDocDefinitions = () => { return { myDoc1: { typeFilter: () => { }, channels: { }, // Must have at least one permission type authorizedRoles: { }, // Must have at least one permission type authorizedUsers: { }, // Must have at least one permission type documentIdRegexPattern: (a, extraParam) => extraParam, // Too many parameters immutable: true, cannotReplace: false, // Must not be defined if "immutable" is also defined allowAttachments: false, // Must be true since "attachmentConstraints" is defined attachmentConstraints: { maximumAttachmentCount: 0, // Must be at least 1 maximumIndividualSize: 20971521, // Must be no greater than 20971520 (the max Sync Gateway attachment size) maximumTotalSize: 20971520, // Must be greater or equal to "maximumIndividualSize" supportedExtensions: (doc, oldDoc, extraParam) => [ extraParam ], // Has too many params supportedContentTypes: [ ], // Must have at least one element filenameRegexPattern: { foo: 'bar' } // Must be a RegExp }, accessAssignments: (a, b, extraParam) => extraParam, // Too many parameters customActions: { }, // Must have at least one property propertyValidators: { conditionalTypeProperty: { type: 'conditional', immutableWhenSetStrict: true, mustNotBeNull: false, // "mustNotBeNull" is deprecated minimumValue: -15, // Unsupported constraint for this validation type validationCandidates: [ { condition: (a, b, c, d, extra) => extra, // Too many parameters and must have a "validator" property foobar: 'baz' // Unsupported property }, { condition: true, // Must be a function validator: { type: 'float', maximumLength: 3, // Unsupported constraint for this validation type mustEqual: (a, b, c, d) => d } }, { condition: () => true, validator: { type: 'object', allowUnknownProperties: 0 // Must be a boolean } } ] }, timeProperty: { type: 'time', immutable: 1, // Must be a boolean minimumValue: '15', // Must have at least hour and minute components maximumValue: '23:49:52.1234', // Must not have more than 3 fractional digits mustEqual: 'foobar' // Must be a valid time string }, timezoneProperty: { type: 'timezone', minimumValueExclusive: '-15', // Must include minute component maximumValueExclusive: '19:00', // Must have a positive or negative sign mustEqual: 'barfoo' // Must be a valid timezone string }, _invalidName: { // Sync Gateway does not allow top-level property names to start with underscore type: 'string' }, nestedObject: { type: 'object', unrecognizedConstraint: true, // Invalid property constraint propertyValidators: { _validName: { type: 'boolean', skipValidationWhenValueUnchanged: 1, // Must be a boolean skipValidationWhenValueUnchangedStrict: true // Must not be defined in conjunction with "skipValidationWhenValueUnchanged" }, dateProperty: { type: 'date', required: true, immutable: true, immutableWhenSet: false, // Must not be defined in conjunction with "immutable" maximumValue: '2018-01-31T17:31:27.283-08:00', // Should not include time and time zone components mustEqualStrict: new Date('1578-11-30'), // Must be a date string for equality customValidation: (a, b, c, d, extraParam) => extraParam // Too many parameters }, enumProperty: { type: 'enum', predefinedValues: [ 'foo', 'bar', 3, 1.9 // Must include only strings and integers ], mustEqual: true, // Must be an integer or string mustEqualStrict: 3 // Must not be defined in conjunction with "mustEqual" }, attachmentReferenceProperty: { type: 'attachmentReference', regexPattern: '^abc$' // Must be a RegExp object }, hashtableProperty: { type: 'hashtable', minimumSize: 2, maximumSize: 1, // Must not be less than "minimumSize" hashtableKeysValidator: { regexPattern: '^[a-z]+$' // Must actually be either a literal regex or a RegExp object }, hashtableValuesValidator: { type: 'datetime', minimumValue: 'Mon, 25 Dec 1995 13:30:00 +0430', // Must be an ISO 8601 date string maximumValueExclusive: new Date(2018, 0, 31, 17, 31, 27, 283), // Must not be defined in conjunction with "mustEqual" mustEqual: new Date('2018-01-31T17:31:27.283-08:00') // Must be a date string for equality } }, arrayProperty: { type: 'array', minimumLength: 3.5, // Must be an integer maximumLength: 3.5, // Must be an integer arrayElementsValidator: { type: 'object', allowUnknownProperties: true, required: (doc, oldDoc, value, oldValue) => oldValue === true, propertyValidators: { stringProperty: { type: 'string', mustEqual: 'FooBar', // Must not be defined in conjunction with "mustEqualIgnoreCase" mustEqualIgnoreCase: null, // Must not be null or defined with "mustEqual" mustBeTrimmed: 0, // Must be a boolean regexPattern: /^[a-z]+$/, minimumLength: () => 9, maximumLength: -1 // Must be at least 0 }, booleanProperty: { type: 'boolean', mustEqual: 'true' // Must be a boolean }, validIntegerProperty: { type: 'integer', minimumValue: 5, maximumValueExclusive: 6 }, invalidIntegerProperty: { type: 'integer', required: true, mustNotBeMissing: true, // "mustNotBeMissing" is deprecated minimumValueExclusive: 1, maximumValue: 1, // Must be greater than "minimumValueExclusive" maximumValueExclusive: 1 // Must be greater than "minimumValueExclusive" }, validFloatProperty: { type: 'float', minimumValueExclusive: 1, maximumValue: 1.0001 }, invalidFloatProperty: { type: 'float', minimumValue: 31.9, maximumValue: 31.89998, // Must be at least "minimumValue" maximumValueExclusive: 31.9 // Must be greater than "minimumValue" }, uuidProperty: { type: 'uuid', minimumValue: '4050b662-4383-4d2E-8771-54d380d11C41', maximumValue: '1234' // Not a valid UUID }, noTypeProperty: { // The "type" property is required required: true }, invalidMustEqualConstraintProperty: { type: 'object', mustEqual: [ ] // Must be an object }, emptyPropertyValidatorsProperty: { type: 'object', mustEqual: (a, b, c, d) => d, propertyValidators: { } // Must specify at least one property validator }, anyProperty: { type: 'any', minimumValue: 32, // Not supported by the "any" type mustNotBeEmpty: false, // Not supported by the "any" type regexPattern: /^foo$/ // Not supported by the "any" type } } } }, unrecognizedTypeProperty: { type: 'foobar' // Not a supported validation constraint type } } } }, expiry: '20180415T1357-0700' // Must be a valid ISO 8601 date string } }; }; const results = validator.validate(fakeDocDefinitions); expect(results).to.have.members( [ 'myDoc1.channels: \"channels\" must have at least 1 children', 'myDoc1.authorizedRoles: \"authorizedRoles\" must have at least 1 children', 'myDoc1.authorizedUsers: \"authorizedUsers\" must have at least 1 children', 'myDoc1.documentIdRegexPattern: \"documentIdRegexPattern\" must have an arity lesser or equal to 1', 'myDoc1.immutable: \"immutable\" conflict with forbidden peer \"cannotReplace\"', 'myDoc1.allowAttachments: \"allowAttachments\" must be one of [true]', 'myDoc1.attachmentConstraints.maximumAttachmentCount: \"maximumAttachmentCount\" must be larger than or equal to 1', 'myDoc1.attachmentConstraints.maximumIndividualSize: \"maximumIndividualSize\" must be less than or equal to 20971520', 'myDoc1.attachmentConstraints.maximumTotalSize: \"maximumTotalSize\" must be larger than or equal to 20971521', 'myDoc1.attachmentConstraints.supportedExtensions: "supportedExtensions" must have an arity lesser or equal to 2', 'myDoc1.attachmentConstraints.supportedContentTypes: \"supportedContentTypes\" must contain at least 1 items', 'myDoc1.attachmentConstraints.filenameRegexPattern: \"filenameRegexPattern\" must be an instance of \"RegExp\"', 'myDoc1.accessAssignments: \"accessAssignments\" must have an arity lesser or equal to 2', 'myDoc1.customActions: \"customActions\" must have at least 1 children', 'myDoc1.propertyValidators.conditionalTypeProperty.mustNotBeNull: \"mustNotBeNull\" is deprecated; use "required" instead', 'myDoc1.propertyValidators.conditionalTypeProperty.minimumValue: \"minimumValue\" is not allowed', 'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.condition: \"condition\" must have an arity lesser or equal to 4', 'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.foobar: \"foobar\" is not allowed', 'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.0.validator: \"validator\" is required', 'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.1.condition: \"condition\" must be a Function', 'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.1.validator.maximumLength: \"maximumLength\" is not allowed', 'myDoc1.propertyValidators.conditionalTypeProperty.validationCandidates.2.validator.allowUnknownProperties: \"allowUnknownProperties\" must be a boolean', 'myDoc1.propertyValidators.timeProperty.immutable: \"immutable\" must be a boolean', 'myDoc1.propertyValidators.timeProperty.minimumValue: \"minimumValue\" with value \"15\" fails to match the required pattern: /^((([01]\\d|2[0-3])(:[0-5]\\d)(:[0-5]\\d(\\.\\d{1,3})?)?)|(24:00(:00(\\.0{1,3})?)?))$/', 'myDoc1.propertyValidators.timeProperty.maximumValue: \"maximumValue\" with value \"23:49:52.1234\" fails to match the required pattern: /^((([01]\\d|2[0-3])(:[0-5]\\d)(:[0-5]\\d(\\.\\d{1,3})?)?)|(24:00(:00(\\.0{1,3})?)?))$/', 'myDoc1.propertyValidators.timeProperty.mustEqual: \"mustEqual\" with value \"foobar\" fails to match the required pattern: /^((([01]\\d|2[0-3])(:[0-5]\\d)(:[0-5]\\d(\\.\\d{1,3})?)?)|(24:00(:00(\\.0{1,3})?)?))$/', 'myDoc1.propertyValidators.timeProperty.minimumValue: \"minimumValue\" conflict with forbidden peer \"mustEqual\"', 'myDoc1.propertyValidators.timeProperty.maximumValue: \"maximumValue\" conflict with forbidden peer \"mustEqual\"', 'myDoc1.propertyValidators.timezoneProperty.minimumValueExclusive: \"minimumValueExclusive\" with value \"-15\" fails to match the required pattern: /^(Z|([+-])([01]\\d|2[0-3]):([0-5]\\d))$/', 'myDoc1.propertyValidators.timezoneProperty.maximumValueExclusive: \"maximumValueExclusive\" with value \"19:00\" fails to match the required pattern: /^(Z|([+-])([01]\\d|2[0-3]):([0-5]\\d))$/', 'myDoc1.propertyValidators.timezoneProperty.mustEqual: \"mustEqual\" with value \"barfoo\" fails to match the required pattern: /^(Z|([+-])([01]\\d|2[0-3]):([0-5]\\d))$/', 'myDoc1.propertyValidators.timezoneProperty.minimumValueExclusive: \"minimumValueExclusive\" conflict with forbidden peer \"mustEqual\"', '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._validName.skipValidationWhenValueUnchanged: \"skipValidationWhenValueUnchanged\" conflict with forbidden peer \"skipValidationWhenValueUnchangedStrict\"', '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]))?)?$/', 'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.maximumValue: \"maximumValue\" conflict with forbidden peer \"mustEqualStrict\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.mustEqualStrict: \"mustEqualStrict\" must be a string', 'myDoc1.propertyValidators.nestedObject.propertyValidators.dateProperty.customValidation: \"customValidation\" must have an arity lesser or equal to 4', 'myDoc1.propertyValidators.nestedObject.propertyValidators.enumProperty.predefinedValues.3: \"predefinedValues\" at position 3 does not match any of the allowed types', 'myDoc1.propertyValidators.nestedObject.propertyValidators.enumProperty.mustEqual: \"mustEqual\" conflict with forbidden peer \"mustEqualStrict\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.enumProperty.mustEqualStrict: \"mustEqualStrict\" conflict with forbidden peer \"mustEqual\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.enumProperty.mustEqual: \"mustEqual\" must be a string', 'myDoc1.propertyValidators.nestedObject.propertyValidators.enumProperty.mustEqual: \"mustEqual\" must be a number', 'myDoc1.propertyValidators.nestedObject.propertyValidators.attachmentReferenceProperty.regexPattern: \"regexPattern\" must be an object', 'myDoc1.propertyValidators.nestedObject.propertyValidators.hashtableProperty.maximumSize: \"maximumSize\" must be larger than or equal to 2', 'myDoc1.propertyValidators.nestedObject.propertyValidators.hashtableProperty.hashtableKeysValidator.regexPattern: "regexPattern" must be an object', 'myDoc1.propertyValidators.nestedObject.propertyValidators.hashtableProperty.hashtableValuesValidator.minimumValue: \"minimumValue\" with value \"Mon, 25 Dec 1995 13:30:00 +0430\" fails to match the required pattern: /^([+-]\\d{6}|\\d{4})(-(0[1-9]|1[0-2])(-(0[1-9]|[12]\\d|3[01]))?)?(T((([01]\\d|2[0-3])(:[0-5]\\d)(:[0-5]\\d(\\.\\d{1,3})?)?)|(24:00(:00(\\.0{1,3})?)?))(Z|([+-])([01]\\d|2[0-3]):([0-5]\\d))?)?$/', 'myDoc1.propertyValidators.nestedObject.propertyValidators.hashtableProperty.hashtableValuesValidator.minimumValue: \"minimumValue\" conflict with forbidden peer \"mustEqual\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.hashtableProperty.hashtableValuesValidator.maximumValueExclusive: "maximumValueExclusive" conflict with forbidden peer "mustEqual"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.hashtableProperty.hashtableValuesValidator.mustEqual: \"mustEqual\" must be a string', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.minimumLength: \"minimumLength\" must be an integer', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.maximumLength: \"maximumLength\" must be an integer', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.stringProperty.mustEqual: \"mustEqual\" conflict with forbidden peer \"mustEqualIgnoreCase\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.stringProperty.mustEqualIgnoreCase: \"mustEqualIgnoreCase\" conflict with forbidden peer \"mustEqual\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.stringProperty.mustEqualIgnoreCase: \"mustEqualIgnoreCase\" must be a string', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.stringProperty.mustBeTrimmed: \"mustBeTrimmed\" must be a boolean', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.stringProperty.maximumLength: \"maximumLength\" must be larger than or equal to 0', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.booleanProperty.mustEqual: \"mustEqual\" must be a boolean', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidIntegerProperty.mustNotBeMissing: \"mustNotBeMissing\" is deprecated; use "required" instead', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidIntegerProperty.maximumValue: \"maximumValue\" must be greater than 1', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidIntegerProperty.maximumValue: \"maximumValue\" conflict with forbidden peer \"maximumValueExclusive\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidIntegerProperty.maximumValueExclusive: \"maximumValueExclusive\" must be greater than 1', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidIntegerProperty.maximumValueExclusive: \"maximumValueExclusive\" conflict with forbidden peer \"maximumValue\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidFloatProperty.maximumValueExclusive: \"maximumValueExclusive\" must be greater than 31.9', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidFloatProperty.maximumValue: \"maximumValue\" must be larger than or equal to 31.9', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidFloatProperty.maximumValue: \"maximumValue\" conflict with forbidden peer \"maximumValueExclusive\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidFloatProperty.maximumValueExclusive: \"maximumValueExclusive\" conflict with forbidden peer \"maximumValue\"', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.uuidProperty.maximumValue: "maximumValue" must be a valid GUID', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.invalidMustEqualConstraintProperty.mustEqual: \"mustEqual\" must be an object', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.emptyPropertyValidatorsProperty.propertyValidators: \"propertyValidators\" must have at least 1 children', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.noTypeProperty.type: "type" is required', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.minimumValue: \"minimumValue\" is not allowed', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.mustNotBeEmpty: \"mustNotBeEmpty\" is not allowed', 'myDoc1.propertyValidators.nestedObject.propertyValidators.arrayProperty.arrayElementsValidator.propertyValidators.anyProperty.regexPattern: \"regexPattern\" is not allowed', 'myDoc1.propertyValidators.nestedObject.propertyValidators.unrecognizedTypeProperty.type: \"type\" must be one of [any, array, attachmentReference, boolean, conditional, date, datetime, enum, float, hashtable, integer, object, string, time, timezone, uuid]', 'myDoc1.expiry: \"expiry\" with value \"20180415T1357-0700\" fails to match the required pattern: /^\\d{4}-(((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01]))|((0[469]|11)-(0[1-9]|[12]\\d|30))|(02-(0[1-9]|[12]\\d)))T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)$/', ]); }); });