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('A rebuild is 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('Please redeploy to apply the new configuration.') ->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'); });