nested-env-schema
Version:
Validate & extract your env variables using nested JSON schema, Ajv and dotenvx
366 lines (352 loc) • 6.96 kB
JavaScript
'use strict';
const t = require('tap');
const Ajv = require('ajv');
const makeTest = require('./make-test');
const { join } = require('node:path');
process.env.VALUE_FROM_ENV = 'pippo';
const tests = [
{
name: 'empty ok',
schema: { type: 'object' },
data: {},
isOk: true,
confExpected: {},
},
{
name: 'simple object - ok',
schema: {
type: 'object',
properties: {
PORT: {
type: 'string',
},
},
},
data: {
PORT: '44',
},
isOk: true,
confExpected: {
PORT: '44',
},
},
{
name: 'simple object - ok - coerce value',
schema: {
type: 'object',
properties: {
PORT: {
type: 'integer',
},
},
},
data: {
PORT: '44',
},
isOk: true,
confExpected: {
PORT: 44,
},
},
{
name: 'simple object - ok - remove additional properties',
schema: {
type: 'object',
properties: {
PORT: {
type: 'integer',
},
},
},
data: {
PORT: '44',
ANOTHER_PORT: '55',
},
isOk: true,
confExpected: {
PORT: 44,
},
},
{
name: 'simple object - ok - use default',
schema: {
type: 'object',
properties: {
PORT: {
type: 'integer',
default: 5555,
},
},
},
data: {},
isOk: true,
confExpected: {
PORT: 5555,
},
},
{
name: 'simple object - ok - required + default',
schema: {
type: 'object',
required: ['PORT'],
properties: {
PORT: {
type: 'integer',
default: 6666,
},
},
},
data: {},
isOk: true,
confExpected: {
PORT: 6666,
},
},
{
name: 'simple object - ok - allow array',
schema: {
type: 'object',
required: ['PORT'],
properties: {
PORT: {
type: 'integer',
default: 6666,
},
},
},
data: [{}],
isOk: true,
confExpected: {
PORT: 6666,
},
},
{
name: 'simple object - ok - merge multiple object + env',
schema: {
type: 'object',
required: ['PORT', 'MONGODB_URL'],
properties: {
PORT: {
type: 'integer',
default: 6666,
},
MONGODB_URL: {
type: 'string',
},
VALUE_FROM_ENV: {
type: 'string',
},
},
},
data: [{ PORT: 3333 }, { MONGODB_URL: 'mongodb://localhost/pippo' }],
isOk: true,
confExpected: {
PORT: 3333,
MONGODB_URL: 'mongodb://localhost/pippo',
VALUE_FROM_ENV: 'pippo',
},
},
{
name: 'simple object - ok - load only from env',
schema: {
type: 'object',
required: ['VALUE_FROM_ENV'],
properties: {
VALUE_FROM_ENV: {
type: 'string',
},
},
},
data: undefined,
isOk: true,
confExpected: {
VALUE_FROM_ENV: 'pippo',
},
},
{
name: 'simple object - ok - opts override environment',
schema: {
type: 'object',
required: ['VALUE_FROM_ENV'],
properties: {
VALUE_FROM_ENV: {
type: 'string',
},
},
},
data: { VALUE_FROM_ENV: 'pluto' },
isOk: true,
confExpected: {
VALUE_FROM_ENV: 'pluto',
},
},
{
name: 'simple object - ok - load only from .env',
schema: {
type: 'object',
required: ['VALUE_FROM_DOTENV'],
properties: {
VALUE_FROM_DOTENV: {
type: 'string',
},
},
},
data: undefined,
isOk: true,
dotenv: { path: join(__dirname, 'envs', 'normal', '.env') },
confExpected: {
VALUE_FROM_DOTENV: 'look ma',
},
},
{
name: 'simple object - KO',
schema: {
type: 'object',
required: ['PORT'],
properties: {
PORT: {
type: 'integer',
},
},
},
data: {},
isOk: false,
errorMessage: "env must have required property 'PORT'",
},
{
name: 'simple object - invalid data',
schema: {
type: 'object',
required: ['PORT'],
properties: {
PORT: {
type: 'integer',
},
},
},
data: [],
isOk: false,
errorMessage:
'opts/data must NOT have fewer than 1 items, opts/data must be object, opts/data must match exactly one schema in oneOf',
},
];
const ajv = new Ajv({
allErrors: true,
removeAdditional: true,
useDefaults: true,
coerceTypes: true,
allowUnionTypes: true,
});
tests.forEach(function (testConf) {
t.test(testConf.name, (t) => {
const options = {
schema: testConf.schema,
data: testConf.data,
dotenv: testConf.dotenv,
dotenvConfig: testConf.dotenvConfig,
ajv,
};
makeTest(
t,
options,
testConf.isOk,
testConf.confExpected,
testConf.errorMessage,
);
});
});
const noCoercionTest = {
name: 'simple object - not ok - should NOT coerce value',
schema: {
type: 'object',
properties: {
PORT: {
type: 'integer',
},
},
},
data: {
PORT: '44',
},
isOk: false,
errorMessage: 'env/PORT must be integer',
confExpected: {
PORT: 44,
},
};
const strictValidator = new Ajv({
allErrors: true,
removeAdditional: true,
useDefaults: true,
coerceTypes: false,
allowUnionTypes: true,
});
[noCoercionTest].forEach(function (testConf) {
t.test(testConf.name, (t) => {
const options = {
schema: testConf.schema,
data: testConf.data,
dotenv: testConf.dotenv,
dotenvConfig: testConf.dotenvConfig,
ajv: strictValidator,
};
makeTest(
t,
options,
testConf.isOk,
testConf.confExpected,
testConf.errorMessage,
);
});
});
t.test('ajv enhancement', (t) => {
t.plan(2);
const testCase = {
schema: {
type: 'object',
required: ['MONGODB_URL'],
properties: {
MONGODB_URL: {
type: 'string',
format: 'uri',
},
},
},
data: [{ PORT: 3333 }, { MONGODB_URL: 'mongodb://localhost/pippo' }],
isOk: true,
confExpected: {
MONGODB_URL: 'mongodb://localhost/pippo',
},
};
t.test('return', (t) => {
const options = {
schema: testCase.schema,
data: testCase.data,
ajv: {
customOptions(ajvInstance) {
require('ajv-formats')(ajvInstance);
return ajvInstance;
},
},
};
makeTest(t, options, testCase.isOk, testCase.confExpected);
});
t.test('no return', (t) => {
const options = {
schema: testCase.schema,
data: testCase.data,
ajv: {
customOptions(ajvInstance) {
// do nothing
},
},
};
makeTest(
t,
options,
false,
undefined,
'customOptions function must return an instance of Ajv',
);
});
});