CaiWuXiTong/tests/Feature/SystemSettings/UpstreamDatabasesTest.php

470 lines
18 KiB
PHP

<?php
namespace Tests\Feature\SystemSettings;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Models\Settings\DatabaseConnection;
use App\Utilities\SystemDatabaseConfiguration;
use App\Utilities\TeamSystemDatabaseProfile;
use App\Utilities\UpstreamCredentialCipher;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Facades\DB;
use Mockery;
use Modules\EcommerceFinance\Services\UpstreamSourceRegistry;
use Modules\EcommerceFinance\Services\UpstreamStoredConfiguration;
use Tests\Feature\FeatureTestCase;
class UpstreamDatabasesTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$connection = (string) config('database.default');
config([
"database.connections.$connection.password" => 'shared-system-database-secret',
'system_database.mode' => 'shared',
'upstreams.stored_configuration.enabled' => true,
]);
}
public function test_super_admin_can_add_multiple_encrypted_database_configurations(): void
{
$this->loginAs()
->post(route('system-settings.databases.store'), $this->payload([
'name' => 'JianSun 测试库',
'source_key' => 'jiansun',
'password' => 'first-secret',
]))
->assertRedirect(route('system-settings.databases.index'));
$this->post(route('system-settings.databases.store'), $this->payload([
'name' => '云数据库二号',
'source_key' => '',
'host' => '127.0.0.2',
'password' => 'second-secret',
]))->assertRedirect(route('system-settings.databases.index'));
$this->assertSame(2, DatabaseConnection::where('company_id', 0)->count());
$connection = DatabaseConnection::where('source_key', 'jiansun')->firstOrFail();
$this->assertSame('first-secret', $connection->password);
$this->assertNotSame(
'first-secret',
DB::table('system_database_connections')->where('id', $connection->id)->value('password')
);
$sharedPassword = DB::table('system_database_connections')
->where('id', $connection->id)
->value('shared_password');
$this->assertIsString($sharedPassword);
$this->assertNotSame('first-secret', $sharedPassword);
$this->assertSame(
'first-secret',
app(UpstreamCredentialCipher::class)->decrypt($sharedPassword)
);
$sources = app(UpstreamSourceRegistry::class);
$this->assertSame('127.0.0.1', $sources->value('jiansun', 'connection.host'));
$this->assertSame('first-secret', $sources->value('jiansun', 'connection.password'));
$this->get(route('system-settings.databases.index'))
->assertOk()
->assertSeeText('JianSun 测试库')
->assertSeeText('云数据库二号')
->assertDontSee('first-secret');
}
public function test_editing_without_password_preserves_existing_password(): void
{
$connection = $this->connection('original-secret');
$this->loginAs()
->patch(route('system-settings.databases.update', $connection->id), $this->payload([
'name' => $connection->name,
'password' => '',
'host' => '127.0.0.2',
]))
->assertRedirect(route('system-settings.databases.index'));
$connection->refresh();
$this->assertSame('original-secret', $connection->password);
$this->assertSame('127.0.0.2', $connection->host);
$this->get(route('system-settings.databases.edit', $connection->id))
->assertOk()
->assertDontSee('original-secret');
}
public function test_local_password_is_used_when_shared_record_uses_another_app_key(): void
{
$connection = $this->connection('shared-secret');
$connection->update(['source_key' => 'jiansun']);
$originalEncrypter = $this->app->make('encrypter');
try {
$this->app->instance(
'encrypter',
new Encrypter(str_repeat('x', 32), (string) config('app.cipher'))
);
config([
'upstreams.local_configuration.enabled' => true,
'jiansun.connection.host' => 'local-db.test',
'jiansun.connection.database' => 'local_database',
'jiansun.connection.username' => 'local_user',
'jiansun.connection.password' => 'local-secret',
'jiansun.connection.port' => 11433,
]);
$sources = new UpstreamSourceRegistry(new UpstreamStoredConfiguration());
$this->assertSame(
'local-secret',
$sources->value('jiansun', 'connection.password')
);
$this->assertSame('local-db.test', $sources->value('jiansun', 'connection.host'));
$this->assertSame('local_database', $sources->value('jiansun', 'connection.database'));
$this->assertSame('local_user', $sources->value('jiansun', 'connection.username'));
$this->assertSame(11433, $sources->value('jiansun', 'connection.port'));
} finally {
$this->app->instance('encrypter', $originalEncrypter);
}
}
public function test_private_override_is_honored_when_explicitly_enabled_in_production(): void
{
$connection = $this->connection('shared-secret');
$connection->update(['source_key' => 'jiansun']);
$this->app->detectEnvironment(fn () => 'production');
try {
config([
'upstreams.local_configuration.enabled' => true,
'jiansun.connection.host' => 'private-tunnel.test',
'jiansun.connection.password' => 'private-secret',
]);
$sources = new UpstreamSourceRegistry(new UpstreamStoredConfiguration());
$this->assertSame(
'private-tunnel.test',
$sources->value('jiansun', 'connection.host')
);
$this->assertSame(
'private-secret',
$sources->value('jiansun', 'connection.password')
);
} finally {
$this->app->detectEnvironment(fn () => 'testing');
}
}
public function test_shared_password_is_available_with_a_different_app_key(): void
{
$connection = $this->connection('shared-secret');
$connection->update(['source_key' => 'jiansun']);
$originalEncrypter = $this->app->make('encrypter');
try {
$this->app->instance(
'encrypter',
new Encrypter(str_repeat('x', 32), (string) config('app.cipher'))
);
config(['upstreams.local_configuration.enabled' => false]);
$sources = new UpstreamSourceRegistry(new UpstreamStoredConfiguration());
// 兼容尚未重建的旧本地镜像:旧模块仍直接读取 password 属性。
$this->assertSame('shared-secret', $connection->fresh()->password);
$this->assertSame(
'shared-secret',
$sources->value('jiansun', 'connection.password')
);
} finally {
$this->app->instance('encrypter', $originalEncrypter);
}
}
public function test_whole_connection_comes_from_shared_system_database(): void
{
$connection = $this->connection('shared-secret');
$connection->update([
'source_key' => 'jiansun',
'host' => 'shared-db.test',
'port' => 14330,
'database_name' => 'shared_database',
'username' => 'shared_reader',
]);
config([
'upstreams.local_configuration.enabled' => false,
'jiansun.connection' => [
'host' => null,
'port' => 1433,
'database' => null,
'username' => null,
'password' => null,
'odbc_driver' => 'FreeTDS',
'tds_version' => '7.4',
'encryption' => 'off',
'connect_timeout' => 5,
'query_timeout' => 15,
'application_intent' => '',
],
]);
$sources = new UpstreamSourceRegistry(new UpstreamStoredConfiguration());
$resolved = $sources->value('jiansun', 'connection', []);
$this->assertSame('shared-db.test', $resolved['host']);
$this->assertSame(14330, $resolved['port']);
$this->assertSame('shared_database', $resolved['database']);
$this->assertSame('shared_reader', $resolved['username']);
$this->assertSame('shared-secret', $resolved['password']);
$this->assertSame('off', $resolved['encryption']);
}
public function test_global_wildcard_scope_is_rejected(): void
{
$this->withExceptionHandling();
$this->loginAs()
->post(route('system-settings.databases.store'), $this->payload([
'allowed_scopes' => '*',
]))
->assertSessionHasErrors('allowed_scopes');
$this->assertDatabaseMissing('system_database_connections', ['name' => '测试云数据库']);
}
public function test_regular_admin_can_open_database_settings(): void
{
$this->user->roles()->sync([
Role::where('name', 'admin')->firstOrFail()->id,
]);
$this->loginAs()
->get(route('system-settings.databases.index'))
->assertOk()
->assertSeeText('财务系统配置数据库')
->assertSeeText('共享 · 已连接')
->assertSeeText('云数据库');
$this->get(route('system-settings.databases.system.edit'))
->assertOk()
->assertSeeText('连接系统数据库')
->assertSee('name="password"', false)
->assertDontSee('name="host"', false)
->assertDontSeeText('要求 TLS 加密连接')
->assertDontSeeText('CA 证书路径');
}
public function test_database_page_separates_system_and_cloud_database_configuration(): void
{
$connectionName = (string) config('database.default');
config(["database.connections.$connectionName.password" => 'system-database-test-secret']);
$this->loginAs()
->get(route('system-settings.databases.index'))
->assertOk()
->assertSee('data-system-database="fixed"', false)
->assertSeeTextInOrder([
'系统数据库',
'财务系统配置数据库',
'共享 · 已连接',
'云数据库',
'添加云数据库',
]);
$this->get(route('system-settings.databases.system.edit'))
->assertOk()
->assertSeeText('连接系统数据库')
->assertDontSee('name="host"', false)
->assertDontSee('name="tls_required"', false)
->assertDontSee('system-database-test-secret');
}
public function test_cloud_database_create_page_opens_inside_system_settings(): void
{
$this->loginAs()
->get(route('system-settings.databases.create'))
->assertOk()
->assertSeeText('添加数据库')
->assertDontSeeText('Not able to create a new user.');
}
public function test_production_stored_configuration_requires_explicit_private_flag(): void
{
$connection = $this->connection('production-secret');
$connection->update(['source_key' => 'jiansun']);
$originalEnvironment = $this->app->environment();
try {
$this->app['env'] = 'production';
config(['upstreams.stored_configuration.enabled' => false]);
$disabled = app(UpstreamStoredConfiguration::class);
$this->assertFalse($disabled->editable());
$this->assertNull($disabled->find('jiansun'));
config(['upstreams.stored_configuration.enabled' => true]);
$enabled = new UpstreamStoredConfiguration();
$this->assertTrue($enabled->editable());
$this->assertSame($connection->id, $enabled->find('jiansun')?->id);
} finally {
$this->app['env'] = $originalEnvironment;
}
}
public function test_bootstrap_page_is_not_presented_as_shared_database(): void
{
config([
'system_database.mode' => 'bootstrap',
'upstreams.stored_configuration.enabled' => false,
]);
$this->loginAs()
->get(route('system-settings.databases.index'))
->assertOk()
->assertSeeText('本地临时引导库')
->assertSeeText('临时 · 待切换')
->assertDontSeeText('添加云数据库');
}
public function test_first_switch_only_requires_system_database_password(): void
{
config(['system_database.mode' => 'bootstrap']);
$this->withExceptionHandling();
$this->loginAs()
->patch(route('system-settings.databases.system.update'), [
'password' => '',
])
->assertSessionHasErrors('password');
}
public function test_wrong_system_database_password_stays_on_the_form(): void
{
config([
'system_database.mode' => 'bootstrap',
'system_database.team_profile' => null,
]);
$this->withExceptionHandling();
$this->loginAs()
->from(route('system-settings.databases.system.edit'))
->patch(route('system-settings.databases.system.update'), [
'password' => 'definitely-not-the-team-password',
])
->assertRedirect(route('system-settings.databases.system.edit'))
->assertSessionHasErrors([
'password' => '系统数据库密码不正确,请重新输入。',
]);
}
public function test_successful_system_database_switch_invalidates_bootstrap_session(): void
{
$configuration = Mockery::mock(SystemDatabaseConfiguration::class);
$configuration->shouldReceive('probe')->once()->andReturn([
'cipher' => 'TLS_AES_256_GCM_SHA384',
'user_count' => 7,
'admin_count' => 2,
'source_count' => 2,
'prefix' => 'ak_',
]);
$configuration->shouldReceive('persist')->once();
$this->app->instance(SystemDatabaseConfiguration::class, $configuration);
$teamProfile = Mockery::mock(TeamSystemDatabaseProfile::class);
$teamProfile->shouldReceive('settings')->once()->with('new-shared-secret')->andReturn([
'driver' => 'mysql',
'host' => 'shared-db.test',
'port' => 3306,
'database' => 'shared_system',
'username' => 'shared_writer',
'password' => 'new-shared-secret',
'tls_required' => true,
'tls_verify_server_cert' => false,
'tls_ca' => '',
]);
$this->app->instance(TeamSystemDatabaseProfile::class, $teamProfile);
$this->loginAs()
->patch(route('system-settings.databases.system.update'), [
'password' => 'new-shared-secret',
])
->assertRedirect(route('login'));
$this->assertGuest();
}
public function test_unexpected_system_database_error_returns_to_form_instead_of_500(): void
{
$this->withExceptionHandling();
$configuration = Mockery::mock(SystemDatabaseConfiguration::class);
$configuration->shouldReceive('probe')->once()->andThrow(new \LogicException('internal detail'));
$this->app->instance(SystemDatabaseConfiguration::class, $configuration);
$teamProfile = Mockery::mock(TeamSystemDatabaseProfile::class);
$teamProfile->shouldReceive('settings')->once()->andReturn([
'driver' => 'mysql',
'host' => 'shared-db.test',
'port' => 3306,
'database' => 'shared_system',
'username' => 'shared_writer',
'password' => 'new-shared-secret',
'tls_required' => true,
'tls_verify_server_cert' => false,
'tls_ca' => '',
]);
$this->app->instance(TeamSystemDatabaseProfile::class, $teamProfile);
$this->loginAs()
->from(route('system-settings.databases.system.edit'))
->patch(route('system-settings.databases.system.update'), [
'password' => 'new-shared-secret',
])
->assertRedirect(route('system-settings.databases.system.edit'))
->assertSessionHasErrors('password');
}
private function payload(array $overrides = []): array
{
return array_merge([
'name' => '测试云数据库',
'source_key' => '',
'host' => '127.0.0.1',
'port' => 1433,
'database_name' => 'test_finance',
'username' => 'readonly_user',
'password' => 'secret',
'odbc_driver' => 'FreeTDS',
'tds_version' => '7.4',
'connect_timeout' => 3,
'query_timeout' => 8,
'allowed_scopes' => 'supplier.options',
'enabled' => '1',
], $overrides);
}
private function connection(string $password): DatabaseConnection
{
return DatabaseConnection::create([
'company_id' => 0,
'source_key' => null,
'name' => '已有云数据库',
'driver' => 'sqlsrv',
'host' => '127.0.0.1',
'port' => 1433,
'database_name' => 'test_finance',
'username' => 'readonly_user',
'password' => $password,
'odbc_driver' => 'FreeTDS',
'tds_version' => '7.4',
'connect_timeout' => 3,
'query_timeout' => 8,
'allowed_scopes' => ['supplier.options'],
'charset' => 'UTF-8',
'enabled' => true,
]);
}
}