├── .gitignore ├── lib ├── joi.js ├── helpers.js ├── exampleGenerator.js ├── index.js ├── joiGenerator.js └── valueGenerator.js ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── Feature_request.md │ └── Bug_report.md ├── CONTRIBUTING.md └── PULL_REQUEST_TEMPLATE.md ├── package.json ├── examples └── example.js ├── test ├── test_helpers.js ├── value_generator_tests.js └── felicity_tests.js ├── .travis.yml ├── LICENSE.txt ├── README.md └── API.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | node_modules/ 4 | 5 | test/test.html 6 | 7 | package-lock.json -------------------------------------------------------------------------------- /lib/joi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Joi = require('joi'); 4 | const JoiDateExtensions = require('@hapi/joi-date'); 5 | 6 | module.exports = Joi.extend(JoiDateExtensions); 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Context 2 | 3 | * *node version*: 4 | * *felicity version*: 5 | * *environment* (node, browser): 6 | * *any relevant modules used with* (hapi, hoek, etc.): 7 | * *any other relevant information*: 8 | 9 | #### What are you trying to achieve or the steps to reproduce ? 10 | 11 | 12 | 13 | #### What result did you expect ? 14 | 15 | #### What result did you observe ? 16 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getDefault = function (schemaDescription) { 4 | 5 | if (schemaDescription.flags.default !== null && typeof schemaDescription.flags.default === 'function') { 6 | return schemaDescription.flags.default(); 7 | } 8 | 9 | return schemaDescription.flags.default; 10 | }; 11 | 12 | const pickRandomFromArray = function (array) { 13 | 14 | return array[Math.floor(Math.random() * array.length)]; 15 | }; 16 | 17 | module.exports = { 18 | pickRandomFromArray, 19 | getDefault 20 | }; 21 | -------------------------------------------------------------------------------- /lib/exampleGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hoek = require('@hapi/hoek'); 4 | const ValueGenerator = require('./valueGenerator'); 5 | 6 | const exampleGenerator = function (schema, options) { 7 | 8 | const exampleResult = ValueGenerator(schema, options); 9 | 10 | if (Hoek.reach(options, 'config.strictExample')) { 11 | const validationResult = schema.validate(exampleResult, { 12 | abortEarly: false 13 | }); 14 | 15 | if (validationResult.error) { 16 | throw validationResult.error; 17 | } 18 | } 19 | 20 | return exampleResult; 21 | }; 22 | 23 | module.exports = exampleGenerator; 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Tell us what we can be doing better! 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I wish I could [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or snippets about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Tell us what's wrong so we can help you figure it out! 4 | 5 | --- 6 | 7 | **Context** 8 | 9 | * *node version*: 10 | * *felicity version*: 11 | * *environment* (node, browser): 12 | * *any relevant modules used with* (hapi, hoek, etc.): 13 | * *any other relevant information*: 14 | 15 | **To Reproduce** 16 | What are you trying to achieve or the steps to reproduce ? 17 | Please include any relevant Joi schemas. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Observed behavior** 23 | A clear and concise description of what actually happened. 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "felicity", 3 | "version": "6.0.0", 4 | "description": "Javascript object instantiation from Joi schema", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "lab -t 100 -a @hapi/code -L", 8 | "test-cover": "lab -a @hapi/code -L -r html > test/test.html && open test/test.html", 9 | "doctoc": "doctoc API.md" 10 | }, 11 | "repository": "git://github.com/xogroup/felicity", 12 | "keywords": [ 13 | "joi", 14 | "validation", 15 | "schema", 16 | "constructor", 17 | "test", 18 | "example", 19 | "mock" 20 | ], 21 | "license": "BSD-3-Clause", 22 | "bugs": { 23 | "url": "https://github.com/xogroup/felicity/issues" 24 | }, 25 | "homepage": "https://github.com/xogroup/felicity#readme", 26 | "dependencies": { 27 | "@hapi/hoek": "9.x.x", 28 | "@hapi/joi-date": "2.x.x", 29 | "joi": "17.x.x", 30 | "moment": "2.x.x", 31 | "randexp": "0.5.x", 32 | "uuid": "8.x.x" 33 | }, 34 | "peerDependencies": { 35 | "joi": "17.x.x" 36 | }, 37 | "devDependencies": { 38 | "@hapi/code": "8.x.x", 39 | "@hapi/lab": "24.x.x", 40 | "doctoc": "2.x.x" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Joi = require('../lib/joi'); 4 | const Felicity = require('../lib/index'); 5 | 6 | const exampleFromSchema = function (callback) { 7 | 8 | const schema = Joi.object().keys({ 9 | id: Joi.string().guid().required(), 10 | meta: Joi.object().keys({ 11 | timestamp: Joi.date().raw().required(), 12 | name: Joi.string().required(), 13 | sequence: Joi.number().integer().required() 14 | }).required() 15 | }); 16 | const example = Felicity.example(schema); 17 | 18 | return callback(example, schema); 19 | }; 20 | 21 | const customConstructor = function (callback) { 22 | 23 | const schema = Joi.object().keys({ 24 | name: Joi.string().required() 25 | }); 26 | const Conversation = Felicity.entityFor(schema); 27 | 28 | const conversation = new Conversation(); 29 | 30 | const validation = conversation.validate(); 31 | const mockConversation = conversation.example(); 32 | 33 | const mockConversation2 = Conversation.example(); 34 | const externalValidation = Conversation.validate({}); 35 | 36 | return callback(Conversation, conversation, mockConversation, validation, mockConversation2, externalValidation); 37 | }; 38 | 39 | module.exports = { 40 | exampleFromSchema, 41 | customConstructor 42 | }; 43 | -------------------------------------------------------------------------------- /test/test_helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hoek = require('@hapi/hoek'); 4 | 5 | const permutations = function (requirements, exclusionSet) { 6 | 7 | const overallSet = {}; 8 | 9 | const recursivePermutations = function (requirementSet, cumulativeSet) { 10 | 11 | requirementSet.forEach((currentRequirement, index) => { 12 | 13 | const newCumulativeSet = Hoek.clone(cumulativeSet); 14 | 15 | if (Hoek.intersect(newCumulativeSet, exclusionSet[currentRequirement]).length === 0) { 16 | newCumulativeSet.push(currentRequirement); 17 | 18 | const stringSet = newCumulativeSet.toString(); 19 | overallSet[stringSet] = true; 20 | } 21 | 22 | if (requirementSet.slice(index + 1).length > 0) { 23 | return recursivePermutations(requirementSet.slice(index + 1), newCumulativeSet); 24 | } 25 | }); 26 | }; 27 | 28 | recursivePermutations(requirements, []); 29 | 30 | return Object.keys(overallSet).map((set) => { 31 | 32 | return set.split(','); 33 | }); 34 | }; 35 | 36 | const expectValidation = (expect) => { 37 | 38 | return (value, schema) => { 39 | 40 | const validationResult = schema.validate(value); 41 | 42 | expect(validationResult.error).to.equal(undefined); 43 | }; 44 | }; 45 | 46 | module.exports = { 47 | permutations, 48 | expectValidation 49 | }; 50 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | We welcome contributions from the community and are pleased to have them. Please follow this guide when logging issues or making code changes. 3 | 4 | ## Logging Issues 5 | All issues should be created using the [new issue form](https://github.com/xogroup/felicity/issues/new). 6 | 7 | Clearly describe the issue including steps to reproduce if there are any. Also, make sure to indicate the Felicity version that has the issue being reported. 8 | 9 | ## Patching Code 10 | Code changes are welcome and should follow the guidelines below. 11 | 12 | * Consult the existing [issues](https://github.com/xogroup/felicity/issues). If there is not already an issue discussing the changes you want to make, please open a [new one](https://github.com/xogroup/felicity/issues/new)! This gives us and the community an opportunity to discuss your ideas. 13 | * Fork the repository on GitHub. 14 | * Fix the issue ensuring that your code follows the [Hapi.js style guide](https://github.com/hapijs/contrib/blob/master/Style.md). 15 | * Add tests for your new code ensuring that you have 100% code coverage (we can help you reach 100% but will not merge without it). 16 | * Run `npm run test-cover` to generate a report of test coverage for your own use. 17 | * Pull requests should be made against the [master branch](https://github.com/xogroup/felicity/tree/master). Please fill out the provided form to the best of your ability. Help us help you! 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | - '14' 5 | - node 6 | deploy: 7 | provider: npm 8 | email: xo-npmjs.group@xogrp.com 9 | api_key: 10 | secure: Eqx10GiLDG0Cbxt+bxlxpDAxEiSCoFlp7fQ0rw28h1jW/9Q7NCbJHAJHm4+jGsk/CeJIExIdqeYqrqv/MzL3a4lQZufe0K8Y2KUB5WTZDtPzxvAFqbVnjVr/vPfG6MQ/M3Ke8V3dPL3x9pDljAhaEMSavCF4qcC7ulFmxleGnRRnejv9PnIu4M4ok2HIAL23aSE3b41yDkWQVR3erqpxKsl4cieEOyyG6lER+Z9OYUpHAlA71hvlRRWYdS1CLm4WNxdPkUa9T2miIRg0rjSLe2AjJ3UQUtEL19Ca2tG9LERM6WwtqvMRXMMpvtHlCzFAUfi9IZTneno7aksUWbxneKGV4AEW3/s7bOJJnFNBWwEPZXZIWOXOP1XDM0RR/74EBIr1FFAq66Imc9pj4/ZyK3/82HrDCOf9FAt0T6rHD4N2YO9OX2dq7glr6dt9rRCLdCMmOXgKSCNS81B8aXDkbILSyI8HI5P8kc70vPUcxcf2lAUZruDZ+c7hBDdBBg6cr3pLRaA0/3EmYyAhlOqCFxIamtb27uvDA69rMRllgZ5+aDySYDphEonQqtFklGq3SiPOy1GS3Gmmc0rpPksObdbEaEilHtdOdroNiLGU4rScCStjwjrekfbmqo4+lRCqT5zw+PvkXW+SFBWFNjIDYrsRtXwT8bqpNQN4W8Xqhz4= 11 | on: 12 | tags: true 13 | repo: xogroup/felicity 14 | notifications: 15 | slack: 16 | secure: TBDxUfHiPNvuzFqFNWfqU4k2uVvhM5GtJzzQNpeezuOY5w3IJzcofA9a9wK/swXejzgtBbqXgwbt2AZJS/YJpsvcJsZa/VaVMUExx1dzHo/m26fTOHpBcV2vOHK2ThkUAIYwA3sdKhJa7h8rn2cKV18ttKBStb+4kJ3FoIus8miAihdiikOGKTdMFx+GPSksi7XtEpPNRyFZTPmTWhuidWSyhLFNKr7hUBJqzlEpJ/fZNXr+mboVOTlWcDyQ+Sul/fdiLiqsABVuA2t7Mn2Tp3JtVwZBUHPk6m4blviW2xYdRjTU4b7dxoRMXvXkYzTofcxY3j9VuP+N/ceqM0I0zx4lFrArZA8k+d0hXQmcrDU5kIPOznY/gtWkXeVElcpFPA9W2JkQw1icbHKpKly09Q4cWGmrEjXx/fwFA5cW4DXRFo71T3crlYBLtFFTCmKT+4ckUugUXL2MYZAuwbk8VqwTKGMNeL1FwhQxu9G/RtCXO5aH3HmQm51vS734f5O0Wukp+wvo4G9H1L7GbHLUs9gi0qVxOE4Wd3ploSUkevj6R58U34Qxft6u6+MyvcEYwQRX1/CvbtanIFWFynwXEJFCcFNqXMBeiV+jJG93qdHwz0TcJuIiNl7KXDvKsXZEOPIdV46YQ9GLtF8J3Pr2fFhfJ0f2ffYROLnFS+eL8Dg= 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | ## Motivation and Context 12 | 13 | 14 | ## Types of changes 15 | 16 | - [ ] Documentation only (no changes to either `lib/` or `test/` files) 17 | - [ ] Bug fix (non-breaking change which fixes an issue. you didn't modify existing tests) 18 | - [ ] New feature (non-breaking change which adds functionality. you added at least one new test) 19 | - [ ] Breaking change (fix or feature that would cause existing functionality to change. you had to modify existing tests.) 20 | 21 | ## Checklist: 22 | 23 | 24 | - [ ] I have read the [**CONTRIBUTING**](https://github.com/xogroup/felicity/blob/master/.github/CONTRIBUTING.md) document. 25 | - [ ] My code follows the [Hapi.js style guide](https://github.com/hapijs/contrib/blob/master/Style.md). 26 | - [ ] I have updated the documentation as needed. 27 | - [ ] I have added tests to cover my changes. 28 | - [ ] All new and existing tests passed. 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016, Project contributors 4 | Copyright (c) 2016, XO Group Inc 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | 33 | * * * 34 | 35 | The complete list of contributors can be found at: https://github.com/xogroup/felicity/graphs/contributors -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hoek = require('@hapi/hoek'); 4 | const Joi = require('./joi'); 5 | const JoiGenerator = require('./joiGenerator'); 6 | const Example = require('./exampleGenerator'); 7 | 8 | const entityFor = function (schema, options) { 9 | 10 | if (!schema) { 11 | throw new Error('You must provide a Joi schema'); 12 | } 13 | 14 | if (!Joi.isSchema(schema)) { 15 | schema = Joi.compile(schema); 16 | } 17 | 18 | const schemaDescription = schema.describe(); 19 | 20 | if (schemaDescription.type !== 'object') { 21 | throw new Error('Joi schema must describe an object for constructor functions'); 22 | } 23 | 24 | 25 | const validateInput = function (input, callback, override) { 26 | 27 | const baseConfig = { 28 | abortEarly: false 29 | }; 30 | 31 | const config = Object.assign({}, baseConfig, override); 32 | 33 | const result = Constructor.schema.validate(input, config); 34 | 35 | const validationResult = { 36 | success: result.error === undefined, 37 | errors: result.error && result.error.details, 38 | value: result.value 39 | }; 40 | 41 | if (callback && validationResult.success) { 42 | callback(null, validationResult); 43 | } 44 | else if (callback) { 45 | callback(validationResult.errors, null); 46 | } 47 | else { 48 | return validationResult; 49 | } 50 | }; 51 | 52 | class Constructor { 53 | 54 | constructor(input, instanceOptions) { 55 | 56 | const configurations = Hoek.clone(options) || {}; 57 | 58 | if (input) { 59 | configurations.input = input; 60 | } 61 | 62 | if (instanceOptions) { 63 | configurations.config = configurations.config ? 64 | Hoek.applyToDefaults(configurations.config, instanceOptions) : 65 | instanceOptions; 66 | } 67 | 68 | if (Hoek.reach(configurations, 'config.strictExample')) { 69 | Object.defineProperty(this, 'strictExample', { 70 | value: true 71 | }); 72 | } 73 | 74 | JoiGenerator(this, schema, configurations); 75 | } 76 | 77 | get schema() { 78 | 79 | return schema; 80 | } 81 | 82 | validate(callback, config) { 83 | 84 | return validateInput(this, callback, config); 85 | } 86 | 87 | example(config) { 88 | 89 | const configurations = config || {}; 90 | 91 | configurations.config = configurations.config || {}; 92 | configurations.config.strictExample = this.strictExample || configurations.config.strictExample; 93 | 94 | return Example(this.schema, configurations); 95 | } 96 | 97 | static get schema() { 98 | 99 | return schema; 100 | } 101 | 102 | static validate(input, callback, config) { 103 | 104 | return validateInput(input, callback, config); 105 | } 106 | 107 | static example(config) { 108 | 109 | const configurations = config || {}; 110 | 111 | configurations.config = configurations.config || {}; 112 | 113 | return Example(this.schema, configurations); 114 | } 115 | } 116 | 117 | return Constructor; 118 | }; 119 | 120 | const Felicity = { 121 | example: Example, 122 | entityFor 123 | }; 124 | 125 | module.exports = Felicity; 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # felicity 2 | Felicity supports [Joi](https://www.github.com/hapijs/joi) schema management by providing 2 primary functions: 3 | 4 | 1. **Testing support** - Felicity will leverage your Joi schema to generate randomized data directly from the schema. This can be used for database seeding or fuzz testing. 5 | 2. **Model management in source code** - Felicity can additionally leverage your Joi schema to create constructor functions that contain immutable copies of the Joi schema as well as a simple `.validate()` method that will run Joi validation of the object instance values against the referenced Joi schema. 6 | 7 | [![npm version](https://badge.fury.io/js/felicity.svg)](https://badge.fury.io/js/felicity) 8 | [![Build Status](https://travis-ci.org/xogroup/felicity.svg?branch=master)](https://travis-ci.org/xogroup/felicity) 9 | [![Known Vulnerabilities](https://snyk.io/test/github/xogroup/felicity/badge.svg)](https://snyk.io/test/github/xogroup/felicity) 10 | 11 | Lead Maintainer: [Wes Tyler](https://github.com/WesTyler) 12 | 13 | ## Introduction 14 | > **fe·lic·i·ty** *noun* intense happiness; the ability to find appropriate expression for one's thoughts or intentions. 15 | 16 | Felicity provides object instances, or expressions, of the data intentions represented by Joi schema. 17 | 18 | Felicity builds upon Joi by allowing validation to be contained cleanly and nicely in constructors while also allowing easy example generation for documentation, tests, and more. 19 | 20 | ## Installation 21 | ``` 22 | npm install felicity 23 | ``` 24 | 25 | ## Usage 26 | ### Model Management 27 | Given a [joi](https://www.github.com/hapijs/joi) schema, create an object Constructor and instantiate skeleton objects: 28 | ```JavaScript 29 | const Joi = require('joi'); 30 | const Felicity = require('felicity'); 31 | 32 | const joiSchema = Joi.object().keys({ 33 | key1: Joi.string().required(), 34 | key2: Joi.array().items(Joi.string().guid()).min(3).required(), 35 | key3: Joi.object().keys({ 36 | innerKey: Joi.number() 37 | }) 38 | }); 39 | 40 | const FelicityModelConstructor = Felicity.entityFor(joiSchema); 41 | const modelInstance = new FelicityModelConstructor({ key1: 'some value' }); 42 | 43 | console.log(modelInstance); 44 | /* 45 | { 46 | key1: 'some value', 47 | key2: [], 48 | key3: { 49 | innerKey: 0 50 | } 51 | } 52 | */ 53 | ``` 54 | 55 | These model instances can self-validate against the schema they were built upon: 56 | ```JavaScript 57 | modelInstance.key3.innerKey = 42; 58 | 59 | const validationResult = modelInstance.validate(); // uses immutable copy of the Joi schema provided to `Felicity.entityFor()` above 60 | 61 | console.log(validationResult); 62 | /* 63 | { 64 | success: false, 65 | errors : [ 66 | { 67 | "message": "\"key2\" must contain at least 3 items", 68 | "path": [ "key2" ], 69 | "type": "array.min", 70 | "context": { 71 | "limit": 3, 72 | "value": [], 73 | "key": "key2", 74 | "label": "key2" 75 | } 76 | }, 77 | // ... 78 | ] 79 | } 80 | */ 81 | ``` 82 | 83 | ### Testing Usage 84 | Additionally, Felicity can be used to randomly generate valid examples from either your [Felicity Models](#model-management) or directly from a Joi schema: 85 | ```Javascript 86 | const randomModelValue = FelicityModelConstructor.example(); // built in by `Felicity.entityFor()` 87 | /* 88 | { 89 | key1: '2iwf8af2v4n', 90 | key2:[ 91 | '077750a4-6e6d-4b74-84e2-cd34de80e95b', 92 | '1a8eb515-72f6-4007-aa73-a33cd4c9accb', 93 | 'c9939d71-0790-417a-b615-6448ca95c30b' 94 | ], 95 | key3: { innerKey: 3.8538257114788257 } 96 | } 97 | */ 98 | 99 | // directly from Joi schemas: 100 | const stringSchema = Joi.string().pattern(/[a-c]{3}-[d-f]{3}-[0-9]{4}/); 101 | const sampleString = Felicity.example(stringSchema); 102 | // sampleString === 'caa-eff-5144' 103 | 104 | const objectSchema = Joi.object().keys({ 105 | id : Joi.string().guid(), 106 | username: Joi.string().min(6).alphanum(), 107 | numbers : Joi.array().items(Joi.number().min(1)) 108 | }); 109 | const sampleObject = Felicity.example(objectSchema); 110 | /* 111 | sampleObject 112 | { 113 | id: '0e740417-1708-4035-a495-6bccce560583', 114 | username: '4dKp2lHj', 115 | numbers: [ 1.0849635479971766 ] 116 | } 117 | */ 118 | ``` 119 | ### Node.js version compatibility 120 | Please note that Felicity follows [Node.js LTS support schedules](https://nodejs.org/en/about/releases/) as well as Joi Node.js version support. 121 | 122 | Beginning with Felicity@6.0.0, only Node.js versions 12 and above will be supported. 123 | 124 | ## API 125 | 126 | For full usage documentation, see the [API Reference](https://github.com/xogroup/felicity/blob/master/API.md). 127 | 128 | ## Contributing 129 | 130 | We love community and contributions! Please check out our [guidelines](https://github.com/xogroup/felicity/blob/master/.github/CONTRIBUTING.md) before making any PRs. 131 | 132 | ## Setting up for development 133 | 134 | Getting yourself setup and bootstrapped is easy. Use the following commands after you clone down. 135 | 136 | ``` 137 | npm install && npm test 138 | ``` 139 | 140 | ## Joi features not yet supported 141 | 142 | Some Joi schema options are not yet fully supported. Most unsupported features should not cause errors, but may be disregarded by Felicity or may result in behavior other than that documented in the Joi api. 143 | 144 | A feature is considered Felicity-supported when it is explicitly covered in tests on both `entityFor` (and associated instance methods) and `example`. 145 | 146 | - Function 147 | - `ref` 148 | - Array 149 | - `unique` 150 | - Object 151 | - `requiredKeys` 152 | - `optionalKeys` 153 | -------------------------------------------------------------------------------- /lib/joiGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hoek = require('@hapi/hoek'); 4 | const Joi = require('joi'); 5 | const Helpers = require('./helpers'); 6 | 7 | const internals = {}; 8 | 9 | internals.joiDictionary = { 10 | string: null, 11 | boolean: false, 12 | date: null, 13 | binary: null, 14 | func: null, 15 | function: null, 16 | number: 0, 17 | array: [], 18 | object: {}, 19 | alternatives: null, 20 | any: null 21 | }; 22 | 23 | internals.reachDelete = function (target, splitPath) { 24 | 25 | if (!Array.isArray(splitPath)) { 26 | splitPath = [splitPath]; 27 | } 28 | 29 | if (splitPath.length === 1) { 30 | delete target[splitPath[0]]; 31 | } 32 | else { 33 | const newTarget = target[splitPath[0]]; 34 | const newPath = splitPath.slice(1).join('.'); 35 | const newPathExists = Hoek.reach(newTarget, newPath) !== undefined; 36 | 37 | if (newPathExists) { 38 | internals.reachDelete(newTarget, newPath); 39 | } 40 | } 41 | }; 42 | 43 | const generate = function (instance, schemaInput, options) { 44 | 45 | let input = Hoek.reach(options, 'input'); 46 | const config = Hoek.reach(options, 'config'); 47 | const ignoreDefaults = Hoek.reach(config, 'ignoreDefaults'); 48 | 49 | const schemaMapper = function (target, schema) { 50 | 51 | const schemaDescription = schema.describe(); 52 | 53 | if (schemaDescription.keys) { 54 | 55 | const parentPresence = Hoek.reach(schemaDescription, 'preferences.presence'); 56 | const childrenKeys = Object.keys(schemaDescription.keys); 57 | 58 | childrenKeys.forEach((childKey) => { 59 | 60 | const childSchemaDescription = schemaDescription.keys[childKey]; 61 | const flagsPresence = Hoek.reach(childSchemaDescription, 'flags.presence'); 62 | const flagsStrip = Hoek.reach(childSchemaDescription, 'flags.result') === 'strip'; 63 | const childIsRequired = flagsPresence === 'required'; 64 | const childIsOptional = (flagsPresence === 'optional') || (parentPresence === 'optional' && !childIsRequired); 65 | const childIsForbidden = flagsPresence === 'forbidden' || flagsStrip; 66 | 67 | if (childIsForbidden || (childIsOptional && !(Hoek.reach(config, 'includeOptional')))) { 68 | return; 69 | } 70 | 71 | const childType = childSchemaDescription.type; 72 | 73 | const childSchemaRaw = schema.$_terms.keys.filter((child) => { 74 | 75 | return child.key === childKey; 76 | })[0].schema; 77 | 78 | if (childType === 'function') { 79 | target[childKey] = Hoek.clone(internals.joiDictionary.function); 80 | } 81 | else if (childType === 'object') { 82 | target[childKey] = {}; 83 | schemaMapper(target[childKey], childSchemaRaw); 84 | } 85 | else if (childType === 'alternatives') { 86 | 87 | const hasTrueCase = childSchemaDescription.matches.filter((option) => { 88 | 89 | return option.ref && option.then; 90 | }).length > 0; 91 | const chosenAlternative = hasTrueCase ? 92 | childSchemaDescription.matches[0].then : 93 | childSchemaDescription.matches[0]; 94 | if (chosenAlternative.type === 'function') { 95 | target[childKey] = Hoek.clone(internals.joiDictionary.function); 96 | } 97 | else if (chosenAlternative.type === 'object') { 98 | target[childKey] = {}; 99 | schemaMapper(target[childKey], Joi.build(chosenAlternative)); 100 | } 101 | else if (chosenAlternative.schema) { 102 | target[childKey] = Hoek.clone(internals.joiDictionary[chosenAlternative.schema.type]); 103 | } 104 | else { 105 | const alternativeHasDefault = Hoek.reach(chosenAlternative, 'flags.default'); 106 | target[childKey] = (alternativeHasDefault && !ignoreDefaults) ? 107 | Helpers.getDefault(chosenAlternative) : 108 | Hoek.clone(internals.joiDictionary[chosenAlternative.type]); 109 | } 110 | } 111 | else { 112 | const childHasDefault = Hoek.reach(childSchemaDescription, 'flags.default'); 113 | target[childKey] = (childHasDefault && !ignoreDefaults) ? 114 | Helpers.getDefault(childSchemaDescription) : 115 | Hoek.clone(internals.joiDictionary[childType]); 116 | } 117 | }); 118 | } 119 | }; 120 | 121 | schemaMapper(instance, schemaInput); 122 | 123 | if (input) { 124 | const validateOptions = { 125 | abortEarly: false 126 | }; 127 | 128 | const validation = schemaInput.validate(input, validateOptions); 129 | 130 | input = validation.value; 131 | 132 | if (Hoek.reach(config, 'validateInput') && validation.error) { 133 | throw validation.error; 134 | } 135 | 136 | if (Hoek.reach(config, 'strictInput')) { 137 | const invalidInputValues = Hoek.reach(validation, 'error.details'); 138 | 139 | if (invalidInputValues) { 140 | invalidInputValues.forEach((error) => { 141 | 142 | internals.reachDelete(input, error.path); 143 | }); 144 | } 145 | } 146 | 147 | Hoek.merge(instance, input); 148 | } 149 | }; 150 | 151 | module.exports = generate; 152 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # 6.0.0 API Reference 2 | 3 | 4 | 5 | 6 | 7 | - [Felicity](#felicity) 8 | - [`entityFor(schema, [options])`](#entityforschema-options) 9 | - [Constructor methods](#constructor-methods) 10 | - [Instance methods](#instance-methods) 11 | - [`example(schema, [options])`](#exampleschema-options) 12 | - [Options](#options) 13 | - [`entityFor` Options](#entityfor-options) 14 | - [`example` Options](#example-options) 15 | 16 | 17 | 18 | ## Felicity 19 | 20 | ### `entityFor(schema, [options])` 21 | 22 | Creates a Constructor function based on the provided Joi schema. Accepts an optional [`[options]`](#entityfor-options) parameter. 23 | 24 | *Please note that JavaScript constructor functions only return objects. Therefore, the Joi schema provided must describe an object.* 25 | 26 | Instances created by `new Constructor()` are "empty skeletons" of the provided Joi schema, and have sugary prototypal [methods](#instance-methods). 27 | 28 | The returned Constructor function has the signature `([input], [options])`. Any input provided will be used to hydrate the `new` object. 29 | 30 | ```Javascript 31 | const schema = Joi.object().keys({ 32 | name: Joi.string().min(3).required(), 33 | id : Joi.string().guid().required(), 34 | keys: Joi.array().items(Joi.object().keys({id: Joi.string().guid()})).min(3).required() 35 | }); 36 | const Conversation = Felicity.entityFor(schema); 37 | const convoInstance = new Conversation(); 38 | const partialInstance = new Conversation({name: 'Felicity', fakeKey: 'invalid'}); 39 | 40 | /* 41 | convoInstance 42 | { 43 | name: null, 44 | id : null, 45 | keys: [] 46 | } 47 | 48 | partialInstance 49 | { 50 | name : 'Felicity', 51 | id : null, 52 | keys : [], 53 | fakeKey: 'invalid' 54 | } 55 | */ 56 | ``` 57 | 58 | ```Javascript 59 | const nonObjectSchema = Joi.number(); 60 | const NeverGonnaHappen = Felicity.entityFor(nonObjectSchema); // throws Error 'Joi schema must describe an object for constructor functions' 61 | ``` 62 | 63 | The returned Constructor is also registered within the runtime with the exact name of *Constructor*. This can be corrected using the ES6 class `extend` expression. 64 | 65 | ```Javascript 66 | const schema = Joi.object().keys({ 67 | id : Joi.number().required(), 68 | name: Joi.string().required() 69 | }); 70 | 71 | const User = Felicity.entityFor(schema); 72 | new User() // constructor will be of name Constructor 73 | 74 | const User = class User extends Felicity.entityFor(schema); 75 | new User() // constructor will be of name User 76 | ``` 77 | 78 | #### Constructor methods 79 | 80 | The Constructor function returned by `entityFor` has the following properties/methods available for use without instantiation of `new` objects. 81 | - `prototype.schema` - The Joi validation schema provided to `entityFor`. 82 | - `example([options])` - Returns a valid pseudo-randomly generated example Javascript Object based on the Constructor's `prototype.schema`. Accepts an optional [`[options]`](#example-options) parameter. 83 | ```Javascript 84 | // using schema and Conversation constructor from "entityFor" code example: 85 | const exampleConversation = Conversation.example(); 86 | /* 87 | exampleConversation 88 | { 89 | name: 'taut9', 90 | id : 'b227cd4c-4e7a-4ba4-a613-30747f7267b8', 91 | keys: [ 92 | { id: '401a7324-8753-4ae2-abcc-6ae96216500e' }, 93 | { id: 'c83c4a88-db1e-4402-8345-8b92be551b4e' }, 94 | { id: 'e4194c87-541c-41f7-9473-783bf0e790fe' } 95 | ] 96 | } 97 | */ 98 | ``` 99 | - `validate(input, [callback])` - Joi-validates the provided input against the Constructor's `prototype.schema`. 100 | Returns the below validationObject unless `callback` is provided, in which case `callback(errors, validationObject)` is called. 101 | - `validationObject` - the result of Joi validation has properties: 102 | - `success` - boolean. `true` if Joi validation is successful, `false` if input fails Joi validation. 103 | - `errors` - null if successful validation, array of all Joi validation error details if unsuccessful validation 104 | - `value` - Validated input value after any native Joi type conversion is applied (if applicable. see [Joi](https://github.com/hapijs/joi/blob/master/API.md#validatevalue-schema-options-callback) docs for more details) 105 | ```Javascript 106 | // Examples 107 | const successValidationObject = { 108 | success: true, 109 | errors : null, 110 | value : {/*...*/} 111 | } 112 | 113 | const failureValidationObject = { 114 | success: false, 115 | errors : [ 116 | { 117 | message: '"name" is required', 118 | path: 'name', 119 | type: 'any.required', 120 | context: { key: 'name' } 121 | } 122 | ], 123 | value : {/*...*/} 124 | } 125 | ``` 126 | 127 | #### Instance methods 128 | 129 | The `new` instances of the Constructor function returned by `entityFor` have the following properties/methods available. 130 | - `schema` - The Joi schema provided to `entityFor`. Non-enumerable, inherited from the Constructor. 131 | - `example([options])` - Returns a new valid pseudo-randomly generated example Javascript Object based on the instance's `schema` property. 132 | Accepts an optional [`[options]`](#example-options) parameter. 133 | 134 | Does not modify the instance. 135 | - `validate([callback])` - Joi-validates the instance against the instance's `schema` property. 136 | Returns the same validationObject as the [`Constructor.validate`](#constructor-methods) method unless `callback` is provided, in which case `callback(errors, validationObject)` is called. 137 | 138 | ```Javascript 139 | const schema = Joi.object().keys({ 140 | name: Joi.string().required(), 141 | id : Joi.string().guid().required() 142 | ); 143 | const Constructor = Felicity.entityFor(schema); 144 | const instance = new Constructor(); // instance === { name: null, id: null } 145 | 146 | const instanceValidation = instance.validate(); // instanceValidation === { success: false, errors: [ { message: '"name" must be a string', path: 'name', type: 'string.base', context: [Object] },{ message: '"id" must be a string', path: 'id', type: 'string.base', context: [Object] }], value: {name: null, id: null} } 147 | 148 | instance.name = 'Felicity'; 149 | instance.id = 'e7db5468-2551-4e42-98ea-47cc57606258'; 150 | 151 | const retryValidation = instance.validate(); // retryValidation === { success: true, errors: null, value: {name: 'Felicity', id: 'e7db5468-2551-4e42-98ea-47cc57606258'}} 152 | ``` 153 | 154 | ### `example(schema, [options])` 155 | 156 | Returns a valid pseudo-randomly generated example Javascript Object based on the provided Joi schema. 157 | 158 | Accepts an optional [`[options]`](#example-options) parameter. 159 | 160 | ```Javascript 161 | const schema = Joi.object().keys({ 162 | name: Joi.string().min(3).required(), 163 | id : Joi.string().guid().required(), 164 | tags: Joi.array().items(Joi.string().max(4)).min(2).required(), 165 | }); 166 | const exampleDoc = Felicity.example(schema); 167 | 168 | /* 169 | exampleDoc 170 | { 171 | name: 'qgrbddv', 172 | id : '6928f0c0-68fa-4b6f-9bc5-961db17d42b0', 173 | tags: [ 'k2a', '31' ] 174 | } 175 | */ 176 | ``` 177 | 178 | ### Options 179 | 180 | All options parameters must be an object with property `config`. Properties on the `config` object are detailed by method below. 181 | 182 | #### `entityFor` Options 183 | 184 | - `strictInput` - default `false`. Default behavior is to not run known properties through Joi validation upon object instantiation. 185 | 186 | If set to `true`, all input will be validated, and only properties that pass validation will be utilized on the returned object. 187 | All others will be returned in nulled/emptied form as if there was no input for that field. 188 | **Note**: this will **not** throw Joi `ValidationError`s. See `validateInput` for error throwing. 189 | 190 | ```Javascript 191 | const schema = Joi.object().keys({ 192 | id: Joi.string().guid() 193 | }); 194 | const input = { 195 | id: '12345678' // not a valid GUID 196 | }; 197 | const Document = Felicity.entityFor(schema); 198 | const document = new Document(input); // { id: '12345678' } 199 | 200 | const StrictDocument = Felicity.entityFor(schema, { config: { strictInput: true } }); 201 | const strictDocument = new StrictDocument(input); // { id: null } 202 | ``` 203 | 204 | - `strictExample` - default `false`. Default behavior is to not run examples through Joi validation before returning. 205 | 206 | If set to `true`, example will be validated prior to returning. 207 | 208 | Note: in most cases, there is no difference. The only known cases where this may result in ValidationErrors are with regex patterns containing lookarounds. 209 | 210 | ```Javascript 211 | const schema = Joi.object().keys({ 212 | name : Joi.string().regex(/abcd(?=efg)/) 213 | }); 214 | 215 | const instance = new (Felicity.entityFor(schema)); // instance === { name: null } 216 | const mockInstance = instance.example(); // mockInstance === { name: 'abcd' } 217 | 218 | const strictInstance = new (Felicity.entityFor(schema, { config: { strictExample: true } })); // strictInstance === { name: null } 219 | const mockStrict = strictInstance.example(); // ValidationError 220 | ``` 221 | 222 | - `ignoreDefaults` - Default `false`. Default behavior is to stamp instances with defaults. 223 | 224 | If set to `true`, then default values of Joi properties with `.default('value')` set will not be stamped into instances. 225 | ```Javascript 226 | const schema = Joi.object().keys({ 227 | name: Joi.string().required().default('felicity') 228 | }); 229 | 230 | const Constructor = Felicity.entityFor(schema); 231 | const instance = new Constructor(); // instance === { name: 'felicity' } 232 | 233 | const NoDefaults = Felicity.entityFor(schema, { config: { ignoreDefaults: true } }); 234 | const noDefaultInstance = new NoDefaults(); // noDefaultInstance === { name: null } 235 | ``` 236 | 237 | - `includeOptional` - Default `false`. Default behavior is to ignore optional properties entirely. 238 | 239 | If set to `true`, then Joi properties with `.optional()` set will be included on instances. 240 | ```Javascript 241 | const schema = Joi.object().keys({ 242 | name : Joi.string().required(), 243 | nickname: Joi.string().optional() 244 | }); 245 | 246 | const Constructor = Felicity.entityFor(schema); 247 | const instance = new Constructor(); // instance === { name: null } 248 | 249 | const WithOptional = Felicity.entityFor(schema, { config: { includeOptional: true } }); 250 | const withOptionalInstance = new WithOptional(); // withOptionalInstance === { name: null, nickname: null } 251 | ``` 252 | 253 | - `validateInput` - Default `false`. Default behavior is to not throw errors if input is not valid. 254 | 255 | If set to `true`, then invalid input passed to the constructor function will result in a thrown `ValidationError`. 256 | ```Javascript 257 | const schema = Joi.object().keys({ 258 | name : Joi.string() 259 | }); 260 | 261 | const Constructor = Felicity.entityFor(schema); 262 | const instance = new Constructor({ name: 12345 }); // instance === { name: 12345 } 263 | 264 | const WithValidateInput = Felicity.entityFor(schema, { config: { validateInput: true } }); 265 | const withValidateInputInstance = new WithValidateInput({ name: 12345 }); // throws ValidationError: child "name" fails because ["name" must be a string] 266 | ``` 267 | 268 | #### `example` Options 269 | 270 | - `strictExample` - default `false`. Default behavior is to not run examples through Joi validation before returning. 271 | 272 | If set to `true`, example will be validated prior to returning. 273 | 274 | Note: in most cases, there is no difference. The only known cases where this may result in no example coming back are with regex patterns containing lookarounds. 275 | 276 | ```Javascript 277 | const schema = Joi.object().keys({ 278 | name : Joi.string().regex(/abcd(?=efg)/) 279 | }); 280 | 281 | const instance = Felicity.example(schema); // instance === { name: 'abcd' } 282 | 283 | const strictInstance = Felicity.example(schema, { config: { strictExample: true } }); // throws ValidationError 284 | ``` 285 | 286 | - `ignoreDefaults` - Default `false`. Default behavior is to stamp instances with default values. 287 | 288 | If set to `true`, then default values of Joi properties with `.default('value')` set will not be stamped into instances but will be generated according to the Joi property rules. 289 | ```Javascript 290 | const schema = Joi.object().keys({ 291 | name: Joi.string().required().default('felicity') 292 | }); 293 | 294 | const example = Felicity.example(schema); // example === { name: 'felicity' } 295 | 296 | const noDefaultsExample = Felicity.example(schema, { config: { ignoreDefaults: true } }); // noDefaultsExample === { name: 'nq5yhu4ttq33di' } 297 | ``` 298 | 299 | - `ignoreValids` - Default `false`. Default behavior is to pick values from `.allow()`ed and `.valid()` sets. 300 | 301 | If set to `true`, then the allowed/valid values will not be used but will be generated according to the Joi property rules. 302 | ```Javascript 303 | const schema = Joi.object().keys({ 304 | name: Joi.string().allow(null).required() 305 | }); 306 | 307 | const example = Felicity.example(schema); // example === { name: null } 308 | 309 | const noValidsExample = Felicity.example(schema, { config: { ignoreValids: true } }); // noValidsExample === { name: 'nq5yhu4ttq33di' } 310 | ``` 311 | 312 | 313 | - `includeOptional` - Default `false`. Default behavior is to ignore optional properties entirely. 314 | 315 | If set to `true`, then Joi properties with `.optional()` set will be included on examples. 316 | ```Javascript 317 | const schema = Joi.object().keys({ 318 | name : Joi.string().required(), 319 | nickname: Joi.string().optional() 320 | }); 321 | 322 | const instance = Felicity.example(schema); // instance === { name: 'ml9mmn0r8m7snhfr' } 323 | 324 | const withOptional = Felicity.example(schema, { config: { includeOptional: true } }); // withOptional === { name: '3cpffhgccgsw0zfr', nickname: '7pfjuxfa4gxk1emi' } 325 | ``` 326 | -------------------------------------------------------------------------------- /lib/valueGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hoek = require('@hapi/hoek'); 4 | const Joi = require('./joi'); 5 | const RandExp = require('randexp'); 6 | const Uuid = require('uuid'); 7 | const Moment = require('moment'); 8 | const Helpers = require('./helpers'); 9 | 10 | const internals = { 11 | JoiArrayProto: Reflect.getPrototypeOf(Joi.array()), 12 | JoiBoolProto: Reflect.getPrototypeOf(Joi.boolean()), 13 | JoiFuncProto: Reflect.getPrototypeOf(Joi.func()), 14 | JoiNumberProto: Reflect.getPrototypeOf(Joi.number()) 15 | }; 16 | 17 | internals.getType = function (schema) { 18 | 19 | const schemaDescription = schema.describe(); 20 | let exampleType = schemaDescription.type; 21 | 22 | if (Examples[exampleType] === undefined) { 23 | exampleType = 'any'; 24 | if (schema.append !== undefined) { 25 | exampleType = 'func'; 26 | } 27 | else if (schema.unique !== undefined) { 28 | exampleType = 'array'; 29 | } 30 | else if (schema.truthy !== undefined) { 31 | exampleType = 'boolean'; 32 | } 33 | else if (schema.greater !== undefined) { 34 | exampleType = 'number'; 35 | } 36 | } 37 | 38 | return exampleType; 39 | }; 40 | 41 | class Any { 42 | 43 | constructor(schema, options) { 44 | 45 | this._schema = schema; 46 | this._options = options && options.config; 47 | } 48 | 49 | generate() { 50 | 51 | const schemaDescription = this._schema.describe(); 52 | 53 | if (Hoek.reach(schemaDescription, 'allow')) { 54 | if (Hoek.reach(this._options, 'ignoreValids') !== true) { 55 | return Helpers.pickRandomFromArray(schemaDescription.allow); 56 | } 57 | } 58 | 59 | const flagDefault = Hoek.reach(schemaDescription, 'flags.default'); 60 | if (Hoek.reach(this, '_options.ignoreDefaults') !== true && Hoek.reach(schemaDescription, 'flags.default') !== undefined) { 61 | if (typeof flagDefault === 'function') { 62 | return flagDefault(); 63 | } 64 | 65 | return this._getDefaults(); 66 | } 67 | 68 | if (Hoek.reach(schemaDescription, 'examples')) { 69 | return Helpers.pickRandomFromArray(schemaDescription.examples.flat(Infinity)); 70 | } 71 | 72 | const rules = this._buildRules(); 73 | 74 | return this._generate(rules); 75 | } 76 | 77 | _generate(rules) { 78 | 79 | return Math.random().toString(36).substr(2); 80 | } 81 | 82 | _buildRules() { 83 | 84 | const rules = this._schema.describe().rules || []; 85 | const options = {}; 86 | 87 | rules.forEach((rule) => { 88 | 89 | options[rule.name] = rule.args === undefined || rule.args === null ? true : rule.args; 90 | }); 91 | 92 | return options; 93 | 94 | } 95 | 96 | _getDefaults() { 97 | 98 | return Helpers.getDefault(this._schema.describe()); 99 | } 100 | } 101 | 102 | class StringExample extends Any { 103 | 104 | _generate(rules) { 105 | 106 | const specials = { 107 | hostname: () => { 108 | 109 | const randexp = new RandExp(/^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/); 110 | randexp.max = 5; 111 | return randexp.gen(); 112 | }, 113 | token: () => { 114 | 115 | return new RandExp(/[a-zA-Z0-9_]+/).gen(); 116 | }, 117 | hex: () => { 118 | 119 | return new RandExp(/^[a-f0-9]+$/i).gen(); 120 | }, 121 | creditCard: () => { 122 | 123 | let creditCardNumber = ''; 124 | let sum = 0; 125 | 126 | while (creditCardNumber.length < 11) { 127 | const randomInt = Math.floor(Math.random() * 10); 128 | 129 | creditCardNumber = randomInt.toString() + creditCardNumber; 130 | 131 | if (creditCardNumber.length % 2 !== 0) { 132 | const doubleInt = randomInt * 2; 133 | const digit = doubleInt > 9 ? 134 | doubleInt - 9 : 135 | doubleInt; 136 | 137 | sum = sum + digit; 138 | } 139 | else { 140 | sum = sum + randomInt; 141 | } 142 | } 143 | 144 | const guardDigit = (sum * 9) % 10; 145 | 146 | return creditCardNumber + guardDigit.toFixed(); 147 | }, 148 | pattern: () => { 149 | 150 | const pattern = (rules.pattern.options && rules.pattern.options.invert) ? /[a-f]{3}/ : rules.pattern.regex; 151 | const result = new RandExp(pattern).gen(); 152 | return result.replace(/^(\s|\/)+|(\s|\/)+$/g, ''); 153 | }, 154 | guid: () => { 155 | 156 | return Uuid.v4(); 157 | }, 158 | ip: () => { 159 | 160 | const possibleResults = []; 161 | let isIPv4 = true; 162 | let isIPv6 = false; 163 | let isCIDR = true; 164 | 165 | if (rules.ip.options.version) { 166 | isIPv4 = rules.ip.options.version.indexOf('ipv4') > -1; 167 | isIPv6 = rules.ip.options.version.indexOf('ipv6') > -1; 168 | } 169 | 170 | if (rules.ip.options.cidr === 'forbidden') { 171 | isCIDR = false; 172 | } 173 | 174 | if (isIPv4) { 175 | possibleResults.push('224.109.242.85'); 176 | 177 | if (isCIDR) { 178 | possibleResults.push('224.109.242.85/24'); 179 | } 180 | } 181 | 182 | if (isIPv6) { 183 | possibleResults.push('8194:426e:9389:5963:1a5:9c75:31ae:ccbb'); 184 | 185 | if (isCIDR) { 186 | // TODO : this needs to be replaced with a IPv6 CIDR. I think Joi has issues validating a real CIRD atm. 187 | possibleResults.push('8194:426e:9389:5963:1a5:9c75:31ae:ccbb'); 188 | } 189 | } 190 | 191 | return possibleResults[Math.floor(Math.random() * (possibleResults.length))]; 192 | }, 193 | email: () => { 194 | 195 | const domains = [ 196 | 'email.com', 197 | 'gmail.com', 198 | 'example.com', 199 | 'domain.io', 200 | 'email.net' 201 | ]; 202 | 203 | return Math.random().toString(36).substr(2) + '@' + Helpers.pickRandomFromArray(domains); 204 | }, 205 | isoDate: () => { 206 | 207 | return (new Date()).toISOString(); 208 | }, 209 | uri: () => { 210 | 211 | return `${['http', 'https', 'ftp'][Math.floor(Math.random() * 3)]}://www.${Math.random().toString(36).substr(2)}.${['com', 'net', 'gov'][Math.floor(Math.random() * 3)]}`; 212 | } 213 | }; 214 | 215 | const specialRules = Hoek.intersect(Object.keys(specials), Object.keys(rules)); 216 | let stringGen = () => Math.random().toString(36).substr(2); 217 | if (specialRules.length > 0) { 218 | if (specialRules[0] === 'hex') { 219 | stringGen = specials[specialRules[0]]; 220 | } 221 | else { 222 | return specials[specialRules[0]](); 223 | } 224 | } 225 | 226 | let stringResult = stringGen(); 227 | let minLength = 1; 228 | 229 | if (rules.length) { 230 | if (stringResult.length < rules.length.limit) { 231 | 232 | while (stringResult.length < rules.length.limit) { 233 | 234 | stringResult = stringResult + stringGen(); 235 | } 236 | } 237 | 238 | stringResult = stringResult.substr(0, rules.length.limit); 239 | } 240 | else if (rules.max && rules.min !== undefined) { 241 | if (stringResult.length < rules.min.limit) { 242 | 243 | while (stringResult.length < rules.min.limit) { 244 | 245 | stringResult = stringResult + stringGen(); 246 | } 247 | } 248 | 249 | const length = rules.min.limit + Math.floor(Math.random() * (rules.max.limit - rules.min.limit)); 250 | 251 | stringResult = stringResult.substr(0, length); 252 | } 253 | else if (rules.max) { 254 | 255 | if (stringResult.length > rules.max.limit) { 256 | 257 | const length = Math.floor(rules.max.limit * Math.random()) + 1; 258 | 259 | stringResult = stringResult.substr(0, length); 260 | } 261 | } 262 | else if (rules.min) { 263 | minLength = rules.min.limit; 264 | 265 | if (stringResult.length < minLength) { 266 | 267 | while (stringResult.length < rules.min.limit) { 268 | 269 | stringResult = stringResult + stringGen(); 270 | } 271 | } 272 | 273 | const length = Math.ceil(minLength * (Math.random() + 1)) + 1; 274 | 275 | stringResult = stringResult.substr(0, length); 276 | } 277 | 278 | if (rules.case) { 279 | const { direction } = rules.case; 280 | stringResult = direction === 'upper' ? stringResult.toLocaleUpperCase() : stringResult.toLocaleLowerCase(); 281 | } 282 | 283 | return stringResult; 284 | } 285 | } 286 | 287 | class NumberExample extends Any { 288 | _generate(rules) { 289 | 290 | let incrementor = 1; 291 | let min = 1; 292 | let max = 5; 293 | let numberResult; 294 | let lockMin; 295 | let lockMax; 296 | 297 | const randNum = (maxVal, minVal, increment) => { 298 | 299 | let rand; 300 | if (increment > 1) { 301 | rand = Math.random() * Math.floor((maxVal - minVal) / increment); 302 | } 303 | else { 304 | rand = Math.random() * (maxVal - minVal); 305 | } 306 | 307 | if (rules.integer !== undefined || rules.multiple !== undefined) { 308 | return Math.floor(rand) * increment; 309 | } 310 | 311 | return rand; 312 | }; 313 | 314 | const setMin = (value) => { 315 | 316 | if (lockMin !== true || value > min) { 317 | min = value; 318 | } 319 | }; 320 | 321 | const setMax = (value) => { 322 | 323 | if (lockMax !== true || value < max) { 324 | max = value; 325 | } 326 | }; 327 | 328 | if (rules.min !== undefined || rules.greater !== undefined) { 329 | min = rules.min !== undefined ? rules.min.limit : rules.greater.limit + 1; 330 | lockMin = true; 331 | 332 | if (rules.max === undefined && rules.less === undefined) { 333 | max = min + 5; 334 | } 335 | } 336 | 337 | if (rules.max !== undefined || rules.less !== undefined) { 338 | max = rules.max !== undefined ? rules.max.limit : rules.less.limit - 1; 339 | lockMax = true; 340 | } 341 | 342 | if (rules.sign && rules.sign.sign === 'negative') { 343 | let cacheMax = max; 344 | setMax(max < 0 ? max : 0); 345 | if (!(min < 0)) { 346 | if (cacheMax === max) { 347 | cacheMax = 5; 348 | } 349 | 350 | setMin(max - cacheMax); 351 | } 352 | } 353 | 354 | if (rules.multiple !== undefined) { 355 | incrementor = rules.multiple.base; 356 | 357 | if (min % incrementor !== 0) { 358 | let diff; 359 | if (min > 0) { 360 | diff = min < incrementor ? 361 | incrementor - min : 362 | incrementor - (min % incrementor); 363 | } 364 | else { 365 | diff = Math.abs(min) < incrementor ? 366 | 0 - (min + incrementor) : 367 | 0 - (min % incrementor); 368 | } 369 | 370 | setMin(min + diff); 371 | 372 | if (max > 0) { 373 | if ((min + incrementor) >= max) { 374 | setMax(min + Math.floor(max / incrementor)); 375 | } 376 | } 377 | } 378 | } 379 | 380 | numberResult = min + randNum(max, min, incrementor); 381 | 382 | if (min === max) { 383 | numberResult = min; 384 | } 385 | 386 | if (rules.precision !== undefined) { 387 | let fixedDigits = numberResult.toFixed(rules.precision.limit); 388 | 389 | if (fixedDigits.split('.')[1] === '00') { 390 | fixedDigits = fixedDigits.split('.').map((digitSet, index) => { 391 | 392 | return index === 0 ? 393 | digitSet : 394 | '05'; 395 | }).join('.'); 396 | } 397 | 398 | numberResult = Number(fixedDigits); 399 | } 400 | 401 | const impossible = this._schema.validate(numberResult).error !== undefined; 402 | 403 | return impossible ? NaN : numberResult; 404 | } 405 | } 406 | 407 | class BooleanExample extends Any { 408 | 409 | _generate() { 410 | 411 | const schemaDescription = this._schema.describe(); 412 | const truthy = schemaDescription.truthy ? schemaDescription.truthy.slice(0) : []; 413 | const falsy = schemaDescription.falsy ? schemaDescription.falsy.slice(0) : []; 414 | 415 | const possibleResult = truthy.concat(falsy); 416 | 417 | return possibleResult.length > 0 418 | ? Helpers.pickRandomFromArray(possibleResult) 419 | : Math.random() > 0.5; 420 | } 421 | } 422 | 423 | class BinaryExample extends Any { 424 | 425 | _generate(rules) { 426 | 427 | let bufferSize = 10; 428 | 429 | if (rules.length) { 430 | bufferSize = rules.length.limit; 431 | } 432 | else if (rules.min && rules.max) { 433 | bufferSize = rules.min.limit + Math.floor(Math.random() * (rules.max.limit - rules.min.limit)); 434 | } 435 | else if (rules.min) { 436 | bufferSize = Math.ceil(rules.min.limit * (Math.random() + 1)); 437 | } 438 | else if (rules.max) { 439 | bufferSize = Math.ceil(rules.max.limit * Math.random()); 440 | } 441 | 442 | const encodingFlag = Hoek.reach(this._schema.describe(), 'flags.encoding'); 443 | const encoding = encodingFlag || 'utf8'; 444 | const bufferResult = Buffer.alloc(bufferSize, Math.random().toString(36).substr(2)); 445 | 446 | return encodingFlag ? bufferResult.toString(encoding) : bufferResult; 447 | } 448 | } 449 | 450 | class DateExample extends Any { 451 | 452 | _generate(rules) { 453 | 454 | const schemaDescription = this._schema.describe(); 455 | 456 | let dateModifier = Math.random() * (new Date()).getTime() / 5; 457 | 458 | let min = 0; 459 | let max = (new Date(min + dateModifier)).getTime(); 460 | 461 | if (rules.min) { 462 | min = rules.min.date === 'now' ? 463 | (new Date()).getTime() : 464 | (new Date(rules.min.date)).getTime(); 465 | 466 | if (rules.max === undefined) { 467 | max = min + dateModifier; 468 | } 469 | } 470 | 471 | if (rules.max) { 472 | max = rules.max.date === 'now' ? 473 | (new Date()).getTime() 474 | : (new Date(rules.max.date)).getTime(); 475 | 476 | if (rules.min === undefined) { 477 | min = max - dateModifier; 478 | } 479 | } 480 | 481 | dateModifier = Math.random() * (max - min); 482 | 483 | let dateResult = new Date(min + dateModifier); 484 | 485 | if (schemaDescription.flags && schemaDescription.flags.format) { 486 | if (schemaDescription.flags.format === 'javascript') { 487 | dateResult = dateResult.getTime() / Number(1); 488 | } 489 | else if (schemaDescription.flags.format === 'unix') { 490 | dateResult = dateResult.getTime() / Number(1000); 491 | } 492 | else { 493 | //ISO formatting is nested as a ISO Regex in format. 494 | //But since date.format() API is no longer natively supported, 495 | //regex pattern does not need to be acknowledged and ISO 496 | //output is implied. 497 | dateResult = dateResult.toISOString(); 498 | 499 | const isArray = Array.isArray(schemaDescription.flags.format); 500 | if (!isArray) { 501 | const moments = new Moment(dateResult); 502 | dateResult = moments.format(schemaDescription.flags.format); 503 | } 504 | else { 505 | const moment = new Moment(dateResult); 506 | const targetFormat = Helpers.pickRandomFromArray(schemaDescription.flags.format); 507 | dateResult = moment.format(targetFormat); 508 | } 509 | } 510 | } 511 | 512 | return dateResult; 513 | } 514 | } 515 | 516 | class FunctionExample extends Any { 517 | 518 | _generate(rules) { 519 | 520 | const parameterNames = []; 521 | let idealArityCount = 0; 522 | const arityCount = rules.arity === undefined ? null : rules.arity.n; 523 | const minArityCount = rules.minArity === undefined ? null : rules.minArity.n; 524 | const maxArityCount = rules.maxArity === undefined ? null : rules.maxArity.n; 525 | 526 | if (arityCount) { 527 | idealArityCount = arityCount; 528 | } 529 | else if (minArityCount && maxArityCount) { 530 | idealArityCount = Math.floor(Math.random() * (maxArityCount - minArityCount) + minArityCount); 531 | } 532 | else if (minArityCount) { 533 | idealArityCount = minArityCount; 534 | } 535 | else if (maxArityCount) { 536 | idealArityCount = maxArityCount; 537 | } 538 | 539 | for (let i = 0; i < idealArityCount; ++i) { 540 | parameterNames.push('param' + i); 541 | } 542 | 543 | return new Function(parameterNames.join(','), 'return 0;'); 544 | } 545 | } 546 | 547 | class ArrayExample extends Any { 548 | 549 | _generate(rules) { 550 | 551 | const schemaDescription = this._schema.describe(); 552 | 553 | const childOptions = { 554 | schemaDescription, 555 | config: this._options 556 | }; 557 | 558 | const arrayIsSparse = schemaDescription.flags && schemaDescription.flags.sparse; 559 | const arrayIsSingle = schemaDescription.flags && schemaDescription.flags.single; 560 | let arrayResult = []; 561 | 562 | if (!arrayIsSparse) { 563 | if (schemaDescription.ordered) { 564 | for (let i = 0; i < schemaDescription.ordered.length; ++i) { 565 | const itemRawSchema = Hoek.reach(this._schema, '$_terms.ordered')[i]; 566 | const itemType = internals.getType(itemRawSchema); 567 | const Item = new Examples[itemType](itemRawSchema, childOptions); 568 | 569 | arrayResult.push(Item.generate()); 570 | } 571 | } 572 | 573 | if (schemaDescription.items) { 574 | for (let i = 0; i < schemaDescription.items.length; ++i) { 575 | const itemIsForbidden = schemaDescription.items[i].flags && schemaDescription.items[i].flags.presence === 'forbidden'; 576 | if (!itemIsForbidden) { 577 | const itemRawSchema = Hoek.reach(this._schema, '$_terms.items')[i]; 578 | const itemType = internals.getType(itemRawSchema); 579 | const Item = new Examples[itemType](itemRawSchema, childOptions); 580 | 581 | arrayResult.push(Item.generate()); 582 | } 583 | } 584 | } 585 | 586 | const itemsToAdd = schemaDescription.items ? schemaDescription.items : [ 587 | { 588 | type: 'string' 589 | }, 590 | { 591 | type: 'number' 592 | } 593 | ]; 594 | 595 | if (rules.length && arrayResult.length !== rules.length.limit) { 596 | if (arrayResult.length > rules.length.limit) { 597 | arrayResult = arrayResult.slice(0, rules.length.limit); 598 | } 599 | else { 600 | while (arrayResult.length < rules.length.limit) { 601 | const itemToAdd = Helpers.pickRandomFromArray(itemsToAdd); 602 | const itemExample = new Examples[itemToAdd.type](Joi.build(itemToAdd)); 603 | 604 | arrayResult.push(itemExample.generate()); 605 | } 606 | } 607 | } 608 | 609 | if (rules.min && arrayResult.length < rules.min.limit) { 610 | while (arrayResult.length < rules.min.limit) { 611 | const itemToAdd = Helpers.pickRandomFromArray(itemsToAdd); 612 | const itemExample = new Examples[itemToAdd.type](Joi.build(itemToAdd)); 613 | 614 | arrayResult.push(itemExample.generate()); 615 | } 616 | } 617 | 618 | if (rules.max && arrayResult.length === 0) { 619 | const arrayLength = Math.ceil(Math.random() * rules.max.limit); 620 | 621 | while (arrayResult.length < arrayLength) { 622 | const itemToAdd = Helpers.pickRandomFromArray(itemsToAdd); 623 | const itemExample = new Examples[itemToAdd.type](Joi.build(itemToAdd)); 624 | 625 | arrayResult.push(itemExample.generate()); 626 | } 627 | } 628 | } 629 | 630 | if (arrayResult.length > 0 && arrayIsSingle) { 631 | arrayResult = arrayResult.pop(); 632 | } 633 | 634 | return arrayResult; 635 | } 636 | } 637 | 638 | class ObjectExample extends Any { 639 | 640 | _generate(rules) { 641 | 642 | const schemaDescription = this._schema.describe(); 643 | const parentPresence = Hoek.reach(schemaDescription, 'preferences.presence'); 644 | 645 | const objectResult = {}; 646 | const randomChildGenerator = function () { 647 | 648 | const randString = Math.random().toString(36).substr(2); 649 | 650 | objectResult[randString.substr(0, 4)] = randString; 651 | }; 652 | 653 | let objectChildGenerator = randomChildGenerator; 654 | 655 | if (schemaDescription.keys) { 656 | const schemaDescriptionKeys = Object.keys(schemaDescription.keys); 657 | const isExistAlternatives = Boolean(Object.values(schemaDescription.keys).find((child) => child.type === 'alternatives')); 658 | 659 | if (isExistAlternatives) { 660 | schemaDescriptionKeys.sort((a, b) => { 661 | 662 | const isAlternatives = schemaDescription.keys[a].type === 'alternatives'; 663 | return isAlternatives ? 1 : -1; 664 | }); 665 | } 666 | 667 | schemaDescriptionKeys.forEach((childKey) => { 668 | 669 | const childSchemaRaw = this._schema._ids._byKey.get(childKey).schema; 670 | const childSchema = schemaDescription.keys[childKey]; 671 | const flagsPresence = Hoek.reach(childSchema, 'flags.presence'); 672 | const childIsRequired = flagsPresence === 'required'; 673 | const childIsOptional = (flagsPresence === 'optional') || (parentPresence === 'optional' && !childIsRequired); 674 | const childIsForbidden = flagsPresence === 'forbidden'; 675 | const shouldStrip = Hoek.reach(childSchema, 'flags.result') === 'strip'; 676 | 677 | if (shouldStrip || childIsForbidden || (childIsOptional && !(Hoek.reach(this._options, 'includeOptional')))) { 678 | return; 679 | } 680 | 681 | const childOptions = { 682 | schemaDescription, 683 | objectResult, 684 | config: this._options 685 | }; 686 | const childType = internals.getType(childSchemaRaw); 687 | const child = new Examples[childType](childSchemaRaw, childOptions); 688 | objectResult[childKey] = child.generate(); 689 | }); 690 | } 691 | 692 | if (schemaDescription.patterns) { 693 | const pattern = Helpers.pickRandomFromArray(schemaDescription.patterns); 694 | const patternRaw = this._schema.$_terms.patterns.filter((patternSchema) => { 695 | 696 | return patternSchema.regex.toString() === pattern.regex; 697 | })[0].rule; 698 | const options = this._options; 699 | objectChildGenerator = function () { 700 | 701 | const initialKeyLength = Object.keys(objectResult).length; 702 | const key = new RandExp(pattern.regex.substr(1, pattern.regex.length - 2)).gen(); 703 | const child = new Examples[pattern.rule.type](patternRaw, { config: options }); 704 | objectResult[key] = child.generate(); 705 | if (initialKeyLength === Object.keys(objectResult).length) { 706 | objectChildGenerator = randomChildGenerator; 707 | } 708 | }; 709 | } 710 | 711 | if (rules.instance) { 712 | return new rules.instance.constructor(); 713 | } 714 | 715 | if (rules.schema) { 716 | return this._schema; 717 | } 718 | 719 | let keyCount = 0; 720 | 721 | if (rules.min && Object.keys(objectResult).length < rules.min.limit) { 722 | keyCount = rules.min.limit; 723 | } 724 | else if (rules.max && Object.keys(objectResult).length === 0) { 725 | keyCount = rules.max.limit - 1; 726 | } 727 | else { 728 | keyCount = rules.length && rules.length.limit; 729 | } 730 | 731 | while (Object.keys(objectResult).length < keyCount) { 732 | objectChildGenerator(); 733 | } 734 | 735 | if (schemaDescription.dependencies) { 736 | const objectDependencies = {}; 737 | 738 | schemaDescription.dependencies.forEach((dependency) => { 739 | 740 | if (dependency.rel === 'with') { 741 | 742 | objectDependencies[dependency.rel] = { 743 | peers: dependency.peers, 744 | key: dependency.key 745 | }; 746 | } 747 | else { 748 | objectDependencies[dependency.rel] = dependency.peers; 749 | } 750 | }); 751 | 752 | if (objectDependencies.nand || objectDependencies.xor || objectDependencies.without) { 753 | 754 | const peers = objectDependencies.nand || objectDependencies.xor || objectDependencies.without; 755 | 756 | if (peers.length > 1) { 757 | peers.splice(Math.floor(Math.random() * peers.length), 1); 758 | } 759 | 760 | peers.forEach((keyToDelete) => { 761 | 762 | delete objectResult[keyToDelete]; 763 | }); 764 | } 765 | 766 | if (objectDependencies.with && Hoek.reach(objectResult, objectDependencies.with.key) !== undefined) { 767 | const options = this._options; 768 | objectDependencies.with.peers.forEach((peerKey) => { 769 | 770 | if (Hoek.reach(objectResult, peerKey) === undefined) { 771 | const peerSchema = Joi.build(schemaDescription).extract(peerKey); 772 | const peerOptions = { 773 | schemaDescription, 774 | objectResult, 775 | config: options 776 | }; 777 | const peer = new Examples[peerSchema.describe().type](peerSchema, peerOptions); 778 | objectResult[peerKey] = peer.generate(); 779 | } 780 | }); 781 | } 782 | } 783 | 784 | return objectResult; 785 | } 786 | } 787 | 788 | class AlternativesExample extends Any { 789 | 790 | constructor(schema, options) { 791 | 792 | super(schema, options); 793 | this._hydratedParent = options && options.objectResult; 794 | } 795 | 796 | _generate(rules) { 797 | 798 | const schemaDescription = this._schema.describe(); 799 | 800 | let resultSchema; 801 | let resultSchemaRaw; 802 | 803 | if (schemaDescription.matches.length > 1) { 804 | const potentialValues = schemaDescription.matches; 805 | resultSchema = Helpers.pickRandomFromArray(potentialValues); 806 | } 807 | else { 808 | if (schemaDescription.matches[0].ref) { 809 | const driverPath = schemaDescription.matches[0].ref.path.join('.'); 810 | const driverValue = Hoek.reach(this._hydratedParent, driverPath); 811 | 812 | let driverIsTruthy = false; 813 | 814 | const { error } = Joi.build(schemaDescription.matches[0].is).validate(driverValue); 815 | 816 | if (!error) { 817 | driverIsTruthy = true; 818 | } 819 | 820 | if (driverIsTruthy) { 821 | resultSchema = schemaDescription.matches[0].then; 822 | 823 | resultSchemaRaw = Hoek.reach(this._schema, '$_terms.matches')[0].then; 824 | } 825 | else { 826 | resultSchema = schemaDescription.matches[0].otherwise; 827 | resultSchemaRaw = Hoek.reach(this._schema, '$_terms.matches')[0].otherwise; 828 | } 829 | } 830 | else { 831 | resultSchema = schemaDescription.matches[0]; 832 | } 833 | } 834 | 835 | const schema = resultSchemaRaw === undefined ? Joi.build(resultSchema.schema) : resultSchemaRaw; 836 | const type = internals.getType(schema); 837 | const result = new Examples[type](schema, { config: this._options }); 838 | 839 | return result.generate(); 840 | } 841 | } 842 | 843 | const Examples = { 844 | any: Any, 845 | string: StringExample, 846 | number: NumberExample, 847 | boolean: BooleanExample, 848 | binary: BinaryExample, 849 | date: DateExample, 850 | func: FunctionExample, 851 | function: FunctionExample, 852 | array: ArrayExample, 853 | object: ObjectExample, 854 | alternatives: AlternativesExample 855 | }; 856 | 857 | const valueGenerator = (schema, options) => { 858 | 859 | const exampleType = internals.getType(schema); 860 | 861 | const Example = Examples[exampleType]; 862 | const example = new Example(schema, options); 863 | 864 | return example.generate(); 865 | }; 866 | 867 | module.exports = valueGenerator; 868 | -------------------------------------------------------------------------------- /test/value_generator_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hoek = require('@hapi/hoek'); 4 | const Lab = require('@hapi/lab'); 5 | const Moment = require('moment'); 6 | const Joi = require('../lib/joi'); 7 | const { permutations, expectValidation } = require('./test_helpers'); 8 | const ValueGenerator = require('../lib/exampleGenerator'); 9 | 10 | const { describe, expect, it } = exports.lab = Lab.script(); 11 | const ExpectValidation = expectValidation(expect); 12 | 13 | describe('Any', () => { 14 | 15 | it('should default to string', () => { 16 | 17 | const schema = Joi.any(); 18 | const example = ValueGenerator(schema); 19 | 20 | ExpectValidation(example, schema); 21 | }); 22 | 23 | it('should return an "allow"ed value', () => { 24 | 25 | let schema = Joi.any().allow('allowed'); 26 | ExpectValidation(ValueGenerator(schema), schema); 27 | 28 | let examples = {}; 29 | schema = Joi.any().allow('allowed1', 'allowed2'); 30 | 31 | for (let i = 0; i < 10; ++i) { 32 | const example = ValueGenerator(schema); 33 | examples[example] = true; 34 | ExpectValidation(example, schema); 35 | } 36 | 37 | expect(examples.allowed1).to.exist(); 38 | expect(examples.allowed2).to.exist(); 39 | 40 | examples = {}; 41 | schema = Joi.any().allow('first', 'second', true, 10); 42 | 43 | for (let i = 0; i < 25; ++i) { 44 | const example = ValueGenerator(schema); 45 | examples[example] = true; 46 | ExpectValidation(example, schema); 47 | } 48 | 49 | expect(['first', 'second', 'true', '10'].filter((valid) => examples[valid] !== undefined).length).to.equal(4); 50 | }); 51 | 52 | it('should ignore "allow"ed values when provided the "ignoreValids" option', () => { 53 | 54 | const schema = Joi.any().allow(null); 55 | const example = ValueGenerator(schema, { config: { ignoreValids: true } }); 56 | 57 | expect(example).to.be.a.string(); 58 | ExpectValidation(example, schema); 59 | }); 60 | 61 | it('should return a "valid" value', () => { 62 | 63 | let schema = Joi.any().valid('allowed'); 64 | let example = ValueGenerator(schema); 65 | 66 | expect(example).to.equal('allowed'); 67 | ExpectValidation(example, schema); 68 | 69 | schema = Joi.any().valid('allowed1', 'allowed2'); 70 | example = ValueGenerator(schema); 71 | 72 | expect(['allowed1', 'allowed2'].indexOf(example)).to.not.equal(-1); 73 | ExpectValidation(example, schema); 74 | 75 | schema = Joi.any().valid(true, 10); 76 | example = ValueGenerator(schema); 77 | 78 | expect([true, 10].indexOf(example)).to.not.equal(-1); 79 | ExpectValidation(example, schema); 80 | }); 81 | 82 | it('should return an "example" value from a single argument', () => { 83 | 84 | const schema = Joi.any().example(123); 85 | const example = ValueGenerator(schema); 86 | 87 | expect(example).to.equal(123); 88 | ExpectValidation(example, schema); 89 | }); 90 | 91 | it('should return an "example" value from multiple arguments', () => { 92 | 93 | const examples = [123, 321]; 94 | const schema = Joi.any().example(examples); 95 | const example = ValueGenerator(schema); 96 | const foundExample = examples.find((ex) => ex === example); 97 | 98 | expect(foundExample).to.equal(example); 99 | ExpectValidation(example, schema); 100 | }); 101 | 102 | it('should return an "example" value from a single array argument', () => { 103 | 104 | const schema = Joi.any().example([123]); 105 | const example = ValueGenerator(schema); 106 | 107 | expect(example).to.equal(123); 108 | ExpectValidation(example, schema); 109 | }); 110 | 111 | it('should return an "example" value from multiple single array arguments', () => { 112 | 113 | const examples = [[123], [321]]; 114 | const schema = Joi.any().example(examples); 115 | const example = ValueGenerator(schema); 116 | 117 | const [foundExample] = examples.find(([ex]) => ex === example); 118 | 119 | expect(foundExample).to.equal(example); 120 | ExpectValidation(example, schema); 121 | }); 122 | 123 | it('should return a default value', () => { 124 | 125 | const schema = Joi.any().default(123); 126 | const example = ValueGenerator(schema); 127 | 128 | expect(example).to.equal(123); 129 | ExpectValidation(example, schema); 130 | }); 131 | 132 | it('should return a dynamic default value', () => { 133 | 134 | const generateDefault = function () { 135 | 136 | return true; 137 | }; 138 | 139 | generateDefault.description = 'generates default'; 140 | const schema = Joi.any().default(generateDefault); 141 | const example = ValueGenerator(schema); 142 | 143 | expect(example).to.equal(true); 144 | ExpectValidation(example, schema); 145 | }); 146 | }); 147 | 148 | describe('String', () => { 149 | 150 | it('should return a basic string', () => { 151 | 152 | const schema = Joi.string(); 153 | const example = ValueGenerator(schema); 154 | 155 | expect(example).to.be.a.string(); 156 | ExpectValidation(example, schema); 157 | }); 158 | 159 | it('should return a string with valid value', () => { 160 | 161 | const schema = Joi.string().valid('a'); 162 | const example = ValueGenerator(schema); 163 | 164 | ExpectValidation(example, schema); 165 | }); 166 | 167 | it('should return a GUID', () => { 168 | 169 | const schema = Joi.string().guid(); 170 | const example = ValueGenerator(schema); 171 | 172 | expect(example).to.be.a.string(); 173 | ExpectValidation(example, schema); 174 | }); 175 | 176 | it('should return a GUID with UUID syntax', () => { 177 | 178 | const schema = Joi.string().uuid(); 179 | const example = ValueGenerator(schema); 180 | 181 | ExpectValidation(example, schema); 182 | }); 183 | 184 | it('should return an email', () => { 185 | 186 | const schema = Joi.string().email(); 187 | const example = ValueGenerator(schema); 188 | 189 | expect(example).to.be.a.string(); 190 | ExpectValidation(example, schema); 191 | }); 192 | 193 | it('should return default value', () => { 194 | 195 | const schema = Joi.string().default('fallback'); 196 | const example = ValueGenerator(schema); 197 | 198 | expect(example).to.equal('fallback'); 199 | ExpectValidation(example, schema); 200 | }); 201 | 202 | it('should return default value when valids are ignored', () => { 203 | 204 | const schema = Joi.string().valid('value1', 'value2', 'fallback').default('fallback'); 205 | const example = ValueGenerator(schema, { config: { ignoreValids: true } }); 206 | 207 | expect(example).to.equal('fallback'); 208 | ExpectValidation(example, schema); 209 | }); 210 | 211 | it('should utilize dynamic default function', () => { 212 | 213 | const defaultGenerator = function () { 214 | 215 | return 'fallback'; 216 | }; 217 | 218 | defaultGenerator.description = 'generates a default'; 219 | const schema = Joi.string().default(defaultGenerator); 220 | const example = ValueGenerator(schema); 221 | 222 | expect(example).to.equal('fallback'); 223 | ExpectValidation(example, schema); 224 | }); 225 | 226 | it('should return a string which adheres to .min requirement', () => { 227 | 228 | for (let i = 0; i <= 5; ++i) { 229 | 230 | const min = Math.ceil((Math.random() + 1) * Math.pow(1 + i, i)); 231 | const schema = Joi.string().min(min); 232 | const example = ValueGenerator(schema); 233 | 234 | expect(example.length).to.be.at.least(min); 235 | ExpectValidation(example, schema); 236 | } 237 | 238 | }); 239 | 240 | it('should return a string which adheres to .max requirement', () => { 241 | 242 | for (let i = 0; i <= 5; ++i) { 243 | 244 | const max = Math.ceil((Math.random() + 1) * Math.pow(1 + i, i)); 245 | const schema = Joi.string().max(max); 246 | const example = ValueGenerator(schema); 247 | 248 | expect(example.length).to.be.at.most(max); 249 | ExpectValidation(example, schema); 250 | } 251 | 252 | }); 253 | 254 | it('should return a string which adheres to both .min and .max requirements', () => { 255 | 256 | for (let i = 4; i <= 25; ++i) { 257 | 258 | const max = Math.ceil(Math.random() * i + 1); 259 | const possibleMin = max - Math.floor(Math.random() * i + 1); 260 | const min = possibleMin < 1 ? 1 : possibleMin; 261 | const schema = Joi.string().min(min).max(max); 262 | const example = ValueGenerator(schema); 263 | 264 | expect(example.length).to.be.at.most(max).and.at.least(min); 265 | ExpectValidation(example, schema); 266 | } 267 | 268 | const largeMax = 750; 269 | const largeMin = 500; 270 | const largeSchema = Joi.string().min(largeMin).max(largeMax); 271 | const largeExample = ValueGenerator(largeSchema); 272 | 273 | expect(largeExample.length).to.be.at.most(largeMax).and.at.least(largeMin); 274 | ExpectValidation(largeExample, largeSchema); 275 | }); 276 | 277 | it('should return a string which adheres to .length requirement', () => { 278 | 279 | for (let i = 0; i <= 5; ++i) { 280 | 281 | const length = Math.ceil((Math.random() + 1) * Math.pow(1 + i, i)); 282 | const schema = Joi.string().length(length); 283 | const example = ValueGenerator(schema); 284 | 285 | expect(example.length).to.equal(length); 286 | ExpectValidation(example, schema); 287 | } 288 | 289 | }); 290 | 291 | it('should return a string which adheres to .isoDate requirement', () => { 292 | 293 | const schema = Joi.string().isoDate(); 294 | const example = ValueGenerator(schema); 295 | 296 | expect((new Date(example)).toISOString()).to.equal(example); 297 | ExpectValidation(example, schema); 298 | }); 299 | 300 | it('should return a string that matches the given regexp', () => { 301 | 302 | const regex = new RegExp(/[a-c]{3}-[d-f]{3}-[0-9]{4}/); 303 | const schema = Joi.string().regex(regex); 304 | const example = ValueGenerator(schema); 305 | 306 | expect(example.match(regex)).to.not.equal(null); 307 | ExpectValidation(example, schema); 308 | }); 309 | 310 | it('should return a string that does not match the inverted regexp', () => { 311 | 312 | const regex = new RegExp(/[a-c]{3}-[d-f]{3}-[0-9]{4}/); 313 | const schema = Joi.string().regex(regex, { invert: true }); 314 | const example = ValueGenerator(schema); 315 | 316 | expect(example.match(regex)).to.equal(null); 317 | ExpectValidation(example, schema); 318 | }); 319 | 320 | it('should return a case-insensitive string', () => { 321 | 322 | const schema = Joi.string().valid('A').insensitive(); 323 | const example = ValueGenerator(schema); 324 | 325 | ExpectValidation(example, schema); 326 | }); 327 | 328 | it('should return a Luhn-valid credit card number', () => { 329 | 330 | const schema = Joi.string().creditCard(); 331 | const example = ValueGenerator(schema); 332 | 333 | ExpectValidation(example, schema); 334 | }); 335 | 336 | it('should return a hexadecimal string', () => { 337 | 338 | const schema = Joi.string().hex(); 339 | const example = ValueGenerator(schema); 340 | 341 | ExpectValidation(example, schema); 342 | }); 343 | 344 | it('should return a hexadecimal string between min and max', () => { 345 | 346 | const schema = Joi.string().hex().min(128).max(130); 347 | const example = ValueGenerator(schema); 348 | 349 | ExpectValidation(example, schema); 350 | }); 351 | 352 | it('should return a token', () => { 353 | 354 | const schema = Joi.string().token(); 355 | const example = ValueGenerator(schema); 356 | 357 | ExpectValidation(example, schema); 358 | }); 359 | 360 | it('should return an alphanumeric string', () => { 361 | 362 | const schema = Joi.string().alphanum(); 363 | const example = ValueGenerator(schema); 364 | 365 | ExpectValidation(example, schema); 366 | }); 367 | 368 | it('should return a hostname', () => { 369 | 370 | const schema = Joi.string().hostname(); 371 | const example = ValueGenerator(schema); 372 | 373 | ExpectValidation(example, schema); 374 | }); 375 | 376 | it('should return a IPv4 when given no options', () => { 377 | 378 | const schema = Joi.string().ip(); 379 | const example = ValueGenerator(schema); 380 | 381 | ExpectValidation(example, schema); 382 | }); 383 | 384 | it('should return a IPv4 when given options and cidr is forbidden', () => { 385 | 386 | const schema = Joi.string().ip( 387 | { 388 | cidr: 'forbidden' 389 | }); 390 | const example = ValueGenerator(schema); 391 | 392 | ExpectValidation(example, schema); 393 | }); 394 | 395 | it('should return a IPv4 when given options', () => { 396 | 397 | const schema = Joi.string().ip( 398 | { 399 | version: ['ipv4'] 400 | }); 401 | const example = ValueGenerator(schema); 402 | 403 | ExpectValidation(example, schema); 404 | }); 405 | 406 | it('should return a IPv4 when given options and cidr is forbidden', () => { 407 | 408 | const schema = Joi.string().ip( 409 | { 410 | version: ['ipv4'], 411 | cidr: 'forbidden' 412 | }); 413 | const example = ValueGenerator(schema); 414 | 415 | ExpectValidation(example, schema); 416 | }); 417 | 418 | it('should return a IPv6 when given options', () => { 419 | 420 | const schema = Joi.string().ip( 421 | { 422 | version: ['ipv6'] 423 | }); 424 | const example = ValueGenerator(schema); 425 | 426 | ExpectValidation(example, schema); 427 | }); 428 | 429 | it('should return a IPv6 when given options and cidr is forbidden', () => { 430 | 431 | const schema = Joi.string().ip( 432 | { 433 | version: ['ipv6'], 434 | cidr: 'forbidden' 435 | }); 436 | const example = ValueGenerator(schema); 437 | 438 | ExpectValidation(example, schema); 439 | }); 440 | 441 | it('should return uppercase value', () => { 442 | 443 | const schema = Joi.string().uppercase(); 444 | const example = ValueGenerator(schema); 445 | 446 | ExpectValidation(example, schema); 447 | }); 448 | 449 | it('should return uppercase value for guid to test chaining', () => { 450 | 451 | const schema = Joi.string().guid().uppercase(); 452 | const example = ValueGenerator(schema); 453 | 454 | ExpectValidation(example, schema); 455 | }); 456 | 457 | it('should return lowercase value', () => { 458 | 459 | const schema = Joi.string().lowercase(); 460 | const example = ValueGenerator(schema); 461 | 462 | ExpectValidation(example, schema); 463 | }); 464 | 465 | it('should return example.com for .uri', () => { 466 | 467 | const schema = Joi.string().uri(); 468 | const example = ValueGenerator(schema); 469 | 470 | ExpectValidation(example, schema); 471 | }); 472 | }); 473 | 474 | describe('Number', () => { 475 | 476 | it('should return a number', () => { 477 | 478 | const schema = Joi.number(); 479 | const example = ValueGenerator(schema); 480 | 481 | expect(example).to.be.a.number(); 482 | ExpectValidation(example, schema); 483 | }); 484 | 485 | it('should return a default value > 0', () => { 486 | 487 | const schema = Joi.number().default(9); 488 | const example = ValueGenerator(schema); 489 | 490 | expect(example).to.equal(9); 491 | ExpectValidation(example, schema); 492 | }); 493 | 494 | it('should return a default value === 0', () => { 495 | 496 | const schema = Joi.number().default(0); 497 | const example = ValueGenerator(schema); 498 | 499 | expect(example).to.equal(0); 500 | ExpectValidation(example, schema); 501 | }); 502 | 503 | it('should return a dynamic default', () => { 504 | 505 | const generateNumber = () => 0; 506 | generateNumber.description = 'default description'; 507 | const schema = Joi.number().default(generateNumber); 508 | const example = ValueGenerator(schema); 509 | 510 | expect(example).to.equal(0); 511 | ExpectValidation(example, schema); 512 | }); 513 | 514 | it('should return a valid value instead of default', () => { 515 | 516 | const schema = Joi.number().valid(2).default(1); 517 | const example = ValueGenerator(schema); 518 | 519 | expect(example).to.equal(2); 520 | ExpectValidation(example, schema); 521 | }); 522 | 523 | it('should return a negative number', () => { 524 | 525 | const schema = Joi.number().negative(); 526 | const example = ValueGenerator(schema); 527 | 528 | expect(example).to.be.below(0); 529 | ExpectValidation(example, schema); 530 | }); 531 | 532 | it('should return an integer', () => { 533 | 534 | const schema = Joi.number().integer(); 535 | const example = ValueGenerator(schema); 536 | 537 | expect(example % 1).to.equal(0); 538 | ExpectValidation(example, schema); 539 | }); 540 | 541 | it('should return a number which adheres to .min requirement', () => { 542 | 543 | const schema = Joi.number().min(20); 544 | const example = ValueGenerator(schema); 545 | 546 | expect(example).to.be.at.least(20); 547 | ExpectValidation(example, schema); 548 | }); 549 | 550 | it('should return a number which adheres to .max requirement', () => { 551 | 552 | const schema = Joi.number().max(2); 553 | const example = ValueGenerator(schema); 554 | 555 | expect(example).to.be.at.most(2); 556 | ExpectValidation(example, schema); 557 | }); 558 | 559 | it('should return a number which has equal .min and .max requirements', () => { 560 | 561 | const schema = Joi.number().min(1).max(1); 562 | const example = ValueGenerator(schema); 563 | 564 | expect(example).to.equal(1); 565 | ExpectValidation(example, schema); 566 | }); 567 | 568 | it('should return a number which adheres to .greater requirement', () => { 569 | 570 | const schema = Joi.number().greater(20); 571 | const example = ValueGenerator(schema); 572 | 573 | expect(example).to.be.at.least(20); 574 | ExpectValidation(example, schema); 575 | }); 576 | 577 | it('should return a number which adheres to .less requirement', () => { 578 | 579 | const schema = Joi.number().less(2); 580 | const example = ValueGenerator(schema); 581 | 582 | expect(example).to.be.at.most(2); 583 | ExpectValidation(example, schema); 584 | }); 585 | 586 | it('should return a number which adheres to .precision requirement', () => { 587 | 588 | for (let i = 0; i < 500; ++i) { 589 | const schema = Joi.number().precision(2); 590 | const example = ValueGenerator(schema); 591 | 592 | expect(example.toString().split('.')[1].length).to.be.at.most(2); 593 | ExpectValidation(example, schema); 594 | } 595 | }); 596 | 597 | it('should return a number which adheres to .multiple requirement', () => { 598 | 599 | const schema = Joi.number().multiple(4); 600 | const example = ValueGenerator(schema); 601 | 602 | expect(example % 4).to.equal(0); 603 | ExpectValidation(example, schema); 604 | }); 605 | 606 | it('should return a number which adheres to .multiple requirement in conjunction with min and max', () => { 607 | 608 | const schema = Joi.number().multiple(6).min(5).max(8); 609 | const example = ValueGenerator(schema); 610 | 611 | expect(example % 2).to.equal(0); 612 | ExpectValidation(example, schema); 613 | }); 614 | 615 | it('should return numbers which adhere to any valid combination of requirements', () => { 616 | 617 | const requirements = [ 618 | 'positive', 619 | 'negative', 620 | 'integer', 621 | 'min', 622 | 'max', 623 | 'greater', 624 | 'less', 625 | 'precision', 626 | 'multiple' 627 | ]; 628 | const requirementExclusions = { 629 | positive: ['positive','negative'], 630 | negative: ['negative','positive'], 631 | precision: ['precision','integer', 'multiple'], 632 | integer: ['integer','precision'], 633 | multiple: ['multiple','precision'], 634 | max: ['max','less'], 635 | less: ['less','max'], 636 | min: ['min','greater'], 637 | greater: ['greater','min'] 638 | }; 639 | const optionArguments = { 640 | min: 16, 641 | max: 56, 642 | greater: 35, 643 | less: 45, 644 | precision: 3, 645 | multiple: 8 646 | }; 647 | 648 | const numberOptions = permutations(requirements, requirementExclusions); 649 | 650 | numberOptions.forEach((optionSet) => { 651 | 652 | let schema = Joi.number(); 653 | const setContainsNegative = optionSet.indexOf('negative') !== -1; 654 | const setContainsMinAndMax = Hoek.intersect(optionSet, ['min', 'greater']).length > 0 && Hoek.intersect(optionSet, ['max', 'less']).length > 0; 655 | 656 | optionSet.forEach((option) => { 657 | 658 | let optionArgument = setContainsNegative ? 0 - optionArguments[option] : optionArguments[option]; 659 | 660 | if (option === 'multiple' || option === 'precision') { 661 | optionArgument = Math.abs(optionArgument); 662 | } 663 | else if (setContainsNegative && setContainsMinAndMax && (option === 'min' || option === 'greater') ) { 664 | optionArgument = 0 - optionArguments.max; 665 | } 666 | else if (setContainsNegative && setContainsMinAndMax && (option === 'max' || option === 'less')) { 667 | optionArgument = 0 - optionArguments.min; 668 | } 669 | 670 | schema = schema[option](optionArgument); 671 | }); 672 | 673 | const example = ValueGenerator(schema); 674 | 675 | ExpectValidation(example, schema); 676 | }); 677 | }); 678 | 679 | it('should return NaN for impossible combinations', () => { 680 | 681 | const impossibleMinSchema = Joi.number().negative().min(1); 682 | let example = ValueGenerator(impossibleMinSchema); 683 | expect(example).to.equal(NaN); 684 | 685 | example = 0; 686 | const impossibleMinMultipleSchema = Joi.number().negative().min(-10).multiple(12); 687 | example = ValueGenerator(impossibleMinMultipleSchema); 688 | expect(example).to.equal(NaN); 689 | 690 | }); 691 | }); 692 | 693 | describe('Boolean', () => { 694 | 695 | it('should return a boolean', () => { 696 | 697 | const schema = Joi.boolean(); 698 | const example = ValueGenerator(schema); 699 | 700 | expect(example).to.be.a.boolean(); 701 | ExpectValidation(example, schema); 702 | }); 703 | 704 | it('should return default "true" value', () => { 705 | 706 | for (let i = 0; i < 10; ++i) { 707 | const schema = Joi.boolean().default(true); 708 | const example = ValueGenerator(schema); 709 | 710 | expect(example).to.equal(true); 711 | } 712 | }); 713 | 714 | it('should return default "false" value', () => { 715 | 716 | for (let i = 0; i < 10; ++i) { 717 | const schema = Joi.boolean().default(false); 718 | const example = ValueGenerator(schema); 719 | 720 | expect(example).to.equal(false); 721 | } 722 | }); 723 | 724 | it('should return valid value', () => { 725 | 726 | for (let i = 0; i < 10; ++i) { 727 | const schema = Joi.boolean().valid(true).default(false); 728 | const example = ValueGenerator(schema); 729 | 730 | expect(example).to.equal(true); 731 | } 732 | }); 733 | 734 | it('should return a truthy value when singlar number', () => { 735 | 736 | const schema = Joi.boolean().truthy(1); 737 | const example = ValueGenerator(schema); 738 | 739 | expect(example).to.be.a.number(); 740 | expect(example).to.equal(1); 741 | ExpectValidation(example, schema); 742 | }); 743 | 744 | it('should return a truthy value when singlar string', () => { 745 | 746 | const schema = Joi.boolean().truthy('y'); 747 | const example = ValueGenerator(schema); 748 | 749 | expect(example).to.be.a.string(); 750 | expect(example).to.equal('y'); 751 | ExpectValidation(example, schema); 752 | }); 753 | 754 | it('should return a truthy value when pluralized', () => { 755 | 756 | const schema = Joi.boolean().truthy(1, 'y'); 757 | const example = ValueGenerator(schema); 758 | 759 | ExpectValidation(example, schema); 760 | }); 761 | 762 | it('should return a falsy value when singlar number', () => { 763 | 764 | const schema = Joi.boolean().falsy(0); 765 | const example = ValueGenerator(schema); 766 | 767 | expect(example).to.be.a.number(); 768 | expect(example).to.equal(0); 769 | ExpectValidation(example, schema); 770 | }); 771 | 772 | it('should return a falsy value when singlar string', () => { 773 | 774 | const schema = Joi.boolean().falsy('n'); 775 | const example = ValueGenerator(schema); 776 | 777 | expect(example).to.be.a.string(); 778 | expect(example).to.equal('n'); 779 | ExpectValidation(example, schema); 780 | }); 781 | 782 | it('should return a falsy value when pluralized', () => { 783 | 784 | const schema = Joi.boolean().falsy(0, 'n'); 785 | const example = ValueGenerator(schema); 786 | 787 | ExpectValidation(example, schema); 788 | }); 789 | 790 | it('should validate when a mix of truthy and falsy is set', () => { 791 | 792 | const schema = Joi.boolean().truthy(1, 'y').falsy(0, 'n'); 793 | const example = ValueGenerator(schema); 794 | 795 | ExpectValidation(example, schema); 796 | }); 797 | }); 798 | 799 | describe('Binary', () => { 800 | 801 | it('should return a buffer', () => { 802 | 803 | const schema = Joi.binary(); 804 | const example = ValueGenerator(schema); 805 | 806 | expect(example).to.be.a.buffer(); 807 | ExpectValidation(example, schema); 808 | }); 809 | 810 | it('should return a string with specified encoding', () => { 811 | 812 | const supportedEncodings = [ 813 | 'base64', 814 | 'utf8', 815 | 'ascii', 816 | 'utf16le', 817 | 'ucs2', 818 | 'hex' 819 | ]; 820 | 821 | supportedEncodings.forEach((encoding) => { 822 | 823 | const schema = Joi.binary().encoding(encoding); 824 | const example = ValueGenerator(schema); 825 | 826 | expect(example).to.be.a.string(); 827 | ExpectValidation(example, schema); 828 | }); 829 | 830 | }); 831 | 832 | it('should return a buffer of minimum size', () => { 833 | 834 | const schema = Joi.binary().min(100); 835 | const example = ValueGenerator(schema); 836 | 837 | ExpectValidation(example, schema); 838 | }); 839 | 840 | it('should return a buffer of maximum size', () => { 841 | 842 | const schema = Joi.binary().max(100); 843 | const example = ValueGenerator(schema); 844 | 845 | ExpectValidation(example, schema); 846 | }); 847 | 848 | it('should return a buffer of specified size', () => { 849 | 850 | const schema = Joi.binary().length(75); 851 | const example = ValueGenerator(schema); 852 | 853 | expect(example.length).to.equal(75); 854 | ExpectValidation(example, schema); 855 | }); 856 | 857 | it('should return a buffer of size between min and max', () => { 858 | 859 | const schema = Joi.binary().min(27).max(35); 860 | const example = ValueGenerator(schema); 861 | 862 | expect(example.length).to.be.at.least(27).and.at.most(35); 863 | ExpectValidation(example, schema); 864 | }); 865 | 866 | it('should return a dynamic default buffer', () => { 867 | 868 | const defaultBuffer = Buffer.alloc(10); 869 | const generateDefault = () => defaultBuffer; 870 | generateDefault.description = 'generates default'; 871 | const schema = Joi.binary().default(generateDefault); 872 | const example = ValueGenerator(schema); 873 | 874 | expect(example).to.be.a.buffer(); 875 | expect(example).to.equal(defaultBuffer); 876 | ExpectValidation(example, schema); 877 | }); 878 | }); 879 | 880 | describe('Date', () => { 881 | 882 | it('should return a date', () => { 883 | 884 | const schema = Joi.date(); 885 | const example = ValueGenerator(schema); 886 | 887 | expect(example).to.be.a.date(); 888 | ExpectValidation(example, schema); 889 | }); 890 | 891 | it('should return a Date more recent than .min value', () => { 892 | 893 | const schema = Joi.date().min('1/01/3016'); 894 | const example = ValueGenerator(schema); 895 | 896 | expect(example).to.be.above(new Date('1/01/3016')); 897 | ExpectValidation(example, schema); 898 | }); 899 | 900 | it('should return a Date more recent than "now"', () => { 901 | 902 | const schema = Joi.date().min('now'); 903 | const now = new Date(); 904 | const example = ValueGenerator(schema); 905 | 906 | expect(example).to.be.above(now); 907 | ExpectValidation(example, schema); 908 | }); 909 | 910 | it('should return a Date less recent than .max value', () => { 911 | 912 | const schema = Joi.date().max('1/01/1968'); 913 | const example = ValueGenerator(schema); 914 | 915 | expect(example).to.be.below(new Date(0)); 916 | ExpectValidation(example, schema); 917 | }); 918 | 919 | it('should return a Date less recent than "now"', () => { 920 | 921 | const schema = Joi.date().max('now'); 922 | const now = new Date(); 923 | const example = ValueGenerator(schema); 924 | 925 | expect(example).to.be.below(now); 926 | ExpectValidation(example, schema); 927 | }); 928 | 929 | it('should return a Date between .min and .max values', () => { 930 | 931 | for (let i = 1; i <= 20; ++i) { 932 | const minYear = 2000 + Math.ceil((Math.random() * 100)); 933 | const maxYear = minYear + Math.ceil((Math.random()) * 10); 934 | const min = '1/01/' + minYear.toString(); 935 | const max = '1/01/' + maxYear.toString(); 936 | const schema = Joi.date().min(min).max(max); 937 | const example = ValueGenerator(schema); 938 | 939 | expect(example).to.be.above(new Date(min)).and.below(new Date(max)); 940 | ExpectValidation(example, schema); 941 | } 942 | 943 | const smallMin = '1/01/2016'; 944 | const smallMax = '3/01/2016'; 945 | const smallSchema = Joi.date().min(smallMin).max(smallMax); 946 | const smallExample = ValueGenerator(smallSchema); 947 | 948 | ExpectValidation(smallExample, smallSchema); 949 | }); 950 | 951 | it('should return a Date in ISO format', () => { 952 | 953 | const schema = Joi.date().iso(); 954 | const example = ValueGenerator(schema); 955 | 956 | expect(example).to.be.a.string(); 957 | ExpectValidation(example, schema); 958 | }); 959 | 960 | it('should return a timestamp', () => { 961 | 962 | const schema = Joi.date().timestamp(); 963 | const example = ValueGenerator(schema); 964 | 965 | expect(example).to.be.a.number(); 966 | ExpectValidation(example, schema); 967 | }); 968 | 969 | it('should return a unix timestamp', () => { 970 | 971 | const schema = Joi.date().timestamp('unix'); 972 | const example = ValueGenerator(schema); 973 | 974 | expect(example).to.be.a.number(); 975 | ExpectValidation(example, schema); 976 | }); 977 | 978 | it('should return a moment formatted date', () => { 979 | 980 | const fmt = 'HH:mm'; 981 | const schema = Joi.date().format(fmt); 982 | const example = ValueGenerator(schema); 983 | const moment = new Moment(example, fmt, true); 984 | 985 | expect(example).to.be.a.string(); 986 | expect(moment.isValid()).to.equal(true); 987 | ExpectValidation(example, schema); 988 | }); 989 | 990 | it('should return a moment formatted date with Joi version <= 10.2.1', () => { 991 | 992 | const fmt = 'HH:mm'; 993 | const schema = Joi.date().format(fmt); 994 | schema._flags.momentFormat = fmt; 995 | const example = ValueGenerator(schema); 996 | const moment = new Moment(example, fmt, true); 997 | 998 | expect(example).to.be.a.string(); 999 | expect(moment.isValid()).to.equal(true); 1000 | ExpectValidation(example, schema); 1001 | }); 1002 | 1003 | it('should return one of the allowed moment formatted dates', () => { 1004 | 1005 | const fmt = ['HH:mm', 'YYYY/MM/DD']; 1006 | const schema = Joi.date().format(fmt); 1007 | const example = ValueGenerator(schema); 1008 | const moment = new Moment(example, fmt, true); 1009 | 1010 | expect(example).to.be.a.string(); 1011 | expect(moment.isValid()).to.equal(true); 1012 | ExpectValidation(example, schema); 1013 | }); 1014 | }); 1015 | 1016 | describe('Function', () => { 1017 | 1018 | it('should return a function', () => { 1019 | 1020 | const schema = Joi.func(); 1021 | const example = ValueGenerator(schema); 1022 | 1023 | expect(example).to.be.a.function(); 1024 | ExpectValidation(example, schema); 1025 | }); 1026 | 1027 | it('should return a function with arity(1)', () => { 1028 | 1029 | const schema = Joi.func().arity(1); 1030 | const example = ValueGenerator(schema); 1031 | 1032 | expect(example).to.be.a.function(); 1033 | ExpectValidation(example, schema); 1034 | }); 1035 | 1036 | it('should return a function with arity(10)', () => { 1037 | 1038 | const schema = Joi.func().arity(10); 1039 | const example = ValueGenerator(schema); 1040 | 1041 | expect(example).to.be.a.function(); 1042 | ExpectValidation(example, schema); 1043 | }); 1044 | 1045 | it('should return a function with minArity(3)', () => { 1046 | 1047 | const schema = Joi.func().minArity(3); 1048 | const example = ValueGenerator(schema); 1049 | 1050 | expect(example).to.be.a.function(); 1051 | ExpectValidation(example, schema); 1052 | }); 1053 | 1054 | it('should return a function with maxArity(4)', () => { 1055 | 1056 | const schema = Joi.func().maxArity(4); 1057 | const example = ValueGenerator(schema); 1058 | 1059 | expect(example).to.be.a.function(); 1060 | ExpectValidation(example, schema); 1061 | }); 1062 | 1063 | it('should return a function with minArity(3) and maxArity(4)', () => { 1064 | 1065 | const schema = Joi.func().minArity(3).maxArity(4); 1066 | const example = ValueGenerator(schema); 1067 | 1068 | expect(example).to.be.a.function(); 1069 | ExpectValidation(example, schema); 1070 | }); 1071 | }); 1072 | 1073 | describe('Array', () => { 1074 | 1075 | it('should return an array', () => { 1076 | 1077 | const schema = Joi.array(); 1078 | const example = ValueGenerator(schema); 1079 | 1080 | expect(example).to.be.an.array(); 1081 | ExpectValidation(example, schema); 1082 | }); 1083 | 1084 | it('should return an array with valid items', () => { 1085 | 1086 | const schema = Joi.array().items(Joi.number().required(), Joi.string().guid().required(), Joi.array().items(Joi.number().integer().min(43).required()).required()); 1087 | const example = ValueGenerator(schema); 1088 | 1089 | ExpectValidation(example, schema); 1090 | }); 1091 | 1092 | it('should return an array with valid items and without forbidden items', () => { 1093 | 1094 | const schema = Joi.array().items(Joi.string().forbidden(), Joi.number().multiple(3)); 1095 | const example = ValueGenerator(schema); 1096 | 1097 | const stringItems = example.filter((item) => { 1098 | 1099 | return typeof item === 'string'; 1100 | }); 1101 | 1102 | expect(stringItems.length).to.equal(0); 1103 | ExpectValidation(example, schema); 1104 | }); 1105 | 1106 | it('should return an empty array with "sparse"', () => { 1107 | 1108 | const schema = Joi.array() 1109 | .items(Joi.number()) 1110 | .sparse(); 1111 | const example = ValueGenerator(schema); 1112 | 1113 | expect(example.length).to.equal(0); 1114 | ExpectValidation(example, schema); 1115 | }); 1116 | 1117 | it('should return an ordered array', () => { 1118 | 1119 | const schema = Joi.array().ordered(Joi.string().max(3).required(), Joi.number().negative().integer().required(), Joi.boolean().required()); 1120 | const example = ValueGenerator(schema); 1121 | 1122 | ExpectValidation(example, schema); 1123 | }); 1124 | 1125 | it('should return an array with "length" random items', () => { 1126 | 1127 | const schema = Joi.array().length(4); 1128 | const example = ValueGenerator(schema); 1129 | 1130 | expect(example.length).to.equal(4); 1131 | ExpectValidation(example, schema); 1132 | }); 1133 | 1134 | it('should return an array with examples that match item types and in the same order', () => { 1135 | 1136 | const schema = Joi.array().length(2).items(Joi.string(), Joi.number().integer()); 1137 | const example = ValueGenerator(schema); 1138 | 1139 | expect(example.length).to.equal(2); 1140 | expect(example[0]).to.be.a.string(); 1141 | expect(example[1]).to.be.a.number(); 1142 | ExpectValidation(example, schema); 1143 | }); 1144 | 1145 | it('should return an array with "length" specified items', () => { 1146 | 1147 | const schema = Joi.array() 1148 | .items(Joi.number().integer(), Joi.string().guid(), Joi.boolean()) 1149 | .length(10); 1150 | const example = ValueGenerator(schema); 1151 | 1152 | expect(example.length).to.equal(10); 1153 | ExpectValidation(example, schema); 1154 | }); 1155 | 1156 | it('should return an array with no more than "length" specified items', () => { 1157 | 1158 | const schema = Joi.array() 1159 | .items(Joi.number().integer(), Joi.string().guid(), Joi.boolean()) 1160 | .length(2); 1161 | const example = ValueGenerator(schema); 1162 | 1163 | expect(example.length).to.equal(2); 1164 | ExpectValidation(example, schema); 1165 | }); 1166 | 1167 | it('should return an array with "min" random items', () => { 1168 | 1169 | const schema = Joi.array().min(4); 1170 | const example = ValueGenerator(schema); 1171 | 1172 | expect(example.length).to.equal(4); 1173 | ExpectValidation(example, schema); 1174 | }); 1175 | 1176 | it('should return an array with "min" specified items', () => { 1177 | 1178 | const schema = Joi.array() 1179 | .items(Joi.number().integer(), Joi.string().guid(), Joi.boolean()) 1180 | .min(3); 1181 | const example = ValueGenerator(schema); 1182 | 1183 | expect(example.length).to.equal(3); 1184 | ExpectValidation(example, schema); 1185 | }); 1186 | 1187 | it('should return an array with "min" specified items that all match the provided types', () => { 1188 | 1189 | const schema = Joi.array() 1190 | .items(Joi.number().integer(), Joi.string().guid()) 1191 | .min(4); 1192 | const example = ValueGenerator(schema); 1193 | const eitherIntOrGuid = example.some((ex) => { 1194 | 1195 | return typeof ex === 'string' || typeof ex === 'number'; 1196 | }); 1197 | 1198 | expect(example.length).to.equal(4); 1199 | expect(eitherIntOrGuid).to.equal(true); 1200 | ExpectValidation(example, schema); 1201 | }); 1202 | 1203 | it('should return an array with "max" random items', () => { 1204 | 1205 | const schema = Joi.array().max(4); 1206 | const example = ValueGenerator(schema); 1207 | 1208 | expect(example.length).to.be.at.least(1); 1209 | expect(example.length).to.be.at.most(4); 1210 | ExpectValidation(example, schema); 1211 | }); 1212 | 1213 | it('should return an array with "max" specified items', () => { 1214 | 1215 | const schema = Joi.array() 1216 | .items(Joi.number().integer(), Joi.string().guid(), Joi.boolean()) 1217 | .max(10); 1218 | const example = ValueGenerator(schema); 1219 | 1220 | expect(example.length).to.be.at.least(1); 1221 | expect(example.length).to.be.at.most(10); 1222 | ExpectValidation(example, schema); 1223 | }); 1224 | 1225 | it('should return an array with "min" and "max" random items', () => { 1226 | 1227 | const schema = Joi.array() 1228 | .min(4) 1229 | .max(5); 1230 | const example = ValueGenerator(schema); 1231 | 1232 | expect(example.length).to.be.at.least(4); 1233 | expect(example.length).to.be.at.most(5); 1234 | ExpectValidation(example, schema); 1235 | }); 1236 | 1237 | it('should return an array with "min" and "max" specified items', () => { 1238 | 1239 | const schema = Joi.array() 1240 | .items(Joi.number().integer(), Joi.string().guid(), Joi.boolean()) 1241 | .min(10) 1242 | .max(15); 1243 | const example = ValueGenerator(schema); 1244 | 1245 | expect(example.length).to.be.at.least(10); 1246 | expect(example.length).to.be.at.most(15); 1247 | ExpectValidation(example, schema); 1248 | }); 1249 | 1250 | it('should return a semi-ordered array with "min" specified items', () => { 1251 | 1252 | const schema = Joi.array() 1253 | .ordered(Joi.string(), Joi.number()) 1254 | .items(Joi.boolean().required()) 1255 | .min(6); 1256 | const example = ValueGenerator(schema); 1257 | 1258 | expect(example[0]).to.be.a.string(); 1259 | expect(example[1]).to.be.a.number(); 1260 | expect(example.length).to.be.at.least(6); 1261 | ExpectValidation(example, schema); 1262 | }); 1263 | 1264 | it('should return a single item array with a number', () => { 1265 | 1266 | const schema = Joi.array().items(Joi.number().required()).single(); 1267 | const example = ValueGenerator(schema); 1268 | expect(example).to.be.a.number(); 1269 | ExpectValidation(example, schema); 1270 | }); 1271 | 1272 | it('should return a single item with a number', () => { 1273 | 1274 | const schema = Joi.array().items(Joi.number().required()).single(false); 1275 | const example = ValueGenerator(schema); 1276 | expect(example[0]).to.be.a.number(); 1277 | ExpectValidation(example, schema); 1278 | }); 1279 | 1280 | it('should return a default array', () => { 1281 | 1282 | const schema = Joi.array().default([1, 2, 3]); 1283 | const example = ValueGenerator(schema); 1284 | expect(example[0]).to.be.a.number(); 1285 | ExpectValidation(example, schema); 1286 | }); 1287 | }); 1288 | 1289 | describe('Alternatives', () => { 1290 | 1291 | it('should return one of the "try" schemas', () => { 1292 | 1293 | const schema = Joi.alternatives() 1294 | .try(Joi.string(), Joi.number()); 1295 | const example = ValueGenerator(schema); 1296 | 1297 | expect(example).to.not.be.undefined(); 1298 | ExpectValidation(example, schema); 1299 | }); 1300 | 1301 | it('should return the single "try" schema', () => { 1302 | 1303 | const schema = Joi.alternatives() 1304 | .try(Joi.string()); 1305 | const example = ValueGenerator(schema); 1306 | 1307 | expect(example).to.be.a.string(); 1308 | ExpectValidation(example, schema); 1309 | }); 1310 | 1311 | it('should return the single "try" object schema', () => { 1312 | 1313 | const schema = Joi.alternatives() 1314 | .try(Joi.object().keys({ a: Joi.string() })); 1315 | const example = ValueGenerator(schema); 1316 | 1317 | expect(example).to.be.an.object(); 1318 | ExpectValidation(example, schema); 1319 | }); 1320 | 1321 | it('should return the single "try" object schema with additional key constraints', () => { 1322 | 1323 | const schema = Joi.alternatives() 1324 | .try(Joi.object().keys({ 1325 | a: Joi.string().lowercase(), 1326 | b: Joi.string().guid(), 1327 | c: Joi.string().regex(/a{3}b{3}c{3}/), 1328 | d: Joi.object().keys({ 1329 | e: Joi.string().alphanum().uppercase() 1330 | }) 1331 | })); 1332 | const example = ValueGenerator(schema); 1333 | 1334 | expect(example).to.be.an.object(); 1335 | ExpectValidation(example, schema); 1336 | }); 1337 | 1338 | it('should return "conditional" alternative', () => { 1339 | 1340 | const schema = Joi.object().keys({ 1341 | dependent: Joi.alternatives().conditional('sibling.driver', { 1342 | is: Joi.string(), 1343 | then: Joi.string().lowercase() 1344 | }), 1345 | sibling: Joi.object().keys({ 1346 | driver: Joi.string() 1347 | }) 1348 | }); 1349 | const example = ValueGenerator(schema); 1350 | 1351 | expect(example.dependent).to.be.a.string(); 1352 | ExpectValidation(example, schema); 1353 | }); 1354 | 1355 | it('should return "when.otherwise" alternative', () => { 1356 | 1357 | const schema = Joi.object().keys({ 1358 | dependent: Joi.alternatives().conditional('sibling.driver', { 1359 | is: Joi.string(), 1360 | then: Joi.string(), 1361 | otherwise: Joi.number().integer() 1362 | }), 1363 | sibling: Joi.object().keys({ 1364 | driver: Joi.boolean() 1365 | }) 1366 | }); 1367 | const example = ValueGenerator(schema); 1368 | 1369 | expect(example.dependent).to.be.a.number(); 1370 | ExpectValidation(example, schema); 1371 | }); 1372 | 1373 | it('should return the base value when "when.otherwise" is undefined', () => { 1374 | 1375 | const schema = Joi.object().keys({ 1376 | dependent: Joi.string().when('sibling.driver', { 1377 | is: Joi.exist(), 1378 | then: Joi.string().guid() 1379 | }), 1380 | sibling: Joi.object() 1381 | }); 1382 | const example = ValueGenerator(schema); 1383 | 1384 | expect(example.dependent).to.be.a.string(); 1385 | ExpectValidation(example, schema); 1386 | }); 1387 | }); 1388 | 1389 | describe('Object', () => { 1390 | 1391 | it('should return an object', () => { 1392 | 1393 | const schema = Joi.object(); 1394 | const example = ValueGenerator(schema); 1395 | 1396 | expect(example).to.be.an.object(); 1397 | ExpectValidation(example, schema); 1398 | }); 1399 | 1400 | it('should return an object with specified keys', () => { 1401 | 1402 | const schema = Joi.object().keys({ 1403 | string: Joi.string().required(), 1404 | number: Joi.number().required(), 1405 | boolean: Joi.bool().required(), 1406 | time: Joi.date().required(), 1407 | buffer: Joi.binary().required(), 1408 | array: Joi.array().items(Joi.string().required()).required(), 1409 | innerObj: Joi.object().keys({ 1410 | innerString: Joi.string().required() 1411 | }).required() 1412 | }); 1413 | const example = ValueGenerator(schema); 1414 | 1415 | expect(example).to.be.an.object(); 1416 | expect(example.string).to.be.a.string(); 1417 | expect(example.number).to.be.a.number(); 1418 | expect(example.boolean).to.be.a.boolean(); 1419 | expect(example.time).to.be.a.date(); 1420 | expect(example.buffer).to.be.a.buffer(); 1421 | expect(example.array).to.be.an.array(); 1422 | expect(example.innerObj).to.be.an.object(); 1423 | expect(example.innerObj.innerString).to.be.a.string(); 1424 | ExpectValidation(example, schema); 1425 | }); 1426 | 1427 | it('should return an object with min number of keys', () => { 1428 | 1429 | const schema = Joi.object().keys({ 1430 | child1: Joi.string() 1431 | }).min(1).options({ allowUnknown: true }); 1432 | const example = ValueGenerator(schema); 1433 | 1434 | expect(Object.keys(example).length).to.be.at.least(1); 1435 | ExpectValidation(example, schema); 1436 | }); 1437 | 1438 | it('should not get stuck on static key pattern generation', () => { 1439 | 1440 | const schema = Joi.object().pattern(/abc/, Joi.string()).min(5).options({ allowUnknown: true }); 1441 | const example = ValueGenerator(schema); 1442 | 1443 | expect(Object.keys(example).length).to.be.at.least(5); 1444 | ExpectValidation(example, schema); 1445 | }); 1446 | 1447 | it('should return an object with max number of keys', () => { 1448 | 1449 | const schema = Joi.object().max(5); 1450 | const example = ValueGenerator(schema); 1451 | 1452 | expect(Object.keys(example).length).to.be.at.most(5).and.at.least(1); 1453 | ExpectValidation(example, schema); 1454 | }); 1455 | 1456 | it('should return an object with max number of keys that are typed correctly', () => { 1457 | 1458 | const schema = Joi.object().keys({ 1459 | prop: Joi.string(), 1460 | prop2: Joi.number() 1461 | }).max(2); 1462 | const example = ValueGenerator(schema); 1463 | 1464 | expect(Object.keys(example).length).to.be.at.most(2).and.at.least(1); 1465 | expect(example.prop).to.be.a.string(); 1466 | expect(example.prop2).to.be.a.number(); 1467 | ExpectValidation(example, schema); 1468 | }); 1469 | 1470 | it('should return an object with exact number of keys', () => { 1471 | 1472 | const schema = Joi.object().length(5); 1473 | const example = ValueGenerator(schema); 1474 | 1475 | expect(Object.keys(example).length).to.equal(5); 1476 | ExpectValidation(example, schema); 1477 | }); 1478 | 1479 | it('should return an object with keys that match the given pattern', () => { 1480 | 1481 | const schema = Joi.object().pattern(/^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$/, Joi.object().keys({ 1482 | id: Joi.string().guid().required(), 1483 | tags: Joi.array().items(Joi.string()).required() 1484 | })).min(2); 1485 | const example = ValueGenerator(schema); 1486 | 1487 | expect(Object.keys(example).length).to.be.at.least(2); 1488 | ExpectValidation(example, schema); 1489 | }); 1490 | 1491 | it('should return an object with one of two "nand" keys', () => { 1492 | 1493 | const schema = Joi.object() 1494 | .keys({ 1495 | a: Joi.string(), 1496 | b: Joi.string(), 1497 | c: Joi.string() 1498 | }) 1499 | .nand('a', 'b'); 1500 | const example = ValueGenerator(schema); 1501 | 1502 | ExpectValidation(example, schema); 1503 | }); 1504 | 1505 | it('should return an object with one of two "xor" keys', () => { 1506 | 1507 | const schema = Joi.object() 1508 | .keys({ 1509 | a: Joi.string(), 1510 | b: Joi.string(), 1511 | c: Joi.string() 1512 | }) 1513 | .xor('a', 'b'); 1514 | const example = ValueGenerator(schema); 1515 | 1516 | ExpectValidation(example, schema); 1517 | }); 1518 | 1519 | it('should return an ojbect with at least a key or peer', () => { 1520 | 1521 | const schema = Joi.object().keys({ password: Joi.string() }).with('username', 'password'); 1522 | const example = ValueGenerator(schema); 1523 | 1524 | ExpectValidation(example, schema); 1525 | }); 1526 | 1527 | it('should return an object with alternatives', () => { 1528 | 1529 | for (let i = 0; i < 10; ++i) { 1530 | const schema = Joi.object().keys({ 1531 | dependent: Joi.alternatives().conditional('sibling.driver', { 1532 | is: true, 1533 | then: Joi.string().guid(), 1534 | otherwise: Joi.number().multiple(4).min(16) 1535 | }), 1536 | unrelated: Joi.number(), 1537 | sibling: Joi.object().keys({ 1538 | driver: Joi.boolean() 1539 | }) 1540 | }); 1541 | const example = ValueGenerator(schema); 1542 | 1543 | ExpectValidation(example, schema); 1544 | } 1545 | }); 1546 | 1547 | it('should return an object with array-syntax alternatives', () => { 1548 | 1549 | const schema = Joi.object().keys({ 1550 | access_token: [Joi.string(), Joi.number()], 1551 | birthyear: Joi.number().integer().min(1900).max(2013) 1552 | }); 1553 | const example = ValueGenerator(schema); 1554 | 1555 | ExpectValidation(example, schema); 1556 | }); 1557 | 1558 | it('should return an object without key set as Any.forbidden()', () => { 1559 | 1560 | const schema = Joi.object().keys({ 1561 | allowed: Joi.any(), 1562 | forbidden: Joi.any().forbidden(), 1563 | forbidStr: Joi.string().forbidden(), 1564 | forbidNum: Joi.number().forbidden() 1565 | }); 1566 | const example = ValueGenerator(schema); 1567 | 1568 | expect(example.forbidden).to.be.undefined(); 1569 | expect(example.forbidStr).to.be.undefined(); 1570 | expect(example.forbidNum).to.be.undefined(); 1571 | ExpectValidation(example, schema); 1572 | }); 1573 | 1574 | it('should return an object without key set as Any.strip()', () => { 1575 | 1576 | const schema = Joi.object().keys({ 1577 | allowed: Joi.any(), 1578 | private: Joi.any().strip(), 1579 | privateStr: Joi.string().strip(), 1580 | privateNum: Joi.number().strip() 1581 | }); 1582 | const example = ValueGenerator(schema); 1583 | 1584 | expect(example.private).to.be.undefined(); 1585 | expect(example.privateStr).to.be.undefined(); 1586 | expect(example.privateNum).to.be.undefined(); 1587 | ExpectValidation(example, schema); 1588 | }); 1589 | 1590 | it('should return an object with single rename() invocation', () => { 1591 | 1592 | const schema = Joi.object().keys({ 1593 | b: Joi.number() 1594 | }).rename('a','b', { ignoreUndefined: true }); 1595 | const example = ValueGenerator(schema); 1596 | 1597 | expect(example.b).to.be.a.number(); 1598 | ExpectValidation(example, schema); 1599 | }); 1600 | 1601 | it('should return an object with double rename() invocation', () => { 1602 | 1603 | const schema = Joi.object().keys({ 1604 | b: Joi.number() 1605 | }).rename('a','b', { ignoreUndefined: true }).rename('c','b', { multiple: true, ignoreUndefined: true }); 1606 | const example = ValueGenerator(schema); 1607 | 1608 | expect(example.b).to.be.a.number(); 1609 | ExpectValidation(example, schema); 1610 | }); 1611 | 1612 | it('should return a schema object with schema() invocation', () => { 1613 | 1614 | const schema = Joi.object().schema(); 1615 | const example = ValueGenerator(schema); 1616 | 1617 | ExpectValidation(example, schema); 1618 | }); 1619 | 1620 | it('should return an object of type Regex', () => { 1621 | 1622 | const schema = Joi.object().instance(RegExp); 1623 | const example = ValueGenerator(schema); 1624 | 1625 | ExpectValidation(example, schema); 1626 | }); 1627 | 1628 | it('should return an object of type Error', () => { 1629 | 1630 | const schema = Joi.object().instance(Error); 1631 | const example = ValueGenerator(schema); 1632 | 1633 | ExpectValidation(example, schema); 1634 | }); 1635 | 1636 | it('should return an object of custom type', () => { 1637 | 1638 | const Class1 = function () {}; 1639 | Class1.prototype.testFunc = function () {}; 1640 | 1641 | const schema = Joi.object().instance(Class1); 1642 | const example = ValueGenerator(schema); 1643 | 1644 | expect(example.testFunc).to.be.a.function(); 1645 | ExpectValidation(example, schema); 1646 | }); 1647 | 1648 | it('should return a default object', () => { 1649 | 1650 | const schema = Joi.object().default({ a: 1 }); 1651 | const example = ValueGenerator(schema); 1652 | 1653 | expect(example.a).to.be.a.number(); 1654 | ExpectValidation(example, schema); 1655 | }); 1656 | }); 1657 | 1658 | describe('Extensions', () => { 1659 | 1660 | it('should fall back to baseType of string', () => { 1661 | 1662 | const customJoi = Joi.extend({ 1663 | type: 'myType' 1664 | }); 1665 | 1666 | const schema = customJoi.myType(); 1667 | const example = ValueGenerator(schema); 1668 | 1669 | expect(example).to.be.a.string(); 1670 | ExpectValidation(example, schema); 1671 | }); 1672 | 1673 | it('should fall back to baseType of number when possible', () => { 1674 | 1675 | const customJoi = Joi.extend({ 1676 | type: 'myNumber', 1677 | base: Joi.number() 1678 | }); 1679 | 1680 | const schema = customJoi.myNumber(); 1681 | const example = ValueGenerator(schema); 1682 | 1683 | expect(example).to.be.a.number(); 1684 | ExpectValidation(example, schema); 1685 | }); 1686 | 1687 | it('should fall back to baseType of boolean when possible', () => { 1688 | 1689 | const customJoi = Joi.extend({ 1690 | type: 'myBoolean', 1691 | base: Joi.boolean() 1692 | }); 1693 | 1694 | const schema = customJoi.myBoolean(); 1695 | const example = ValueGenerator(schema); 1696 | 1697 | expect(example).to.be.a.boolean(); 1698 | ExpectValidation(example, schema); 1699 | }); 1700 | 1701 | it('should fall back to baseType of array when possible', () => { 1702 | 1703 | const customJoi = Joi.extend({ 1704 | type: 'myArray', 1705 | base: Joi.array() 1706 | }); 1707 | 1708 | const schema = customJoi.myArray(); 1709 | const example = ValueGenerator(schema); 1710 | 1711 | expect(example).to.be.an.array(); 1712 | ExpectValidation(example, schema); 1713 | }); 1714 | 1715 | it('should fall back to baseType of func when possible', () => { 1716 | 1717 | const customJoi = Joi.extend({ 1718 | type: 'myFunc', 1719 | base: Joi.func() 1720 | }); 1721 | 1722 | const schema = customJoi.myFunc(); 1723 | const example = ValueGenerator(schema); 1724 | 1725 | expect(example).to.be.a.function(); 1726 | ExpectValidation(example, schema); 1727 | }); 1728 | 1729 | it('should support child extensions', () => { 1730 | 1731 | const customJoi = Joi.extend({ 1732 | type: 'myType' 1733 | }); 1734 | 1735 | const schema = Joi.object().keys({ custom: customJoi.myType() }); 1736 | const example = ValueGenerator(schema); 1737 | 1738 | expect(example.custom).to.be.a.string(); 1739 | ExpectValidation(example, schema); 1740 | }); 1741 | 1742 | it('should support extensions in arrays', () => { 1743 | 1744 | const customJoi = Joi.extend({ 1745 | type: 'myType' 1746 | }); 1747 | 1748 | const schema = Joi.array().items( customJoi.myType() ); 1749 | const example = ValueGenerator(schema); 1750 | 1751 | expect(example[0]).to.be.a.string(); 1752 | ExpectValidation(example, schema); 1753 | }); 1754 | 1755 | it('should support extensions in alternatives', () => { 1756 | 1757 | const customJoi = Joi.extend({ 1758 | type: 'myType' 1759 | }); 1760 | 1761 | const schema = Joi.object().keys({ 1762 | driver: Joi.any(), 1763 | child: Joi.alternatives().conditional('driver', { 1764 | is: Joi.string(), 1765 | then: customJoi.myType() 1766 | }) 1767 | }); 1768 | const example = ValueGenerator(schema); 1769 | 1770 | expect(example.child).to.be.a.string(); 1771 | ExpectValidation(example, schema); 1772 | }); 1773 | }); 1774 | -------------------------------------------------------------------------------- /test/felicity_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Felicity = require('../lib'); 4 | const Joi = require('../lib/joi'); 5 | const Lab = require('@hapi/lab'); 6 | const Uuid = require('uuid'); 7 | const Moment = require('moment'); 8 | 9 | const { describe, expect, it } = exports.lab = Lab.script(); 10 | const ExpectValidation = require('./test_helpers').expectValidation.bind({}, expect); 11 | 12 | const generateUuid = (parent, helpers) => Uuid.v4(); 13 | generateUuid.description = 'Generates UUIDs'; 14 | 15 | describe('Felicity Example', () => { 16 | 17 | it('should return a string', () => { 18 | 19 | const schema = Joi.string(); 20 | const example = Felicity.example(schema); 21 | 22 | expect(example).to.be.a.string(); 23 | ExpectValidation(example, schema); 24 | }); 25 | 26 | it('should return a string that ignores regex lookarounds', () => { 27 | 28 | const lookAheadPattern = /abcd(?=efg)/; 29 | const schema = Joi.string().regex(lookAheadPattern); 30 | const example = Felicity.example(schema); 31 | expect(example).to.equal('abcd'); 32 | expect(example.match(lookAheadPattern)).to.equal(null); 33 | }); 34 | 35 | it('should throw validation error on regex lookarounds when provided strictExample config', () => { 36 | 37 | const schema = Joi.string().regex(/abcd(?=efg)/); 38 | const options = { 39 | config: { 40 | strictExample: true 41 | } 42 | }; 43 | const callExample = function () { 44 | 45 | return Felicity.example(schema, options); 46 | }; 47 | 48 | expect(callExample).to.throw('\"value\" with value \"abcd\" fails to match the required pattern: /abcd(?=efg)/'); 49 | }); 50 | 51 | it('should not throw validation error on supported regex when provided strictExample config', () => { 52 | 53 | const schema = Joi.string().regex(/abcd/); 54 | const options = { 55 | config: { 56 | strictExample: true 57 | } 58 | }; 59 | const callExample = function () { 60 | 61 | return Felicity.example(schema, options); 62 | }; 63 | 64 | expect(callExample).to.not.throw(); 65 | ExpectValidation(callExample(), schema); 66 | }); 67 | 68 | it('should return a number', () => { 69 | 70 | const schema = Joi.number(); 71 | const example = Felicity.example(schema); 72 | 73 | expect(example).to.be.a.number(); 74 | ExpectValidation(example, schema); 75 | }); 76 | 77 | it('should return a boolean', () => { 78 | 79 | const schema = Joi.boolean(); 80 | const example = Felicity.example(schema); 81 | 82 | expect(example).to.be.a.boolean(); 83 | ExpectValidation(example, schema); 84 | }); 85 | 86 | it('should return a buffer', () => { 87 | 88 | const schema = Joi.binary(); 89 | const example = Felicity.example(schema); 90 | 91 | expect(example).to.be.a.buffer(); 92 | ExpectValidation(example, schema); 93 | }); 94 | 95 | it('should return a date', () => { 96 | 97 | const schema = Joi.date(); 98 | const example = Felicity.example(schema); 99 | 100 | expect(example).to.be.a.date(); 101 | ExpectValidation(example, schema); 102 | }); 103 | 104 | it('should return a moment formatted date', () => { 105 | 106 | const fmt = 'HH:mm'; 107 | const schema = Joi.date().format(fmt); 108 | const example = Felicity.example(schema); 109 | const moment = new Moment(example, fmt, true); 110 | 111 | expect(example).to.be.a.string(); 112 | expect(moment.isValid()).to.equal(true); 113 | ExpectValidation(example, schema); 114 | }); 115 | 116 | it('should return a function', () => { 117 | 118 | const schema = Joi.func(); 119 | const example = Felicity.example(schema); 120 | 121 | expect(example).to.be.a.function(); 122 | ExpectValidation(example, schema); 123 | }); 124 | 125 | it('should return function keys', (done) => { 126 | 127 | const schema = Joi.object().keys({ 128 | func: Joi.func() 129 | }); 130 | const example = Felicity.example(schema); 131 | 132 | expect(example.func).to.be.a.function(); 133 | ExpectValidation(example, schema, done); 134 | }); 135 | 136 | describe('Array', () => { 137 | 138 | it('should return an array', () => { 139 | 140 | const schema = Joi.array(); 141 | const example = Felicity.example(schema); 142 | 143 | expect(example).to.be.an.array(); 144 | ExpectValidation(example, schema); 145 | }); 146 | 147 | describe('items', () => { 148 | 149 | it('should return optional items when includeOptional is set to true', () => { 150 | 151 | const schema = Joi.array().items(Joi.string().optional()); 152 | const example = Felicity.example(schema); 153 | 154 | expect(example).to.be.an.array(); 155 | expect(example[0]).to.be.a.string(); 156 | ExpectValidation(example, schema); 157 | }); 158 | 159 | it('should return objects with optional keys when includeOptional is set to true ', () => { 160 | 161 | const schema = Joi.array().items(Joi.object().keys({ 162 | key1: Joi.string(), 163 | key2: Joi.string().optional() 164 | })); 165 | const options = { 166 | config: { 167 | includeOptional: true 168 | } 169 | }; 170 | const example = Felicity.example(schema, options); 171 | 172 | expect(example).to.be.an.array(); 173 | expect(example[0].key1).to.exist(); 174 | expect(example[0].key2).to.exist(); 175 | ExpectValidation(example, schema); 176 | }); 177 | 178 | it('should return ordered object items with optional keys when includeOptional is set to true ', () => { 179 | 180 | const schema = Joi.array().ordered(Joi.object().keys({ 181 | key1: Joi.string(), 182 | key2: Joi.string().optional() 183 | })); 184 | const options = { 185 | config: { 186 | includeOptional: true 187 | } 188 | }; 189 | const example = Felicity.example(schema, options); 190 | 191 | expect(example).to.be.an.array(); 192 | expect(example[0].key1).to.exist(); 193 | expect(example[0].key2).to.exist(); 194 | ExpectValidation(example, schema); 195 | }); 196 | }); 197 | 198 | }); 199 | 200 | it('should return an object with default values', () => { 201 | 202 | const schema = Joi.object().keys({ 203 | string: Joi.string().required().default('-----'), 204 | number: Joi.number().default(4) 205 | }); 206 | const example = Felicity.example(schema); 207 | expect(example.string).to.equal('-----'); 208 | expect(example.number).to.equal(4); 209 | ExpectValidation(example, schema); 210 | }); 211 | 212 | it('should return an object with null default', () => { 213 | 214 | const schema = Joi.object().keys({ 215 | string: Joi.string().allow(null).default(null) 216 | }); 217 | const example = Felicity.example(schema, { config: { ignoreValids: true } }); 218 | 219 | expect(example.string).to.equal(null); 220 | ExpectValidation(example, schema); 221 | }); 222 | 223 | it('should return an object with dynamic defaults', () => { 224 | 225 | const generateDefaultString = () => { 226 | 227 | return '-----'; 228 | }; 229 | 230 | generateDefaultString.description = 'generates default'; 231 | const generateDefaultNumber = () => { 232 | 233 | return 4; 234 | }; 235 | 236 | generateDefaultNumber.description = 'generates default'; 237 | const generateDefaultBool = () => { 238 | 239 | return true; 240 | }; 241 | 242 | generateDefaultBool.description = 'generates default'; 243 | const schema = Joi.object().keys({ 244 | string: Joi.string().required().default(generateDefaultString), 245 | number: Joi.number().default(generateDefaultNumber), 246 | bool: Joi.boolean().default(generateDefaultBool) 247 | }); 248 | const example = Felicity.example(schema); 249 | 250 | expect(example.string).to.equal('-----'); 251 | expect(example.number).to.equal(4); 252 | expect(example.bool).to.equal(true); 253 | ExpectValidation(example, schema); 254 | }); 255 | 256 | it('should return an object with specified valid properties', () => { 257 | 258 | const schema = Joi.object().keys({ 259 | string: Joi.string().required().valid('-----'), 260 | number: Joi.number().valid(4) 261 | }); 262 | const example = Felicity.example(schema); 263 | 264 | expect(example.string).to.equal('-----'); 265 | expect(example.number).to.equal(4); 266 | ExpectValidation(example, schema); 267 | }); 268 | 269 | it('should return an object with "allowed" values', () => { 270 | 271 | const schema = Joi.object().keys({ 272 | string: Joi.string().allow(null).required() 273 | }); 274 | const example = Felicity.example(schema); 275 | 276 | expect(example.string).to.equal(null); 277 | ExpectValidation(example, schema); 278 | }); 279 | 280 | it('should ignore "allowed" values when provided "ignoreValids" config', () => { 281 | 282 | const schema = Joi.object().keys({ 283 | string: Joi.string().allow(null).required() 284 | }); 285 | const options = { 286 | config: { 287 | ignoreValids: true 288 | } 289 | }; 290 | const example = Felicity.example(schema, options); 291 | 292 | expect(example.string).to.not.equal(null); 293 | expect(example.string).to.be.a.string(); 294 | ExpectValidation(example, schema); 295 | }); 296 | 297 | it('should return an object with custom type', () => { 298 | 299 | const Class1 = function () {}; 300 | Class1.prototype.testFunc = function () {}; 301 | 302 | const schema = Joi.object().instance(Class1); 303 | const example = Felicity.example(schema); 304 | 305 | expect(example).to.be.an.instanceof(Class1); 306 | expect(example.testFunc).to.be.a.function(); 307 | ExpectValidation(example, schema); 308 | }); 309 | 310 | it('should not return an object with default values when provided ignoreDefaults config', () => { 311 | 312 | const schema = Joi.object().keys({ 313 | string: Joi.string().alphanum().required().default('-----'), 314 | number: Joi.number().default(4) 315 | }); 316 | const options = { 317 | config: { 318 | ignoreDefaults: true 319 | } 320 | }; 321 | const example = Felicity.example(schema, options); 322 | 323 | expect(example.string).to.not.equal('-----'); 324 | expect(example.number).to.not.equal(4); 325 | ExpectValidation(example, schema); 326 | }); 327 | 328 | it('should return an object without optional keys', () => { 329 | 330 | const schema = Joi.object().keys({ 331 | required: Joi.string().required(), 332 | present: Joi.string(), 333 | optional: Joi.string().optional() 334 | }); 335 | const example = Felicity.example(schema); 336 | 337 | expect(example.required).to.be.a.string(); 338 | expect(example.present).to.be.a.string(); 339 | expect(example.optional).to.be.undefined(); 340 | ExpectValidation(example, schema); 341 | }); 342 | 343 | it('should return an object without optional keys when using .options({ presence: "optional" }) syntax', () => { 344 | 345 | const schema = Joi.object().keys({ 346 | required: Joi.string().required(), 347 | parentOptional: Joi.string(), 348 | optional: Joi.string().optional() 349 | }).options({ presence: 'optional' }); 350 | const example = Felicity.example(schema); 351 | 352 | expect(example.required).to.be.a.string(); 353 | expect(example.parentOptional).to.be.undefined(); 354 | expect(example.optional).to.be.undefined(); 355 | ExpectValidation(example, schema); 356 | }); 357 | 358 | it('should return an object with optional keys when given includeOptional config', () => { 359 | 360 | const schema = Joi.object().keys({ 361 | required: Joi.string().required(), 362 | present: Joi.string(), 363 | optional: Joi.string().optional() 364 | }); 365 | const options = { 366 | config: { 367 | includeOptional: true 368 | } 369 | }; 370 | const example = Felicity.example(schema, options); 371 | 372 | expect(example.required).to.be.a.string(); 373 | expect(example.present).to.be.a.string(); 374 | expect(example.optional).to.be.a.string(); 375 | ExpectValidation(example, schema); 376 | }); 377 | 378 | it('should return the Joi facebook example', () => { 379 | 380 | const passwordPattern = /^[a-zA-Z0-9]{3,30}$/; 381 | const schema = Joi.object().keys({ 382 | username: Joi.string().alphanum().min(3).max(30).insensitive().required(), 383 | password: Joi.string().regex(passwordPattern), 384 | access_token: [Joi.string(), Joi.number()], 385 | birthyear: Joi.number().integer().min(1900).max(2013), 386 | email: Joi.string().email() 387 | }).with('username', 'birthyear').without('password', 'access_token'); 388 | const example = Felicity.example(schema); 389 | 390 | expect(example.password.match(passwordPattern)).to.not.equal(null); 391 | ExpectValidation(example, schema); 392 | }); 393 | 394 | it('should return the Joi facebook example with an optional key', () => { 395 | 396 | const passwordPattern = /^[a-zA-Z0-9]{3,30}$/; 397 | const schema = Joi.object().keys({ 398 | username: Joi.string().alphanum().min(3).max(30).required(), 399 | password: Joi.string().regex(passwordPattern), 400 | access_token: [Joi.string(), Joi.number()], 401 | birthyear: Joi.number().integer().min(1900).max(2013).optional(), 402 | email: Joi.string().email() 403 | }).with('username', 'birthyear').without('password', 'access_token'); 404 | const example = Felicity.example(schema); 405 | 406 | expect(example.password.match(passwordPattern)).to.not.equal(null); 407 | expect(example.birthyear).to.be.a.number(); 408 | ExpectValidation(example, schema); 409 | }); 410 | }); 411 | 412 | describe('Felicity EntityFor', () => { 413 | 414 | it('should fail when calling without proper schema', () => { 415 | 416 | expect(Felicity.entityFor).to.throw(Error, 'You must provide a Joi schema'); 417 | }); 418 | 419 | it('should return a constructor function', () => { 420 | 421 | const schema = {}; 422 | const Constructor = Felicity.entityFor(schema); 423 | 424 | expect(Constructor).to.be.a.function(); 425 | 426 | const skeleton = new Constructor(); 427 | 428 | expect(skeleton).to.be.an.object(); 429 | }); 430 | 431 | it('should enforce "new" instantiation on returned Constructor', () => { 432 | 433 | const schema = {}; 434 | const Constructor = Felicity.entityFor(schema); 435 | 436 | expect(Constructor).to.be.a.function(); 437 | 438 | expect(() => { 439 | 440 | return Constructor(); 441 | }).to.throw(TypeError); 442 | }); 443 | 444 | it('should error on non-object schema', () => { 445 | 446 | const numberSchema = Joi.number().max(1); 447 | const entityFor = function () { 448 | 449 | return Felicity.entityFor(numberSchema); 450 | }; 451 | 452 | expect(entityFor).to.throw(Error, 'Joi schema must describe an object for constructor functions'); 453 | }); 454 | 455 | it('should provide an example with dynamic defaults', () => { 456 | 457 | const generateDynamic = () => 'dynamic default'; 458 | generateDynamic.description = 'generates a default'; 459 | const schema = Joi.object().keys({ 460 | version: Joi.string().min(5).default('1.0.0'), 461 | number: Joi.number().default(10), 462 | identity: Joi.object().keys({ 463 | id: Joi.string().guid().default(generateUuid) 464 | }), 465 | array: Joi.array().items(Joi.object().keys({ 466 | id: Joi.string().guid().default(generateUuid) 467 | })), 468 | condition: Joi.alternatives().conditional('version', { 469 | is: Joi.string(), 470 | then: Joi.string().default('defaultValue'), 471 | otherwise: Joi.number() 472 | }), 473 | dynamicCondition: Joi.alternatives().conditional('version', { 474 | is: Joi.string(), 475 | then: Joi.string().default(generateDynamic), 476 | otherwise: Joi.number() 477 | }) 478 | }); 479 | const Entity = Felicity.entityFor(schema); 480 | const example = Entity.example(); 481 | 482 | expect(example.version).to.equal('1.0.0'); 483 | expect(example.number).to.equal(10); 484 | expect(example.array.length).to.be.above(0); 485 | expect(example.array[0].id).to.be.a.string(); 486 | expect(example.identity.id).to.be.a.string(); 487 | expect(example.condition).to.equal('defaultValue'); 488 | expect(example.dynamicCondition).to.equal('dynamic default'); 489 | }); 490 | 491 | describe('Constructor functions', () => { 492 | 493 | it('should accept override options', () => { 494 | 495 | const defaultOptions = { 496 | includeOptional: false, 497 | ignoreDefaults: true 498 | }; 499 | const schema = Joi.object().keys({ 500 | version: Joi.string().optional(), 501 | name: Joi.string().default('default value') 502 | }); 503 | const Entity = Felicity.entityFor(schema, { config: defaultOptions }); 504 | const instance = new Entity(); 505 | const instanceWithOptional = new Entity(null, { includeOptional: true }); 506 | const instanceWithDefault = new Entity(null, { ignoreDefaults: false }); 507 | const instanceWithBothOptions = new Entity(null, { includeOptional: true, ignoreDefaults: false }); 508 | 509 | expect(instance.version).to.equal(undefined); 510 | expect(instance.name).to.equal(null); 511 | 512 | expect(instanceWithOptional.version).to.equal(null); 513 | expect(instanceWithOptional.name).to.equal(null); 514 | 515 | expect(instanceWithDefault.version).to.equal(undefined); 516 | expect(instanceWithDefault.name).to.equal('default value'); 517 | 518 | expect(instanceWithBothOptions.version).to.equal(null); 519 | expect(instanceWithBothOptions.name).to.equal('default value'); 520 | }); 521 | 522 | it('should accept options', () => { 523 | 524 | const schema = Joi.object().keys({ 525 | version: Joi.string().optional(), 526 | name: Joi.string().default('default value') 527 | }); 528 | const Entity = Felicity.entityFor(schema); 529 | const instance = new Entity(); 530 | const instanceWithOptional = new Entity(null, { includeOptional: true }); 531 | const instanceWithDefault = new Entity(null, { ignoreDefaults: true }); 532 | const instanceWithBothOptions = new Entity(null, { includeOptional: true, ignoreDefaults: true }); 533 | 534 | expect(instance.version).to.equal(undefined); 535 | expect(instance.name).to.equal('default value'); 536 | 537 | expect(instanceWithOptional.version).to.equal(null); 538 | expect(instanceWithOptional.name).to.equal('default value'); 539 | 540 | expect(instanceWithDefault.version).to.equal(undefined); 541 | expect(instanceWithDefault.name).to.equal(null); 542 | 543 | expect(instanceWithBothOptions.version).to.equal(null); 544 | expect(instanceWithBothOptions.name).to.equal(null); 545 | }); 546 | 547 | it('should validate input when given validateInput: true', () => { 548 | 549 | const subSchema = Joi.object().keys({ 550 | name: Joi.string(), 551 | title: Joi.string() 552 | }).options({ 553 | presence: 'required' 554 | }); 555 | const schema = Joi.object().keys({ 556 | title: Joi.string(), 557 | director: Joi.number(), 558 | producers: Joi.array().items(subSchema).allow(null, '').optional().default(null) 559 | }); 560 | const Constructor = Felicity.entityFor(schema); 561 | const input = { 562 | name: 'Blade Runner', 563 | director: 'Denis Villeneuve', 564 | writers: [ 565 | { 566 | name: 'Hampton Fancher' 567 | } 568 | ] 569 | }; 570 | 571 | const instance = new Constructor(input); 572 | expect(instance.title).to.equal(null); 573 | expect(instance.producers).to.equal(null); 574 | expect(instance.director).to.equal(input.director); 575 | expect(() => new Constructor(input, { validateInput: true })).to.throw(`"director" must be a number. "name" is not allowed. "writers" is not allowed`); 576 | }); 577 | 578 | it('should validate and assign input when validateInput is true and input passes validation', () => { 579 | 580 | const schema = Joi.object().keys({ 581 | title: Joi.string(), 582 | director: Joi.string() 583 | }); 584 | const Constructor = Felicity.entityFor(schema); 585 | const input = { 586 | title: 'American Beauty', 587 | director: 'Sam Mendes' 588 | }; 589 | 590 | const instance = new Constructor(input, { validateInput: true }); 591 | expect(instance.title).to.equal(input.title); 592 | 593 | expect(instance.director).to.equal(input.director); 594 | }); 595 | 596 | it('should not validate input when given validateInput: false', () => { 597 | 598 | const subSchema = Joi.object().keys({ 599 | name: Joi.string(), 600 | title: Joi.string() 601 | }).options({ 602 | presence: 'required' 603 | }); 604 | const schema = Joi.object().keys({ 605 | title: Joi.string(), 606 | director: Joi.number(), 607 | producers: Joi.array().items(subSchema).allow(null, '').optional().default(null) 608 | }); 609 | const Constructor = Felicity.entityFor(schema, { config: { validateInput: true } }); 610 | const input = { 611 | name: 'Blade Runner', 612 | director: 'Denis Villeneuve', 613 | writers: [ 614 | { 615 | name: 'Hampton Fancher' 616 | } 617 | ] 618 | }; 619 | 620 | expect(() => new Constructor(input)).to.throw(`"director" must be a number. "name" is not allowed. "writers" is not allowed`); 621 | 622 | const instance = new Constructor(input, { validateInput: false }); 623 | expect(instance.title).to.equal(null); 624 | expect(instance.producers).to.equal(null); 625 | expect(instance.director).to.equal(input.director); 626 | }); 627 | }); 628 | 629 | describe('Constructor instances', () => { 630 | 631 | it('should accept override options when validating', () => { 632 | 633 | const schema = Joi.object().keys({ 634 | a: Joi.string() 635 | }); 636 | const options = { stripUnknown: true }; 637 | 638 | const Subject = Felicity.entityFor(schema); 639 | const subject = new Subject({ a: 'a' }); 640 | subject.b = 'b'; 641 | 642 | subject.validate((err) => { 643 | 644 | expect(err).to.be.null(); 645 | }, options); 646 | }); 647 | 648 | it('should return a validation object', () => { 649 | 650 | const schema = Joi.object().keys({ 651 | name: Joi.string().required() 652 | }); 653 | const Thing = Felicity.entityFor(schema); 654 | const thing = new Thing(); 655 | 656 | expect(thing.validate().success).to.exist().and.equal(false); 657 | expect(thing.validate().errors).to.exist().and.be.an.array(); 658 | expect(thing.validate().value).to.exist().and.equal({ 659 | name: null 660 | }); 661 | }); 662 | 663 | it('should follow the standard Node callback signature for .validate', () => { 664 | 665 | const schema = Joi.object().keys({ 666 | name: Joi.string().required() 667 | }); 668 | const Thing = Felicity.entityFor(schema); 669 | const thing = new Thing(); 670 | 671 | thing.validate((err, result) => { 672 | 673 | expect(err).to.exist(); 674 | expect(err).to.exist().and.be.an.array(); 675 | expect(result).to.be.null(); 676 | 677 | const validThing = new Thing({ 678 | name: 'pass' 679 | }); 680 | 681 | validThing.validate((err, validationResult) => { 682 | 683 | expect(err).to.be.null(); 684 | expect(validationResult).to.be.an.object(); 685 | expect(validationResult.success).to.exist().and.equal(true); 686 | expect(validationResult.value).to.exist().and.be.an.object(); 687 | expect(validationResult.errors).to.be.undefined(); 688 | }); 689 | }); 690 | }); 691 | 692 | it('should not trigger V8 JSON.stringify bug in Node v4.x', () => { 693 | 694 | const schema = Joi.object(); 695 | const Thing = Felicity.entityFor(schema); 696 | const thing = new Thing(); 697 | expect(JSON.stringify(thing, null, null)).to.equal('{}'); 698 | }); 699 | }); 700 | 701 | describe('"Action" schema options', () => { 702 | 703 | it('should not interfere with String.truncate', () => { 704 | 705 | const schema = Joi.object().keys({ 706 | name: Joi.string().max(5).truncate() 707 | }); 708 | const Constructor = Felicity.entityFor(schema); 709 | const instance = new Constructor(); 710 | const example = instance.example(); 711 | const validation = Constructor.validate({ 712 | name: 'longer than 5 chars' 713 | }); 714 | 715 | expect(instance.name).to.equal(null); 716 | expect(example.name.length).to.be.at.most(5); 717 | expect(validation.errors).to.equal(undefined); 718 | expect(validation.value).to.equal({ name: 'longe' }); 719 | ExpectValidation(example, schema); 720 | }); 721 | 722 | it('should not interfere with String.replace', () => { 723 | 724 | const schema = Joi.object().keys({ 725 | name: Joi.string().replace(/b/gi, 'a') 726 | }); 727 | const Constructor = Felicity.entityFor(schema); 728 | const instance = new Constructor(); 729 | const example = instance.example(); 730 | const validation = Constructor.validate({ name: 'abbabba' }); 731 | 732 | expect(instance.name).to.equal(null); 733 | expect(validation.errors).to.equal(undefined); 734 | expect(validation.value).to.equal({ name: 'aaaaaaa' }); 735 | ExpectValidation(example, schema); 736 | }); 737 | 738 | it('should not interfere with String.trim', () => { 739 | 740 | const schema = Joi.object().keys({ 741 | name: Joi.string().trim() 742 | }); 743 | const Constructor = Felicity.entityFor(schema); 744 | const instance = new Constructor({ name: ' abbabba ' }); 745 | const example = instance.example(); 746 | const validation = instance.validate(); 747 | 748 | expect(instance.name).to.equal('abbabba'); 749 | expect(validation.errors).to.equal(undefined); 750 | expect(validation.value).to.equal({ name: 'abbabba' }); 751 | ExpectValidation(example, schema); 752 | }); 753 | }); 754 | 755 | describe('"Presence" object binary schema options', () => { 756 | 757 | it('should not interfere with and', () => { 758 | 759 | const schema = Joi.object().keys({ 760 | a: Joi.string(), 761 | b: Joi.string() 762 | }).and('a', 'b'); 763 | const Constructor = Felicity.entityFor(schema); 764 | const instance = new Constructor({ a: 'abc', b: 'xyz' }); 765 | const example = instance.example(); 766 | 767 | ExpectValidation(example, schema); 768 | }); 769 | 770 | it('should not interfere with or', () => { 771 | 772 | const schema = Joi.object().keys({ 773 | a: Joi.string(), 774 | b: Joi.string() 775 | }).or('a', 'b'); 776 | const Constructor = Felicity.entityFor(schema); 777 | const instance = new Constructor({ a: 'abc', b: 'xyz' }); 778 | const example = instance.example(); 779 | 780 | ExpectValidation(example, schema); 781 | }); 782 | }); 783 | 784 | describe('"Presence" object property check schema options', () => { 785 | 786 | it('should not interfere with unknown when set to true', () => { 787 | 788 | const schema = Joi.object().keys({ 789 | a: Joi.string(), 790 | b: Joi.string() 791 | }).unknown(true); 792 | const Constructor = Felicity.entityFor(schema); 793 | const instance = new Constructor({ a: 'abc', b: 'xyz' }); 794 | const example = instance.example(); 795 | 796 | ExpectValidation(example, schema); 797 | }); 798 | 799 | it('should not interfere with unknown when set to false', () => { 800 | 801 | const schema = Joi.object().keys({ 802 | a: Joi.string(), 803 | b: Joi.string() 804 | }).unknown(false); 805 | const Constructor = Felicity.entityFor(schema); 806 | const instance = new Constructor({ a: 'abc', b: 'xyz' }); 807 | const example = instance.example(); 808 | 809 | ExpectValidation(example, schema); 810 | }); 811 | }); 812 | 813 | describe('Conditional', () => { 814 | 815 | it('should default to the "true" driver', () => { 816 | 817 | const schema = Joi.object().keys({ 818 | driver: true, 819 | myConditional: Joi.when('driver', { 820 | is: true, 821 | then: Joi.string().required(), 822 | otherwise: Joi.number().required() 823 | }) 824 | }); 825 | const Entity = Felicity.entityFor(schema); 826 | const felicityInstance = new Entity(); 827 | 828 | expect(felicityInstance.myConditional).to.equal(null); 829 | expect(felicityInstance.validate).to.be.a.function(); 830 | }); 831 | }); 832 | 833 | describe('Object', () => { 834 | 835 | it('should return an object with no keys', () => { 836 | 837 | const schema = Joi.object().keys(); 838 | const Entity = Felicity.entityFor(schema); 839 | const felicityInstance = new Entity(); 840 | 841 | expect(felicityInstance).to.be.an.object(); 842 | expect(felicityInstance.validate).to.be.a.function(); 843 | }); 844 | 845 | it('should return an object with keys', () => { 846 | 847 | const schema = Joi.object().keys({ 848 | key1: Joi.object().keys().required() 849 | }); 850 | const Entity = Felicity.entityFor(schema); 851 | const felicityInstance = new Entity(); 852 | 853 | expect(felicityInstance.key1).to.equal({}); 854 | expect(felicityInstance.validate).to.be.a.function(); 855 | }); 856 | 857 | it('should return an object without .strip\'ed keys', () => { 858 | 859 | const schema = Joi.object().keys({ 860 | key1: Joi.object().keys().required(), 861 | key2: Joi.any().strip() 862 | }); 863 | const Entity = Felicity.entityFor(schema); 864 | const felicityInstance = new Entity(); 865 | 866 | expect(felicityInstance.key1).to.equal({}); 867 | expect(felicityInstance.key2).to.equal(undefined); 868 | expect(felicityInstance.validate).to.be.a.function(); 869 | }); 870 | 871 | 872 | 873 | it('should return an object with mixed-type keys', () => { 874 | 875 | const schema = Joi.object().keys({ 876 | innerObject: Joi.object().keys({ 877 | innerArray: Joi.array().items(Joi.number()).min(3).max(6).required(), 878 | number: Joi.number() 879 | }), 880 | string: Joi.string().email().required(), 881 | date: Joi.date().raw().required(), 882 | bool: Joi.boolean().required(), 883 | func: Joi.func().required(), 884 | conditional: Joi.alternatives().conditional('bool', { 885 | is: true, 886 | then: Joi.object().keys().required(), 887 | otherwise: Joi.boolean().required() 888 | }), 889 | any: Joi.any(), 890 | anyStrip: Joi.any().strip(), 891 | anyForbid: Joi.any().forbidden() 892 | }); 893 | const Entity = Felicity.entityFor(schema); 894 | const felicityInstance = new Entity(); 895 | 896 | expect(felicityInstance.innerObject).to.be.an.object(); 897 | expect(felicityInstance.innerObject.innerArray).to.equal([]); 898 | expect(felicityInstance.innerObject.number).to.equal(0); 899 | expect(felicityInstance.string).to.equal(null); 900 | expect(felicityInstance.date).to.equal(null); 901 | expect(felicityInstance.bool).to.equal(false); 902 | expect(felicityInstance.func).to.equal(null); 903 | expect(felicityInstance.conditional).to.equal({}); 904 | expect(felicityInstance.any).to.equal(null); 905 | expect(felicityInstance.anyStrip).to.equal(undefined); 906 | expect(felicityInstance.anyForbid).to.be.undefined(); 907 | expect(felicityInstance.validate).to.be.a.function(); 908 | 909 | const mockInstance = felicityInstance.example(); 910 | 911 | ExpectValidation(mockInstance, schema); 912 | }); 913 | 914 | it('should return an object with mixed-type keys for non-compiled schema', () => { 915 | 916 | const schema = Joi.object().keys({ 917 | innerObject: Joi.object().keys({ 918 | innerArray: Joi.array().items(Joi.number()).min(3).max(6).required(), 919 | number: Joi.number() 920 | }), 921 | string: Joi.string().email().required(), 922 | date: Joi.date().raw().required(), 923 | bool: Joi.boolean().required(), 924 | conditional: Joi.alternatives().conditional('bool', { 925 | is: true, 926 | then: Joi.object().keys().required(), 927 | otherwise: Joi.boolean().required() 928 | }), 929 | optional: Joi.string().optional(), 930 | otherCond: Joi.alternatives().conditional('bool', { 931 | is: true, 932 | then: Joi.string().required(), 933 | otherwise: Joi.boolean().required() 934 | }) 935 | }); 936 | const Entity = Felicity.entityFor(schema); 937 | const felicityInstance = new Entity(); 938 | 939 | expect(felicityInstance.innerObject).to.be.an.object(); 940 | expect(felicityInstance.innerObject.innerArray).to.equal([]); 941 | expect(felicityInstance.innerObject.number).to.equal(0); 942 | expect(felicityInstance.string).to.equal(null); 943 | expect(felicityInstance.date).to.equal(null); 944 | expect(felicityInstance.bool).to.equal(false); 945 | expect(felicityInstance.conditional).to.equal({}); 946 | expect(felicityInstance.optional).to.be.undefined(); 947 | expect(felicityInstance.otherCond).to.equal(null); 948 | expect(felicityInstance.validate).to.be.a.function(); 949 | }); 950 | 951 | it('should not include keys with "optional" flag', () => { 952 | 953 | const schema = Joi.object().keys({ 954 | key1: Joi.string().required(), 955 | key2: Joi.string(), 956 | key3: Joi.string().optional() 957 | }); 958 | const Entity = Felicity.entityFor(schema); 959 | const felicityInstance = new Entity(); 960 | 961 | expect(felicityInstance.key1).to.equal(null); 962 | expect(felicityInstance.key2).to.equal(null); 963 | expect(felicityInstance.key3).to.not.exist(); 964 | expect(felicityInstance.validate).to.be.a.function(); 965 | }); 966 | 967 | it('should not include keys with "optional" flag when using .options({ presence: "optional" }) syntax', () => { 968 | 969 | const schema = Joi.object().keys({ 970 | key1: Joi.string().required(), 971 | key2: Joi.string(), 972 | key3: Joi.string().optional(), 973 | key4: Joi.object().keys({ 974 | a: Joi.string() 975 | }).options({ presence: 'optional' }) 976 | }).options({ presence: 'optional' }); 977 | const Entity = Felicity.entityFor(schema); 978 | const felicityInstance = new Entity(); 979 | 980 | expect(felicityInstance.key1).to.equal(null); 981 | expect(felicityInstance.key2).to.not.exist(); 982 | expect(felicityInstance.key3).to.not.exist(); 983 | expect(felicityInstance.key4).to.not.exist(); 984 | expect(felicityInstance.validate).to.be.a.function(); 985 | }); 986 | 987 | it('should include keys with "optional" flag if provided includeOptional config', () => { 988 | 989 | const schema = Joi.object().keys({ 990 | key1: Joi.string().required(), 991 | key2: Joi.string(), 992 | key3: Joi.string().optional() 993 | }); 994 | const options = { 995 | config: { 996 | includeOptional: true 997 | } 998 | }; 999 | const Entity = Felicity.entityFor(schema, options); 1000 | const felicityInstance = new Entity(); 1001 | 1002 | expect(felicityInstance.key1).to.equal(null); 1003 | expect(felicityInstance.key2).to.equal(null); 1004 | expect(felicityInstance.key3).to.equal(null); 1005 | expect(felicityInstance.validate).to.be.a.function(); 1006 | }); 1007 | 1008 | it('should utilize default values', () => { 1009 | 1010 | const schema = Joi.object().keys({ 1011 | version: Joi.string().min(5).default('1.0.0'), 1012 | number: Joi.number().default(10), 1013 | identity: Joi.object().keys({ 1014 | id: Joi.string().default('abcdefg') 1015 | }), 1016 | condition: Joi.alternatives().conditional('version', { 1017 | is: Joi.string(), 1018 | then: Joi.string().default('defaultValue'), 1019 | otherwise: Joi.number() 1020 | }) 1021 | }); 1022 | const Entity = Felicity.entityFor(schema); 1023 | const felicityInstance = new Entity(); 1024 | 1025 | expect(felicityInstance.version).to.equal('1.0.0'); 1026 | expect(felicityInstance.number).to.equal(10); 1027 | expect(felicityInstance.identity.id).to.equal('abcdefg'); 1028 | expect(felicityInstance.condition).to.equal('defaultValue'); 1029 | }); 1030 | 1031 | it('should not utilize default values when provided ignoreDefaults config', () => { 1032 | 1033 | const schema = Joi.object().keys({ 1034 | version: Joi.string().min(5).default('1.0.0'), 1035 | number: Joi.number().default(10), 1036 | identity: Joi.object().keys({ 1037 | id: Joi.string().default('abcdefg') 1038 | }), 1039 | condition: Joi.alternatives().conditional('version', { 1040 | is: Joi.string(), 1041 | then: Joi.string().default('defaultValue'), 1042 | otherwise: Joi.number() 1043 | }) 1044 | }); 1045 | const options = { 1046 | config: { 1047 | ignoreDefaults: true 1048 | } 1049 | }; 1050 | const Entity = Felicity.entityFor(schema, options); 1051 | const felicityInstance = new Entity(); 1052 | 1053 | expect(felicityInstance.version).to.equal(null); 1054 | expect(felicityInstance.number).to.equal(0); 1055 | expect(felicityInstance.identity.id).to.equal(null); 1056 | expect(felicityInstance.condition).to.equal(null); 1057 | }); 1058 | 1059 | it('should utilize default values for non-compiled schema', () => { 1060 | 1061 | const schema = { 1062 | version: Joi.string().min(5).default('1.0.0'), 1063 | number: Joi.number().default(10), 1064 | identity: Joi.object().keys({ 1065 | id: Joi.string().default('abcdefg') 1066 | }), 1067 | condition: Joi.alternatives().conditional('version', { 1068 | is: Joi.string(), 1069 | then: Joi.string().default('defaultValue'), 1070 | otherwise: Joi.number() 1071 | }) 1072 | }; 1073 | const Entity = Felicity.entityFor(schema); 1074 | const felicityInstance = new Entity(); 1075 | 1076 | expect(felicityInstance.version).to.equal('1.0.0'); 1077 | expect(felicityInstance.number).to.equal(10); 1078 | expect(felicityInstance.identity.id).to.equal('abcdefg'); 1079 | expect(felicityInstance.condition).to.equal('defaultValue'); 1080 | }); 1081 | 1082 | it('should not utilize default values for non-compiled schema when provided ignoreDefaults config', () => { 1083 | 1084 | const schema = { 1085 | version: Joi.string().min(5).default('1.0.0'), 1086 | number: Joi.number().default(10), 1087 | identity: Joi.object().keys({ 1088 | id: Joi.string().default('abcdefg') 1089 | }), 1090 | condition: Joi.alternatives().conditional('version', { 1091 | is: Joi.string(), 1092 | then: Joi.string().default('defaultValue'), 1093 | otherwise: Joi.number() 1094 | }) 1095 | }; 1096 | const options = { 1097 | config: { 1098 | ignoreDefaults: true 1099 | } 1100 | }; 1101 | const Entity = Felicity.entityFor(schema, options); 1102 | const felicityInstance = new Entity(); 1103 | 1104 | expect(felicityInstance.version).to.equal(null); 1105 | expect(felicityInstance.number).to.equal(0); 1106 | expect(felicityInstance.identity.id).to.equal(null); 1107 | expect(felicityInstance.condition).to.equal(null); 1108 | }); 1109 | 1110 | it('should utilize dynamic default values', () => { 1111 | 1112 | const schema = Joi.object().keys({ 1113 | version: Joi.string().min(5).default('1.0.0'), 1114 | number: Joi.number().default(10), 1115 | identity: Joi.object().keys({ 1116 | id: Joi.string().guid().default(generateUuid) 1117 | }), 1118 | condition: Joi.alternatives().conditional('version', { 1119 | is: Joi.string(), 1120 | then: Joi.string().default('defaultValue'), 1121 | otherwise: Joi.number() 1122 | }) 1123 | }); 1124 | const Entity = Felicity.entityFor(schema); 1125 | const felicityInstance = new Entity(); 1126 | 1127 | expect(felicityInstance.version).to.equal('1.0.0'); 1128 | expect(felicityInstance.number).to.equal(10); 1129 | expect(felicityInstance.identity.id).to.be.a.string(); 1130 | expect(felicityInstance.condition).to.equal('defaultValue'); 1131 | }); 1132 | 1133 | it('should not utilize dynamic default values when provided ignoreDefaults config', () => { 1134 | 1135 | const schema = Joi.object().keys({ 1136 | version: Joi.string().min(5).default('1.0.0'), 1137 | number: Joi.number().default(10), 1138 | identity: Joi.object().keys({ 1139 | id: Joi.string().guid().default(generateUuid) 1140 | }), 1141 | condition: Joi.alternatives().conditional('version', { 1142 | is: Joi.string(), 1143 | then: Joi.string().default('defaultValue'), 1144 | otherwise: Joi.number() 1145 | }) 1146 | }); 1147 | const options = { 1148 | config: { 1149 | ignoreDefaults: true 1150 | } 1151 | }; 1152 | const Entity = Felicity.entityFor(schema, options); 1153 | const felicityInstance = new Entity(); 1154 | 1155 | expect(felicityInstance.version).to.equal(null); 1156 | expect(felicityInstance.number).to.equal(0); 1157 | expect(felicityInstance.identity.id).to.equal(null); 1158 | expect(felicityInstance.condition).to.equal(null); 1159 | }); 1160 | 1161 | it('should utilize dynamic default values for non-compiled schema', () => { 1162 | 1163 | const schema = { 1164 | version: Joi.string().min(5).default('1.0.0'), 1165 | number: Joi.number().default(10), 1166 | identity: Joi.object().keys({ 1167 | id: Joi.string().guid().default(generateUuid) 1168 | }), 1169 | condition: Joi.alternatives().conditional('version', { 1170 | is: Joi.string(), 1171 | then: Joi.string().default('defaultValue'), 1172 | otherwise: Joi.number() 1173 | }) 1174 | }; 1175 | const Entity = Felicity.entityFor(schema); 1176 | const felicityInstance = new Entity(); 1177 | 1178 | expect(felicityInstance.version).to.equal('1.0.0'); 1179 | expect(felicityInstance.number).to.equal(10); 1180 | expect(felicityInstance.identity.id).to.be.a.string(); 1181 | expect(felicityInstance.condition).to.equal('defaultValue'); 1182 | }); 1183 | 1184 | it('should not utilize dynamic default values for non-compiled schema when provided ignoreDefaults config', () => { 1185 | 1186 | const schema = { 1187 | version: Joi.string().min(5).default('1.0.0'), 1188 | number: Joi.number().default(10), 1189 | identity: Joi.object().keys({ 1190 | id: Joi.string().guid().default(generateUuid) 1191 | }), 1192 | condition: Joi.alternatives().conditional('version', { 1193 | is: Joi.string(), 1194 | then: Joi.string().default('defaultValue'), 1195 | otherwise: Joi.number() 1196 | }) 1197 | }; 1198 | const options = { 1199 | config: { 1200 | ignoreDefaults: true 1201 | } 1202 | }; 1203 | const Entity = Felicity.entityFor(schema, options); 1204 | const felicityInstance = new Entity(); 1205 | 1206 | expect(felicityInstance.version).to.equal(null); 1207 | expect(felicityInstance.number).to.equal(0); 1208 | expect(felicityInstance.identity.id).to.equal(null); 1209 | expect(felicityInstance.condition).to.equal(null); 1210 | }); 1211 | 1212 | it('should return an object with alternatives keys', () => { 1213 | 1214 | const schema = Joi.object({ 1215 | id: Joi.alternatives().try(Joi.number().integer().min(1), Joi.string().guid().lowercase()).required(), 1216 | func: Joi.alternatives().conditional('id', { is: Joi.any(), then: Joi.func() }) 1217 | }); 1218 | const Entity = Felicity.entityFor(schema); 1219 | const felicityInstance = new Entity(); 1220 | 1221 | expect(felicityInstance.id).to.equal(0); 1222 | expect(felicityInstance.func).to.equal(null); 1223 | }); 1224 | }); 1225 | 1226 | describe('Input', () => { 1227 | 1228 | it('should include valid input', () => { 1229 | 1230 | const schema = { 1231 | string: Joi.string().guid().required(), 1232 | number: Joi.number().multiple(13).min(26).required(), 1233 | object: Joi.object().keys({ 1234 | id: Joi.string().min(3).default('OKC').required(), 1235 | code: Joi.number().required() 1236 | }).required() 1237 | }; 1238 | const hydratedInput = { 1239 | string: Uuid.v4(), 1240 | number: 39, 1241 | object: { 1242 | id: 'ATX', 1243 | code: 200 1244 | } 1245 | }; 1246 | const felicityInstance = new (Felicity.entityFor(schema))(hydratedInput); 1247 | 1248 | expect(felicityInstance.string).to.equal(hydratedInput.string); 1249 | expect(felicityInstance.number).to.equal(hydratedInput.number); 1250 | expect(felicityInstance.object).to.equal(hydratedInput.object); 1251 | }); 1252 | 1253 | it('should include valid input with strictInput set to true', () => { 1254 | 1255 | const schema = { 1256 | string: Joi.string().guid().required(), 1257 | number: Joi.number().multiple(13).min(26).required(), 1258 | object: Joi.object().keys({ 1259 | id: Joi.string().min(3).default('OKC').required(), 1260 | code: Joi.number().required() 1261 | }).required() 1262 | }; 1263 | const hydratedInput = { 1264 | string: Uuid.v4(), 1265 | number: 39, 1266 | object: { 1267 | id: 'ATX', 1268 | code: 200 1269 | } 1270 | }; 1271 | const felicityInstance = new (Felicity.entityFor(schema, { config: { strictInput: true } }))(hydratedInput); 1272 | 1273 | expect(felicityInstance.string).to.equal(hydratedInput.string); 1274 | expect(felicityInstance.number).to.equal(hydratedInput.number); 1275 | expect(felicityInstance.object).to.equal(hydratedInput.object); 1276 | }); 1277 | 1278 | it('should not strip unknown input values', () => { 1279 | 1280 | const schema = Joi.object().keys({ 1281 | innerObject: Joi.object().keys({ 1282 | innerArray: Joi.array().items(Joi.number()).min(3).max(6).required(), 1283 | number: Joi.number().required().default(3), 1284 | innerString: Joi.string().required() 1285 | }), 1286 | string: Joi.string().email().required(), 1287 | date: Joi.date().raw().required(), 1288 | binary: Joi.binary().required(), 1289 | bool: Joi.boolean().required(), 1290 | conditional: Joi.alternatives().conditional('bool', { 1291 | is: true, 1292 | then: Joi.object().keys().required(), 1293 | otherwise: Joi.boolean().required() 1294 | }), 1295 | array: Joi.array().items(Joi.number()) 1296 | }); 1297 | const hydrationData = { 1298 | innerObject: { 1299 | innerString: false 1300 | }, 1301 | string: 'example@email.com', 1302 | date: 'not a date', 1303 | binary: 74, 1304 | fake: true, 1305 | bool: false, 1306 | conditional: true, 1307 | array: ['a', 'b', 'c'] 1308 | }; 1309 | const felicityInstance = new (Felicity.entityFor(schema))(hydrationData); 1310 | 1311 | expect(felicityInstance.innerObject).to.be.an.object(); 1312 | expect(felicityInstance.innerObject.innerArray).to.equal([]); 1313 | expect(felicityInstance.innerObject.innerString).to.equal(hydrationData.innerObject.innerString); 1314 | expect(felicityInstance.innerObject.number).to.equal(3); 1315 | expect(felicityInstance.string).to.equal(hydrationData.string); 1316 | expect(felicityInstance.date).to.equal(hydrationData.date); 1317 | expect(felicityInstance.binary).to.equal(hydrationData.binary); 1318 | expect(felicityInstance.fake).to.equal(hydrationData.fake); 1319 | expect(felicityInstance.bool).to.equal(hydrationData.bool); 1320 | expect(felicityInstance.conditional).to.equal(hydrationData.conditional); 1321 | expect(felicityInstance.array).to.equal(hydrationData.array); 1322 | expect(felicityInstance.validate).to.be.a.function(); 1323 | }); 1324 | 1325 | it('should strip unknown and invalid input values with strictInput set to true', () => { 1326 | 1327 | const schema = Joi.object().keys({ 1328 | innerObject: Joi.object().keys({ 1329 | innerArray: Joi.array().items(Joi.number()).min(3).max(6).required(), 1330 | number: Joi.number().required().default(3), 1331 | innerString: Joi.string().required() 1332 | }), 1333 | string: Joi.string().email().required(), 1334 | date: Joi.date().raw().required(), 1335 | binary: Joi.binary().required(), 1336 | bool: Joi.boolean().required(), 1337 | conditional: Joi.alternatives().conditional('bool', { 1338 | is: true, 1339 | then: Joi.object().keys().required(), 1340 | otherwise: Joi.boolean().required() 1341 | }) 1342 | }); 1343 | const hydrationData = { 1344 | innerObject: { 1345 | innerString: false 1346 | }, 1347 | string: 'example@email.com', 1348 | date: 'not a date', 1349 | binary: 74, 1350 | fake: true, 1351 | bool: false, 1352 | conditional: true 1353 | }; 1354 | const felicityInstance = new (Felicity.entityFor(schema, { config: { strictInput: true } }))(hydrationData); 1355 | 1356 | expect(felicityInstance.innerObject).to.be.an.object(); 1357 | expect(felicityInstance.innerObject.innerArray).to.equal([]); 1358 | expect(felicityInstance.innerObject.innerString).to.equal(null); 1359 | expect(felicityInstance.innerObject.number).to.equal(3); 1360 | expect(felicityInstance.string).to.equal(hydrationData.string); 1361 | expect(felicityInstance.date).to.equal(null); 1362 | expect(felicityInstance.binary).to.equal(null); 1363 | expect(felicityInstance.fake).to.be.undefined(); 1364 | expect(felicityInstance.bool).to.equal(hydrationData.bool); 1365 | expect(felicityInstance.conditional).to.equal(hydrationData.conditional); 1366 | expect(felicityInstance.validate).to.be.a.function(); 1367 | }); 1368 | 1369 | it('should utilize dynamic defaults for missing input', () => { 1370 | 1371 | const schema = Joi.object().keys({ 1372 | id: Joi.string().guid().required().default(generateUuid) 1373 | }); 1374 | const felicityInstance = new (Felicity.entityFor(schema))({}); 1375 | 1376 | expect(felicityInstance.id).to.be.a.string(); 1377 | }); 1378 | }); 1379 | 1380 | describe('Skeleton Validate', () => { 1381 | 1382 | it('should return an object when no callback is provided', () => { 1383 | 1384 | const schema = Joi.object().keys({ 1385 | key1: Joi.string(), 1386 | key2: Joi.number(), 1387 | key3: Joi.array().min(4) 1388 | }); 1389 | const Entity = Felicity.entityFor(schema); 1390 | const felicityInstance = new Entity(); 1391 | const instanceValidity = felicityInstance.validate(); 1392 | 1393 | expect(instanceValidity).to.be.an.object(); 1394 | expect(instanceValidity.errors).to.be.an.array(); 1395 | expect(instanceValidity.success).to.equal(false); 1396 | expect(instanceValidity.value).to.be.an.object(); 1397 | }); 1398 | 1399 | it('should set properties when validation is successful', () => { 1400 | 1401 | const schema = Joi.object().keys({ 1402 | key1: Joi.string() 1403 | }); 1404 | const Entity = Felicity.entityFor(schema); 1405 | const felicityInstance = new Entity(); 1406 | 1407 | felicityInstance.key1 = 'A string'; 1408 | 1409 | const instanceValidity = felicityInstance.validate(); 1410 | 1411 | expect(instanceValidity.errors).to.equal(undefined); 1412 | expect(instanceValidity.success).to.equal(true); 1413 | expect(instanceValidity.value).to.be.an.object(); 1414 | 1415 | }); 1416 | 1417 | it('should accept a callback', () => { 1418 | 1419 | const schema = Joi.object().keys({ 1420 | key1: Joi.string() 1421 | }); 1422 | const Entity = Felicity.entityFor(schema); 1423 | const felicityInstance = new Entity(); 1424 | const validationCallback = function (err, result) { 1425 | 1426 | expect(err).to.be.an.array(); 1427 | expect(result).to.not.exist(); 1428 | }; 1429 | 1430 | felicityInstance.validate(validationCallback); 1431 | }); 1432 | 1433 | it('should pass (err, success) to callback when validation is successful', () => { 1434 | 1435 | const schema = Joi.object().keys({ 1436 | key1: Joi.string() 1437 | }); 1438 | const Entity = Felicity.entityFor(schema); 1439 | const felicityInstance = new Entity(); 1440 | 1441 | felicityInstance.key1 = 'A string.'; 1442 | 1443 | const validationCallback = function (err, result) { 1444 | 1445 | expect(err).to.equal(null); 1446 | expect(result.success).to.equal(true); 1447 | expect(result.value).to.be.an.object(); 1448 | }; 1449 | 1450 | felicityInstance.validate(validationCallback); 1451 | }); 1452 | }); 1453 | 1454 | describe('Skeleton Example', () => { 1455 | 1456 | it('should return an empty instance', () => { 1457 | 1458 | const schema = Joi.object(); 1459 | const Entity = Felicity.entityFor(schema); 1460 | const felicityInstance = new Entity(); 1461 | const felicityExample = felicityInstance.example(); 1462 | 1463 | expect(felicityExample).to.be.an.object(); 1464 | expect(Object.keys(felicityExample).length).to.equal(0); 1465 | }); 1466 | 1467 | it('should return an a hydrated valid instance', () => { 1468 | 1469 | const schema = Joi.object().keys({ 1470 | key1: Joi.string().creditCard(), 1471 | key2: Joi.number().integer(), 1472 | key3: Joi.boolean(), 1473 | func: Joi.func() 1474 | }); 1475 | const Entity = Felicity.entityFor(schema); 1476 | const felicityInstance = new Entity(); 1477 | const felicityExample = felicityInstance.example(); 1478 | 1479 | expect(felicityExample.key1).to.be.a.string(); 1480 | expect(felicityExample.key2).to.be.a.number(); 1481 | expect(felicityExample.key3).to.be.a.boolean(); 1482 | expect(felicityExample.func).to.be.a.function(); 1483 | ExpectValidation(felicityExample, felicityInstance.schema); 1484 | }); 1485 | 1486 | it('should respect "strictExample" config at Constructor declaration', () => { 1487 | 1488 | const schema = Joi.object().keys({ 1489 | name: Joi.string().regex(/abcd(?=efg)/) 1490 | }); 1491 | const options = { 1492 | config: { 1493 | strictExample: true 1494 | } 1495 | }; 1496 | const Constructor = Felicity.entityFor(schema, options); 1497 | const instance = new Constructor(); 1498 | 1499 | expect(() => { 1500 | 1501 | return instance.example(); 1502 | }).to.throw(`"name" with value "abcd" fails to match the required pattern: /abcd(?=efg)/`); 1503 | }); 1504 | 1505 | it('should respect "strictExample" config at instance example call', () => { 1506 | 1507 | const schema = Joi.object().keys({ 1508 | name: Joi.string().regex(/abcd(?=efg)/) 1509 | }); 1510 | const options = { 1511 | config: { 1512 | strictExample: true 1513 | } 1514 | }; 1515 | const Constructor = Felicity.entityFor(schema); 1516 | const instance = new Constructor(); 1517 | 1518 | expect(instance.example().name).to.equal('abcd'); 1519 | expect(() => { 1520 | 1521 | return instance.example(options); 1522 | }).to.throw(`"name" with value "abcd" fails to match the required pattern: /abcd(?=efg)/`); 1523 | }); 1524 | 1525 | it('should respect "includeOptional" config for static example call', () => { 1526 | 1527 | const schema = Joi.object().keys({ 1528 | required: Joi.string().required(), 1529 | optional: Joi.string().optional(), 1530 | implicitOptional: Joi.string() 1531 | }).options({ presence: 'optional' }); 1532 | const options = { 1533 | config: { 1534 | includeOptional: true 1535 | } 1536 | }; 1537 | const Constructor = Felicity.entityFor(schema); 1538 | const example = Constructor.example(options); 1539 | 1540 | expect(example.required).to.be.a.string(); 1541 | expect(example.optional).to.be.a.string(); 1542 | expect(example.implicitOptional).to.be.a.string(); 1543 | 1544 | const exampleWithoutOptions = Constructor.example(); 1545 | expect(exampleWithoutOptions.required).to.be.a.string(); 1546 | expect(exampleWithoutOptions.optional).to.be.undefined(); 1547 | expect(exampleWithoutOptions.implicitOptional).to.be.undefined(); 1548 | }); 1549 | }); 1550 | }); 1551 | --------------------------------------------------------------------------------