lthn.io/app/Core/Tests/Unit/Services/BunnyCdnServiceTest.php
Claude 41a90cbff8
feat: lthn.io API serving live chain data
Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.

Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 17:17:42 +01:00

297 lines
8.9 KiB
PHP

<?php
declare(strict_types=1);
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
use Core\Cdn\Services\BunnyCdnService;
use Core\Config\ConfigService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
uses(TestCase::class);
function createMockedService(bool $configured = true): BunnyCdnService
{
$configMock = Mockery::mock(ConfigService::class);
$configMock->shouldReceive('isConfigured')->with('cdn.bunny')->andReturn($configured);
$configMock->shouldReceive('get')->with('cdn.bunny.api_key')->andReturn('test-api-key');
$configMock->shouldReceive('get')->with('cdn.bunny.pull_zone_id')->andReturn('12345');
$configMock->shouldReceive('get')->with('cdn.bunny.storage.public.api_key')->andReturn('test-storage-key');
$configMock->shouldReceive('get')->with('cdn.bunny.storage.public.hostname', 'storage.bunnycdn.com')->andReturn('storage.bunnycdn.com');
return new BunnyCdnService($configMock);
}
describe('BunnyCdnService Configuration', function () {
it('reports configured when api key and pull zone are set', function () {
$service = createMockedService(configured: true);
expect($service->isConfigured())->toBeTrue();
});
it('reports not configured when api key is missing', function () {
$service = createMockedService(configured: false);
expect($service->isConfigured())->toBeFalse();
});
});
describe('BunnyCdnService URL Purging', function () {
it('purges single url successfully', function () {
Http::fake([
'api.bunny.net/purge' => Http::response(null, 200),
]);
$service = createMockedService();
$result = $service->purgeUrls(['https://example.com/page']);
expect($result)->toBeTrue();
Http::assertSent(function ($request) {
return $request->url() === 'https://api.bunny.net/purge'
&& $request['url'] === 'https://example.com/page';
});
});
it('purges multiple urls', function () {
Http::fake([
'api.bunny.net/purge' => Http::response(null, 200),
]);
$service = createMockedService();
$urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
];
$result = $service->purgeUrls($urls);
expect($result)->toBeTrue();
Http::assertSentCount(3);
});
it('returns true for empty url array', function () {
Http::fake();
$service = createMockedService();
$result = $service->purgeUrls([]);
expect($result)->toBeTrue();
Http::assertNothingSent();
});
it('returns false when not configured', function () {
Log::spy();
$service = createMockedService(configured: false);
$result = $service->purgeUrls(['https://example.com/page']);
expect($result)->toBeFalse();
Log::shouldHaveReceived('warning')->with('BunnyCDN: Cannot purge - not configured');
});
it('handles purge failure gracefully', function () {
Http::fake([
'api.bunny.net/purge' => Http::response(['error' => 'Not found'], 404),
]);
Log::spy();
$service = createMockedService();
$result = $service->purgeUrls(['https://example.com/page']);
expect($result)->toBeFalse();
Log::shouldHaveReceived('error');
});
it('returns partial success when some urls fail', function () {
Http::fake([
'*' => Http::sequence()
->push(null, 200)
->push(['error' => 'Failed'], 500)
->push(null, 200),
]);
$service = createMockedService();
$result = $service->purgeUrls([
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
]);
expect($result)->toBeFalse();
Http::assertSentCount(2); // Stops at first failure
});
});
describe('BunnyCdnService Full Cache Purge', function () {
it('purges all cache successfully', function () {
Http::fake([
'api.bunny.net/pullzone/12345/purgeCache' => Http::response(null, 204),
]);
$service = createMockedService();
$result = $service->purgeAll();
expect($result)->toBeTrue();
Http::assertSent(function ($request) {
return str_contains($request->url(), 'pullzone/12345/purgeCache');
});
});
it('returns false when not configured', function () {
$service = createMockedService(configured: false);
$result = $service->purgeAll();
expect($result)->toBeFalse();
});
it('handles purge all failure', function () {
Http::fake([
'*' => Http::response(['error' => 'Unauthorized'], 401),
]);
Log::spy();
$service = createMockedService();
$result = $service->purgeAll();
expect($result)->toBeFalse();
});
});
describe('BunnyCdnService Tag Purging', function () {
it('purges by tag successfully', function () {
Http::fake([
'api.bunny.net/pullzone/12345/purgeCache' => Http::response(null, 204),
]);
$service = createMockedService();
$result = $service->purgeByTag('workspace:abc123');
expect($result)->toBeTrue();
Http::assertSent(function ($request) {
return str_contains($request->url(), 'purgeCache')
&& $request['CacheTag'] === 'workspace:abc123';
});
});
it('returns false when not configured', function () {
$service = createMockedService(configured: false);
$result = $service->purgeByTag('test-tag');
expect($result)->toBeFalse();
});
});
describe('BunnyCdnService Stats', function () {
it('gets pull zone stats', function () {
Http::fake([
'api.bunny.net/statistics*' => Http::response([
'TotalBandwidthUsed' => 1000000,
'CacheHitRate' => 0.95,
'TotalOriginTraffic' => 50000,
], 200),
]);
$service = createMockedService();
$stats = $service->getStats();
expect($stats)->toBeArray()
->and($stats['TotalBandwidthUsed'])->toBe(1000000)
->and($stats['CacheHitRate'])->toBe(0.95);
});
it('returns null when not configured', function () {
$service = createMockedService(configured: false);
$stats = $service->getStats();
expect($stats)->toBeNull();
});
it('returns null on api failure', function () {
Http::fake([
'*' => Http::response(['error' => 'Not found'], 404),
]);
$service = createMockedService();
$stats = $service->getStats();
expect($stats)->toBeNull();
});
});
describe('BunnyCdnService Storage Operations', function () {
it('lists storage files', function () {
Http::fake([
'storage.bunnycdn.com/my-zone/*' => Http::response([
['ObjectName' => 'file1.jpg', 'Length' => 1024],
['ObjectName' => 'file2.jpg', 'Length' => 2048],
], 200),
]);
$service = createMockedService();
$files = $service->listStorageFiles('my-zone', '/images/');
expect($files)->toBeArray()
->and($files)->toHaveCount(2);
});
it('returns null when not configured', function () {
$service = createMockedService(configured: false);
$files = $service->listStorageFiles('my-zone');
expect($files)->toBeNull();
});
it('uploads file to storage', function () {
Http::fake([
'storage.bunnycdn.com/*' => Http::response(null, 201),
]);
$service = createMockedService();
$result = $service->uploadFile('my-zone', 'images/test.jpg', 'file-content');
expect($result)->toBeTrue();
Http::assertSent(function ($request) {
return str_contains($request->url(), 'my-zone/images/test.jpg')
&& $request->method() === 'PUT';
});
});
it('returns false on upload failure', function () {
Http::fake([
'*' => Http::response(['error' => 'Storage full'], 507),
]);
$service = createMockedService();
$result = $service->uploadFile('my-zone', 'images/test.jpg', 'content');
expect($result)->toBeFalse();
});
it('deletes file from storage', function () {
Http::fake([
'storage.bunnycdn.com/*' => Http::response(null, 200),
]);
$service = createMockedService();
$result = $service->deleteFile('my-zone', 'images/test.jpg');
expect($result)->toBeTrue();
Http::assertSent(function ($request) {
return str_contains($request->url(), 'my-zone/images/test.jpg')
&& $request->method() === 'DELETE';
});
});
});