Add an application Internal access section that loads the running container hostname (skipped for Compose apps), densify configuration diff and popup-small defaults, dock the server-timing HUD into the mobile top bar, restyle the 2FA challenge with the auth shell, and enforce 16px mobile form fonts to prevent iOS zoom.
220 lines
8.9 KiB
PHP
220 lines
8.9 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Project\Shared\ConfigurationChecker;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Models\Environment;
|
|
use App\Models\EnvironmentVariable;
|
|
use App\Models\LocalFileVolume;
|
|
use App\Models\Project;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
$this->actingAs($this->user);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
|
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
|
});
|
|
|
|
function configurationCheckerApplication(Environment $environment, array $attributes = []): Application
|
|
{
|
|
return Application::factory()->create(array_merge([
|
|
'environment_id' => $environment->id,
|
|
'status' => 'running:healthy',
|
|
'build_command' => 'npm run build',
|
|
'fqdn' => 'https://example.com',
|
|
], $attributes));
|
|
}
|
|
|
|
function markConfigurationCheckerApplicationDeployed(Application $application): void
|
|
{
|
|
$deployment = ApplicationDeploymentQueue::create([
|
|
'application_id' => (string) $application->id,
|
|
'deployment_uuid' => (string) Str::uuid(),
|
|
'status' => 'finished',
|
|
'commit' => 'HEAD',
|
|
]);
|
|
|
|
$application->markDeploymentConfigurationApplied($deployment);
|
|
}
|
|
|
|
it('does not render the notification for preview deployment toggles', function () {
|
|
$application = configurationCheckerApplication($this->environment);
|
|
markConfigurationCheckerApplicationDeployed($application);
|
|
|
|
$application->settings->update(['is_preview_deployments_enabled' => true]);
|
|
|
|
Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
|
->assertDontSee('The latest deployment is not using the current configuration')
|
|
->assertSet('isConfigurationChanged', false);
|
|
});
|
|
|
|
it('renders the changed configuration labels without a second backend request', function () {
|
|
$application = configurationCheckerApplication($this->environment);
|
|
markConfigurationCheckerApplicationDeployed($application);
|
|
|
|
$application->update(['build_command' => 'pnpm build']);
|
|
|
|
Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
|
->assertSee('The latest configuration has not been applied')
|
|
->assertSee('Rebuild required.')
|
|
->assertSee('Build command');
|
|
|
|
$view = file_get_contents(resource_path('views/livewire/project/shared/configuration-checker.blade.php'));
|
|
|
|
expect($view)
|
|
->toContain('x-on:click="configurationDiffModalOpen = true"')
|
|
->not->toContain('$wire.refreshConfigurationChanges()');
|
|
});
|
|
|
|
it('refreshes configuration changes when the event is received', function () {
|
|
$application = configurationCheckerApplication($this->environment);
|
|
markConfigurationCheckerApplicationDeployed($application);
|
|
|
|
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
|
->assertSet('isConfigurationChanged', false)
|
|
->assertDontSee('The latest configuration has not been applied');
|
|
|
|
$application->update(['build_command' => 'pnpm build']);
|
|
|
|
$component
|
|
->dispatch('configurationChanged')
|
|
->assertSet('isConfigurationChanged', true)
|
|
->assertSee('The latest configuration has not been applied')
|
|
->assertSee('Build command');
|
|
});
|
|
|
|
it('shows an unapplied configuration warning after a directory mount is added', function () {
|
|
$application = configurationCheckerApplication($this->environment);
|
|
markConfigurationCheckerApplicationDeployed($application);
|
|
|
|
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
|
->assertSet('isConfigurationChanged', false);
|
|
|
|
LocalFileVolume::withoutEvents(fn () => LocalFileVolume::forceCreate([
|
|
'uuid' => (string) Str::uuid(),
|
|
'fs_path' => application_configuration_dir().'/'.$application->uuid.'/data',
|
|
'mount_path' => '/app/data',
|
|
'is_directory' => true,
|
|
'resource_id' => $application->id,
|
|
'resource_type' => $application->getMorphClass(),
|
|
]));
|
|
|
|
$component
|
|
->dispatch('configurationChanged')
|
|
->assertSet('isConfigurationChanged', true)
|
|
->assertSee('The latest configuration has not been applied')
|
|
->assertSee('Redeploy to apply.')
|
|
->assertSee('Directory mount');
|
|
});
|
|
|
|
it('refreshes stale modal configuration diff before opening changes', function () {
|
|
$application = configurationCheckerApplication($this->environment);
|
|
markConfigurationCheckerApplicationDeployed($application);
|
|
|
|
$application->update(['build_command' => 'pnpm build']);
|
|
|
|
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
|
->assertSee('Build command')
|
|
->assertDontSee('Start command');
|
|
|
|
$application->update([
|
|
'build_command' => 'npm run build',
|
|
'start_command' => 'node server.js',
|
|
]);
|
|
|
|
$component
|
|
->dispatch('configurationChanged')
|
|
->assertSet('isConfigurationChanged', true)
|
|
->assertSee('Start command')
|
|
->assertDontSee('Build command');
|
|
});
|
|
|
|
it('includes full configuration change rows in the initial Livewire snapshot', function () {
|
|
$application = configurationCheckerApplication($this->environment);
|
|
markConfigurationCheckerApplicationDeployed($application);
|
|
$application->update(['build_command' => 'pnpm build']);
|
|
|
|
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()]);
|
|
|
|
expect($component->get('isConfigurationChanged'))->toBeTrue()
|
|
->and($component->get('configurationDiff'))->toHaveKey('changes')
|
|
->and(data_get($component->get('configurationDiff'), 'changes'))->not->toBeEmpty();
|
|
});
|
|
|
|
it('redacts unlocked environment values for team members in the change list', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$this->actingAs($member);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$application = configurationCheckerApplication($this->environment);
|
|
EnvironmentVariable::create([
|
|
'key' => 'API_TOKEN',
|
|
'value' => 'old-secret',
|
|
'is_buildtime' => false,
|
|
'is_runtime' => true,
|
|
'is_preview' => false,
|
|
'resourceable_type' => Application::class,
|
|
'resourceable_id' => $application->id,
|
|
]);
|
|
markConfigurationCheckerApplicationDeployed($application->refresh());
|
|
|
|
$application->environment_variables()->where('key', 'API_TOKEN')->first()->update(['value' => 'new-secret']);
|
|
|
|
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()]);
|
|
|
|
$envChange = collect(data_get($component->get('configurationDiff'), 'changes', []))
|
|
->first(fn (array $change): bool => str_contains((string) data_get($change, 'key'), 'API_TOKEN')
|
|
|| str_contains((string) data_get($change, 'label'), 'API_TOKEN'));
|
|
|
|
expect($envChange)->not->toBeNull()
|
|
->and(data_get($envChange, 'old_display_value'))->toBe('••••••••')
|
|
->and(data_get($envChange, 'new_display_value'))->toBe('••••••••')
|
|
->and(data_get($envChange, 'old_full_value'))->toBeNull()
|
|
->and(data_get($envChange, 'new_full_value'))->toBeNull();
|
|
});
|
|
|
|
it('redacts newly added environment values for team members', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$this->actingAs($member);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$application = configurationCheckerApplication($this->environment);
|
|
markConfigurationCheckerApplicationDeployed($application);
|
|
|
|
EnvironmentVariable::create([
|
|
'key' => 'API_TOKEN',
|
|
'value' => 'new-secret',
|
|
'is_buildtime' => false,
|
|
'is_runtime' => true,
|
|
'is_preview' => false,
|
|
'resourceable_type' => Application::class,
|
|
'resourceable_id' => $application->id,
|
|
]);
|
|
|
|
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
|
->assertSee('API_TOKEN')
|
|
->assertSee('Current')
|
|
->assertSee('New');
|
|
|
|
$envChange = collect(data_get($component->get('configurationDiff'), 'changes', []))
|
|
->first(fn (array $change): bool => str_contains((string) data_get($change, 'key'), 'API_TOKEN')
|
|
|| str_contains((string) data_get($change, 'label'), 'API_TOKEN'));
|
|
|
|
expect($envChange)->not->toBeNull()
|
|
->and(data_get($envChange, 'old_display_value'))->toBe('-')
|
|
->and(data_get($envChange, 'new_display_value'))->toBe('••••••••')
|
|
->and(data_get($envChange, 'type'))->toBe('added');
|
|
});
|