Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 428 lines 12 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Tests\Unit\Services; 4 5use Orchestra\Testbench\TestCase; 6use SocialDept\AtpSchema\Contracts\LexiconRegistry; 7use SocialDept\AtpSchema\Data\LexiconDocument; 8use SocialDept\AtpSchema\Exceptions\RecordValidationException; 9use SocialDept\AtpSchema\Parser\Nsid; 10use SocialDept\AtpSchema\Services\UnionResolver; 11 12class UnionResolverTest extends TestCase 13{ 14 protected UnionResolver $resolver; 15 16 protected function setUp(): void 17 { 18 parent::setUp(); 19 20 $this->resolver = new UnionResolver(); 21 } 22 23 public function test_it_resolves_discriminated_union(): void 24 { 25 $data = ['$type' => 'app.bsky.feed.post']; 26 27 $unionDef = [ 28 'type' => 'union', 29 'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'], 30 'closed' => true, 31 ]; 32 33 $type = $this->resolver->resolve($data, $unionDef); 34 35 $this->assertEquals('app.bsky.feed.post', $type); 36 } 37 38 public function test_it_returns_null_for_open_union(): void 39 { 40 $data = ['text' => 'Hello']; 41 42 $unionDef = [ 43 'type' => 'union', 44 'refs' => ['app.bsky.feed.post'], 45 'closed' => false, 46 ]; 47 48 $type = $this->resolver->resolve($data, $unionDef); 49 50 $this->assertNull($type); 51 } 52 53 public function test_it_throws_exception_for_discriminated_union_without_type(): void 54 { 55 $this->expectException(RecordValidationException::class); 56 57 $data = ['text' => 'Hello']; 58 59 $unionDef = [ 60 'type' => 'union', 61 'refs' => ['app.bsky.feed.post'], 62 'closed' => true, 63 ]; 64 65 $this->resolver->resolve($data, $unionDef); 66 } 67 68 public function test_it_throws_exception_for_invalid_type(): void 69 { 70 $this->expectException(RecordValidationException::class); 71 72 $data = ['$type' => 'app.bsky.feed.invalid']; 73 74 $unionDef = [ 75 'type' => 'union', 76 'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'], 77 'closed' => true, 78 ]; 79 80 $this->resolver->resolve($data, $unionDef); 81 } 82 83 public function test_it_throws_exception_for_non_object_discriminated_union(): void 84 { 85 $this->expectException(RecordValidationException::class); 86 87 $unionDef = [ 88 'type' => 'union', 89 'refs' => ['app.bsky.feed.post'], 90 'closed' => true, 91 ]; 92 93 $this->resolver->resolve('not an object', $unionDef); 94 } 95 96 public function test_it_checks_if_data_matches_type(): void 97 { 98 $data = ['$type' => 'app.bsky.feed.post']; 99 100 $unionDef = [ 101 'type' => 'union', 102 'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'], 103 'closed' => true, 104 ]; 105 106 $this->assertTrue($this->resolver->matches($data, 'app.bsky.feed.post', $unionDef)); 107 $this->assertFalse($this->resolver->matches($data, 'app.bsky.feed.repost', $unionDef)); 108 } 109 110 public function test_it_returns_false_for_invalid_data_when_checking_match(): void 111 { 112 $data = ['text' => 'Hello']; 113 114 $unionDef = [ 115 'type' => 'union', 116 'refs' => ['app.bsky.feed.post'], 117 'closed' => true, 118 ]; 119 120 $this->assertFalse($this->resolver->matches($data, 'app.bsky.feed.post', $unionDef)); 121 } 122 123 public function test_it_returns_false_for_open_union_when_checking_match(): void 124 { 125 $data = ['$type' => 'app.bsky.feed.post']; 126 127 $unionDef = [ 128 'type' => 'union', 129 'refs' => ['app.bsky.feed.post'], 130 'closed' => false, 131 ]; 132 133 $this->assertFalse($this->resolver->matches($data, 'app.bsky.feed.post', $unionDef)); 134 } 135 136 public function test_it_gets_type_definition_with_registry(): void 137 { 138 // Create a simple registry implementation 139 $registry = new class () implements LexiconRegistry { 140 public function register(LexiconDocument $document): void 141 { 142 } 143 144 public function get(string $nsid): ?LexiconDocument 145 { 146 return new LexiconDocument( 147 1, 148 Nsid::parse('app.bsky.feed.post'), 149 ['main' => ['type' => 'record']] 150 ); 151 } 152 153 public function has(string $nsid): bool 154 { 155 return true; 156 } 157 158 public function all(): array 159 { 160 return []; 161 } 162 163 public function clear(): void 164 { 165 } 166 }; 167 168 $this->resolver->setRegistry($registry); 169 170 $data = ['$type' => 'app.bsky.feed.post']; 171 172 $unionDef = [ 173 'type' => 'union', 174 'refs' => ['app.bsky.feed.post'], 175 'closed' => true, 176 ]; 177 178 $result = $this->resolver->getTypeDefinition($data, $unionDef); 179 180 $this->assertInstanceOf(LexiconDocument::class, $result); 181 $this->assertEquals('app.bsky.feed.post', $result->getNsid()); 182 } 183 184 public function test_it_returns_null_for_type_definition_without_registry(): void 185 { 186 $data = ['$type' => 'app.bsky.feed.post']; 187 188 $unionDef = [ 189 'type' => 'union', 190 'refs' => ['app.bsky.feed.post'], 191 'closed' => true, 192 ]; 193 194 $result = $this->resolver->getTypeDefinition($data, $unionDef); 195 196 $this->assertNull($result); 197 } 198 199 public function test_it_returns_null_for_type_definition_with_open_union(): void 200 { 201 // Create a simple registry implementation 202 $registry = new class () implements LexiconRegistry { 203 public function register(LexiconDocument $document): void 204 { 205 } 206 207 public function get(string $nsid): ?LexiconDocument 208 { 209 return null; 210 } 211 212 public function has(string $nsid): bool 213 { 214 return false; 215 } 216 217 public function all(): array 218 { 219 return []; 220 } 221 222 public function clear(): void 223 { 224 } 225 }; 226 227 $this->resolver->setRegistry($registry); 228 229 $data = ['text' => 'Hello']; 230 231 $unionDef = [ 232 'type' => 'union', 233 'refs' => ['app.bsky.feed.post'], 234 'closed' => false, 235 ]; 236 237 $result = $this->resolver->getTypeDefinition($data, $unionDef); 238 239 $this->assertNull($result); 240 } 241 242 public function test_it_validates_discriminated_union(): void 243 { 244 $data = ['$type' => 'app.bsky.feed.post']; 245 $refs = ['app.bsky.feed.post', 'app.bsky.feed.repost']; 246 247 $this->resolver->validateDiscriminated($data, $refs); 248 249 $this->assertTrue(true); // No exception thrown 250 } 251 252 public function test_it_throws_exception_when_validating_non_object(): void 253 { 254 $this->expectException(RecordValidationException::class); 255 256 $this->resolver->validateDiscriminated('not an object', ['app.bsky.feed.post']); 257 } 258 259 public function test_it_throws_exception_when_validating_without_type(): void 260 { 261 $this->expectException(RecordValidationException::class); 262 263 $this->resolver->validateDiscriminated(['text' => 'Hello'], ['app.bsky.feed.post']); 264 } 265 266 public function test_it_throws_exception_when_validating_invalid_type(): void 267 { 268 $this->expectException(RecordValidationException::class); 269 270 $data = ['$type' => 'app.bsky.feed.invalid']; 271 272 $this->resolver->validateDiscriminated($data, ['app.bsky.feed.post']); 273 } 274 275 public function test_it_extracts_type_from_data(): void 276 { 277 $data = ['$type' => 'app.bsky.feed.post', 'text' => 'Hello']; 278 279 $type = $this->resolver->extractType($data); 280 281 $this->assertEquals('app.bsky.feed.post', $type); 282 } 283 284 public function test_it_returns_null_when_extracting_type_from_non_object(): void 285 { 286 $type = $this->resolver->extractType('not an object'); 287 288 $this->assertNull($type); 289 } 290 291 public function test_it_returns_null_when_extracting_type_without_type_field(): void 292 { 293 $data = ['text' => 'Hello']; 294 295 $type = $this->resolver->extractType($data); 296 297 $this->assertNull($type); 298 } 299 300 public function test_it_creates_discriminated_union_data(): void 301 { 302 $data = $this->resolver->createDiscriminated('app.bsky.feed.post', [ 303 'text' => 'Hello', 304 'createdAt' => '2024-01-01T00:00:00Z', 305 ]); 306 307 $this->assertEquals([ 308 '$type' => 'app.bsky.feed.post', 309 'text' => 'Hello', 310 'createdAt' => '2024-01-01T00:00:00Z', 311 ], $data); 312 } 313 314 public function test_it_checks_if_union_is_closed(): void 315 { 316 $closedUnion = ['closed' => true]; 317 $openUnion = ['closed' => false]; 318 $defaultUnion = []; 319 320 $this->assertTrue($this->resolver->isClosed($closedUnion)); 321 $this->assertFalse($this->resolver->isClosed($openUnion)); 322 $this->assertFalse($this->resolver->isClosed($defaultUnion)); 323 } 324 325 public function test_it_gets_union_types(): void 326 { 327 $unionDef = [ 328 'type' => 'union', 329 'refs' => ['app.bsky.feed.post', 'app.bsky.feed.repost'], 330 ]; 331 332 $types = $this->resolver->getTypes($unionDef); 333 334 $this->assertEquals(['app.bsky.feed.post', 'app.bsky.feed.repost'], $types); 335 } 336 337 public function test_it_returns_empty_array_for_union_without_refs(): void 338 { 339 $unionDef = ['type' => 'union']; 340 341 $types = $this->resolver->getTypes($unionDef); 342 343 $this->assertEquals([], $types); 344 } 345 346 public function test_it_allows_setting_registry(): void 347 { 348 // Create a simple registry implementation 349 $registry = new class () implements LexiconRegistry { 350 public function register(LexiconDocument $document): void 351 { 352 } 353 354 public function get(string $nsid): ?LexiconDocument 355 { 356 return null; 357 } 358 359 public function has(string $nsid): bool 360 { 361 return false; 362 } 363 364 public function all(): array 365 { 366 return []; 367 } 368 369 public function clear(): void 370 { 371 } 372 }; 373 374 $result = $this->resolver->setRegistry($registry); 375 376 $this->assertSame($this->resolver, $result); 377 } 378 379 public function test_it_handles_multiple_types_in_discriminated_union(): void 380 { 381 $refs = [ 382 'app.bsky.feed.post', 383 'app.bsky.feed.repost', 384 'app.bsky.feed.like', 385 ]; 386 387 $unionDef = [ 388 'type' => 'union', 389 'refs' => $refs, 390 'closed' => true, 391 ]; 392 393 foreach ($refs as $ref) { 394 $data = ['$type' => $ref]; 395 $type = $this->resolver->resolve($data, $unionDef); 396 $this->assertEquals($ref, $type); 397 } 398 } 399 400 public function test_it_preserves_data_when_creating_discriminated_union(): void 401 { 402 $originalData = [ 403 'field1' => 'value1', 404 'field2' => 123, 405 'field3' => ['nested' => 'data'], 406 ]; 407 408 $data = $this->resolver->createDiscriminated('app.bsky.feed.post', $originalData); 409 410 $this->assertEquals('app.bsky.feed.post', $data['$type']); 411 $this->assertEquals('value1', $data['field1']); 412 $this->assertEquals(123, $data['field2']); 413 $this->assertEquals(['nested' => 'data'], $data['field3']); 414 } 415 416 public function test_it_overwrites_existing_type_when_creating_discriminated_union(): void 417 { 418 $originalData = [ 419 '$type' => 'old.type', 420 'text' => 'Hello', 421 ]; 422 423 $data = $this->resolver->createDiscriminated('app.bsky.feed.post', $originalData); 424 425 $this->assertEquals('app.bsky.feed.post', $data['$type']); 426 $this->assertEquals('Hello', $data['text']); 427 } 428}