lthn.io/app/Core/Tests/Feature/Config/ChannelTest.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

245 lines
8.1 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
use Core\Config\Models\Channel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
uses(RefreshDatabase::class);
describe('Channel', function () {
describe('inheritanceChain', function () {
it('returns single channel when no parent', function () {
$channel = Channel::create([
'code' => 'web',
'name' => 'Web',
]);
$chain = $channel->inheritanceChain();
expect($chain)->toHaveCount(1);
expect($chain->first()->code)->toBe('web');
});
it('builds chain from child to parent', function () {
$parent = Channel::create([
'code' => 'social',
'name' => 'Social',
]);
$child = Channel::create([
'code' => 'instagram',
'name' => 'Instagram',
'parent_id' => $parent->id,
]);
$chain = $child->inheritanceChain();
expect($chain)->toHaveCount(2);
expect($chain[0]->code)->toBe('instagram');
expect($chain[1]->code)->toBe('social');
});
it('builds multi-level chain', function () {
$grandparent = Channel::create([
'code' => 'digital',
'name' => 'Digital',
]);
$parent = Channel::create([
'code' => 'social',
'name' => 'Social',
'parent_id' => $grandparent->id,
]);
$child = Channel::create([
'code' => 'instagram',
'name' => 'Instagram',
'parent_id' => $parent->id,
]);
$chain = $child->inheritanceChain();
expect($chain)->toHaveCount(3);
expect($chain->pluck('code')->all())->toBe(['instagram', 'social', 'digital']);
});
it('detects and breaks circular references', function () {
Log::shouldReceive('error')
->once()
->withArgs(function ($message, $context) {
return str_contains($message, 'Circular reference');
});
// Create channels
$channelA = Channel::create(['code' => 'a', 'name' => 'A']);
$channelB = Channel::create(['code' => 'b', 'name' => 'B', 'parent_id' => $channelA->id]);
// Create cycle by updating A to point to B
$channelA->update(['parent_id' => $channelB->id]);
$channelA->refresh();
// Should not hang - breaks on cycle detection
$chain = $channelA->inheritanceChain();
expect($chain->count())->toBeLessThanOrEqual(2);
});
});
describe('inheritanceCodes', function () {
it('returns array of codes in chain', function () {
$parent = Channel::create(['code' => 'social', 'name' => 'Social']);
$child = Channel::create(['code' => 'instagram', 'name' => 'Instagram', 'parent_id' => $parent->id]);
$codes = $child->inheritanceCodes();
expect($codes)->toBe(['instagram', 'social']);
});
});
describe('inheritsFrom', function () {
it('returns true for direct parent', function () {
$parent = Channel::create(['code' => 'social', 'name' => 'Social']);
$child = Channel::create(['code' => 'instagram', 'name' => 'Instagram', 'parent_id' => $parent->id]);
expect($child->inheritsFrom('social'))->toBeTrue();
});
it('returns true for grandparent', function () {
$grandparent = Channel::create(['code' => 'digital', 'name' => 'Digital']);
$parent = Channel::create(['code' => 'social', 'name' => 'Social', 'parent_id' => $grandparent->id]);
$child = Channel::create(['code' => 'instagram', 'name' => 'Instagram', 'parent_id' => $parent->id]);
expect($child->inheritsFrom('digital'))->toBeTrue();
});
it('returns true for self', function () {
$channel = Channel::create(['code' => 'web', 'name' => 'Web']);
expect($channel->inheritsFrom('web'))->toBeTrue();
});
it('returns false for unrelated channel', function () {
$channel = Channel::create(['code' => 'web', 'name' => 'Web']);
expect($channel->inheritsFrom('api'))->toBeFalse();
});
});
describe('byCode', function () {
it('finds system channel by code', function () {
Channel::create(['code' => 'web', 'name' => 'Web']);
$found = Channel::byCode('web');
expect($found)->not->toBeNull();
expect($found->code)->toBe('web');
});
it('returns null for non-existent code', function () {
$found = Channel::byCode('nonexistent');
expect($found)->toBeNull();
});
it('prefers workspace channel over system channel', function () {
$system = Channel::create(['code' => 'web', 'name' => 'Web System']);
$workspace = Channel::create(['code' => 'web', 'name' => 'Web Workspace', 'workspace_id' => 1]);
$found = Channel::byCode('web', 1);
expect($found->id)->toBe($workspace->id);
});
it('falls back to system channel when workspace has none', function () {
$system = Channel::create(['code' => 'web', 'name' => 'Web System']);
$found = Channel::byCode('web', 999);
expect($found->id)->toBe($system->id);
});
});
describe('ensure', function () {
it('creates channel if not exists', function () {
$channel = Channel::ensure('api', 'API');
expect($channel->code)->toBe('api');
expect($channel->name)->toBe('API');
expect(Channel::where('code', 'api')->count())->toBe(1);
});
it('returns existing channel without updating', function () {
$existing = Channel::create(['code' => 'api', 'name' => 'Original Name']);
$channel = Channel::ensure('api', 'New Name');
expect($channel->id)->toBe($existing->id);
expect($channel->name)->toBe('Original Name');
});
it('creates with parent when specified', function () {
$parent = Channel::create(['code' => 'social', 'name' => 'Social']);
$child = Channel::ensure('instagram', 'Instagram', 'social');
expect($child->parent_id)->toBe($parent->id);
});
it('creates with metadata', function () {
$channel = Channel::ensure('api', 'API', null, null, ['version' => 'v2']);
expect($channel->meta('version'))->toBe('v2');
});
});
describe('isSystem', function () {
it('returns true when workspace_id is null', function () {
$channel = Channel::create(['code' => 'web', 'name' => 'Web']);
expect($channel->isSystem())->toBeTrue();
});
it('returns false when workspace_id is set', function () {
$channel = Channel::create(['code' => 'web', 'name' => 'Web', 'workspace_id' => 1]);
expect($channel->isSystem())->toBeFalse();
});
});
describe('meta', function () {
it('returns metadata value', function () {
$channel = Channel::create([
'code' => 'api',
'name' => 'API',
'metadata' => ['rate_limit' => 100],
]);
expect($channel->meta('rate_limit'))->toBe(100);
});
it('returns default for missing key', function () {
$channel = Channel::create(['code' => 'api', 'name' => 'API']);
expect($channel->meta('missing', 'default'))->toBe('default');
});
it('supports dot notation for nested values', function () {
$channel = Channel::create([
'code' => 'api',
'name' => 'API',
'metadata' => ['limits' => ['rate' => 100]],
]);
expect($channel->meta('limits.rate'))->toBe(100);
});
});
});