Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add comprehensive test suite

+979
+362
tests/Integration/CompleteWorkflowTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Integration; 4 + 5 + use Illuminate\Http\UploadedFile; 6 + use Illuminate\Support\Facades\Storage; 7 + use Orchestra\Testbench\TestCase; 8 + use SocialDept\Schema\Contracts\LexiconRegistry; 9 + use SocialDept\Schema\Data\LexiconDocument; 10 + use SocialDept\Schema\Parser\SchemaLoader; 11 + use SocialDept\Schema\Services\BlobHandler; 12 + use SocialDept\Schema\Services\UnionResolver; 13 + use SocialDept\Schema\Support\ExtensionManager; 14 + use SocialDept\Schema\Validation\Validator; 15 + 16 + class CompleteWorkflowTest extends TestCase 17 + { 18 + protected function setUp(): void 19 + { 20 + parent::setUp(); 21 + 22 + Storage::fake('local'); 23 + } 24 + 25 + public function test_complete_post_creation_workflow(): void 26 + { 27 + // Step 1: Load schema 28 + $schemaLoader = new SchemaLoader([]); 29 + $schema = LexiconDocument::fromArray([ 30 + 'lexicon' => 1, 31 + 'id' => 'app.bsky.feed.post', 32 + 'defs' => [ 33 + 'main' => [ 34 + 'type' => 'record', 35 + 'key' => 'tid', 36 + 'record' => [ 37 + 'type' => 'object', 38 + 'required' => ['text', 'createdAt'], 39 + 'properties' => [ 40 + 'text' => [ 41 + 'type' => 'string', 42 + 'maxLength' => 300, 43 + 'maxGraphemes' => 300, 44 + ], 45 + 'createdAt' => [ 46 + 'type' => 'string', 47 + 'format' => 'datetime', 48 + ], 49 + 'embed' => [ 50 + 'type' => 'union', 51 + 'refs' => ['app.bsky.embed.images', 'app.bsky.embed.external'], 52 + 'closed' => true, 53 + ], 54 + ], 55 + ], 56 + ], 57 + ], 58 + ]); 59 + 60 + // Step 2: Create post data 61 + $postData = [ 62 + 'text' => 'Check out this amazing photo!', 63 + 'createdAt' => '2024-01-01T12:00:00Z', 64 + 'embed' => [ 65 + '$type' => 'app.bsky.embed.images', 66 + 'images' => [], 67 + ], 68 + ]; 69 + 70 + // Step 3: Validate 71 + $validator = new Validator($schemaLoader); 72 + $isValid = $validator->validate($postData, $schema); 73 + 74 + $this->assertTrue($isValid); 75 + 76 + // Step 4: Verify union type 77 + $unionResolver = new UnionResolver(); 78 + $embedType = $unionResolver->extractType($postData['embed']); 79 + 80 + $this->assertEquals('app.bsky.embed.images', $embedType); 81 + } 82 + 83 + public function test_image_upload_with_validation_workflow(): void 84 + { 85 + // Step 1: Upload image 86 + $blobHandler = new BlobHandler('local'); 87 + $file = UploadedFile::fake()->image('photo.jpg', 800, 600); 88 + 89 + $constraints = [ 90 + 'accept' => ['image/*'], 91 + 'maxSize' => 1024 * 1024 * 5, // 5MB 92 + ]; 93 + 94 + $blob = $blobHandler->store($file, $constraints); 95 + 96 + // Step 2: Verify blob 97 + $this->assertStringStartsWith('bafyrei', $blob->ref); 98 + $this->assertTrue($blob->isImage()); 99 + 100 + // Step 3: Create record with blob 101 + $schemaLoader = new SchemaLoader([]); 102 + $schema = LexiconDocument::fromArray([ 103 + 'lexicon' => 1, 104 + 'id' => 'app.bsky.embed.images', 105 + 'defs' => [ 106 + 'main' => [ 107 + 'type' => 'record', 108 + 'key' => 'tid', 109 + 'record' => [ 110 + 'type' => 'object', 111 + 'properties' => [ 112 + 'images' => [ 113 + 'type' => 'array', 114 + 'maxLength' => 4, 115 + 'items' => [ 116 + 'type' => 'object', 117 + 'properties' => [ 118 + 'image' => [ 119 + 'type' => 'blob', 120 + 'accept' => ['image/*'], 121 + ], 122 + 'alt' => [ 123 + 'type' => 'string', 124 + 'maxLength' => 1000, 125 + ], 126 + ], 127 + ], 128 + ], 129 + ], 130 + ], 131 + ], 132 + ], 133 + ]); 134 + 135 + $data = [ 136 + 'images' => [ 137 + [ 138 + 'image' => $blob->toArray(), 139 + 'alt' => 'A beautiful sunset', 140 + ], 141 + ], 142 + ]; 143 + 144 + // Step 4: Validate 145 + $validator = new Validator($schemaLoader); 146 + $isValid = $validator->validate($data, $schema); 147 + 148 + $this->assertTrue($isValid); 149 + 150 + // Step 5: Retrieve blob content 151 + $content = $blobHandler->get($blob->ref); 152 + 153 + $this->assertNotNull($content); 154 + } 155 + 156 + public function test_validation_error_formatting_workflow(): void 157 + { 158 + $schemaLoader = new SchemaLoader([]); 159 + $schema = LexiconDocument::fromArray([ 160 + 'lexicon' => 1, 161 + 'id' => 'app.bsky.feed.post', 162 + 'defs' => [ 163 + 'main' => [ 164 + 'type' => 'record', 165 + 'key' => 'tid', 166 + 'record' => [ 167 + 'type' => 'object', 168 + 'required' => ['text', 'createdAt'], 169 + 'properties' => [ 170 + 'text' => ['type' => 'string', 'maxLength' => 10], 171 + 'createdAt' => ['type' => 'string', 'format' => 'datetime'], 172 + ], 173 + ], 174 + ], 175 + ], 176 + ]); 177 + 178 + $invalidData = [ 179 + 'text' => 'This text is way too long for the constraint', 180 + ]; 181 + 182 + $validator = new Validator($schemaLoader); 183 + $validator->validate($invalidData, $schema); 184 + 185 + // Get errors 186 + $errors = $validator->validateWithErrors($invalidData, $schema); 187 + 188 + $this->assertNotEmpty($errors); 189 + 190 + // Errors are in Laravel format 191 + $this->assertArrayHasKey('text', $errors); 192 + $this->assertArrayHasKey('createdAt', $errors); // Missing required field 193 + 194 + // Verify error messages 195 + $this->assertIsArray($errors['text']); 196 + $this->assertNotEmpty($errors['text'][0]); 197 + $this->assertIsArray($errors['createdAt']); 198 + $this->assertNotEmpty($errors['createdAt'][0]); 199 + } 200 + 201 + public function test_extension_hooks_workflow(): void 202 + { 203 + $extensions = new ExtensionManager(); 204 + 205 + // Register validation hook 206 + $extensions->hook('before:validate', function ($data) { 207 + // Transform data before validation 208 + if (isset($data['text'])) { 209 + $data['text'] = trim($data['text']); 210 + } 211 + 212 + return $data; 213 + }); 214 + 215 + // Register post-validation hook 216 + $executed = false; 217 + $extensions->hook('after:validate', function ($result) use (&$executed) { 218 + $executed = true; 219 + 220 + return $result; 221 + }); 222 + 223 + // Execute hooks 224 + $data = ['text' => ' Hello, world! ']; 225 + $transformed = $extensions->filter('before:validate', $data); 226 + 227 + $this->assertEquals('Hello, world!', $transformed['text']); 228 + 229 + $extensions->execute('after:validate', true); 230 + 231 + $this->assertTrue($executed); 232 + } 233 + 234 + public function test_schema_registry_workflow(): void 235 + { 236 + $registry = new SimpleRegistry(); 237 + 238 + // Register schemas 239 + $schemaLoader = new SchemaLoader([]); 240 + 241 + $postSchema = LexiconDocument::fromArray([ 242 + 'lexicon' => 1, 243 + 'id' => 'app.bsky.feed.post', 244 + 'defs' => ['main' => ['type' => 'record', 'key' => 'tid']], 245 + ]); 246 + 247 + $repostSchema = LexiconDocument::fromArray([ 248 + 'lexicon' => 1, 249 + 'id' => 'app.bsky.feed.repost', 250 + 'defs' => ['main' => ['type' => 'record', 'key' => 'tid']], 251 + ]); 252 + 253 + $registry->register($postSchema); 254 + $registry->register($repostSchema); 255 + 256 + // Retrieve schemas 257 + $this->assertTrue($registry->has('app.bsky.feed.post')); 258 + $this->assertTrue($registry->has('app.bsky.feed.repost')); 259 + 260 + $retrieved = $registry->get('app.bsky.feed.post'); 261 + $this->assertInstanceOf(LexiconDocument::class, $retrieved); 262 + $this->assertEquals('app.bsky.feed.post', $retrieved->getNsid()); 263 + 264 + // Use with union resolver 265 + $unionResolver = new UnionResolver($registry); 266 + 267 + $data = ['$type' => 'app.bsky.feed.post']; 268 + $unionDef = [ 269 + 'type' => 'union', 270 + 'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'], 271 + 'closed' => true, 272 + ]; 273 + 274 + $typeDef = $unionResolver->getTypeDefinition($data, $unionDef); 275 + 276 + $this->assertInstanceOf(LexiconDocument::class, $typeDef); 277 + $this->assertEquals('app.bsky.feed.post', $typeDef->getNsid()); 278 + } 279 + 280 + public function test_multimode_validation_workflow(): void 281 + { 282 + $schemaLoader = new SchemaLoader([]); 283 + $schema = LexiconDocument::fromArray([ 284 + 'lexicon' => 1, 285 + 'id' => 'app.test.record', 286 + 'defs' => [ 287 + 'main' => [ 288 + 'type' => 'record', 289 + 'key' => 'tid', 290 + 'record' => [ 291 + 'type' => 'object', 292 + 'required' => ['name'], 293 + 'properties' => [ 294 + 'name' => ['type' => 'string'], 295 + 'age' => ['type' => 'integer', 'minimum' => 0], 296 + ], 297 + ], 298 + ], 299 + ], 300 + ]); 301 + 302 + $validator = new Validator($schemaLoader); 303 + 304 + $dataWithExtra = [ 305 + 'name' => 'Alice', 306 + 'age' => 30, 307 + 'unknownField' => 'value', 308 + ]; 309 + 310 + // Strict mode 311 + $validator->setMode(Validator::MODE_STRICT); 312 + $this->assertFalse($validator->validate($dataWithExtra, $schema)); 313 + 314 + // Optimistic mode 315 + $validator->setMode(Validator::MODE_OPTIMISTIC); 316 + $this->assertTrue($validator->validate($dataWithExtra, $schema)); 317 + 318 + // Lenient mode 319 + $validator->setMode(Validator::MODE_LENIENT); 320 + $this->assertTrue($validator->validate($dataWithExtra, $schema)); 321 + 322 + // Lenient mode ignores constraints 323 + $invalidAge = ['name' => 'Bob', 'age' => -5]; 324 + $validator->setMode(Validator::MODE_LENIENT); 325 + $this->assertTrue($validator->validate($invalidAge, $schema)); 326 + 327 + // But optimistic/strict catch it 328 + $validator->setMode(Validator::MODE_OPTIMISTIC); 329 + $this->assertFalse($validator->validate($invalidAge, $schema)); 330 + } 331 + } 332 + 333 + // Simple registry implementation for testing 334 + class SimpleRegistry implements LexiconRegistry 335 + { 336 + protected array $schemas = []; 337 + 338 + public function register(LexiconDocument $document): void 339 + { 340 + $this->schemas[$document->getNsid()] = $document; 341 + } 342 + 343 + public function get(string $nsid): ?LexiconDocument 344 + { 345 + return $this->schemas[$nsid] ?? null; 346 + } 347 + 348 + public function has(string $nsid): bool 349 + { 350 + return isset($this->schemas[$nsid]); 351 + } 352 + 353 + public function all(): array 354 + { 355 + return $this->schemas; 356 + } 357 + 358 + public function clear(): void 359 + { 360 + $this->schemas = []; 361 + } 362 + }
+264
tests/Integration/ModelMappingIntegrationTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Integration; 4 + 5 + use Orchestra\Testbench\TestCase; 6 + use SocialDept\Schema\Contracts\Transformer; 7 + use SocialDept\Schema\Data\LexiconDocument; 8 + use SocialDept\Schema\Parser\SchemaLoader; 9 + use SocialDept\Schema\Services\ModelMapper; 10 + use SocialDept\Schema\Validation\Validator; 11 + 12 + class ModelMappingIntegrationTest extends TestCase 13 + { 14 + protected ModelMapper $mapper; 15 + 16 + protected Validator $validator; 17 + 18 + protected SchemaLoader $schemaLoader; 19 + 20 + protected function setUp(): void 21 + { 22 + parent::setUp(); 23 + 24 + $this->mapper = new ModelMapper(); 25 + $this->schemaLoader = new SchemaLoader([]); 26 + $this->validator = new Validator($this->schemaLoader); 27 + } 28 + 29 + public function test_it_transforms_and_validates_complete_workflow(): void 30 + { 31 + // Register transformer 32 + $this->mapper->register('app.bsky.feed.post', new PostTransformer()); 33 + 34 + // Load schema 35 + $schema = LexiconDocument::fromArray([ 36 + 'lexicon' => 1, 37 + 'id' => 'app.bsky.feed.post', 38 + 'defs' => [ 39 + 'main' => [ 40 + 'type' => 'record', 41 + 'key' => 'tid', 42 + 'record' => [ 43 + 'type' => 'object', 44 + 'required' => ['text', 'createdAt'], 45 + 'properties' => [ 46 + 'text' => ['type' => 'string', 'maxLength' => 300], 47 + 'createdAt' => ['type' => 'string', 'format' => 'datetime'], 48 + ], 49 + ], 50 + ], 51 + ], 52 + ]); 53 + 54 + // Transform to model 55 + $model = $this->mapper->fromArray('app.bsky.feed.post', [ 56 + 'text' => 'Hello, world!', 57 + 'createdAt' => '2024-01-01T00:00:00Z', 58 + ]); 59 + 60 + // Verify model 61 + $this->assertInstanceOf(Post::class, $model); 62 + $this->assertEquals('Hello, world!', $model->text); 63 + 64 + // Transform back to array 65 + $data = $this->mapper->toArray('app.bsky.feed.post', $model); 66 + 67 + // Validate transformed data 68 + $result = $this->validator->validate($data, $schema); 69 + 70 + $this->assertTrue($result); 71 + } 72 + 73 + public function test_it_handles_multiple_model_transformations(): void 74 + { 75 + $this->mapper->registerMany([ 76 + 'app.bsky.feed.post' => new PostTransformer(), 77 + 'app.bsky.feed.repost' => new RepostTransformer(), 78 + ]); 79 + 80 + $posts = [ 81 + ['text' => 'First post', 'createdAt' => '2024-01-01T00:00:00Z'], 82 + ['text' => 'Second post', 'createdAt' => '2024-01-02T00:00:00Z'], 83 + ]; 84 + 85 + $models = $this->mapper->fromArrayMany('app.bsky.feed.post', $posts); 86 + 87 + $this->assertCount(2, $models); 88 + $this->assertContainsOnlyInstancesOf(Post::class, $models); 89 + 90 + $arrays = $this->mapper->toArrayMany('app.bsky.feed.post', $models); 91 + 92 + $this->assertEquals($posts, $arrays); 93 + } 94 + 95 + public function test_it_extends_mapper_with_macros(): void 96 + { 97 + ModelMapper::macro('validateAndTransform', function ($type, $data, $schema) { 98 + // Validate first 99 + if (! $this->validator->validate($data, $schema)) { 100 + return null; 101 + } 102 + 103 + // Then transform 104 + return $this->fromArray($type, $data); 105 + }); 106 + 107 + $this->mapper->validator = $this->validator; 108 + $this->mapper->register('app.bsky.feed.post', new PostTransformer()); 109 + 110 + $schema = LexiconDocument::fromArray([ 111 + 'lexicon' => 1, 112 + 'id' => 'app.bsky.feed.post', 113 + 'defs' => [ 114 + 'main' => [ 115 + 'type' => 'record', 116 + 'key' => 'tid', 117 + 'record' => [ 118 + 'type' => 'object', 119 + 'required' => ['text'], 120 + 'properties' => [ 121 + 'text' => ['type' => 'string'], 122 + 'createdAt' => ['type' => 'string'], 123 + ], 124 + ], 125 + ], 126 + ], 127 + ]); 128 + 129 + $validData = ['text' => 'Hello', 'createdAt' => '2024-01-01T00:00:00Z']; 130 + 131 + $result = $this->mapper->validateAndTransform('app.bsky.feed.post', $validData, $schema); 132 + 133 + $this->assertInstanceOf(Post::class, $result); 134 + 135 + ModelMapper::flushMacros(); 136 + } 137 + 138 + public function test_it_handles_nested_transformations(): void 139 + { 140 + $this->mapper->register('app.bsky.actor.profile', new ProfileTransformer()); 141 + 142 + $data = [ 143 + 'displayName' => 'Alice', 144 + 'description' => 'Developer', 145 + 'avatar' => [ 146 + 'url' => 'https://example.com/avatar.jpg', 147 + 'size' => 12345, 148 + ], 149 + ]; 150 + 151 + $model = $this->mapper->fromArray('app.bsky.actor.profile', $data); 152 + 153 + $this->assertInstanceOf(Profile::class, $model); 154 + $this->assertEquals('Alice', $model->displayName); 155 + $this->assertIsArray($model->avatar); 156 + 157 + $transformed = $this->mapper->toArray('app.bsky.actor.profile', $model); 158 + 159 + $this->assertEquals($data, $transformed); 160 + } 161 + } 162 + 163 + // Test models and transformers 164 + class Post 165 + { 166 + public function __construct( 167 + public string $text, 168 + public string $createdAt 169 + ) { 170 + } 171 + } 172 + 173 + class PostTransformer implements Transformer 174 + { 175 + public function fromArray(array $data): Post 176 + { 177 + return new Post( 178 + text: $data['text'], 179 + createdAt: $data['createdAt'] 180 + ); 181 + } 182 + 183 + public function toArray(mixed $model): array 184 + { 185 + return [ 186 + 'text' => $model->text, 187 + 'createdAt' => $model->createdAt, 188 + ]; 189 + } 190 + 191 + public function supports(string $type): bool 192 + { 193 + return $type === 'app.bsky.feed.post'; 194 + } 195 + } 196 + 197 + class Repost 198 + { 199 + public function __construct( 200 + public string $uri, 201 + public string $createdAt 202 + ) { 203 + } 204 + } 205 + 206 + class RepostTransformer implements Transformer 207 + { 208 + public function fromArray(array $data): Repost 209 + { 210 + return new Repost( 211 + uri: $data['uri'], 212 + createdAt: $data['createdAt'] 213 + ); 214 + } 215 + 216 + public function toArray(mixed $model): array 217 + { 218 + return [ 219 + 'uri' => $model->uri, 220 + 'createdAt' => $model->createdAt, 221 + ]; 222 + } 223 + 224 + public function supports(string $type): bool 225 + { 226 + return $type === 'app.bsky.feed.repost'; 227 + } 228 + } 229 + 230 + class Profile 231 + { 232 + public function __construct( 233 + public string $displayName, 234 + public string $description, 235 + public array $avatar 236 + ) { 237 + } 238 + } 239 + 240 + class ProfileTransformer implements Transformer 241 + { 242 + public function fromArray(array $data): Profile 243 + { 244 + return new Profile( 245 + displayName: $data['displayName'], 246 + description: $data['description'], 247 + avatar: $data['avatar'] 248 + ); 249 + } 250 + 251 + public function toArray(mixed $model): array 252 + { 253 + return [ 254 + 'displayName' => $model->displayName, 255 + 'description' => $model->description, 256 + 'avatar' => $model->avatar, 257 + ]; 258 + } 259 + 260 + public function supports(string $type): bool 261 + { 262 + return $type === 'app.bsky.actor.profile'; 263 + } 264 + }
+353
tests/Integration/ValidationIntegrationTest.php
··· 1 + <?php 2 + 3 + namespace SocialDept\Schema\Tests\Integration; 4 + 5 + use Illuminate\Support\Facades\Storage; 6 + use Orchestra\Testbench\TestCase; 7 + use SocialDept\Schema\Data\LexiconDocument; 8 + use SocialDept\Schema\Parser\SchemaLoader; 9 + use SocialDept\Schema\Services\BlobHandler; 10 + use SocialDept\Schema\Validation\Validator; 11 + 12 + class ValidationIntegrationTest extends TestCase 13 + { 14 + protected SchemaLoader $schemaLoader; 15 + 16 + protected Validator $validator; 17 + 18 + protected function setUp(): void 19 + { 20 + parent::setUp(); 21 + 22 + $this->schemaLoader = new SchemaLoader([]); 23 + $this->validator = new Validator($this->schemaLoader); 24 + } 25 + 26 + public function test_it_validates_complete_record_with_all_types(): void 27 + { 28 + $schema = LexiconDocument::fromArray([ 29 + 'lexicon' => 1, 30 + 'id' => 'app.test.post', 31 + 'defs' => [ 32 + 'main' => [ 33 + 'type' => 'record', 34 + 'key' => 'tid', 35 + 'record' => [ 36 + 'type' => 'object', 37 + 'required' => ['text', 'createdAt'], 38 + 'properties' => [ 39 + 'text' => [ 40 + 'type' => 'string', 41 + 'maxLength' => 300, 42 + 'maxGraphemes' => 300, 43 + ], 44 + 'createdAt' => [ 45 + 'type' => 'string', 46 + 'format' => 'datetime', 47 + ], 48 + 'facets' => [ 49 + 'type' => 'array', 50 + 'items' => [ 51 + 'type' => 'object', 52 + 'properties' => [ 53 + 'index' => ['type' => 'integer'], 54 + 'features' => [ 55 + 'type' => 'array', 56 + 'items' => ['type' => 'string'], 57 + ], 58 + ], 59 + ], 60 + ], 61 + 'embed' => [ 62 + 'type' => 'union', 63 + 'refs' => ['app.test.images', 'app.test.external'], 64 + 'closed' => true, 65 + ], 66 + ], 67 + ], 68 + ], 69 + ], 70 + ]); 71 + 72 + $validData = [ 73 + 'text' => 'Hello, world!', 74 + 'createdAt' => '2024-01-01T00:00:00Z', 75 + 'facets' => [ 76 + [ 77 + 'index' => 0, 78 + 'features' => ['mention', 'link'], 79 + ], 80 + ], 81 + 'embed' => [ 82 + '$type' => 'app.test.images', 83 + 'images' => [], 84 + ], 85 + ]; 86 + 87 + $result = $this->validator->validate($validData, $schema); 88 + 89 + $this->assertTrue($result); 90 + } 91 + 92 + public function test_it_detects_multiple_validation_errors(): void 93 + { 94 + $schema = LexiconDocument::fromArray([ 95 + 'lexicon' => 1, 96 + 'id' => 'app.test.post', 97 + 'defs' => [ 98 + 'main' => [ 99 + 'type' => 'record', 100 + 'key' => 'tid', 101 + 'record' => [ 102 + 'type' => 'object', 103 + 'required' => ['text', 'createdAt'], 104 + 'properties' => [ 105 + 'text' => [ 106 + 'type' => 'string', 107 + 'maxLength' => 10, 108 + ], 109 + 'createdAt' => [ 110 + 'type' => 'string', 111 + 'format' => 'datetime', 112 + ], 113 + 'count' => [ 114 + 'type' => 'integer', 115 + 'minimum' => 0, 116 + 'maximum' => 100, 117 + ], 118 + ], 119 + ], 120 + ], 121 + ], 122 + ]); 123 + 124 + $invalidData = [ 125 + 'text' => 'This is a very long text that exceeds the maximum length', 126 + 'count' => 150, 127 + ]; 128 + 129 + $result = $this->validator->validate($invalidData, $schema); 130 + 131 + $this->assertFalse($result); 132 + 133 + $errors = $this->validator->validateWithErrors($invalidData, $schema); 134 + 135 + $this->assertArrayHasKey('text', $errors); 136 + $this->assertArrayHasKey('createdAt', $errors); // Missing required field 137 + $this->assertArrayHasKey('count', $errors); 138 + } 139 + 140 + public function test_it_validates_nested_objects_deeply(): void 141 + { 142 + $schema = LexiconDocument::fromArray([ 143 + 'lexicon' => 1, 144 + 'id' => 'app.test.nested', 145 + 'defs' => [ 146 + 'main' => [ 147 + 'type' => 'record', 148 + 'key' => 'tid', 149 + 'record' => [ 150 + 'type' => 'object', 151 + 'properties' => [ 152 + 'user' => [ 153 + 'type' => 'object', 154 + 'required' => ['name'], 155 + 'properties' => [ 156 + 'name' => ['type' => 'string'], 157 + 'profile' => [ 158 + 'type' => 'object', 159 + 'properties' => [ 160 + 'bio' => ['type' => 'string', 'maxLength' => 100], 161 + 'avatar' => [ 162 + 'type' => 'object', 163 + 'properties' => [ 164 + 'url' => ['type' => 'string', 'format' => 'uri'], 165 + ], 166 + ], 167 + ], 168 + ], 169 + ], 170 + ], 171 + ], 172 + ], 173 + ], 174 + ], 175 + ]); 176 + 177 + $validData = [ 178 + 'user' => [ 179 + 'name' => 'Alice', 180 + 'profile' => [ 181 + 'bio' => 'Software developer', 182 + 'avatar' => [ 183 + 'url' => 'https://example.com/avatar.jpg', 184 + ], 185 + ], 186 + ], 187 + ]; 188 + 189 + $result = $this->validator->validate($validData, $schema); 190 + 191 + $this->assertTrue($result); 192 + } 193 + 194 + public function test_it_validates_with_blob_handler_integration(): void 195 + { 196 + Storage::fake('local'); 197 + 198 + $blobHandler = new BlobHandler('local'); 199 + 200 + $schema = LexiconDocument::fromArray([ 201 + 'lexicon' => 1, 202 + 'id' => 'app.test.image', 203 + 'defs' => [ 204 + 'main' => [ 205 + 'type' => 'record', 206 + 'key' => 'tid', 207 + 'record' => [ 208 + 'type' => 'object', 209 + 'required' => ['image'], 210 + 'properties' => [ 211 + 'image' => [ 212 + 'type' => 'blob', 213 + 'accept' => ['image/*'], 214 + 'maxSize' => 1024 * 1024, 215 + ], 216 + ], 217 + ], 218 + ], 219 + ], 220 + ]); 221 + 222 + // Create a blob 223 + $blob = $blobHandler->storeFromString('test image content', 'image/png'); 224 + 225 + // Validate with blob data 226 + $validData = [ 227 + 'image' => $blob->toArray(), 228 + ]; 229 + 230 + $result = $this->validator->validate($validData, $schema); 231 + 232 + $this->assertTrue($result); 233 + } 234 + 235 + public function test_it_handles_array_validation_with_constraints(): void 236 + { 237 + $schema = LexiconDocument::fromArray([ 238 + 'lexicon' => 1, 239 + 'id' => 'app.test.list', 240 + 'defs' => [ 241 + 'main' => [ 242 + 'type' => 'record', 243 + 'key' => 'tid', 244 + 'record' => [ 245 + 'type' => 'object', 246 + 'properties' => [ 247 + 'tags' => [ 248 + 'type' => 'array', 249 + 'minLength' => 1, 250 + 'maxLength' => 5, 251 + 'items' => [ 252 + 'type' => 'string', 253 + 'maxLength' => 20, 254 + ], 255 + ], 256 + ], 257 + ], 258 + ], 259 + ], 260 + ]); 261 + 262 + $validData = [ 263 + 'tags' => ['tag1', 'tag2', 'tag3'], 264 + ]; 265 + 266 + $result = $this->validator->validate($validData, $schema); 267 + 268 + $this->assertTrue($result); 269 + 270 + // Invalid: tag item too long 271 + $invalidData = [ 272 + 'tags' => ['tag1', 'this is a very long tag that exceeds the maximum length of 20 characters'], 273 + ]; 274 + 275 + $result = $this->validator->validate($invalidData, $schema); 276 + 277 + $this->assertFalse($result); 278 + } 279 + 280 + public function test_it_validates_different_modes(): void 281 + { 282 + $schema = LexiconDocument::fromArray([ 283 + 'lexicon' => 1, 284 + 'id' => 'app.test.strict', 285 + 'defs' => [ 286 + 'main' => [ 287 + 'type' => 'record', 288 + 'key' => 'tid', 289 + 'record' => [ 290 + 'type' => 'object', 291 + 'required' => ['name'], 292 + 'properties' => [ 293 + 'name' => ['type' => 'string'], 294 + ], 295 + ], 296 + ], 297 + ], 298 + ]); 299 + 300 + $dataWithUnknownField = [ 301 + 'name' => 'Alice', 302 + 'unknownField' => 'value', 303 + ]; 304 + 305 + // STRICT mode - should reject unknown fields 306 + $this->validator->setMode(Validator::MODE_STRICT); 307 + $result = $this->validator->validate($dataWithUnknownField, $schema); 308 + $this->assertFalse($result); 309 + 310 + // OPTIMISTIC mode - should allow unknown fields 311 + $this->validator->setMode(Validator::MODE_OPTIMISTIC); 312 + $result = $this->validator->validate($dataWithUnknownField, $schema); 313 + $this->assertTrue($result); 314 + } 315 + 316 + public function test_it_validates_with_all_format_types(): void 317 + { 318 + $schema = LexiconDocument::fromArray([ 319 + 'lexicon' => 1, 320 + 'id' => 'app.test.formats', 321 + 'defs' => [ 322 + 'main' => [ 323 + 'type' => 'record', 324 + 'key' => 'tid', 325 + 'record' => [ 326 + 'type' => 'object', 327 + 'properties' => [ 328 + 'datetime' => ['type' => 'string', 'format' => 'datetime'], 329 + 'uri' => ['type' => 'string', 'format' => 'uri'], 330 + 'atUri' => ['type' => 'string', 'format' => 'at-uri'], 331 + 'did' => ['type' => 'string', 'format' => 'did'], 332 + 'nsid' => ['type' => 'string', 'format' => 'nsid'], 333 + 'cid' => ['type' => 'string', 'format' => 'cid'], 334 + ], 335 + ], 336 + ], 337 + ], 338 + ]); 339 + 340 + $validData = [ 341 + 'datetime' => '2024-01-01T00:00:00Z', 342 + 'uri' => 'https://example.com', 343 + 'atUri' => 'at://did:plc:abc123/app.bsky.feed.post/123', 344 + 'did' => 'did:plc:abc123', 345 + 'nsid' => 'app.bsky.feed.post', 346 + 'cid' => 'bafyreigabcdefghijklmnopqrstuvwxyz234567', 347 + ]; 348 + 349 + $result = $this->validator->validate($validData, $schema); 350 + 351 + $this->assertTrue($result); 352 + } 353 + }