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>
373 lines
12 KiB
PHP
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();
|
|
});
|
|
});
|