Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 12 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Tests\Integration; 4 5use Illuminate\Support\Facades\Storage; 6use Orchestra\Testbench\TestCase; 7use SocialDept\AtpSchema\Data\LexiconDocument; 8use SocialDept\AtpSchema\Parser\SchemaLoader; 9use SocialDept\AtpSchema\Services\BlobHandler; 10use SocialDept\AtpSchema\Validation\Validator; 11 12class 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}