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

373 lines
12 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\Enums\ConfigType;
use Core\Config\Enums\ScopeType;
use Core\Config\Models\Channel;
use Core\Config\Models\ConfigKey;
use Core\Config\Models\ConfigProfile;
use Core\Config\Models\ConfigResolved;
use Core\Config\Models\ConfigValue;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create essential test fixtures
$this->systemProfile = ConfigProfile::create([
'name' => 'System Default',
'scope_type' => ScopeType::SYSTEM,
'scope_id' => null,
'priority' => 0,
]);
$this->stringKey = ConfigKey::create([
'code' => 'app.name',
'type' => ConfigType::STRING,
'category' => 'app',
'default_value' => 'Default App',
]);
$this->boolKey = ConfigKey::create([
'code' => 'app.debug',
'type' => ConfigType::BOOL,
'category' => 'app',
'default_value' => false,
]);
$this->intKey = ConfigKey::create([
'code' => 'app.limit',
'type' => ConfigType::INT,
'category' => 'app',
'default_value' => 100,
]);
});
describe('findValue', function () {
it('finds value by profile, key, and channel', function () {
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Test App',
]);
$found = ConfigValue::findValue($this->systemProfile->id, $this->stringKey->id);
expect($found)->not->toBeNull();
expect($found->id)->toBe($value->id);
expect($found->value)->toBe('Test App');
});
it('returns null when not found', function () {
$found = ConfigValue::findValue($this->systemProfile->id, 99999);
expect($found)->toBeNull();
});
it('distinguishes between channel-scoped values', function () {
$channel = Channel::create(['code' => 'web', 'name' => 'Web']);
// Create value without channel
ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Default Value',
'channel_id' => null,
]);
// Create value with channel
ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Web Value',
'channel_id' => $channel->id,
]);
$withoutChannel = ConfigValue::findValue($this->systemProfile->id, $this->stringKey->id, null);
$withChannel = ConfigValue::findValue($this->systemProfile->id, $this->stringKey->id, $channel->id);
expect($withoutChannel->value)->toBe('Default Value');
expect($withChannel->value)->toBe('Web Value');
});
});
describe('setValue', function () {
it('creates new value', function () {
$result = ConfigValue::setValue(
$this->systemProfile->id,
$this->stringKey->id,
'New Value'
);
expect($result->value)->toBe('New Value');
expect(ConfigValue::count())->toBe(1);
});
it('updates existing value', function () {
ConfigValue::setValue($this->systemProfile->id, $this->stringKey->id, 'Original');
ConfigValue::setValue($this->systemProfile->id, $this->stringKey->id, 'Updated');
expect(ConfigValue::count())->toBe(1);
expect(ConfigValue::first()->value)->toBe('Updated');
});
it('sets locked flag', function () {
$result = ConfigValue::setValue(
$this->systemProfile->id,
$this->stringKey->id,
'Locked Value',
locked: true
);
expect($result->locked)->toBeTrue();
expect($result->isLocked())->toBeTrue();
});
it('sets inherited_from', function () {
$parentProfile = ConfigProfile::create([
'name' => 'Parent',
'scope_type' => ScopeType::SYSTEM,
'priority' => 10,
]);
$result = ConfigValue::setValue(
$this->systemProfile->id,
$this->stringKey->id,
'Inherited',
inheritedFrom: $parentProfile->id
);
expect($result->inherited_from)->toBe($parentProfile->id);
expect($result->isInherited())->toBeTrue();
});
it('handles channel-scoped values', function () {
$channel = Channel::create(['code' => 'api', 'name' => 'API']);
ConfigValue::setValue($this->systemProfile->id, $this->stringKey->id, 'Default');
ConfigValue::setValue($this->systemProfile->id, $this->stringKey->id, 'API Specific', channelId: $channel->id);
expect(ConfigValue::count())->toBe(2);
$default = ConfigValue::findValue($this->systemProfile->id, $this->stringKey->id, null);
$apiScoped = ConfigValue::findValue($this->systemProfile->id, $this->stringKey->id, $channel->id);
expect($default->value)->toBe('Default');
expect($apiScoped->value)->toBe('API Specific');
});
it('clears resolved cache on update', function () {
// Create a resolved entry - setValue clears where workspace_id = null for system profile
// Note: ConfigResolved uses 0 for storage but queries use null for system scope
ConfigResolved::create([
'key_code' => $this->stringKey->code,
'workspace_id' => null,
'channel_id' => null,
'value' => 'Old Cached',
'type' => 'string',
'locked' => false,
'virtual' => false,
'computed_at' => now(),
]);
expect(ConfigResolved::count())->toBe(1);
// Update value should clear resolved
ConfigValue::setValue($this->systemProfile->id, $this->stringKey->id, 'New Value');
expect(ConfigResolved::count())->toBe(0);
});
it('returns created value even when cache clear has nothing to do', function () {
// Test that setValue works even when there's no resolved cache to clear
$result = ConfigValue::setValue(
$this->systemProfile->id,
$this->stringKey->id,
'New Value'
);
expect($result->value)->toBe('New Value');
expect(ConfigResolved::count())->toBe(0);
});
});
describe('getTypedValue', function () {
it('casts string value', function () {
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 123,
]);
expect($value->getTypedValue())->toBe('123');
});
it('casts bool value', function () {
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->boolKey->id,
'value' => 'true',
]);
expect($value->getTypedValue())->toBeTrue();
});
it('casts int value', function () {
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->intKey->id,
'value' => '42',
]);
expect($value->getTypedValue())->toBe(42);
});
it('returns raw value when key relationship is null', function () {
// Create a value with valid key, then delete the key to simulate orphaned value
$tempKey = ConfigKey::create([
'code' => 'temp.key',
'type' => ConfigType::STRING,
'category' => 'temp',
]);
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $tempKey->id,
'value' => 'raw',
]);
// Delete the key to orphan the value (FK has ON DELETE CASCADE but we test the method)
// Instead, test by clearing the relationship
$value->setRelation('key', null);
expect($value->getTypedValue())->toBe('raw');
});
});
describe('forKeyInProfiles', function () {
it('retrieves values across multiple profiles', function () {
$profile2 = ConfigProfile::create([
'name' => 'Profile 2',
'scope_type' => ScopeType::WORKSPACE,
'scope_id' => 1,
'priority' => 10,
]);
ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'System Value',
]);
ConfigValue::create([
'profile_id' => $profile2->id,
'key_id' => $this->stringKey->id,
'value' => 'Workspace Value',
]);
$values = ConfigValue::forKeyInProfiles(
$this->stringKey->id,
[$this->systemProfile->id, $profile2->id]
);
expect($values)->toHaveCount(2);
expect($values->pluck('value')->all())->toContain('System Value', 'Workspace Value');
});
it('includes null channel values when channelIds specified', function () {
$channel = Channel::create(['code' => 'web', 'name' => 'Web']);
// Create value without channel
ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Default',
'channel_id' => null,
]);
// Create value with channel
ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Web Specific',
'channel_id' => $channel->id,
]);
$values = ConfigValue::forKeyInProfiles(
$this->stringKey->id,
[$this->systemProfile->id],
[$channel->id]
);
// Should include both null channel and specified channel
expect($values)->toHaveCount(2);
});
it('returns empty collection for non-existent profiles', function () {
$values = ConfigValue::forKeyInProfiles($this->stringKey->id, [99999]);
expect($values)->toHaveCount(0);
});
});
describe('isLocked', function () {
it('returns true when locked', function () {
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Locked',
'locked' => true,
]);
expect($value->isLocked())->toBeTrue();
});
it('returns false when not locked', function () {
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Not Locked',
'locked' => false,
]);
expect($value->isLocked())->toBeFalse();
});
});
describe('isInherited', function () {
it('returns true when inherited_from is set', function () {
// Create a parent profile to inherit from
$parentProfile = ConfigProfile::create([
'name' => 'Parent Profile',
'scope_type' => ScopeType::SYSTEM,
'priority' => 10,
]);
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Inherited',
'inherited_from' => $parentProfile->id,
]);
expect($value->isInherited())->toBeTrue();
});
it('returns false when inherited_from is null', function () {
$value = ConfigValue::create([
'profile_id' => $this->systemProfile->id,
'key_id' => $this->stringKey->id,
'value' => 'Direct',
]);
expect($value->isInherited())->toBeFalse();
});
});