/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ import { expect } from '../../../test-utils'; // The shim uses `module.exports = extendedJoi`, so `require` is the correct import form. const Joi = require('../joi'); describe('joi-shim', function () { describe('v11 array-argument compatibility on valid/invalid/allow and aliases', function () { it('accepts .valid([...]) array syntax and treats items as separate valids', function () { const schema = Joi.string().valid(['a', 'b']); expect(schema.validate('a').error).to.equal(null); expect(schema.validate('b').error).to.equal(null); expect(schema.validate('c').error).to.not.equal(null); }); it('accepts .valid(a, b, ...) spread syntax (v18 native)', function () { const schema = Joi.string().valid('a', 'b'); expect(schema.validate('a').error).to.equal(null); expect(schema.validate('c').error).to.not.equal(null); }); it('accepts .invalid([...]) array syntax', function () { const schema = Joi.string().invalid(['bad', 'worse']); expect(schema.validate('bad').error).to.not.equal(null); expect(schema.validate('fine').error).to.equal(null); }); it('patches the .equal alias the same as .valid', function () { const schema = Joi.string().equal(['a', 'b']); expect(schema.validate('a').error).to.equal(null); expect(schema.validate('b').error).to.equal(null); expect(schema.validate('z').error).to.not.equal(null); }); it('patches the .not alias the same as .invalid', function () { const schema = Joi.string().not(['bad']); expect(schema.validate('bad').error).to.not.equal(null); expect(schema.validate('ok').error).to.equal(null); }); it('patches the .disallow alias the same as .invalid', function () { const schema = Joi.string().disallow(['bad']); expect(schema.validate('bad').error).to.not.equal(null); expect(schema.validate('ok').error).to.equal(null); }); it('accepts .allow([...]) array syntax', function () { const schema = Joi.string().allow(['', null]); expect(schema.validate('').error).to.equal(null); expect(schema.validate(null).error).to.equal(null); }); it('recursively flattens nested array arguments to match v11 semantics', function () { // Joi v11's `_valueValid` ran `Hoek.flatten([].concat(values))` — fully recursive — // so all of the forms below produced the same valid list `[a, b, c]`. Joi v18 // rejects any array arg via `verifyFlat`, so the shim must flatten to infinity. // Assert behaviourally (validate succeeds on each valid, fails on the negative). const s1 = Joi.any().valid(['a', 'b', 'c']); const s2 = Joi.any().valid([['a', 'b'], 'c']); const s3 = Joi.any().valid([[['a'], 'b'], 'c']); for (const s of [s1, s2, s3]) { expect(s.validate('a').error).to.equal(null); expect(s.validate('b').error).to.equal(null); expect(s.validate('c').error).to.equal(null); expect(s.validate('z').error).to.not.equal(null); } }); }); describe('.when({ is, then, else }) v11 → v18 otherwise rename', function () { it('maps `else` to `otherwise`', function () { const schema = Joi.object({ flag: Joi.boolean().required(), value: Joi.when('flag', { is: true, then: Joi.string().required(), else: Joi.number().required(), }), }); expect(schema.validate({ flag: true, value: 'hi' }).error).to.equal(null); expect(schema.validate({ flag: false, value: 42 }).error).to.equal(null); expect(schema.validate({ flag: true, value: 42 }).error).to.not.equal(null); expect(schema.validate({ flag: false, value: 'hi' }).error).to.not.equal(null); }); it('does not mutate a reused options object across multiple .when() calls', function () { const opts = { is: 'A', then: Joi.string(), else: Joi.number() }; Joi.object({ k: Joi.string(), v: Joi.any().when('k', opts) }); Joi.object({ k: Joi.string(), v: Joi.any().when('k', opts) }); expect('else' in opts).to.equal(true); expect('otherwise' in opts).to.equal(false); }); }); describe('.error(fn) callback return shapes', function () { it('preserves context.key in details when callback returns { ...error, message }', function () { // v11 pattern: spread original error and override message only. const schema = Joi.object({ name: Joi.string() .required() .error((errors: any[]) => errors.map((e) => ({ ...e, message: 'custom name error' }))), }); const result = schema.validate({}); expect(result.error).to.not.equal(null); expect(result.error.details[0].message).to.equal('custom name error'); // Report-mutation path must preserve v11 details shape so helpers can read context. expect(result.error.details[0].context).to.be.an('object'); // label should be on the context (Joi v18 puts it there) expect(result.error.details[0].context.label).to.equal('name'); }); it('exposes v11 aliases (type, context) on errors passed to the callback', function () { let seenType: string | undefined; let seenContextKey: string | undefined; const schema = Joi.object({ age: Joi.number() .min(18) .required() .error((errors: any[]) => { seenType = errors[0].type; seenContextKey = errors[0].context?.key; return errors; }), }); schema.validate({ age: 10 }); expect(seenType).to.equal('number.min'); expect(seenContextKey).to.equal('age'); }); it('falls back to Error wrapper when a Report cannot be mutated (frozen)', function () { const schema = Joi.string() .min(5) .error((errors: any[]) => { for (const e of errors) Object.freeze(e); return errors.map((e) => ({ ...e, message: 'frozen-fallback' })); }); const result = schema.validate('ab'); expect(result.error).to.not.equal(null); expect(result.error.details[0].message).to.equal('frozen-fallback'); }); it('accepts a callback returning a plain-object error (no matching Report)', function () { // Pathological case: callback returns a fresh object not tied to the input errors array. const schema = Joi.string() .min(5) .error(() => [{ message: 'hand-rolled' }]); const result = schema.validate('ab'); expect(result.error).to.not.equal(null); expect(result.error.details[0].message).to.equal('hand-rolled'); }); }); describe('details[].path normalization', function () { it('ensures details[*].path is always an array', function () { const schema = Joi.object({ nested: Joi.object({ field: Joi.string().required(), }).required(), }); const result = schema.validate({ nested: {} }); expect(result.error).to.not.equal(null); for (const d of result.error.details) { expect(Array.isArray(d.path)).to.equal(true); } }); }); describe('v11 message-text overrides', function () { it('uses "larger than" wording for number.min (v11 style)', function () { const schema = Joi.number().min(10); const result = schema.validate(5); expect(result.error.details[0].message).to.contain('larger than or equal to'); }); it('uses "larger than" wording for number.greater (v11 style)', function () { const schema = Joi.number().greater(10); const result = schema.validate(5); expect(result.error.details[0].message).to.contain('larger than'); }); it('uses "one of" wording for any.only with a single valid value (v11 style)', function () { // v18 drops "one of " when there's a single valid; v11 always included it. const schema = Joi.string().valid('cellphone'); const result = schema.validate('other'); expect(result.error.details[0].message).to.contain('must be one of'); }); it('formats date.min limits using v11 JS Date.toString() style via dateFormat=string', function () { const limit = new Date('2020-01-01T00:00:00Z'); const schema = Joi.date().iso().min(limit); const result = schema.validate(new Date('2000-01-01').toISOString()); // v18 default: ISO-8601 like "2020-01-01T00:00:00.000Z". // v11 / our shim: JS toString like "Wed Jan 01 2020 ... GMT+xxxx (...)". expect(result.error.details[0].message).to.match(/GMT[+-]\d{4}/); expect(result.error.details[0].message).to.not.contain('T00:00:00.000Z'); }); }); describe('ancestor-path message wrapping', function () { it('wraps nested path messages with `child "X" fails because [...]` (v11 shape)', function () { const schema = Joi.object({ parent: Joi.object({ leaf: Joi.string().required(), }).required(), }); const result = schema.validate({ parent: {} }); expect(result.error.message).to.contain('"parent"'); expect(result.error.message).to.contain('child'); expect(result.error.message).to.contain('"leaf"'); }); }); describe('validate() result-shape normalization', function () { it('returns { error: null } on success via schema.validate (v11 shape)', function () { const schema = Joi.string(); const result = schema.validate('hi'); expect(result.error).to.equal(null); }); it('returns { error: null } on success via top-level Joi.validate (v11 legacy form)', function () { const schema = Joi.string(); const result = Joi.validate('hi', schema); expect(result.error).to.equal(null); }); it('applies v11 label=key in details labels via top-level Joi.validate', function () { const schema = Joi.object({ field_name: Joi.string().required() }); const result = Joi.validate({}, schema); expect(result.error).to.not.equal(null); // v11 used the key as the label; v18 defaults to full paths. expect(result.error.details[0].message).to.contain('"field_name"'); }); it('formats date limits consistently via top-level Joi.validate', function () { const limit = new Date('2020-01-01T00:00:00Z'); const schema = Joi.object({ dob: Joi.date().iso().min(limit).required() }); const viaSchema = schema.validate({ dob: new Date('2000-01-01').toISOString() }); const viaTopLevel = Joi.validate({ dob: new Date('2000-01-01').toISOString() }, schema); expect(viaTopLevel.error.details[0].message).to.equal(viaSchema.error.details[0].message); }); it('does not mutate the caller-provided options object (schema.validate)', function () { // Sibling of the `.when({ is, then, else })` non-mutation test: the shim's internal // `errors` / `messages` / `dateFormat` merges must build a fresh object, not patch // the user's options in place. A product module that snapshots / reuses an options // object across multiple validates must see its original shape preserved. const schema = Joi.string(); const opts = { abortEarly: false }; const snapshot = { ...opts }; schema.validate('hi', opts); schema.validate('ok', opts); expect(opts).to.deep.equal(snapshot); expect('errors' in opts).to.equal(false); expect('messages' in opts).to.equal(false); expect('dateFormat' in opts).to.equal(false); }); it('does not mutate the caller-provided options object (top-level Joi.validate)', function () { const schema = Joi.string(); const opts = { abortEarly: false }; const snapshot = { ...opts }; Joi.validate('hi', schema, opts); Joi.validate('ok', schema, opts); expect(opts).to.deep.equal(snapshot); expect('errors' in opts).to.equal(false); expect('messages' in opts).to.equal(false); expect('dateFormat' in opts).to.equal(false); }); }); describe('custom extension types survive the shim', function () { it('keeps Joi.string().idNumber() working end-to-end', function () { // Valid SA ID example taken from common test fixtures. const valid = Joi.string().idNumber().validate('8001015009087'); expect(valid.error).to.equal(null); const invalid = Joi.string().idNumber().validate('1234567890123'); expect(invalid.error).to.not.equal(null); }); it('keeps Joi.dateOfBirth() working', function () { const valid = Joi.dateOfBirth().validate('1990-01-01'); expect(valid.error).to.equal(null); const futureFails = Joi.dateOfBirth().validate('3000-01-01'); expect(futureFails.error).to.not.equal(null); }); it('keeps Joi.shortDate() working', function () { const valid = Joi.shortDate().validate('2000-01-01'); expect(valid.error).to.equal(null); const invalid = Joi.shortDate().validate('not-a-date'); expect(invalid.error).to.not.equal(null); }); }); });