+90
src/Validation/TypeValidators/ArrayValidator.php
+90
src/Validation/TypeValidators/ArrayValidator.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Validation\TypeValidators;
4
+
5
+
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
7
+
class ArrayValidator
8
+
{
9
+
/**
10
+
* Validate an array value against constraints.
11
+
*
12
+
* @param array<string, mixed> $definition
13
+
*/
14
+
public function validate(mixed $value, array $definition, string $path): void
15
+
{
16
+
if (! is_array($value)) {
17
+
throw RecordValidationException::invalidType($path, 'array', gettype($value));
18
+
}
19
+
20
+
$count = count($value);
21
+
22
+
// MaxItems constraint
23
+
if (isset($definition['maxItems'])) {
24
+
if ($count > $definition['maxItems']) {
25
+
throw RecordValidationException::invalidValue(
26
+
$path,
27
+
"Array length ({$count}) exceeds maximum ({$definition['maxItems']})"
28
+
);
29
+
}
30
+
}
31
+
32
+
// MinItems constraint
33
+
if (isset($definition['minItems'])) {
34
+
if ($count < $definition['minItems']) {
35
+
throw RecordValidationException::invalidValue(
36
+
$path,
37
+
"Array length ({$count}) is below minimum ({$definition['minItems']})"
38
+
);
39
+
}
40
+
}
41
+
42
+
// Validate items if item schema is provided
43
+
if (isset($definition['items']) && is_array($definition['items'])) {
44
+
$this->validateItems($value, $definition['items'], $path);
45
+
}
46
+
}
47
+
48
+
/**
49
+
* Validate array items.
50
+
*
51
+
* @param array<mixed> $items
52
+
* @param array<string, mixed> $itemDefinition
53
+
*/
54
+
protected function validateItems(array $items, array $itemDefinition, string $path): void
55
+
{
56
+
$itemType = $itemDefinition['type'] ?? null;
57
+
58
+
if ($itemType === null) {
59
+
return;
60
+
}
61
+
62
+
foreach ($items as $index => $item) {
63
+
$itemPath = "{$path}[{$index}]";
64
+
$this->validateItem($item, $itemDefinition, $itemPath);
65
+
}
66
+
}
67
+
68
+
/**
69
+
* Validate a single array item.
70
+
*
71
+
* @param array<string, mixed> $definition
72
+
*/
73
+
protected function validateItem(mixed $value, array $definition, string $path): void
74
+
{
75
+
$type = $definition['type'] ?? null;
76
+
77
+
$validator = match ($type) {
78
+
'string' => new StringValidator(),
79
+
'integer' => new IntegerValidator(),
80
+
'boolean' => new BooleanValidator(),
81
+
'object' => new ObjectValidator(),
82
+
'array' => new ArrayValidator(),
83
+
default => null,
84
+
};
85
+
86
+
if ($validator !== null) {
87
+
$validator->validate($value, $definition, $path);
88
+
}
89
+
}
90
+
}
+87
src/Validation/TypeValidators/BlobValidator.php
+87
src/Validation/TypeValidators/BlobValidator.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Validation\TypeValidators;
4
+
5
+
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
7
+
class BlobValidator
8
+
{
9
+
/**
10
+
* Validate a blob value against constraints.
11
+
*
12
+
* @param array<string, mixed> $definition
13
+
*/
14
+
public function validate(mixed $value, array $definition, string $path): void
15
+
{
16
+
// Blob should be an object with specific structure
17
+
if (! is_array($value)) {
18
+
throw RecordValidationException::invalidType($path, 'blob', gettype($value));
19
+
}
20
+
21
+
// Check for required blob structure
22
+
if (! isset($value['$type']) || $value['$type'] !== 'blob') {
23
+
throw RecordValidationException::invalidValue(
24
+
$path,
25
+
'Blob must have $type field set to "blob"'
26
+
);
27
+
}
28
+
29
+
// Validate ref (CID)
30
+
if (! isset($value['ref'])) {
31
+
throw RecordValidationException::invalidValue($path, 'Blob must have ref field');
32
+
}
33
+
34
+
// Validate mimeType
35
+
if (! isset($value['mimeType']) || ! is_string($value['mimeType'])) {
36
+
throw RecordValidationException::invalidValue($path, 'Blob must have valid mimeType');
37
+
}
38
+
39
+
// Validate size
40
+
if (! isset($value['size']) || ! is_int($value['size'])) {
41
+
throw RecordValidationException::invalidValue($path, 'Blob must have valid size');
42
+
}
43
+
44
+
// Validate MIME type constraint
45
+
if (isset($definition['accept']) && is_array($definition['accept'])) {
46
+
$this->validateMimeType($value['mimeType'], $definition['accept'], $path);
47
+
}
48
+
49
+
// Validate size constraints
50
+
if (isset($definition['maxSize'])) {
51
+
if ($value['size'] > $definition['maxSize']) {
52
+
throw RecordValidationException::invalidValue(
53
+
$path,
54
+
"Blob size ({$value['size']}) exceeds maximum ({$definition['maxSize']})"
55
+
);
56
+
}
57
+
}
58
+
}
59
+
60
+
/**
61
+
* Validate MIME type against accepted types.
62
+
*
63
+
* @param array<string> $acceptedTypes
64
+
*/
65
+
protected function validateMimeType(string $mimeType, array $acceptedTypes, string $path): void
66
+
{
67
+
foreach ($acceptedTypes as $acceptedType) {
68
+
// Handle wildcards (e.g., image/*)
69
+
if (str_contains($acceptedType, '*')) {
70
+
// Quote everything except the asterisk, then replace * with .*
71
+
$pattern = '/^'.str_replace('\\*', '.*', preg_quote($acceptedType, '/')).'$/';
72
+
if (preg_match($pattern, $mimeType)) {
73
+
return;
74
+
}
75
+
} elseif ($mimeType === $acceptedType) {
76
+
return;
77
+
}
78
+
}
79
+
80
+
$allowed = implode(', ', $acceptedTypes);
81
+
82
+
throw RecordValidationException::invalidValue(
83
+
$path,
84
+
"MIME type '{$mimeType}' not accepted. Allowed: {$allowed}"
85
+
);
86
+
}
87
+
}
+32
src/Validation/TypeValidators/BooleanValidator.php
+32
src/Validation/TypeValidators/BooleanValidator.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Validation\TypeValidators;
4
+
5
+
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
7
+
class BooleanValidator
8
+
{
9
+
/**
10
+
* Validate a boolean value.
11
+
*
12
+
* @param array<string, mixed> $definition
13
+
*/
14
+
public function validate(mixed $value, array $definition, string $path): void
15
+
{
16
+
if (! is_bool($value)) {
17
+
throw RecordValidationException::invalidType($path, 'boolean', gettype($value));
18
+
}
19
+
20
+
// Const constraint
21
+
if (isset($definition['const'])) {
22
+
if ($value !== $definition['const']) {
23
+
$expectedValue = $definition['const'] ? 'true' : 'false';
24
+
25
+
throw RecordValidationException::invalidValue(
26
+
$path,
27
+
"Value must be {$expectedValue}"
28
+
);
29
+
}
30
+
}
31
+
}
32
+
}
+62
src/Validation/TypeValidators/IntegerValidator.php
+62
src/Validation/TypeValidators/IntegerValidator.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Validation\TypeValidators;
4
+
5
+
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
7
+
class IntegerValidator
8
+
{
9
+
/**
10
+
* Validate an integer value against constraints.
11
+
*
12
+
* @param array<string, mixed> $definition
13
+
*/
14
+
public function validate(mixed $value, array $definition, string $path): void
15
+
{
16
+
if (! is_int($value)) {
17
+
throw RecordValidationException::invalidType($path, 'integer', gettype($value));
18
+
}
19
+
20
+
// Maximum constraint
21
+
if (isset($definition['maximum'])) {
22
+
if ($value > $definition['maximum']) {
23
+
throw RecordValidationException::invalidValue(
24
+
$path,
25
+
"Value ({$value}) exceeds maximum ({$definition['maximum']})"
26
+
);
27
+
}
28
+
}
29
+
30
+
// Minimum constraint
31
+
if (isset($definition['minimum'])) {
32
+
if ($value < $definition['minimum']) {
33
+
throw RecordValidationException::invalidValue(
34
+
$path,
35
+
"Value ({$value}) is below minimum ({$definition['minimum']})"
36
+
);
37
+
}
38
+
}
39
+
40
+
// Enum constraint
41
+
if (isset($definition['enum']) && is_array($definition['enum'])) {
42
+
if (! in_array($value, $definition['enum'], true)) {
43
+
$allowed = implode(', ', $definition['enum']);
44
+
45
+
throw RecordValidationException::invalidValue(
46
+
$path,
47
+
"Value must be one of: {$allowed}"
48
+
);
49
+
}
50
+
}
51
+
52
+
// Const constraint
53
+
if (isset($definition['const'])) {
54
+
if ($value !== $definition['const']) {
55
+
throw RecordValidationException::invalidValue(
56
+
$path,
57
+
"Value must be {$definition['const']}"
58
+
);
59
+
}
60
+
}
61
+
}
62
+
}
+63
src/Validation/TypeValidators/ObjectValidator.php
+63
src/Validation/TypeValidators/ObjectValidator.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Validation\TypeValidators;
4
+
5
+
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
7
+
class ObjectValidator
8
+
{
9
+
/**
10
+
* Validate an object value against constraints.
11
+
*
12
+
* @param array<string, mixed> $definition
13
+
*/
14
+
public function validate(mixed $value, array $definition, string $path): void
15
+
{
16
+
if (! is_array($value)) {
17
+
throw RecordValidationException::invalidType($path, 'object', gettype($value));
18
+
}
19
+
20
+
$properties = $definition['properties'] ?? [];
21
+
$required = $definition['required'] ?? [];
22
+
23
+
// Validate required fields
24
+
foreach ($required as $field) {
25
+
if (! array_key_exists($field, $value)) {
26
+
throw RecordValidationException::invalidValue(
27
+
"{$path}.{$field}",
28
+
'Required field is missing'
29
+
);
30
+
}
31
+
}
32
+
33
+
// Validate properties
34
+
foreach ($properties as $name => $propDef) {
35
+
if (array_key_exists($name, $value)) {
36
+
$this->validateProperty($value[$name], $propDef, "{$path}.{$name}");
37
+
}
38
+
}
39
+
}
40
+
41
+
/**
42
+
* Validate a single property.
43
+
*
44
+
* @param array<string, mixed> $definition
45
+
*/
46
+
protected function validateProperty(mixed $value, array $definition, string $path): void
47
+
{
48
+
$type = $definition['type'] ?? null;
49
+
50
+
$validator = match ($type) {
51
+
'string' => new StringValidator(),
52
+
'integer' => new IntegerValidator(),
53
+
'boolean' => new BooleanValidator(),
54
+
'object' => new ObjectValidator(),
55
+
'array' => new ArrayValidator(),
56
+
default => null,
57
+
};
58
+
59
+
if ($validator !== null) {
60
+
$validator->validate($value, $definition, $path);
61
+
}
62
+
}
63
+
}
+235
src/Validation/TypeValidators/StringValidator.php
+235
src/Validation/TypeValidators/StringValidator.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Validation\TypeValidators;
4
+
5
+
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
7
+
class StringValidator
8
+
{
9
+
/**
10
+
* Validate a string value against constraints.
11
+
*
12
+
* @param array<string, mixed> $definition
13
+
*/
14
+
public function validate(mixed $value, array $definition, string $path): void
15
+
{
16
+
if (! is_string($value)) {
17
+
throw RecordValidationException::invalidType($path, 'string', gettype($value));
18
+
}
19
+
20
+
// Length constraints
21
+
if (isset($definition['maxLength'])) {
22
+
$length = strlen($value);
23
+
if ($length > $definition['maxLength']) {
24
+
throw RecordValidationException::invalidValue(
25
+
$path,
26
+
"String length ({$length}) exceeds maximum ({$definition['maxLength']})"
27
+
);
28
+
}
29
+
}
30
+
31
+
if (isset($definition['minLength'])) {
32
+
$length = strlen($value);
33
+
if ($length < $definition['minLength']) {
34
+
throw RecordValidationException::invalidValue(
35
+
$path,
36
+
"String length ({$length}) is below minimum ({$definition['minLength']})"
37
+
);
38
+
}
39
+
}
40
+
41
+
// Grapheme constraints
42
+
if (isset($definition['maxGraphemes'])) {
43
+
$graphemes = grapheme_strlen($value);
44
+
if ($graphemes > $definition['maxGraphemes']) {
45
+
throw RecordValidationException::invalidValue(
46
+
$path,
47
+
"String graphemes ({$graphemes}) exceeds maximum ({$definition['maxGraphemes']})"
48
+
);
49
+
}
50
+
}
51
+
52
+
if (isset($definition['minGraphemes'])) {
53
+
$graphemes = grapheme_strlen($value);
54
+
if ($graphemes < $definition['minGraphemes']) {
55
+
throw RecordValidationException::invalidValue(
56
+
$path,
57
+
"String graphemes ({$graphemes}) is below minimum ({$definition['minGraphemes']})"
58
+
);
59
+
}
60
+
}
61
+
62
+
// Enum constraint
63
+
if (isset($definition['enum']) && is_array($definition['enum'])) {
64
+
if (! in_array($value, $definition['enum'], true)) {
65
+
$allowed = implode(', ', $definition['enum']);
66
+
67
+
throw RecordValidationException::invalidValue(
68
+
$path,
69
+
"Value must be one of: {$allowed}"
70
+
);
71
+
}
72
+
}
73
+
74
+
// Const constraint
75
+
if (isset($definition['const'])) {
76
+
if ($value !== $definition['const']) {
77
+
throw RecordValidationException::invalidValue(
78
+
$path,
79
+
"Value must be '{$definition['const']}'"
80
+
);
81
+
}
82
+
}
83
+
84
+
// Format validation
85
+
if (isset($definition['format'])) {
86
+
$this->validateFormat($value, $definition['format'], $path);
87
+
}
88
+
}
89
+
90
+
/**
91
+
* Validate string format.
92
+
*/
93
+
protected function validateFormat(string $value, string $format, string $path): void
94
+
{
95
+
$valid = match ($format) {
96
+
'datetime' => $this->validateDatetime($value),
97
+
'uri' => $this->validateUri($value),
98
+
'at-uri' => $this->validateAtUri($value),
99
+
'did' => $this->validateDid($value),
100
+
'handle' => $this->validateHandle($value),
101
+
'at-identifier' => $this->validateAtIdentifier($value),
102
+
'nsid' => $this->validateNsid($value),
103
+
'cid' => $this->validateCid($value),
104
+
'language' => $this->validateLanguage($value),
105
+
default => true, // Unknown formats pass
106
+
};
107
+
108
+
if (! $valid) {
109
+
throw RecordValidationException::invalidValue($path, "Invalid format: {$format}");
110
+
}
111
+
}
112
+
113
+
/**
114
+
* Validate datetime format.
115
+
*/
116
+
protected function validateDatetime(string $value): bool
117
+
{
118
+
$datetime = \DateTime::createFromFormat(\DateTime::ATOM, $value);
119
+
if ($datetime !== false) {
120
+
return true;
121
+
}
122
+
123
+
$datetime = \DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $value);
124
+
if ($datetime !== false) {
125
+
return true;
126
+
}
127
+
128
+
$datetime = \DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $value);
129
+
130
+
return $datetime !== false;
131
+
}
132
+
133
+
/**
134
+
* Validate URI format.
135
+
*/
136
+
protected function validateUri(string $value): bool
137
+
{
138
+
return filter_var($value, FILTER_VALIDATE_URL) !== false;
139
+
}
140
+
141
+
/**
142
+
* Validate AT URI format.
143
+
*/
144
+
protected function validateAtUri(string $value): bool
145
+
{
146
+
return str_starts_with($value, 'at://') && strlen($value) > 5;
147
+
}
148
+
149
+
/**
150
+
* Validate DID format.
151
+
*/
152
+
protected function validateDid(string $value): bool
153
+
{
154
+
return (bool) preg_match('/^did:[a-z]+:[a-zA-Z0-9._:%-]+$/', $value);
155
+
}
156
+
157
+
/**
158
+
* Validate handle format.
159
+
*/
160
+
protected function validateHandle(string $value): bool
161
+
{
162
+
if (strlen($value) < 3 || strlen($value) > 253) {
163
+
return false;
164
+
}
165
+
166
+
return (bool) preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/', $value);
167
+
}
168
+
169
+
/**
170
+
* Validate AT identifier (DID or handle).
171
+
*/
172
+
protected function validateAtIdentifier(string $value): bool
173
+
{
174
+
return $this->validateDid($value) || $this->validateHandle($value);
175
+
}
176
+
177
+
/**
178
+
* Validate NSID format.
179
+
*/
180
+
protected function validateNsid(string $value): bool
181
+
{
182
+
try {
183
+
\SocialDept\Schema\Parser\Nsid::parse($value);
184
+
185
+
return true;
186
+
} catch (\Exception) {
187
+
return false;
188
+
}
189
+
}
190
+
191
+
/**
192
+
* Validate CID format.
193
+
*/
194
+
protected function validateCid(string $value): bool
195
+
{
196
+
// CIDv0 or CIDv1
197
+
if (str_starts_with($value, 'Qm') && strlen($value) === 46) {
198
+
return (bool) preg_match('/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/', $value);
199
+
}
200
+
201
+
if ((str_starts_with($value, 'b') || str_starts_with($value, 'bafy')) && strlen($value) > 10) {
202
+
return (bool) preg_match('/^[a-z2-7]+$/', $value);
203
+
}
204
+
205
+
if (str_starts_with($value, 'z') && strlen($value) > 10) {
206
+
return (bool) preg_match('/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/', $value);
207
+
}
208
+
209
+
return false;
210
+
}
211
+
212
+
/**
213
+
* Validate language code (BCP 47).
214
+
*/
215
+
protected function validateLanguage(string $value): bool
216
+
{
217
+
$pattern = '/^
218
+
([a-z]{2,3}|[a-z]{4}|[a-z]{5,8})
219
+
(-[A-Z][a-z]{3})?
220
+
(-([A-Z]{2}|[0-9]{3}))?
221
+
(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*
222
+
(-[a-z]-[a-z0-9]{2,8})*
223
+
(-x-[a-z0-9]{1,8})?
224
+
$/xi';
225
+
226
+
if (! preg_match($pattern, $value)) {
227
+
return false;
228
+
}
229
+
230
+
$primaryLanguage = strtolower(explode('-', $value)[0]);
231
+
$length = strlen($primaryLanguage);
232
+
233
+
return $length >= 2 && $length <= 8;
234
+
}
235
+
}
+84
src/Validation/TypeValidators/UnionValidator.php
+84
src/Validation/TypeValidators/UnionValidator.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Validation\TypeValidators;
4
+
5
+
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
7
+
class UnionValidator
8
+
{
9
+
/**
10
+
* Validate a union value against constraints.
11
+
*
12
+
* @param array<string, mixed> $definition
13
+
*/
14
+
public function validate(mixed $value, array $definition, string $path): void
15
+
{
16
+
$refs = $definition['refs'] ?? [];
17
+
18
+
if (empty($refs)) {
19
+
throw RecordValidationException::invalidValue($path, 'Union must have refs defined');
20
+
}
21
+
22
+
// Check if union is discriminated (closed)
23
+
$closed = $definition['closed'] ?? false;
24
+
25
+
if ($closed) {
26
+
$this->validateDiscriminatedUnion($value, $refs, $path);
27
+
} else {
28
+
$this->validateOpenUnion($value, $refs, $path);
29
+
}
30
+
}
31
+
32
+
/**
33
+
* Validate discriminated (closed) union.
34
+
*
35
+
* @param array<string> $refs
36
+
*/
37
+
protected function validateDiscriminatedUnion(mixed $value, array $refs, string $path): void
38
+
{
39
+
if (! is_array($value)) {
40
+
throw RecordValidationException::invalidType($path, 'object', gettype($value));
41
+
}
42
+
43
+
// Check for $type discriminator
44
+
if (! isset($value['$type'])) {
45
+
throw RecordValidationException::invalidValue(
46
+
$path,
47
+
'Discriminated union must have $type field'
48
+
);
49
+
}
50
+
51
+
$type = $value['$type'];
52
+
53
+
// Validate that $type is one of the allowed refs
54
+
if (! in_array($type, $refs, true)) {
55
+
$allowed = implode(', ', $refs);
56
+
57
+
throw RecordValidationException::invalidValue(
58
+
$path,
59
+
"Union type '{$type}' not allowed. Must be one of: {$allowed}"
60
+
);
61
+
}
62
+
}
63
+
64
+
/**
65
+
* Validate open (undiscriminated) union.
66
+
*
67
+
* @param array<string> $refs
68
+
*/
69
+
protected function validateOpenUnion(mixed $value, array $refs, string $path): void
70
+
{
71
+
// For open unions, we just verify it's valid data
72
+
// The actual type checking would require schema resolution which is complex
73
+
// For now, we just ensure it's an object or primitive type
74
+
75
+
if (is_null($value)) {
76
+
throw RecordValidationException::invalidValue($path, 'Union value cannot be null');
77
+
}
78
+
79
+
// Open unions are flexible, so we allow objects and primitives
80
+
if (! is_array($value) && ! is_string($value) && ! is_int($value) && ! is_bool($value)) {
81
+
throw RecordValidationException::invalidType($path, 'valid union value', gettype($value));
82
+
}
83
+
}
84
+
}
+134
tests/Unit/Validation/TypeValidators/ArrayValidatorTest.php
+134
tests/Unit/Validation/TypeValidators/ArrayValidatorTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Validation\TypeValidators;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\RecordValidationException;
7
+
use SocialDept\Schema\Validation\TypeValidators\ArrayValidator;
8
+
9
+
class ArrayValidatorTest extends TestCase
10
+
{
11
+
protected ArrayValidator $validator;
12
+
13
+
protected function setUp(): void
14
+
{
15
+
parent::setUp();
16
+
17
+
$this->validator = new ArrayValidator();
18
+
}
19
+
20
+
public function test_it_validates_valid_array(): void
21
+
{
22
+
$this->validator->validate([1, 2, 3], ['type' => 'array'], '$.field');
23
+
24
+
$this->assertTrue(true);
25
+
}
26
+
27
+
public function test_it_rejects_non_array(): void
28
+
{
29
+
$this->expectException(RecordValidationException::class);
30
+
31
+
$this->validator->validate('not an array', ['type' => 'array'], '$.field');
32
+
}
33
+
34
+
public function test_it_validates_max_items(): void
35
+
{
36
+
$this->validator->validate([1, 2, 3], ['type' => 'array', 'maxItems' => 5], '$.field');
37
+
38
+
$this->assertTrue(true);
39
+
}
40
+
41
+
public function test_it_rejects_array_exceeding_max_items(): void
42
+
{
43
+
$this->expectException(RecordValidationException::class);
44
+
45
+
$this->validator->validate([1, 2, 3, 4, 5, 6], ['type' => 'array', 'maxItems' => 5], '$.field');
46
+
}
47
+
48
+
public function test_it_validates_min_items(): void
49
+
{
50
+
$this->validator->validate([1, 2, 3], ['type' => 'array', 'minItems' => 2], '$.field');
51
+
52
+
$this->assertTrue(true);
53
+
}
54
+
55
+
public function test_it_rejects_array_below_min_items(): void
56
+
{
57
+
$this->expectException(RecordValidationException::class);
58
+
59
+
$this->validator->validate([1], ['type' => 'array', 'minItems' => 3], '$.field');
60
+
}
61
+
62
+
public function test_it_validates_array_items(): void
63
+
{
64
+
$this->validator->validate(
65
+
['a', 'b', 'c'],
66
+
['type' => 'array', 'items' => ['type' => 'string']],
67
+
'$.field'
68
+
);
69
+
70
+
$this->assertTrue(true);
71
+
}
72
+
73
+
public function test_it_rejects_invalid_array_item(): void
74
+
{
75
+
$this->expectException(RecordValidationException::class);
76
+
77
+
$this->validator->validate(
78
+
['a', 123, 'c'],
79
+
['type' => 'array', 'items' => ['type' => 'string']],
80
+
'$.field'
81
+
);
82
+
}
83
+
84
+
public function test_it_validates_array_of_integers(): void
85
+
{
86
+
$this->validator->validate(
87
+
[1, 2, 3],
88
+
['type' => 'array', 'items' => ['type' => 'integer']],
89
+
'$.field'
90
+
);
91
+
92
+
$this->assertTrue(true);
93
+
}
94
+
95
+
public function test_it_validates_array_of_objects(): void
96
+
{
97
+
$this->validator->validate(
98
+
[
99
+
['name' => 'John'],
100
+
['name' => 'Jane'],
101
+
],
102
+
[
103
+
'type' => 'array',
104
+
'items' => [
105
+
'type' => 'object',
106
+
'required' => ['name'],
107
+
'properties' => [
108
+
'name' => ['type' => 'string'],
109
+
],
110
+
],
111
+
],
112
+
'$.field'
113
+
);
114
+
115
+
$this->assertTrue(true);
116
+
}
117
+
118
+
public function test_it_validates_nested_arrays(): void
119
+
{
120
+
$this->validator->validate(
121
+
[[1, 2], [3, 4]],
122
+
[
123
+
'type' => 'array',
124
+
'items' => [
125
+
'type' => 'array',
126
+
'items' => ['type' => 'integer'],
127
+
],
128
+
],
129
+
'$.field'
130
+
);
131
+
132
+
$this->assertTrue(true);
133
+
}
134
+
}
+182
tests/Unit/Validation/TypeValidators/BlobValidatorTest.php
+182
tests/Unit/Validation/TypeValidators/BlobValidatorTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Validation\TypeValidators;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\RecordValidationException;
7
+
use SocialDept\Schema\Validation\TypeValidators\BlobValidator;
8
+
9
+
class BlobValidatorTest extends TestCase
10
+
{
11
+
protected BlobValidator $validator;
12
+
13
+
protected function setUp(): void
14
+
{
15
+
parent::setUp();
16
+
17
+
$this->validator = new BlobValidator();
18
+
}
19
+
20
+
public function test_it_validates_valid_blob(): void
21
+
{
22
+
$this->validator->validate(
23
+
[
24
+
'$type' => 'blob',
25
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
26
+
'mimeType' => 'image/png',
27
+
'size' => 1024,
28
+
],
29
+
['type' => 'blob'],
30
+
'$.field'
31
+
);
32
+
33
+
$this->assertTrue(true);
34
+
}
35
+
36
+
public function test_it_rejects_non_array_blob(): void
37
+
{
38
+
$this->expectException(RecordValidationException::class);
39
+
40
+
$this->validator->validate('not a blob', ['type' => 'blob'], '$.field');
41
+
}
42
+
43
+
public function test_it_rejects_blob_without_type_field(): void
44
+
{
45
+
$this->expectException(RecordValidationException::class);
46
+
47
+
$this->validator->validate(
48
+
[
49
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
50
+
'mimeType' => 'image/png',
51
+
'size' => 1024,
52
+
],
53
+
['type' => 'blob'],
54
+
'$.field'
55
+
);
56
+
}
57
+
58
+
public function test_it_rejects_blob_without_ref(): void
59
+
{
60
+
$this->expectException(RecordValidationException::class);
61
+
62
+
$this->validator->validate(
63
+
[
64
+
'$type' => 'blob',
65
+
'mimeType' => 'image/png',
66
+
'size' => 1024,
67
+
],
68
+
['type' => 'blob'],
69
+
'$.field'
70
+
);
71
+
}
72
+
73
+
public function test_it_rejects_blob_without_mime_type(): void
74
+
{
75
+
$this->expectException(RecordValidationException::class);
76
+
77
+
$this->validator->validate(
78
+
[
79
+
'$type' => 'blob',
80
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
81
+
'size' => 1024,
82
+
],
83
+
['type' => 'blob'],
84
+
'$.field'
85
+
);
86
+
}
87
+
88
+
public function test_it_rejects_blob_without_size(): void
89
+
{
90
+
$this->expectException(RecordValidationException::class);
91
+
92
+
$this->validator->validate(
93
+
[
94
+
'$type' => 'blob',
95
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
96
+
'mimeType' => 'image/png',
97
+
],
98
+
['type' => 'blob'],
99
+
'$.field'
100
+
);
101
+
}
102
+
103
+
public function test_it_validates_accepted_mime_type(): void
104
+
{
105
+
$this->validator->validate(
106
+
[
107
+
'$type' => 'blob',
108
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
109
+
'mimeType' => 'image/png',
110
+
'size' => 1024,
111
+
],
112
+
['type' => 'blob', 'accept' => ['image/png', 'image/jpeg']],
113
+
'$.field'
114
+
);
115
+
116
+
$this->assertTrue(true);
117
+
}
118
+
119
+
public function test_it_rejects_unaccepted_mime_type(): void
120
+
{
121
+
$this->expectException(RecordValidationException::class);
122
+
123
+
$this->validator->validate(
124
+
[
125
+
'$type' => 'blob',
126
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
127
+
'mimeType' => 'video/mp4',
128
+
'size' => 1024,
129
+
],
130
+
['type' => 'blob', 'accept' => ['image/png', 'image/jpeg']],
131
+
'$.field'
132
+
);
133
+
}
134
+
135
+
public function test_it_validates_wildcard_mime_type(): void
136
+
{
137
+
$this->validator->validate(
138
+
[
139
+
'$type' => 'blob',
140
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
141
+
'mimeType' => 'image/webp',
142
+
'size' => 1024,
143
+
],
144
+
['type' => 'blob', 'accept' => ['image/*']],
145
+
'$.field'
146
+
);
147
+
148
+
$this->assertTrue(true);
149
+
}
150
+
151
+
public function test_it_validates_max_size(): void
152
+
{
153
+
$this->validator->validate(
154
+
[
155
+
'$type' => 'blob',
156
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
157
+
'mimeType' => 'image/png',
158
+
'size' => 1024,
159
+
],
160
+
['type' => 'blob', 'maxSize' => 2048],
161
+
'$.field'
162
+
);
163
+
164
+
$this->assertTrue(true);
165
+
}
166
+
167
+
public function test_it_rejects_blob_exceeding_max_size(): void
168
+
{
169
+
$this->expectException(RecordValidationException::class);
170
+
171
+
$this->validator->validate(
172
+
[
173
+
'$type' => 'blob',
174
+
'ref' => 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi',
175
+
'mimeType' => 'image/png',
176
+
'size' => 3000,
177
+
],
178
+
['type' => 'blob', 'maxSize' => 2048],
179
+
'$.field'
180
+
);
181
+
}
182
+
}
+68
tests/Unit/Validation/TypeValidators/BooleanValidatorTest.php
+68
tests/Unit/Validation/TypeValidators/BooleanValidatorTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Validation\TypeValidators;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\RecordValidationException;
7
+
use SocialDept\Schema\Validation\TypeValidators\BooleanValidator;
8
+
9
+
class BooleanValidatorTest extends TestCase
10
+
{
11
+
protected BooleanValidator $validator;
12
+
13
+
protected function setUp(): void
14
+
{
15
+
parent::setUp();
16
+
17
+
$this->validator = new BooleanValidator();
18
+
}
19
+
20
+
public function test_it_validates_true(): void
21
+
{
22
+
$this->validator->validate(true, ['type' => 'boolean'], '$.field');
23
+
24
+
$this->assertTrue(true);
25
+
}
26
+
27
+
public function test_it_validates_false(): void
28
+
{
29
+
$this->validator->validate(false, ['type' => 'boolean'], '$.field');
30
+
31
+
$this->assertTrue(true);
32
+
}
33
+
34
+
public function test_it_rejects_non_boolean(): void
35
+
{
36
+
$this->expectException(RecordValidationException::class);
37
+
38
+
$this->validator->validate('not a boolean', ['type' => 'boolean'], '$.field');
39
+
}
40
+
41
+
public function test_it_rejects_integer_zero(): void
42
+
{
43
+
$this->expectException(RecordValidationException::class);
44
+
45
+
$this->validator->validate(0, ['type' => 'boolean'], '$.field');
46
+
}
47
+
48
+
public function test_it_validates_const_true(): void
49
+
{
50
+
$this->validator->validate(true, ['type' => 'boolean', 'const' => true], '$.field');
51
+
52
+
$this->assertTrue(true);
53
+
}
54
+
55
+
public function test_it_validates_const_false(): void
56
+
{
57
+
$this->validator->validate(false, ['type' => 'boolean', 'const' => false], '$.field');
58
+
59
+
$this->assertTrue(true);
60
+
}
61
+
62
+
public function test_it_rejects_value_not_matching_const(): void
63
+
{
64
+
$this->expectException(RecordValidationException::class);
65
+
66
+
$this->validator->validate(false, ['type' => 'boolean', 'const' => true], '$.field');
67
+
}
68
+
}
+89
tests/Unit/Validation/TypeValidators/IntegerValidatorTest.php
+89
tests/Unit/Validation/TypeValidators/IntegerValidatorTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Validation\TypeValidators;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\RecordValidationException;
7
+
use SocialDept\Schema\Validation\TypeValidators\IntegerValidator;
8
+
9
+
class IntegerValidatorTest extends TestCase
10
+
{
11
+
protected IntegerValidator $validator;
12
+
13
+
protected function setUp(): void
14
+
{
15
+
parent::setUp();
16
+
17
+
$this->validator = new IntegerValidator();
18
+
}
19
+
20
+
public function test_it_validates_valid_integer(): void
21
+
{
22
+
$this->validator->validate(42, ['type' => 'integer'], '$.field');
23
+
24
+
$this->assertTrue(true);
25
+
}
26
+
27
+
public function test_it_rejects_non_integer(): void
28
+
{
29
+
$this->expectException(RecordValidationException::class);
30
+
31
+
$this->validator->validate('not an integer', ['type' => 'integer'], '$.field');
32
+
}
33
+
34
+
public function test_it_validates_maximum_constraint(): void
35
+
{
36
+
$this->validator->validate(50, ['type' => 'integer', 'maximum' => 100], '$.field');
37
+
38
+
$this->assertTrue(true);
39
+
}
40
+
41
+
public function test_it_rejects_value_exceeding_maximum(): void
42
+
{
43
+
$this->expectException(RecordValidationException::class);
44
+
45
+
$this->validator->validate(150, ['type' => 'integer', 'maximum' => 100], '$.field');
46
+
}
47
+
48
+
public function test_it_validates_minimum_constraint(): void
49
+
{
50
+
$this->validator->validate(50, ['type' => 'integer', 'minimum' => 10], '$.field');
51
+
52
+
$this->assertTrue(true);
53
+
}
54
+
55
+
public function test_it_rejects_value_below_minimum(): void
56
+
{
57
+
$this->expectException(RecordValidationException::class);
58
+
59
+
$this->validator->validate(5, ['type' => 'integer', 'minimum' => 10], '$.field');
60
+
}
61
+
62
+
public function test_it_validates_enum_constraint(): void
63
+
{
64
+
$this->validator->validate(2, ['type' => 'integer', 'enum' => [1, 2, 3]], '$.field');
65
+
66
+
$this->assertTrue(true);
67
+
}
68
+
69
+
public function test_it_rejects_value_not_in_enum(): void
70
+
{
71
+
$this->expectException(RecordValidationException::class);
72
+
73
+
$this->validator->validate(5, ['type' => 'integer', 'enum' => [1, 2, 3]], '$.field');
74
+
}
75
+
76
+
public function test_it_validates_const_constraint(): void
77
+
{
78
+
$this->validator->validate(42, ['type' => 'integer', 'const' => 42], '$.field');
79
+
80
+
$this->assertTrue(true);
81
+
}
82
+
83
+
public function test_it_rejects_value_not_matching_const(): void
84
+
{
85
+
$this->expectException(RecordValidationException::class);
86
+
87
+
$this->validator->validate(100, ['type' => 'integer', 'const' => 42], '$.field');
88
+
}
89
+
}
+166
tests/Unit/Validation/TypeValidators/ObjectValidatorTest.php
+166
tests/Unit/Validation/TypeValidators/ObjectValidatorTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Validation\TypeValidators;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\RecordValidationException;
7
+
use SocialDept\Schema\Validation\TypeValidators\ObjectValidator;
8
+
9
+
class ObjectValidatorTest extends TestCase
10
+
{
11
+
protected ObjectValidator $validator;
12
+
13
+
protected function setUp(): void
14
+
{
15
+
parent::setUp();
16
+
17
+
$this->validator = new ObjectValidator();
18
+
}
19
+
20
+
public function test_it_validates_valid_object(): void
21
+
{
22
+
$this->validator->validate(
23
+
['name' => 'John'],
24
+
[
25
+
'type' => 'object',
26
+
'required' => ['name'],
27
+
'properties' => [
28
+
'name' => ['type' => 'string'],
29
+
],
30
+
],
31
+
'$.field'
32
+
);
33
+
34
+
$this->assertTrue(true);
35
+
}
36
+
37
+
public function test_it_rejects_non_object(): void
38
+
{
39
+
$this->expectException(RecordValidationException::class);
40
+
41
+
$this->validator->validate('not an object', ['type' => 'object'], '$.field');
42
+
}
43
+
44
+
public function test_it_validates_object_with_multiple_properties(): void
45
+
{
46
+
$this->validator->validate(
47
+
['name' => 'John', 'age' => 30],
48
+
[
49
+
'type' => 'object',
50
+
'required' => ['name', 'age'],
51
+
'properties' => [
52
+
'name' => ['type' => 'string'],
53
+
'age' => ['type' => 'integer'],
54
+
],
55
+
],
56
+
'$.field'
57
+
);
58
+
59
+
$this->assertTrue(true);
60
+
}
61
+
62
+
public function test_it_rejects_missing_required_field(): void
63
+
{
64
+
$this->expectException(RecordValidationException::class);
65
+
66
+
$this->validator->validate(
67
+
['name' => 'John'],
68
+
[
69
+
'type' => 'object',
70
+
'required' => ['name', 'age'],
71
+
'properties' => [
72
+
'name' => ['type' => 'string'],
73
+
'age' => ['type' => 'integer'],
74
+
],
75
+
],
76
+
'$.field'
77
+
);
78
+
}
79
+
80
+
public function test_it_validates_optional_properties(): void
81
+
{
82
+
$this->validator->validate(
83
+
['name' => 'John'],
84
+
[
85
+
'type' => 'object',
86
+
'required' => ['name'],
87
+
'properties' => [
88
+
'name' => ['type' => 'string'],
89
+
'age' => ['type' => 'integer'],
90
+
],
91
+
],
92
+
'$.field'
93
+
);
94
+
95
+
$this->assertTrue(true);
96
+
}
97
+
98
+
public function test_it_validates_nested_objects(): void
99
+
{
100
+
$this->validator->validate(
101
+
[
102
+
'user' => [
103
+
'name' => 'John',
104
+
'profile' => [
105
+
'bio' => 'Developer',
106
+
],
107
+
],
108
+
],
109
+
[
110
+
'type' => 'object',
111
+
'required' => ['user'],
112
+
'properties' => [
113
+
'user' => [
114
+
'type' => 'object',
115
+
'required' => ['name', 'profile'],
116
+
'properties' => [
117
+
'name' => ['type' => 'string'],
118
+
'profile' => [
119
+
'type' => 'object',
120
+
'required' => ['bio'],
121
+
'properties' => [
122
+
'bio' => ['type' => 'string'],
123
+
],
124
+
],
125
+
],
126
+
],
127
+
],
128
+
],
129
+
'$.field'
130
+
);
131
+
132
+
$this->assertTrue(true);
133
+
}
134
+
135
+
public function test_it_rejects_invalid_property_type(): void
136
+
{
137
+
$this->expectException(RecordValidationException::class);
138
+
139
+
$this->validator->validate(
140
+
['name' => 123],
141
+
[
142
+
'type' => 'object',
143
+
'required' => ['name'],
144
+
'properties' => [
145
+
'name' => ['type' => 'string'],
146
+
],
147
+
],
148
+
'$.field'
149
+
);
150
+
}
151
+
152
+
public function test_it_validates_empty_object(): void
153
+
{
154
+
$this->validator->validate(
155
+
[],
156
+
[
157
+
'type' => 'object',
158
+
'required' => [],
159
+
'properties' => [],
160
+
],
161
+
'$.field'
162
+
);
163
+
164
+
$this->assertTrue(true);
165
+
}
166
+
}
+213
tests/Unit/Validation/TypeValidators/StringValidatorTest.php
+213
tests/Unit/Validation/TypeValidators/StringValidatorTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Validation\TypeValidators;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\RecordValidationException;
7
+
use SocialDept\Schema\Validation\TypeValidators\StringValidator;
8
+
9
+
class StringValidatorTest extends TestCase
10
+
{
11
+
protected StringValidator $validator;
12
+
13
+
protected function setUp(): void
14
+
{
15
+
parent::setUp();
16
+
17
+
$this->validator = new StringValidator();
18
+
}
19
+
20
+
public function test_it_validates_valid_string(): void
21
+
{
22
+
$this->validator->validate('test', ['type' => 'string'], '$.field');
23
+
24
+
$this->assertTrue(true);
25
+
}
26
+
27
+
public function test_it_rejects_non_string(): void
28
+
{
29
+
$this->expectException(RecordValidationException::class);
30
+
31
+
$this->validator->validate(123, ['type' => 'string'], '$.field');
32
+
}
33
+
34
+
public function test_it_validates_max_length(): void
35
+
{
36
+
$this->validator->validate('test', ['type' => 'string', 'maxLength' => 10], '$.field');
37
+
38
+
$this->assertTrue(true);
39
+
}
40
+
41
+
public function test_it_rejects_string_exceeding_max_length(): void
42
+
{
43
+
$this->expectException(RecordValidationException::class);
44
+
45
+
$this->validator->validate('this is too long', ['type' => 'string', 'maxLength' => 5], '$.field');
46
+
}
47
+
48
+
public function test_it_validates_min_length(): void
49
+
{
50
+
$this->validator->validate('test', ['type' => 'string', 'minLength' => 3], '$.field');
51
+
52
+
$this->assertTrue(true);
53
+
}
54
+
55
+
public function test_it_rejects_string_below_min_length(): void
56
+
{
57
+
$this->expectException(RecordValidationException::class);
58
+
59
+
$this->validator->validate('ab', ['type' => 'string', 'minLength' => 5], '$.field');
60
+
}
61
+
62
+
public function test_it_validates_max_graphemes(): void
63
+
{
64
+
$this->validator->validate('😀😁😂', ['type' => 'string', 'maxGraphemes' => 5], '$.field');
65
+
66
+
$this->assertTrue(true);
67
+
}
68
+
69
+
public function test_it_rejects_string_exceeding_max_graphemes(): void
70
+
{
71
+
$this->expectException(RecordValidationException::class);
72
+
73
+
$this->validator->validate('😀😁😂😃😄😅', ['type' => 'string', 'maxGraphemes' => 5], '$.field');
74
+
}
75
+
76
+
public function test_it_validates_min_graphemes(): void
77
+
{
78
+
$this->validator->validate('😀😁😂😃😄', ['type' => 'string', 'minGraphemes' => 3], '$.field');
79
+
80
+
$this->assertTrue(true);
81
+
}
82
+
83
+
public function test_it_rejects_string_below_min_graphemes(): void
84
+
{
85
+
$this->expectException(RecordValidationException::class);
86
+
87
+
$this->validator->validate('😀😁', ['type' => 'string', 'minGraphemes' => 5], '$.field');
88
+
}
89
+
90
+
public function test_it_validates_enum_constraint(): void
91
+
{
92
+
$this->validator->validate('active', [
93
+
'type' => 'string',
94
+
'enum' => ['active', 'inactive', 'pending'],
95
+
], '$.field');
96
+
97
+
$this->assertTrue(true);
98
+
}
99
+
100
+
public function test_it_rejects_value_not_in_enum(): void
101
+
{
102
+
$this->expectException(RecordValidationException::class);
103
+
104
+
$this->validator->validate('unknown', [
105
+
'type' => 'string',
106
+
'enum' => ['active', 'inactive', 'pending'],
107
+
], '$.field');
108
+
}
109
+
110
+
public function test_it_validates_const_constraint(): void
111
+
{
112
+
$this->validator->validate('post', ['type' => 'string', 'const' => 'post'], '$.field');
113
+
114
+
$this->assertTrue(true);
115
+
}
116
+
117
+
public function test_it_rejects_value_not_matching_const(): void
118
+
{
119
+
$this->expectException(RecordValidationException::class);
120
+
121
+
$this->validator->validate('comment', ['type' => 'string', 'const' => 'post'], '$.field');
122
+
}
123
+
124
+
public function test_it_validates_datetime_format(): void
125
+
{
126
+
$this->validator->validate('2024-01-01T00:00:00Z', [
127
+
'type' => 'string',
128
+
'format' => 'datetime',
129
+
], '$.field');
130
+
131
+
$this->assertTrue(true);
132
+
}
133
+
134
+
public function test_it_rejects_invalid_datetime(): void
135
+
{
136
+
$this->expectException(RecordValidationException::class);
137
+
138
+
$this->validator->validate('not-a-datetime', [
139
+
'type' => 'string',
140
+
'format' => 'datetime',
141
+
], '$.field');
142
+
}
143
+
144
+
public function test_it_validates_uri_format(): void
145
+
{
146
+
$this->validator->validate('https://example.com', [
147
+
'type' => 'string',
148
+
'format' => 'uri',
149
+
], '$.field');
150
+
151
+
$this->assertTrue(true);
152
+
}
153
+
154
+
public function test_it_validates_did_format(): void
155
+
{
156
+
$this->validator->validate('did:plc:z72i7hdynmk6r22z27h6tvur', [
157
+
'type' => 'string',
158
+
'format' => 'did',
159
+
], '$.field');
160
+
161
+
$this->assertTrue(true);
162
+
}
163
+
164
+
public function test_it_validates_handle_format(): void
165
+
{
166
+
$this->validator->validate('user.bsky.social', [
167
+
'type' => 'string',
168
+
'format' => 'handle',
169
+
], '$.field');
170
+
171
+
$this->assertTrue(true);
172
+
}
173
+
174
+
public function test_it_validates_nsid_format(): void
175
+
{
176
+
$this->validator->validate('app.bsky.feed.post', [
177
+
'type' => 'string',
178
+
'format' => 'nsid',
179
+
], '$.field');
180
+
181
+
$this->assertTrue(true);
182
+
}
183
+
184
+
public function test_it_validates_cid_format(): void
185
+
{
186
+
$this->validator->validate('bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi', [
187
+
'type' => 'string',
188
+
'format' => 'cid',
189
+
], '$.field');
190
+
191
+
$this->assertTrue(true);
192
+
}
193
+
194
+
public function test_it_validates_language_format(): void
195
+
{
196
+
$this->validator->validate('en-US', [
197
+
'type' => 'string',
198
+
'format' => 'language',
199
+
], '$.field');
200
+
201
+
$this->assertTrue(true);
202
+
}
203
+
204
+
public function test_it_passes_unknown_formats(): void
205
+
{
206
+
$this->validator->validate('anything', [
207
+
'type' => 'string',
208
+
'format' => 'unknown-format',
209
+
], '$.field');
210
+
211
+
$this->assertTrue(true);
212
+
}
213
+
}
+153
tests/Unit/Validation/TypeValidators/UnionValidatorTest.php
+153
tests/Unit/Validation/TypeValidators/UnionValidatorTest.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Validation\TypeValidators;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Exceptions\RecordValidationException;
7
+
use SocialDept\Schema\Validation\TypeValidators\UnionValidator;
8
+
9
+
class UnionValidatorTest extends TestCase
10
+
{
11
+
protected UnionValidator $validator;
12
+
13
+
protected function setUp(): void
14
+
{
15
+
parent::setUp();
16
+
17
+
$this->validator = new UnionValidator();
18
+
}
19
+
20
+
public function test_it_validates_discriminated_union(): void
21
+
{
22
+
$this->validator->validate(
23
+
['$type' => 'app.bsky.feed.post'],
24
+
[
25
+
'type' => 'union',
26
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
27
+
'closed' => true,
28
+
],
29
+
'$.field'
30
+
);
31
+
32
+
$this->assertTrue(true);
33
+
}
34
+
35
+
public function test_it_rejects_discriminated_union_without_type(): void
36
+
{
37
+
$this->expectException(RecordValidationException::class);
38
+
39
+
$this->validator->validate(
40
+
['text' => 'Hello'],
41
+
[
42
+
'type' => 'union',
43
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
44
+
'closed' => true,
45
+
],
46
+
'$.field'
47
+
);
48
+
}
49
+
50
+
public function test_it_rejects_discriminated_union_with_invalid_type(): void
51
+
{
52
+
$this->expectException(RecordValidationException::class);
53
+
54
+
$this->validator->validate(
55
+
['$type' => 'app.bsky.feed.invalid'],
56
+
[
57
+
'type' => 'union',
58
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
59
+
'closed' => true,
60
+
],
61
+
'$.field'
62
+
);
63
+
}
64
+
65
+
public function test_it_rejects_non_object_for_discriminated_union(): void
66
+
{
67
+
$this->expectException(RecordValidationException::class);
68
+
69
+
$this->validator->validate(
70
+
'not an object',
71
+
[
72
+
'type' => 'union',
73
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
74
+
'closed' => true,
75
+
],
76
+
'$.field'
77
+
);
78
+
}
79
+
80
+
public function test_it_validates_open_union_with_object(): void
81
+
{
82
+
$this->validator->validate(
83
+
['data' => 'value'],
84
+
[
85
+
'type' => 'union',
86
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
87
+
'closed' => false,
88
+
],
89
+
'$.field'
90
+
);
91
+
92
+
$this->assertTrue(true);
93
+
}
94
+
95
+
public function test_it_validates_open_union_with_string(): void
96
+
{
97
+
$this->validator->validate(
98
+
'some value',
99
+
[
100
+
'type' => 'union',
101
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
102
+
'closed' => false,
103
+
],
104
+
'$.field'
105
+
);
106
+
107
+
$this->assertTrue(true);
108
+
}
109
+
110
+
public function test_it_validates_open_union_with_integer(): void
111
+
{
112
+
$this->validator->validate(
113
+
123,
114
+
[
115
+
'type' => 'union',
116
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
117
+
'closed' => false,
118
+
],
119
+
'$.field'
120
+
);
121
+
122
+
$this->assertTrue(true);
123
+
}
124
+
125
+
public function test_it_rejects_open_union_with_null(): void
126
+
{
127
+
$this->expectException(RecordValidationException::class);
128
+
129
+
$this->validator->validate(
130
+
null,
131
+
[
132
+
'type' => 'union',
133
+
'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'],
134
+
'closed' => false,
135
+
],
136
+
'$.field'
137
+
);
138
+
}
139
+
140
+
public function test_it_rejects_union_without_refs(): void
141
+
{
142
+
$this->expectException(RecordValidationException::class);
143
+
144
+
$this->validator->validate(
145
+
['$type' => 'app.bsky.feed.post'],
146
+
[
147
+
'type' => 'union',
148
+
'closed' => true,
149
+
],
150
+
'$.field'
151
+
);
152
+
}
153
+
}