Files
piratepoet/mailpoet/tests/integration/Validator/ValidatorTest.php
Jan Jakes 1611bbae99 Add arbitrary other fields to schema
[MAILPOET-4195]
2022-03-30 11:29:58 +02:00

743 lines
36 KiB
PHP

<?php declare(strict_types = 1);
namespace MailPoet\Test\Util;
use MailPoet\Validator\Builder;
use MailPoet\Validator\Schema;
use MailPoet\Validator\ValidationException;
use MailPoet\Validator\Validator;
use MailPoetTest;
use stdClass;
class ValidatorTest extends MailPoetTest {
public function testString(): void {
// valid
$this->assertValidationPassed(Builder::string(), '');
$this->assertValidationPassed(Builder::string(), 'abc');
$this->assertValidationPassed(Builder::string()->minLength(3), 'abc');
$this->assertValidationPassed(Builder::string()->maxLength(3), 'abc');
$this->assertValidationPassed(Builder::string()->pattern('^[a-z]+$'), 'abc');
$this->assertValidationPassed(Builder::string()->formatDateTime(), '2022-03-18T12:35:27+01:00');
$this->assertValidationPassed(Builder::string()->formatEmail(), 'test@example.com');
$this->assertValidationPassed(Builder::string()->formatHexColor(), '#00aaff');
$this->assertValidationPassed(Builder::string()->formatHexColor(), '#ccc');
$this->assertValidationPassed(Builder::string()->formatIp(), '127.0.0.1');
$this->assertValidationPassed(Builder::string()->formatIp(), '::1');
$this->assertValidationPassed(Builder::string()->formatUri(), 'https://wordpress.org?x=y#1');
$this->assertValidationPassed(Builder::string()->formatUri(), '/');
$this->assertValidationPassed(Builder::string()->formatUri(), 'https://example.org/hello world', 'https://example.org/hello%20world');
$this->assertValidationPassed(Builder::string()->formatUri(), '/test[a]=1&a=[2]', '/test%5Ba%5D=1&a=%5B2%5D');
$this->assertValidationPassed(Builder::string()->formatUuid(), 'b2c70356-0e19-4f30-87da-1d2eadaf2d39');
// invalid
$this->assertValidationFailed(Builder::string()->minLength(3), 'ab', 'value must be at least 3 characters long.');
$this->assertValidationFailed(Builder::string()->maxLength(3), 'abcd', 'value must be at most 3 characters long.');
$this->assertValidationFailed(Builder::string()->pattern('^[a-z]+$'), 'a123', 'value does not match pattern ^[a-z]+$.');
$this->assertValidationFailed(Builder::string()->formatDateTime(), 'abc', 'Invalid date.');
$this->assertValidationFailed(Builder::string()->formatDateTime(), '2022-03-18', 'Invalid date.');
$this->assertValidationFailed(Builder::string()->formatDateTime(), '12:00:00', 'Invalid date.');
$this->assertValidationFailed(Builder::string()->formatEmail(), 'abc@', 'Invalid email address.');
$this->assertValidationFailed(Builder::string()->formatEmail(), 'example.com', 'Invalid email address.');
$this->assertValidationFailed(Builder::string()->formatEmail(), '@example.com', 'Invalid email address.');
$this->assertValidationFailed(Builder::string()->formatHexColor(), '00aaff', 'Invalid hex color.');
$this->assertValidationFailed(Builder::string()->formatHexColor(), 'ccc', 'Invalid hex color.');
$this->assertValidationFailed(Builder::string()->formatHexColor(), '#00xxzz', 'Invalid hex color.');
$this->assertValidationFailed(Builder::string()->formatIp(), '127.0.0.', 'value is not a valid IP address.');
$this->assertValidationFailed(Builder::string()->formatIp(), '127.0.0', 'value is not a valid IP address.');
$this->assertValidationFailed(Builder::string()->formatIp(), ':::1', 'value is not a valid IP address.');
$this->assertValidationFailed(Builder::string()->formatIp(), ':', 'value is not a valid IP address.');
$this->assertValidationFailed(Builder::string()->formatUuid(), 'b2c703560e194f3087da1d2eadaf2d39', 'value is not a valid UUID.');
$this->assertValidationFailed(Builder::string(), 1, 'value is not of type string.');
$this->assertValidationFailed(Builder::string(), null, 'value is not of type string.');
$this->assertValidationFailed(Builder::string(), true, 'value is not of type string.');
$this->assertValidationFailed(Builder::string(), false, 'value is not of type string.');
$this->assertValidationFailed(Builder::string(), [], 'value is not of type string.');
$this->assertValidationFailed(Builder::string(), new stdClass(), 'value is not of type string.');
}
public function testNumber(): void {
// valid
$this->assertValidationPassed(Builder::number(), 5, 5.0);
$this->assertValidationPassed(Builder::number(), 0.123);
$this->assertValidationPassed(Builder::number(), 1e3);
$this->assertValidationPassed(Builder::number(), -5, -5.0);
$this->assertValidationPassed(Builder::number(), -0.123);
$this->assertValidationPassed(Builder::number(), -1e3);
$this->assertValidationPassed(Builder::number(), 0, 0.0);
$this->assertValidationPassed(Builder::number(), -0, 0.0);
// invalid
$this->assertValidationFailed(Builder::number(), '0', 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), '5', 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), '5.0', 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), '-5', 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), '1e3', 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), '', 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), null, 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), true, 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), false, 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), [], 'value is not of type number.');
$this->assertValidationFailed(Builder::number(), new stdClass(), 'value is not of type number.');
}
public function testInteger(): void {
// valid
$this->assertValidationPassed(Builder::integer(), 5);
$this->assertValidationPassed(Builder::integer(), -5);
$this->assertValidationPassed(Builder::integer(), 0);
$this->assertValidationPassed(Builder::integer(), -0);
// invalid
$this->assertValidationFailed(Builder::integer(), '0', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), '5', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), '5.0', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), '-5', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), '1e3', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), '5', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), '5.0', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), 5.0, 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), 5.1, 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), 1e3, 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), '', 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), null, 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), true, 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), false, 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), [], 'value is not of type integer.');
$this->assertValidationFailed(Builder::integer(), new stdClass(), 'value is not of type integer.');
}
public function testBoolean(): void {
// valid
$this->assertValidationPassed(Builder::boolean(), true);
$this->assertValidationPassed(Builder::boolean(), false);
// invalid
$this->assertValidationFailed(Builder::boolean(), 'true', 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), 'false', 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), '0', 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), '1', 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), '', 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), 1, 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), null, 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), [], 'value is not of type boolean.');
$this->assertValidationFailed(Builder::boolean(), new stdClass(), 'value is not of type boolean.');
}
public function testNull(): void {
// valid
$this->assertValidationPassed(Builder::null(), null);
// invalid
$this->assertValidationFailed(Builder::null(), '', 'value is not of type null.');
$this->assertValidationFailed(Builder::null(), 'null', 'value is not of type null.');
$this->assertValidationFailed(Builder::null(), 0, 'value is not of type null.');
$this->assertValidationFailed(Builder::null(), [], 'value is not of type null.');
$this->assertValidationFailed(Builder::null(), new stdClass(), 'value is not of type null.');
}
public function testArray(): void {
// valid
$this->assertValidationPassed(Builder::array(Builder::number()), []);
$this->assertValidationPassed(Builder::array(Builder::number()), [1, 2, 3], [1.0, 2.0, 3.0]);
$this->assertValidationPassed(Builder::array(Builder::number()), [-1e5, -0.123, 0.0, 1e3, 5.1]);
$this->assertValidationPassed(Builder::array(Builder::number()), [1e3]);
$this->assertValidationPassed(Builder::array(Builder::number())->minItems(3)->maxItems(3), [1.0, 2.0, 3.0]);
$this->assertValidationPassed(Builder::array(Builder::number())->uniqueItems(), [1, 2, 3], [1.0, 2.0, 3.0]);
// invalid
$this->assertValidationFailed(Builder::array(), '', 'value is not of type array.');
$this->assertValidationFailed(Builder::array(), 'null', 'value is not of type array.');
$this->assertValidationFailed(Builder::array(), 0, 'value is not of type array.');
$this->assertValidationFailed(Builder::array(), new stdClass(), 'value is not of type array.');
$this->assertValidationFailed(Builder::array(), 'a,b', 'value is not of type array.');
$this->assertValidationFailed(Builder::array(), ['x' => 'x', 'y' => 'y'], 'value is not of type array.');
$this->assertValidationFailed(Builder::array(Builder::number()), [1, '2'], 'value[1] is not of type number.');
$this->assertValidationFailed(Builder::array(Builder::number()), [1, null], 'value[1] is not of type number.');
$this->assertValidationFailed(Builder::array(Builder::number()), [1, false], 'value[1] is not of type number.');
$this->assertValidationFailed(Builder::array(Builder::number())->minItems(3), [1, 2], 'value must contain at least 3 items.');
$this->assertValidationFailed(Builder::array(Builder::number())->maxItems(1), [1, 2], 'value must contain at most 1 item.');
$this->assertValidationFailed(Builder::array(Builder::number())->uniqueItems(), [1, 2, 1], 'value has duplicate items.');
$this->assertValidationFailed(Builder::array(Builder::number())->uniqueItems(), [1.0, 1], 'value has duplicate items.');
$this->assertValidationFailed(Builder::array(Builder::number())->uniqueItems(), [0, -0], 'value has duplicate items.');
}
public function testObject(): void {
// valid - basics
$this->assertValidationPassed(Builder::object(['n' => Builder::number()]), []);
$this->assertValidationPassed(Builder::object(['n' => Builder::number()]), new stdClass(), []);
$this->assertValidationPassed(Builder::object(['n' => Builder::integer()]), ['i' => 123]);
$this->assertValidationPassed(Builder::object(['n' => Builder::integer()]), ['i' => 123, 's' => 'abc']);
$this->assertValidationPassed(
Builder::object(['i' => Builder::integer(), 's' => Builder::string()]),
['i' => 5, 's' => 'abc']
);
// valid - required, min-properties, max-properties
$this->assertValidationPassed(
Builder::object(['i' => Builder::integer()->required(), 's' => Builder::string()]),
['i' => 5]
);
$this->assertValidationPassed(
Builder::object(['i' => Builder::integer(), 's' => Builder::string()])->minProperties(1),
['s' => 'abc']
);
$this->assertValidationPassed(
Builder::object(['i' => Builder::integer(), 's' => Builder::string()])->minProperties(1),
['s' => 'abc']
);
$this->assertValidationPassed(
Builder::object(['i' => Builder::integer(), 's' => Builder::string()])->maxProperties(1),
['i' => 5]
);
$this->assertValidationPassed(
Builder::object(['i' => Builder::integer(), 's' => Builder::string()])->maxProperties(1),
['s' => 'abc']
);
// valid - no additional properties
$this->assertValidationPassed(
Builder::object(['i' => Builder::integer()])->disableAdditionalProperties(),
['i' => 5]
);
// valid - additional properties
$this->assertValidationPassed(
Builder::object()->additionalProperties(Builder::integer()),
['a' => 1, 'b' => 2, 'c' => 3]
);
// valid - pattern properties
$this->assertValidationPassed(
Builder::object()->patternProperties(['^i_' => Builder::integer(), '^s_' => Builder::string()]),
['i_1' => 1, 'i_2' => 2, 's_1' => 'abc', 's_2' => '', 's_3' => 'xyz']
);
// invalid - basics
$this->assertValidationFailed(
Builder::object(['n' => Builder::number(), 's' => Builder::string()]),
['n' => '1', 's' => 'abc'],
'value[n] is not of type number.'
);
$this->assertValidationFailed(Builder::object(['n' => Builder::number()]), ['abc'], 'value is not of type object.');
$this->assertValidationFailed(Builder::object(['n' => Builder::number()]), [1, 2, 3], 'value is not of type object.');
$this->assertValidationFailed(Builder::object(['n' => Builder::number()]), '', 'value is not of type object.');
$this->assertValidationFailed(Builder::object(['n' => Builder::number()]), null, 'value is not of type object.');
$this->assertValidationFailed(Builder::object(['n' => Builder::number()]), true, 'value is not of type object.');
$this->assertValidationFailed(Builder::object(['n' => Builder::number()]), false, 'value is not of type object.');
// invalid - required, min-properties, max-properties
$this->assertValidationFailed(Builder::object(['n' => Builder::number()->required()]), [], 'n is a required property of value.');
$this->assertValidationFailed(Builder::object(['n' => Builder::number()->required()]), new stdClass(), 'n is a required property of value.');
$this->assertValidationFailed(
Builder::object(['i' => Builder::integer()->required(), 's' => Builder::string()]),
['s' => 'abc'],
'i is a required property of value.'
);
$this->assertValidationFailed(
Builder::object()->minProperties(1),
[],
'value must contain at least 1 property.'
);
$this->assertValidationFailed(
Builder::object()->maxProperties(1),
['i' => 5, 's' => 'abc'],
'value must contain at most 1 property.'
);
// invalid - no additional properties
$this->assertValidationFailed(
Builder::object(['i' => Builder::integer()])->disableAdditionalProperties(),
['i' => 5, 's' => 'abc'],
's is not a valid property of Object.'
);
// invalid - additional properties
$this->assertValidationFailed(
Builder::object()->additionalProperties(Builder::integer()),
['a' => 1, 'b' => 'abc', 'c' => 3],
'value[b] is not of type integer.'
);
// invalid - pattern properties
$this->assertValidationFailed(
Builder::object()->patternProperties(['^i_' => Builder::integer(), '^s_' => Builder::string()]),
['i_1' => 'abc', 's_1' => 'abc'],
'value[i_1] is not of type integer.'
);
$this->assertValidationFailed(
Builder::object()->patternProperties(['^i_' => Builder::integer(), '^s_' => Builder::string()]),
['i_1' => 5, 's_1' => 5],
'value[s_1] is not of type string.'
);
}
public function testOneOf(): void {
// valid
$this->assertValidationPassed(Builder::oneOf([Builder::string()]), 'abc');
$this->assertValidationPassed(Builder::oneOf([Builder::number(), Builder::string(), Builder::integer()]), '123');
$this->assertValidationPassed(Builder::oneOf([Builder::boolean(), Builder::string(), Builder::integer()]), '1');
$this->assertValidationPassed(Builder::oneOf([Builder::integer(), Builder::number()]), 5.0);
$this->assertValidationPassed(Builder::oneOf([Builder::boolean(), Builder::number()]), true);
$this->assertValidationPassed(Builder::oneOf([Builder::array(), Builder::object()]), ['abc']);
$this->assertValidationPassed(Builder::oneOf([Builder::array(), Builder::object()]), new stdClass(), []);
$this->assertValidationPassed(Builder::oneOf([Builder::integer(), Builder::null()]), null);
// valid - nested object has different property type
$this->assertValidationPassed(
Builder::oneOf([
Builder::object([
'n' => Builder::number(),
's' => Builder::string(),
'o' => Builder::object([
'test' => Builder::integer(),
]),
]),
Builder::object([
'n' => Builder::number(),
's' => Builder::string(),
'o' => Builder::object([
'test' => Builder::boolean(),
]),
]),
]),
['n' => 5.2, 's' => 'abc', 'o' => ['test' => false]]
);
// valid - nested arrays have different item types
$this->assertValidationPassed(
Builder::oneOf([
Builder::object([
'n' => Builder::number(),
's' => Builder::string(),
'o' => Builder::object([
'test' => Builder::array(Builder::string()->nullable()),
]),
]),
Builder::object([
'n' => Builder::number(),
's' => Builder::string(),
'o' => Builder::object([
'test' => Builder::array(Builder::boolean()),
]),
]),
]),
['n' => 5.2, 's' => 'abc', 'o' => ['test' => ['a', 'b', 'c', '', null]]]
);
// invalid
$this->assertValidationFailed(Builder::oneOf([Builder::number(), Builder::integer()]), 5, 'value matches more than one of the expected formats.');
$this->assertValidationFailed(Builder::oneOf([Builder::array(), Builder::object()]), [], 'value matches more than one of the expected formats.');
$this->assertValidationFailed(Builder::oneOf([]), null, 'value is not a valid ');
$this->assertValidationFailed(Builder::oneOf([]), '', 'value is not a valid ');
$this->assertValidationFailed(Builder::oneOf([]), 'abc', 'value is not a valid ');
$this->assertValidationFailed(Builder::oneOf([]), [], 'value is not a valid ');
$this->assertValidationFailed(Builder::oneOf([]), true, 'value is not a valid ');
$this->assertValidationFailed(Builder::oneOf([]), false, 'value is not a valid ');
$this->assertValidationFailed(Builder::oneOf([]), 0, 'value is not a valid ');
// invalid (integer and number both match, error on positions 1, 2)
$e = $this->assertValidationFailed(
Builder::oneOf([Builder::string(), Builder::integer(), Builder::number()]),
5,
'value matches more than one of the expected formats.'
);
$this->assertSame(['rest_one_of_multiple_matches' => ['positions' => [1, 2]]], $e->getWpError()->error_data);
// invalid (string used twice, error on positions 1, 3)
$e = $this->assertValidationFailed(
Builder::oneOf([Builder::boolean(), Builder::string(), Builder::number(), Builder::string()]),
'5',
'value matches more than one of the expected formats.'
);
$this->assertSame(['rest_one_of_multiple_matches' => ['positions' => [1, 3]]], $e->getWpError()->error_data);
}
public function testAnyOf(): void {
// valid
$this->assertValidationPassed(Builder::anyOf([Builder::string()]), 'abc');
$this->assertValidationPassed(Builder::anyOf([Builder::number(), Builder::string(), Builder::integer()]), '123');
$this->assertValidationPassed(Builder::anyOf([Builder::boolean(), Builder::string(), Builder::integer()]), '1');
$this->assertValidationPassed(Builder::anyOf([Builder::integer(), Builder::number()]), 5.0);
$this->assertValidationPassed(Builder::anyOf([Builder::boolean(), Builder::number()]), true);
$this->assertValidationPassed(Builder::anyOf([Builder::array(), Builder::object()]), ['abc']);
$this->assertValidationPassed(Builder::anyOf([Builder::array(), Builder::object()]), new stdClass(), []);
$this->assertValidationPassed(Builder::anyOf([Builder::integer(), Builder::null()]), null);
// valid - int (can be coerced to float, order of integer/number is important)
$this->assertValidationPassed(Builder::anyOf([Builder::integer(), Builder::number()]), 5);
$this->assertValidationPassed(Builder::anyOf([Builder::number(), Builder::integer()]), 5, 5.0);
// valid - float (can't be coerced to int, order of integer/number not important)
$this->assertValidationPassed(Builder::anyOf([Builder::integer(), Builder::number()]), 5.0);
$this->assertValidationPassed(Builder::anyOf([Builder::number(), Builder::integer()]), 5.0);
// invalid
$this->assertValidationFailed(Builder::anyOf([Builder::number(), Builder::integer()]), '5', 'value does not match any of the expected formats.');
$this->assertValidationFailed(Builder::anyOf([]), null, 'value is not a valid ');
$this->assertValidationFailed(Builder::anyOf([]), '', 'value is not a valid ');
$this->assertValidationFailed(Builder::anyOf([]), 'abc', 'value is not a valid ');
$this->assertValidationFailed(Builder::anyOf([]), [], 'value is not a valid ');
$this->assertValidationFailed(Builder::anyOf([]), true, 'value is not a valid ');
$this->assertValidationFailed(Builder::anyOf([]), false, 'value is not a valid ');
$this->assertValidationFailed(Builder::anyOf([]), 0, 'value is not a valid ');
}
public function testNullable(): void {
// valid
$this->assertValidationPassed(Builder::string()->nullable(), null);
$this->assertValidationPassed(Builder::number()->nullable(), null);
$this->assertValidationPassed(Builder::integer()->nullable(), null);
$this->assertValidationPassed(Builder::boolean()->nullable(), null);
$this->assertValidationPassed(Builder::null()->nullable(), null);
$this->assertValidationPassed(Builder::null()->nonNullable(), null);
$this->assertValidationPassed(Builder::array()->nullable(), null);
$this->assertValidationPassed(Builder::object()->nullable(), null);
$this->assertValidationPassed(Builder::oneOf([])->nullable(), null);
$this->assertValidationPassed(Builder::anyOf([])->nullable(), null);
// valid - oneOf/anyOf with schemas and values
$this->assertValidationPassed(Builder::oneOf([Builder::number(), Builder::string()])->nullable(), null);
$this->assertValidationPassed(Builder::anyOf([Builder::number(), Builder::string()])->nullable(), null);
$this->assertValidationPassed(Builder::oneOf([Builder::number(), Builder::string()])->nullable(), 5.0);
$this->assertValidationPassed(Builder::oneOf([Builder::number(), Builder::string()])->nullable(), 'abc');
$this->assertValidationPassed(Builder::anyOf([Builder::number(), Builder::string()])->nullable(), 5.0);
$this->assertValidationPassed(Builder::anyOf([Builder::number(), Builder::string()])->nullable(), 'abc');
// invalid
$this->assertValidationFailed(Builder::number()->nullable(), '', 'value is not of type number,null.');
$this->assertValidationFailed(Builder::number()->nullable(), '0', 'value is not of type number,null.');
$this->assertValidationFailed(Builder::number()->nullable(), false, 'value is not of type number,null.');
$this->assertValidationFailed(Builder::number()->nullable(), [], 'value is not of type number,null.');
$this->assertValidationFailed(Builder::string()->nullable(), 0, 'value is not of type string,null.');
$this->assertValidationFailed(Builder::boolean()->nullable(), 0, 'value is not of type boolean,null.');
}
public function testComplex(): void {
$schema = Builder::object()
->title('User')
->description('User schema definition')
->field('name', 'user')
->field('version', 1)
->properties([
'id' => Builder::string()->required()->formatUuid(),
'created_at' => Builder::string()->required()->formatDateTime(),
'username' => Builder::string()->required()->minLength(2)->maxLength(30)->pattern('^[a-z0-9]+$'),
'password' => Builder::string()->required()->minLength(8)->maxLength(1024),
'email' => Builder::string()->required()->formatEmail(),
'ip' => Builder::string()->required()->formatIp(),
'refresh_interval' => Builder::integer()->required()->multipleOf(3600),
'subscribed' => Builder::boolean()->default(false),
// nested object
'profile' => Builder::object([
'url' => Builder::string()->required()->formatUri(),
'photo_url' => Builder::string()->required()->nullable(),
'color' => Builder::string()->required()->formatHexColor(),
'age' => Builder::integer()->required(),
'rating' => Builder::integer()->required()->minimum(0)->maximum(5),
'score' => Builder::number()->required()->exclusiveMinimum(0)->maximum(100),
'distance' => Builder::number()->required(),
]),
// array of unique objects
'preferences' => Builder::array(
Builder::object([
'key' => Builder::string()->required(),
'value' => Builder::anyOf([Builder::string(), Builder::integer(), Builder::number()])->required(),
'meta' => Builder::string(),
])
)->uniqueItems(),
// pattern properties
'properties' => Builder::object()->patternProperties([
'^number_' => Builder::anyOf([Builder::integer(), Builder::number()]),
'^string_' => Builder::string(),
'^bool_' => Builder::boolean(),
]),
// oneOf
'linked_accounts' => Builder::array(
Builder::oneOf([
Builder::object([
'apple_id' => Builder::string()->required(),
]),
Builder::object([
'facebook_id' => Builder::string()->required(),
]),
Builder::object([
'google_id' => Builder::string()->required(),
]),
])
),
// additional properties, anyOf
'attributes' => Builder::object()->additionalProperties(
Builder::anyOf([
Builder::string(),
Builder::integer(),
Builder::boolean(),
Builder::number(),
Builder::null(),
])
),
]);
$this->assertSame(
[
'type' => 'object',
'title' => 'User',
'description' => 'User schema definition',
'name' => 'user',
'version' => 1,
'properties' => [
'id' => [
'type' => 'string',
'required' => true,
'format' => 'uuid',
],
'created_at' => [
'type' => 'string',
'required' => true,
'format' => 'date-time',
],
'username' => [
'type' => 'string',
'required' => true,
'minLength' => 2,
'maxLength' => 30,
'pattern' => '^[a-z0-9]+$',
],
'password' => [
'type' => 'string',
'required' => true,
'minLength' => 8,
'maxLength' => 1024,
],
'email' => [
'type' => 'string',
'required' => true,
'format' => 'email',
],
'ip' => [
'type' => 'string',
'required' => true,
'format' => 'ip',
],
'refresh_interval' => [
'type' => 'integer',
'required' => true,
'multipleOf' => 3600,
],
'subscribed' => [
'type' => 'boolean',
'default' => false,
],
'profile' => [
'type' => 'object',
'properties' => [
'url' => [
'type' => 'string',
'required' => true,
'format' => 'uri',
],
'photo_url' => [
'type' => ['string', 'null'],
'required' => true,
],
'color' => [
'type' => 'string',
'required' => true,
'format' => 'hex-color',
],
'age' => [
'type' => 'integer',
'required' => true,
],
'rating' => [
'type' => 'integer',
'required' => true,
'minimum' => 0,
'maximum' => 5,
],
'score' => [
'type' => 'number',
'required' => true,
'minimum' => 0.0,
'exclusiveMinimum' => true,
'maximum' => 100.0,
],
'distance' => [
'type' => 'number',
'required' => true,
],
],
],
'preferences' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'key' => [
'type' => 'string',
'required' => true,
],
'value' => [
'anyOf' => [
['type' => 'string'],
['type' => 'integer'],
['type' => 'number'],
],
'required' => true,
],
'meta' => [
'type' => 'string',
],
],
],
'uniqueItems' => true,
],
'properties' => [
'type' => 'object',
'patternProperties' => [
'^number_' => [
'anyOf' => [
['type' => 'integer'],
['type' => 'number'],
],
],
'^string_' => ['type' => 'string'],
'^bool_' => ['type' => 'boolean'],
],
],
'linked_accounts' => [
'type' => 'array',
'items' => [
'oneOf' => [
[
'type' => 'object',
'properties' => [
'apple_id' => [
'type' => 'string',
'required' => true,
],
],
],
[
'type' => 'object',
'properties' => [
'facebook_id' => [
'type' => 'string',
'required' => true,
],
],
],
[
'type' => 'object',
'properties' => [
'google_id' => [
'type' => 'string',
'required' => true,
],
],
],
],
],
],
'attributes' => [
'type' => 'object',
'additionalProperties' => [
'anyOf' => [
['type' => 'string'],
['type' => 'integer'],
['type' => 'boolean'],
['type' => 'number'],
['type' => 'null'],
],
],
],
],
],
$schema->toArray()
);
$this->assertSame(
$schema->toString(),
'{"type":"object","title":"User","description":"User schema definition","name":"user","version":1,"properties":{"id":{"type":"string","required":true,"format":"uuid"},"created_at":{"type":"string","required":true,"format":"date-time"},"username":{"type":"string","required":true,"minLength":2,"maxLength":30,"pattern":"^[a-z0-9]+$"},"password":{"type":"string","required":true,"minLength":8,"maxLength":1024},"email":{"type":"string","required":true,"format":"email"},"ip":{"type":"string","required":true,"format":"ip"},"refresh_interval":{"type":"integer","required":true,"multipleOf":3600},"subscribed":{"type":"boolean","default":false},"profile":{"type":"object","properties":{"url":{"type":"string","required":true,"format":"uri"},"photo_url":{"type":["string","null"],"required":true},"color":{"type":"string","required":true,"format":"hex-color"},"age":{"type":"integer","required":true},"rating":{"type":"integer","required":true,"minimum":0,"maximum":5},"score":{"type":"number","required":true,"minimum":0.0,"exclusiveMinimum":true,"maximum":100.0},"distance":{"type":"number","required":true}}},"preferences":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","required":true},"value":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"}],"required":true},"meta":{"type":"string"}}},"uniqueItems":true},"properties":{"type":"object","patternProperties":{"^number_":{"anyOf":[{"type":"integer"},{"type":"number"}]},"^string_":{"type":"string"},"^bool_":{"type":"boolean"}}},"linked_accounts":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"apple_id":{"type":"string","required":true}}},{"type":"object","properties":{"facebook_id":{"type":"string","required":true}}},{"type":"object","properties":{"google_id":{"type":"string","required":true}}}]}},"attributes":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"boolean"},{"type":"number"},{"type":"null"}]}}}}'
);
$this->assertValidationPassed(
$schema,
[
'id' => '4c30b777-9067-4a7b-b498-e295eabbf6ef',
'created_at' => '2022-03-23T15:21:37',
'username' => 'mailpoet',
'password' => '4f38f3eb301923fd38f3',
'email' => 'mailpoet@example.com',
'ip' => '145.8.23.191',
'refresh_interval' => 7200,
'subscribed' => false,
'profile' => [
'url' => 'https://example.com',
'photo_url' => null,
'color' => '#a5a5a5',
'age' => 38,
'rating' => 4,
'score' => 87.372,
'distance' => 1023.28,
],
'preferences' => [
['key' => 'theme', 'value' => 'dark'],
['key' => 'screen_with', 'value' => 1170, 'meta' => 'ios/safari'],
['key' => 'screen_with', 'value' => 3072, 'meta' => 'macos/chrome'],
],
'properties' => [
'bool_tracking_enabled' => true,
'number_tracking_page_views' => 12,
'string_tracking_id' => 'az0x8kw25as',
],
'linked_accounts' => [
['apple_id' => 'f794613bd965'],
['google_id' => 'f794613bd965'],
],
'attributes' => [
'browser' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36',
'language' => 'en_US',
'timezone' => 'Europe/Prague',
'login_attempts' => 2,
'is_admin' => false,
'referrer' => null,
'ping' => 173,
'risk_score' => 0.10378,
],
]
);
}
private function assertValidationPassed(Schema $schema, $value, $sanitizedValue = null): void {
$validator = $this->diContainer->get(Validator::class);
$sanitized = $validator->validate($schema, $value);
$this->assertSame($sanitizedValue ?? $value, $sanitized);
}
private function assertValidationFailed(Schema $schema, $value, string $message): ValidationException {
try {
$validator = $this->diContainer->get(Validator::class);
$validator->validate($schema, $value);
} catch (ValidationException $e) {
$this->assertSame($message, $e->getMessage());
return $e;
}
$class = ValidationException::class;
$this->fail("Exception '$class' with message '$message' was not thrown.");
}
}