Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Tests\Unit\Services;
4
5use Illuminate\Http\UploadedFile;
6use Illuminate\Support\Facades\Storage;
7use Orchestra\Testbench\TestCase;
8use SocialDept\AtpSchema\Data\BlobReference;
9use SocialDept\AtpSchema\Exceptions\RecordValidationException;
10use SocialDept\AtpSchema\Services\BlobHandler;
11
12class BlobHandlerTest extends TestCase
13{
14 protected BlobHandler $handler;
15
16 protected function setUp(): void
17 {
18 parent::setUp();
19
20 Storage::fake('local');
21 $this->handler = new BlobHandler('local');
22 }
23
24 public function test_it_stores_uploaded_file(): void
25 {
26 $file = UploadedFile::fake()->image('photo.jpg', 100, 100);
27
28 $blob = $this->handler->store($file);
29
30 $this->assertInstanceOf(BlobReference::class, $blob);
31 $this->assertStringStartsWith('bafyrei', $blob->ref);
32 $this->assertEquals('image/jpeg', $blob->mimeType);
33 $this->assertGreaterThan(0, $blob->size);
34 $this->assertTrue($this->handler->exists($blob->ref));
35 }
36
37 public function test_it_stores_file_with_valid_constraints(): void
38 {
39 $file = UploadedFile::fake()->image('photo.jpg', 100, 100);
40
41 $constraints = [
42 'accept' => ['image/*'],
43 'maxSize' => 1024 * 1024 * 10, // 10MB
44 ];
45
46 $blob = $this->handler->store($file, $constraints);
47
48 $this->assertInstanceOf(BlobReference::class, $blob);
49 $this->assertTrue($this->handler->exists($blob->ref));
50 }
51
52 public function test_it_rejects_file_with_invalid_mime_type(): void
53 {
54 $this->expectException(RecordValidationException::class);
55
56 $file = UploadedFile::fake()->image('photo.jpg');
57
58 $constraints = [
59 'accept' => ['video/*'],
60 ];
61
62 $this->handler->store($file, $constraints);
63 }
64
65 public function test_it_rejects_file_exceeding_max_size(): void
66 {
67 $this->expectException(RecordValidationException::class);
68
69 $file = UploadedFile::fake()->create('large.pdf', 2000); // 2MB
70
71 $constraints = [
72 'maxSize' => 1024, // 1KB
73 ];
74
75 $this->handler->store($file, $constraints);
76 }
77
78 public function test_it_rejects_file_below_min_size(): void
79 {
80 $this->expectException(RecordValidationException::class);
81
82 $file = UploadedFile::fake()->create('small.txt', 1);
83
84 $constraints = [
85 'minSize' => 1024 * 1024, // 1MB
86 ];
87
88 $this->handler->store($file, $constraints);
89 }
90
91 public function test_it_stores_from_string(): void
92 {
93 $content = 'Hello, world!';
94
95 $blob = $this->handler->storeFromString($content, 'text/plain');
96
97 $this->assertInstanceOf(BlobReference::class, $blob);
98 $this->assertEquals('text/plain', $blob->mimeType);
99 $this->assertEquals(strlen($content), $blob->size);
100 $this->assertTrue($this->handler->exists($blob->ref));
101 }
102
103 public function test_it_stores_string_with_valid_constraints(): void
104 {
105 $content = 'Test content';
106
107 $constraints = [
108 'accept' => ['text/*'],
109 'maxSize' => 1024,
110 ];
111
112 $blob = $this->handler->storeFromString($content, 'text/plain', $constraints);
113
114 $this->assertInstanceOf(BlobReference::class, $blob);
115 }
116
117 public function test_it_rejects_string_with_invalid_mime_type(): void
118 {
119 $this->expectException(RecordValidationException::class);
120
121 $constraints = [
122 'accept' => ['image/*'],
123 ];
124
125 $this->handler->storeFromString('content', 'text/plain', $constraints);
126 }
127
128 public function test_it_retrieves_blob_content(): void
129 {
130 $content = 'Test content';
131 $blob = $this->handler->storeFromString($content, 'text/plain');
132
133 $retrieved = $this->handler->get($blob->ref);
134
135 $this->assertEquals($content, $retrieved);
136 }
137
138 public function test_it_returns_null_for_nonexistent_blob(): void
139 {
140 $content = $this->handler->get('nonexistent-cid');
141
142 $this->assertNull($content);
143 }
144
145 public function test_it_checks_blob_existence(): void
146 {
147 $blob = $this->handler->storeFromString('content', 'text/plain');
148
149 $this->assertTrue($this->handler->exists($blob->ref));
150 $this->assertFalse($this->handler->exists('nonexistent-cid'));
151 }
152
153 public function test_it_deletes_blob(): void
154 {
155 $blob = $this->handler->storeFromString('content', 'text/plain');
156
157 $this->assertTrue($this->handler->exists($blob->ref));
158
159 $result = $this->handler->delete($blob->ref);
160
161 $this->assertTrue($result);
162 $this->assertFalse($this->handler->exists($blob->ref));
163 }
164
165 public function test_it_returns_false_when_deleting_nonexistent_blob(): void
166 {
167 $result = $this->handler->delete('nonexistent-cid');
168
169 $this->assertFalse($result);
170 }
171
172 public function test_it_gets_blob_size(): void
173 {
174 $content = 'Test content';
175 $blob = $this->handler->storeFromString($content, 'text/plain');
176
177 $size = $this->handler->size($blob->ref);
178
179 $this->assertEquals(strlen($content), $size);
180 }
181
182 public function test_it_returns_null_size_for_nonexistent_blob(): void
183 {
184 $size = $this->handler->size('nonexistent-cid');
185
186 $this->assertNull($size);
187 }
188
189 public function test_it_validates_blob_reference(): void
190 {
191 $blob = new BlobReference('cid', 'image/png', 1024);
192
193 $constraints = [
194 'accept' => ['image/*'],
195 'maxSize' => 2048,
196 ];
197
198 $this->handler->validate($blob, $constraints);
199
200 $this->assertTrue(true); // No exception thrown
201 }
202
203 public function test_it_rejects_blob_with_invalid_mime_type(): void
204 {
205 $this->expectException(RecordValidationException::class);
206
207 $blob = new BlobReference('cid', 'text/plain', 1024);
208
209 $constraints = [
210 'accept' => ['image/*'],
211 ];
212
213 $this->handler->validate($blob, $constraints);
214 }
215
216 public function test_it_rejects_blob_exceeding_max_size(): void
217 {
218 $this->expectException(RecordValidationException::class);
219
220 $blob = new BlobReference('cid', 'image/png', 2048);
221
222 $constraints = [
223 'maxSize' => 1024,
224 ];
225
226 $this->handler->validate($blob, $constraints);
227 }
228
229 public function test_it_rejects_blob_below_min_size(): void
230 {
231 $this->expectException(RecordValidationException::class);
232
233 $blob = new BlobReference('cid', 'image/png', 512);
234
235 $constraints = [
236 'minSize' => 1024,
237 ];
238
239 $this->handler->validate($blob, $constraints);
240 }
241
242 public function test_it_validates_multiple_mime_types(): void
243 {
244 $blob = new BlobReference('cid', 'image/png', 1024);
245
246 $constraints = [
247 'accept' => ['image/jpeg', 'image/png', 'image/webp'],
248 ];
249
250 $this->handler->validate($blob, $constraints);
251
252 $this->assertTrue(true);
253 }
254
255 public function test_it_validates_wildcard_mime_types(): void
256 {
257 $imageBlob = new BlobReference('cid1', 'image/png', 1024);
258 $videoBlob = new BlobReference('cid2', 'video/mp4', 1024);
259
260 $imageConstraints = ['accept' => ['image/*']];
261 $videoConstraints = ['accept' => ['video/*']];
262
263 $this->handler->validate($imageBlob, $imageConstraints);
264 $this->handler->validate($videoBlob, $videoConstraints);
265
266 $this->assertTrue(true);
267 }
268
269 public function test_it_generates_consistent_cids_for_same_content(): void
270 {
271 $content = 'Test content';
272
273 $blob1 = $this->handler->storeFromString($content, 'text/plain');
274 $blob2 = $this->handler->storeFromString($content, 'text/plain');
275
276 $this->assertEquals($blob1->ref, $blob2->ref);
277 }
278
279 public function test_it_generates_different_cids_for_different_content(): void
280 {
281 $blob1 = $this->handler->storeFromString('Content 1', 'text/plain');
282 $blob2 = $this->handler->storeFromString('Content 2', 'text/plain');
283
284 $this->assertNotEquals($blob1->ref, $blob2->ref);
285 }
286
287 public function test_it_uses_directory_partitioning_for_storage(): void
288 {
289 $blob = $this->handler->storeFromString('content', 'text/plain');
290
291 // CID should be used to create partitioned path
292 $prefix = substr($blob->ref, 0, 2);
293 $middle = substr($blob->ref, 2, 2);
294 $expectedPath = "blobs/{$prefix}/{$middle}/{$blob->ref}";
295
296 Storage::disk('local')->assertExists($expectedPath);
297 }
298
299 public function test_it_can_change_storage_disk(): void
300 {
301 Storage::fake('custom');
302
303 $this->handler->setDisk('custom');
304
305 $blob = $this->handler->storeFromString('content', 'text/plain');
306
307 $prefix = substr($blob->ref, 0, 2);
308 $middle = substr($blob->ref, 2, 2);
309 $expectedPath = "blobs/{$prefix}/{$middle}/{$blob->ref}";
310
311 Storage::disk('custom')->assertExists($expectedPath);
312 }
313
314 public function test_it_can_change_base_path(): void
315 {
316 $this->handler->setBasePath('custom-path');
317
318 $blob = $this->handler->storeFromString('content', 'text/plain');
319
320 $prefix = substr($blob->ref, 0, 2);
321 $middle = substr($blob->ref, 2, 2);
322 $expectedPath = "custom-path/{$prefix}/{$middle}/{$blob->ref}";
323
324 Storage::disk('local')->assertExists($expectedPath);
325 }
326
327 public function test_it_gets_disk_name(): void
328 {
329 $this->assertEquals('local', $this->handler->getDisk());
330
331 $this->handler->setDisk('custom');
332
333 $this->assertEquals('custom', $this->handler->getDisk());
334 }
335
336 public function test_it_gets_base_path(): void
337 {
338 $this->assertEquals('blobs', $this->handler->getBasePath());
339
340 $this->handler->setBasePath('custom-path');
341
342 $this->assertEquals('custom-path', $this->handler->getBasePath());
343 }
344
345 public function test_it_handles_file_without_mime_type(): void
346 {
347 $file = UploadedFile::fake()->create('unknown.bin');
348
349 $blob = $this->handler->store($file);
350
351 // Should default to application/octet-stream if mime type is null
352 $this->assertNotNull($blob->mimeType);
353 }
354
355 public function test_it_accepts_constraints_as_array(): void
356 {
357 $blob = new BlobReference('cid', 'image/png', 1024);
358
359 $constraints = [
360 'accept' => ['image/png', 'image/jpeg'], // Array of types
361 'maxSize' => 2048,
362 ];
363
364 $this->handler->validate($blob, $constraints);
365
366 $this->assertTrue(true);
367 }
368
369 public function test_it_stores_binary_content(): void
370 {
371 $binaryContent = random_bytes(1024);
372
373 $blob = $this->handler->storeFromString($binaryContent, 'application/octet-stream');
374
375 $retrieved = $this->handler->get($blob->ref);
376
377 $this->assertEquals($binaryContent, $retrieved);
378 }
379}