Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Tests\Unit\Parser;
4
5use Illuminate\Support\Facades\Cache;
6use Orchestra\Testbench\TestCase;
7use SocialDept\AtpSchema\Data\LexiconDocument;
8use SocialDept\AtpSchema\Exceptions\SchemaNotFoundException;
9use SocialDept\AtpSchema\Exceptions\SchemaParseException;
10use SocialDept\AtpSchema\Parser\SchemaLoader;
11
12class SchemaLoaderTest extends TestCase
13{
14 protected string $fixturesPath;
15
16 protected function setUp(): void
17 {
18 parent::setUp();
19
20 $this->fixturesPath = __DIR__ . '/../../fixtures';
21
22 // Clear cache before each test
23 Cache::flush();
24 }
25
26 public function test_it_loads_schema_from_hierarchical_json(): void
27 {
28 $loader = new SchemaLoader([$this->fixturesPath], false);
29
30 $schema = $loader->load('app.bsky.feed.post');
31
32 $this->assertInstanceOf(LexiconDocument::class, $schema);
33 $this->assertSame(1, $schema->lexicon);
34 $this->assertSame('app.bsky.feed.post', $schema->getNsid());
35 }
36
37 public function test_it_loads_schema_from_flat_php(): void
38 {
39 $loader = new SchemaLoader([$this->fixturesPath], false);
40
41 $schema = $loader->load('com.atproto.repo.getRecord');
42
43 $this->assertInstanceOf(LexiconDocument::class, $schema);
44 $this->assertSame(1, $schema->lexicon);
45 $this->assertSame('com.atproto.repo.getRecord', $schema->getNsid());
46 }
47
48 public function test_it_checks_if_schema_exists(): void
49 {
50 $loader = new SchemaLoader([$this->fixturesPath], false);
51
52 $this->assertTrue($loader->exists('app.bsky.feed.post'));
53 $this->assertTrue($loader->exists('com.atproto.repo.getRecord'));
54 $this->assertFalse($loader->exists('nonexistent.schema'));
55 }
56
57 public function test_it_throws_when_schema_not_found(): void
58 {
59 $loader = new SchemaLoader([$this->fixturesPath], false);
60
61 $this->expectException(SchemaNotFoundException::class);
62 $this->expectExceptionMessage('Schema not found for NSID: nonexistent.schema');
63
64 $loader->load('nonexistent.schema');
65 }
66
67 public function test_it_caches_schemas_in_memory(): void
68 {
69 $loader = new SchemaLoader([$this->fixturesPath], false);
70
71 // First load
72 $schema1 = $loader->load('app.bsky.feed.post');
73
74 // Second load should come from memory
75 $schema2 = $loader->load('app.bsky.feed.post');
76
77 $this->assertSame($schema1, $schema2);
78 $this->assertContains('app.bsky.feed.post', $loader->getCachedNsids());
79 }
80
81 public function test_it_caches_schemas_in_laravel_cache(): void
82 {
83 $loader = new SchemaLoader([$this->fixturesPath], true, 3600, 'schema');
84
85 // First load - should store in cache
86 $schema = $loader->load('app.bsky.feed.post');
87
88 // Clear memory cache to force Laravel cache lookup
89 $loader->clearCache('app.bsky.feed.post');
90
91 // Manually put raw array back in Laravel cache
92 Cache::put('schema:parsed:app.bsky.feed.post', $schema->toArray(), 3600);
93
94 // This should retrieve from Laravel cache
95 $cached = $loader->load('app.bsky.feed.post');
96
97 // The schemas should be equivalent (different object instances but same data)
98 $this->assertEquals($schema->toArray(), $cached->toArray());
99 }
100
101 public function test_it_retrieves_from_laravel_cache(): void
102 {
103 $loader = new SchemaLoader([$this->fixturesPath], true);
104
105 // First load to cache it
106 $originalSchema = $loader->load('app.bsky.feed.post');
107
108 // Clear memory cache
109 $loader->clearCache('app.bsky.feed.post');
110
111 // Second load should come from Laravel cache
112 $cachedSchema = $loader->load('app.bsky.feed.post');
113
114 $this->assertEquals($originalSchema->toArray(), $cachedSchema->toArray());
115 $this->assertSame('app.bsky.feed.post', $cachedSchema->getNsid());
116 }
117
118 public function test_it_clears_specific_schema_cache(): void
119 {
120 $loader = new SchemaLoader([$this->fixturesPath], true);
121
122 // Load to populate caches
123 $loader->load('app.bsky.feed.post');
124
125 $this->assertContains('app.bsky.feed.post', $loader->getCachedNsids());
126
127 // Clear cache
128 $loader->clearCache('app.bsky.feed.post');
129
130 // Memory cache should be cleared
131 $this->assertNotContains('app.bsky.feed.post', $loader->getCachedNsids());
132
133 // Laravel cache should also be cleared (verify by loading again and checking it comes from file)
134 $this->assertFalse(Cache::has('schema:parsed:app.bsky.feed.post'));
135 }
136
137 public function test_it_clears_all_memory_cache(): void
138 {
139 $loader = new SchemaLoader([$this->fixturesPath], false);
140
141 // Load multiple schemas
142 $loader->load('app.bsky.feed.post');
143 $loader->load('com.atproto.repo.getRecord');
144
145 $this->assertCount(2, $loader->getCachedNsids());
146
147 // Clear all
148 $loader->clearCache();
149
150 $this->assertCount(0, $loader->getCachedNsids());
151 }
152
153 public function test_it_searches_multiple_sources_in_order(): void
154 {
155 $source1 = $this->fixturesPath . '/source1';
156 $source2 = $this->fixturesPath;
157
158 // Schema only exists in source2
159 $loader = new SchemaLoader([$source1, $source2], false);
160
161 $schema = $loader->load('app.bsky.feed.post');
162
163 $this->assertSame('app.bsky.feed.post', $schema->getNsid());
164 }
165
166 public function test_it_throws_on_invalid_json(): void
167 {
168 $invalidPath = $this->fixturesPath . '/invalid';
169 @mkdir($invalidPath, 0755, true);
170 file_put_contents($invalidPath . '/invalid.json', '{invalid json}');
171
172 $loader = new SchemaLoader([$invalidPath], false);
173
174 $this->expectException(SchemaParseException::class);
175 $this->expectExceptionMessage('Failed to parse schema JSON');
176
177 try {
178 $loader->load('invalid');
179 } finally {
180 @unlink($invalidPath . '/invalid.json');
181 @rmdir($invalidPath);
182 }
183 }
184
185 public function test_it_throws_on_php_file_not_returning_array(): void
186 {
187 $invalidPath = $this->fixturesPath . '/invalid';
188 @mkdir($invalidPath, 0755, true);
189 file_put_contents($invalidPath . '/invalid.php', '<?php return "not an array";');
190
191 $loader = new SchemaLoader([$invalidPath], false);
192
193 $this->expectException(SchemaParseException::class);
194 $this->expectExceptionMessage('PHP file must return an array');
195
196 try {
197 $loader->load('invalid');
198 } finally {
199 @unlink($invalidPath . '/invalid.php');
200 @rmdir($invalidPath);
201 }
202 }
203
204}