mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-16 06:14:21 +00:00
Merge remote-tracking branch 'origin/next' into audit-policies
This commit is contained in:
commit
6ab88c46b2
66 files changed
+2155
-792
No files matched your search
@@ -85,8 +85,7 @@ describe('PATCH /api/v1/applications/{uuid} build_pack=railpack', function () {
|
||||
$app->refresh();
|
||||
expect($app->build_pack)->toBe('railpack');
|
||||
expect($app->dockerfile)->toBeNull();
|
||||
// NOTE: dockerfile_location is normalized to '/Dockerfile' by the model
|
||||
// mutator when set to null, so we cannot assert it becomes null here.
|
||||
expect($app->dockerfile_location)->toBeNull();
|
||||
expect($app->dockerfile_target_build)->toBeNull();
|
||||
expect((bool) $app->custom_healthcheck_found)->toBeFalse();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Notifications\ApiTokenExpiringNotification;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Notifications\Dispatcher;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
@@ -29,11 +30,12 @@ beforeEach(function () {
|
||||
Notification::fake();
|
||||
});
|
||||
|
||||
function createTokenExpiring(User $user, Team $team, ?Carbon $expiresAt): PersonalAccessToken
|
||||
function createTokenExpiring(User $user, Team $team, ?Carbon $expiresAt, ?Carbon $warningSentAt = null): PersonalAccessToken
|
||||
{
|
||||
$plain = $user->createToken('t-'.uniqid(), ['read'], $expiresAt);
|
||||
$token = $plain->accessToken;
|
||||
$token->team_id = $team->id;
|
||||
$token->api_token_expiration_warning_sent_at = $warningSentAt;
|
||||
$token->save();
|
||||
|
||||
return $token->fresh();
|
||||
@@ -41,14 +43,30 @@ function createTokenExpiring(User $user, Team $team, ?Carbon $expiresAt): Person
|
||||
|
||||
describe('ApiTokenExpirationWarningJob', function () {
|
||||
test('notifies team when token expires within 24h', function () {
|
||||
createTokenExpiring($this->user, $this->team, now()->addHours(23));
|
||||
$token = createTokenExpiring($this->user, $this->team, now()->addHours(23));
|
||||
|
||||
(new ApiTokenExpirationWarningJob)->handle();
|
||||
|
||||
Notification::assertSentTo($this->team, ApiTokenExpiringNotification::class);
|
||||
expect($token->fresh()->api_token_expiration_warning_sent_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('rate limiter prevents duplicate warnings on repeat runs', function () {
|
||||
test('does not mark token as warned when notification fails', function () {
|
||||
$token = createTokenExpiring($this->user, $this->team, now()->addHours(23));
|
||||
$dispatcher = Mockery::mock(Dispatcher::class);
|
||||
$dispatcher->shouldReceive('send')
|
||||
->once()
|
||||
->andThrow(new RuntimeException('Notification failed'));
|
||||
|
||||
$this->app->instance(Dispatcher::class, $dispatcher);
|
||||
|
||||
expect(fn () => (new ApiTokenExpirationWarningJob)->handle())
|
||||
->toThrow(RuntimeException::class, 'Notification failed');
|
||||
|
||||
expect($token->fresh()->api_token_expiration_warning_sent_at)->toBeNull();
|
||||
});
|
||||
|
||||
test('database marker prevents duplicate warnings on repeat runs', function () {
|
||||
createTokenExpiring($this->user, $this->team, now()->addHours(12));
|
||||
|
||||
(new ApiTokenExpirationWarningJob)->handle();
|
||||
@@ -57,6 +75,35 @@ describe('ApiTokenExpirationWarningJob', function () {
|
||||
Notification::assertSentToTimes($this->team, ApiTokenExpiringNotification::class, 1);
|
||||
});
|
||||
|
||||
test('database marker prevents duplicate warnings after cache is flushed', function () {
|
||||
createTokenExpiring($this->user, $this->team, now()->addHours(12));
|
||||
|
||||
(new ApiTokenExpirationWarningJob)->handle();
|
||||
|
||||
Cache::flush();
|
||||
|
||||
(new ApiTokenExpirationWarningJob)->handle();
|
||||
|
||||
Notification::assertSentToTimes($this->team, ApiTokenExpiringNotification::class, 1);
|
||||
});
|
||||
|
||||
test('skips tokens that already have an expiration warning marker', function () {
|
||||
createTokenExpiring($this->user, $this->team, now()->addHours(12), now()->subHour());
|
||||
|
||||
(new ApiTokenExpirationWarningJob)->handle();
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
test('notifies once for each unmarked expiring token', function () {
|
||||
createTokenExpiring($this->user, $this->team, now()->addHours(12));
|
||||
createTokenExpiring($this->user, $this->team, now()->addHours(23));
|
||||
|
||||
(new ApiTokenExpirationWarningJob)->handle();
|
||||
|
||||
Notification::assertSentToTimes($this->team, ApiTokenExpiringNotification::class, 2);
|
||||
});
|
||||
|
||||
test('skips tokens expiring more than 24h out', function () {
|
||||
createTokenExpiring($this->user, $this->team, now()->addDays(3));
|
||||
|
||||
|
||||
@@ -240,6 +240,27 @@ describe('Application Model Buildpack Cleanup', function () {
|
||||
expect($application->dockerfile)->toBeNull();
|
||||
});
|
||||
|
||||
test('dockerfile location defaults only for dockerfile buildpack', function () {
|
||||
$team = Team::factory()->create();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
$nixpacksApplication = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'build_pack' => 'nixpacks',
|
||||
'dockerfile_location' => null,
|
||||
]);
|
||||
|
||||
$dockerfileApplication = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'build_pack' => 'dockerfile',
|
||||
'dockerfile_location' => null,
|
||||
]);
|
||||
|
||||
expect($nixpacksApplication->refresh()->dockerfile_location)->toBeNull();
|
||||
expect($dockerfileApplication->refresh()->dockerfile_location)->toBe('/Dockerfile');
|
||||
});
|
||||
|
||||
test('model does not trigger cleanup when build_pack is not changed', function () {
|
||||
$team = Team::factory()->create();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function configurationChangedTestApplication(array $attributes = []): Application
|
||||
{
|
||||
$team = Team::factory()->create();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
return Application::factory()->create(array_merge([
|
||||
'environment_id' => $environment->id,
|
||||
'status' => 'running:healthy',
|
||||
'build_command' => 'npm run build',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function configurationChangedDeployment(Application $application): ApplicationDeploymentQueue
|
||||
{
|
||||
return ApplicationDeploymentQueue::create([
|
||||
'application_id' => (string) $application->id,
|
||||
'deployment_uuid' => (string) Str::uuid(),
|
||||
'status' => 'finished',
|
||||
'commit' => 'HEAD',
|
||||
]);
|
||||
}
|
||||
|
||||
it('stores deployment configuration snapshot and clears pending changes', function () {
|
||||
$application = configurationChangedTestApplication();
|
||||
$deployment = configurationChangedDeployment($application);
|
||||
|
||||
$application->markDeploymentConfigurationApplied($deployment);
|
||||
|
||||
expect($deployment->refresh()->configuration_hash)->not->toBeNull()
|
||||
->and($deployment->configuration_snapshot)->toBeArray()
|
||||
->and($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse();
|
||||
});
|
||||
|
||||
it('stores a diff between successful deployments', function () {
|
||||
$application = configurationChangedTestApplication();
|
||||
$firstDeployment = configurationChangedDeployment($application);
|
||||
$application->markDeploymentConfigurationApplied($firstDeployment);
|
||||
|
||||
$application->update(['build_command' => 'pnpm build']);
|
||||
$secondDeployment = configurationChangedDeployment($application->refresh());
|
||||
$application->markDeploymentConfigurationApplied($secondDeployment);
|
||||
|
||||
expect($secondDeployment->refresh()->configuration_diff['count'])->toBe(1)
|
||||
->and(data_get($secondDeployment->configuration_diff, 'changes.0.label'))->toBe('Build command');
|
||||
});
|
||||
|
||||
it('checks legacy preview deployment configuration hash using preview environment variable query', function () {
|
||||
$application = configurationChangedTestApplication();
|
||||
|
||||
EnvironmentVariable::create([
|
||||
'key' => 'APP_ENV',
|
||||
'value' => 'preview',
|
||||
'is_preview' => true,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
'is_buildtime' => true,
|
||||
'is_runtime' => true,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
|
||||
$application->forceFill([
|
||||
'config_hash' => 'legacy-hash',
|
||||
'pull_request_id' => 123,
|
||||
]);
|
||||
|
||||
$diff = $application->pendingDeploymentConfigurationDiff();
|
||||
|
||||
expect($diff->isLegacyFallback())->toBeTrue()
|
||||
->and($diff->isChanged())->toBeTrue();
|
||||
});
|
||||
|
||||
it('falls back to legacy configuration hash when no deployment snapshot exists', function () {
|
||||
$application = configurationChangedTestApplication();
|
||||
$application->isConfigurationChanged(save: true);
|
||||
|
||||
expect($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse();
|
||||
|
||||
$application->update(['build_command' => 'pnpm build']);
|
||||
|
||||
expect($application->refresh()->pendingDeploymentConfigurationDiff()->isLegacyFallback())->toBeTrue()
|
||||
->and($application->pendingDeploymentConfigurationDiff()->isChanged())->toBeTrue();
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$plainTextToken = Str::random(40);
|
||||
$token = $this->user->tokens()->create([
|
||||
'name' => 'custom-nginx-api-test-'.Str::random(6),
|
||||
'token' => hash('sha256', $plainTextToken),
|
||||
'abilities' => ['*'],
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
$this->bearerToken = $token->getKey().'|'.$plainTextToken;
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
});
|
||||
|
||||
function customNginxApiHeaders(string $bearerToken): array
|
||||
{
|
||||
return [
|
||||
'Authorization' => 'Bearer '.$bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
}
|
||||
|
||||
function customNginxConfig(): string
|
||||
{
|
||||
return <<<'NGINX'
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
NGINX;
|
||||
}
|
||||
|
||||
function makeCustomNginxApplication(array $overrides = []): Application
|
||||
{
|
||||
return Application::factory()->create(array_merge([
|
||||
'environment_id' => test()->environment->id,
|
||||
'destination_id' => test()->destination->id,
|
||||
'destination_type' => test()->destination->getMorphClass(),
|
||||
'build_pack' => 'static',
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
describe('PATCH /api/v1/applications/{uuid} custom_nginx_configuration', function () {
|
||||
test('decodes base64 custom nginx configuration before storing it', function () {
|
||||
$application = makeCustomNginxApplication();
|
||||
$configuration = customNginxConfig();
|
||||
$encodedConfiguration = base64_encode($configuration);
|
||||
|
||||
$response = $this->withHeaders(customNginxApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$application->uuid}", [
|
||||
'custom_nginx_configuration' => $encodedConfiguration,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$application->refresh();
|
||||
expect($application->custom_nginx_configuration)->toBe($configuration);
|
||||
|
||||
$storedConfiguration = DB::table('applications')
|
||||
->where('id', $application->id)
|
||||
->value('custom_nginx_configuration');
|
||||
|
||||
expect($storedConfiguration)->toBe(base64_encode($configuration));
|
||||
|
||||
$this->withHeaders(customNginxApiHeaders($this->bearerToken))
|
||||
->getJson("/api/v1/applications/{$application->uuid}")
|
||||
->assertOk()
|
||||
->assertJsonPath('custom_nginx_configuration', $configuration);
|
||||
});
|
||||
|
||||
test('rejects custom nginx configuration that is not base64 encoded', function () {
|
||||
$application = makeCustomNginxApplication();
|
||||
|
||||
$response = $this->withHeaders(customNginxApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$application->uuid}", [
|
||||
'custom_nginx_configuration' => customNginxConfig(),
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonPath('errors.custom_nginx_configuration', 'The custom_nginx_configuration should be base64 encoded.');
|
||||
});
|
||||
|
||||
test('can clear custom nginx configuration with null', function () {
|
||||
$application = makeCustomNginxApplication([
|
||||
'custom_nginx_configuration' => customNginxConfig(),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders(customNginxApiHeaders($this->bearerToken))
|
||||
->patchJson("/api/v1/applications/{$application->uuid}", [
|
||||
'custom_nginx_configuration' => null,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$application->refresh();
|
||||
expect($application->custom_nginx_configuration)->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/applications/public custom_nginx_configuration', function () {
|
||||
test('decodes base64 custom nginx configuration before storing it on create', function () {
|
||||
$configuration = customNginxConfig();
|
||||
|
||||
$response = $this->withHeaders(customNginxApiHeaders($this->bearerToken))
|
||||
->postJson('/api/v1/applications/public', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'git_repository' => 'https://gitlab.com/coolify/test-static-app',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'static',
|
||||
'ports_exposes' => '80',
|
||||
'custom_nginx_configuration' => base64_encode($configuration),
|
||||
'autogenerate_domain' => false,
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
|
||||
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
|
||||
|
||||
expect($application->custom_nginx_configuration)->toBe($configuration);
|
||||
});
|
||||
});
|
||||
@@ -67,7 +67,7 @@ test('existing application buildpack selector lists nixpacks before railpack', f
|
||||
], false);
|
||||
});
|
||||
|
||||
test('existing application shows railpack beta badge in build helper copy', function () {
|
||||
test('existing application shows railpack beta label in build pack selector', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
@@ -81,6 +81,5 @@ test('existing application shows railpack beta badge in build helper copy', func
|
||||
|
||||
Livewire::test(General::class, ['application' => $application])
|
||||
->assertSuccessful()
|
||||
->assertSee('Railpack')
|
||||
->assertSee('Beta');
|
||||
->assertSee('Railpack (Beta)');
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
|
||||
function makePreviewImageNameJob(string $commit, int $pullRequestId = 42, ?string $registryImageName = null, string $deploymentUuid = 'deployment-uuid'): object
|
||||
{
|
||||
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
||||
$job = $reflection->newInstanceWithoutConstructor();
|
||||
|
||||
$application = new Application;
|
||||
$application->uuid = 'preview-app';
|
||||
$application->build_pack = 'dockerfile';
|
||||
$application->dockerfile = null;
|
||||
$application->docker_registry_image_name = $registryImageName;
|
||||
|
||||
foreach ([
|
||||
'application' => $application,
|
||||
'pull_request_id' => $pullRequestId,
|
||||
'commit' => $commit,
|
||||
'deployment_uuid' => $deploymentUuid,
|
||||
] as $property => $value) {
|
||||
$reflectionProperty = $reflection->getProperty($property);
|
||||
$reflectionProperty->setAccessible(true);
|
||||
$reflectionProperty->setValue($job, $value);
|
||||
}
|
||||
|
||||
return $job;
|
||||
}
|
||||
|
||||
function generatePreviewImageNames(object $job): array
|
||||
{
|
||||
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
||||
$method = $reflection->getMethod('generate_image_names');
|
||||
$method->setAccessible(true);
|
||||
$method->invoke($job);
|
||||
|
||||
$buildImageName = $reflection->getProperty('build_image_name');
|
||||
$buildImageName->setAccessible(true);
|
||||
|
||||
$productionImageName = $reflection->getProperty('production_image_name');
|
||||
$productionImageName->setAccessible(true);
|
||||
|
||||
return [
|
||||
'build' => $buildImageName->getValue($job),
|
||||
'production' => $productionImageName->getValue($job),
|
||||
];
|
||||
}
|
||||
|
||||
it('includes the pull request id and commit in preview image names', function () {
|
||||
$names = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: '111222333444555666777888999000aaabbbccc1',
|
||||
pullRequestId: 123,
|
||||
));
|
||||
|
||||
expect($names['production'])->toBe('preview-app:pr-123-111222333444555666777888999000aaabbbccc1')
|
||||
->and($names['build'])->toBe('preview-app:pr-123-111222333444555666777888999000aaabbbccc1-build');
|
||||
});
|
||||
|
||||
it('generates different preview image names for different commits on the same pull request', function () {
|
||||
$firstCommitNames = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
pullRequestId: 123,
|
||||
));
|
||||
$secondCommitNames = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
pullRequestId: 123,
|
||||
));
|
||||
|
||||
expect($firstCommitNames['production'])->not->toBe($secondCommitNames['production'])
|
||||
->and($firstCommitNames['build'])->not->toBe($secondCommitNames['build']);
|
||||
});
|
||||
|
||||
it('uses the deployment uuid for preview image names when commit is HEAD', function () {
|
||||
$firstDeploymentNames = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: 'HEAD',
|
||||
pullRequestId: 123,
|
||||
deploymentUuid: 'deployment-one',
|
||||
));
|
||||
$secondDeploymentNames = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: 'HEAD',
|
||||
pullRequestId: 123,
|
||||
deploymentUuid: 'deployment-two',
|
||||
));
|
||||
|
||||
expect($firstDeploymentNames['production'])->toBe('preview-app:pr-123-deployment-one')
|
||||
->and($firstDeploymentNames['build'])->toBe('preview-app:pr-123-deployment-one-build')
|
||||
->and($secondDeploymentNames['production'])->toBe('preview-app:pr-123-deployment-two')
|
||||
->and($secondDeploymentNames['build'])->toBe('preview-app:pr-123-deployment-two-build');
|
||||
});
|
||||
|
||||
it('uses the configured registry image name for commit-specific preview tags', function () {
|
||||
$names = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: '111222333444555666777888999000aaabbbccc1',
|
||||
pullRequestId: 123,
|
||||
registryImageName: 'registry.example.com/team/app',
|
||||
));
|
||||
|
||||
expect($names['production'])->toBe('registry.example.com/team/app:pr-123-111222333444555666777888999000aaabbbccc1')
|
||||
->and($names['build'])->toBe('registry.example.com/team/app:pr-123-111222333444555666777888999000aaabbbccc1-build');
|
||||
});
|
||||
|
||||
it('sanitizes and truncates preview image tags to docker tag limits', function () {
|
||||
$names = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: str_repeat('feature/add dockerfile changes/', 10),
|
||||
pullRequestId: 123,
|
||||
));
|
||||
|
||||
$productionTag = str($names['production'])->after(':')->toString();
|
||||
$buildTag = str($names['build'])->after(':')->toString();
|
||||
|
||||
expect(strlen($productionTag))->toBeLessThanOrEqual(128)
|
||||
->and(strlen($buildTag))->toBeLessThanOrEqual(128)
|
||||
->and($productionTag)->toMatch('/^pr-123-[A-Za-z0-9_.-]+$/')
|
||||
->and($buildTag)->toMatch('/^pr-123-[A-Za-z0-9_.-]+-build$/');
|
||||
});
|
||||
|
||||
it('keeps non-preview dockerfile image names commit based', function () {
|
||||
$names = generatePreviewImageNames(makePreviewImageNameJob(
|
||||
commit: '111222333444555666777888999000aaabbbccc1',
|
||||
pullRequestId: 0,
|
||||
));
|
||||
|
||||
expect($names['production'])->toBe('preview-app:111222333444555666777888999000aaabbbccc1')
|
||||
->and($names['build'])->toBe('preview-app:111222333444555666777888999000aaabbbccc1-build');
|
||||
});
|
||||
@@ -24,12 +24,23 @@ beforeEach(function () {
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
});
|
||||
|
||||
function applicationSourceValidPrivateKey(): string
|
||||
{
|
||||
return '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
|
||||
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
|
||||
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
|
||||
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----';
|
||||
}
|
||||
|
||||
describe('Application Source with localhost key (id=0)', function () {
|
||||
test('renders deploy key section when private_key_id is 0', function () {
|
||||
$privateKey = PrivateKey::create([
|
||||
'id' => 0,
|
||||
'name' => 'localhost',
|
||||
'private_key' => 'test-key-content',
|
||||
'private_key' => applicationSourceValidPrivateKey(),
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
@@ -56,4 +67,19 @@ describe('Application Source with localhost key (id=0)', function () {
|
||||
->assertDontSee('Deploy Key')
|
||||
->assertSee('No source connected');
|
||||
});
|
||||
|
||||
test('dispatches configuration changed when source settings are saved', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
'git_commit_sha' => 'HEAD',
|
||||
]);
|
||||
|
||||
Livewire::test(Source::class, ['application' => $application])
|
||||
->set('gitBranch', 'next')
|
||||
->call('submit')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('configurationChanged');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
<?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\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', 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('Build command')
|
||||
->assertSee('A rebuild is required.');
|
||||
});
|
||||
|
||||
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('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
|
||||
->call('refreshConfigurationChanges')
|
||||
->assertSet('isConfigurationChanged', true)
|
||||
->assertSee('Start command')
|
||||
->assertDontSee('Build command');
|
||||
});
|
||||
|
||||
it('does not render environment variable secret values', function () {
|
||||
$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']);
|
||||
|
||||
Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
||||
->assertSee('API_TOKEN')
|
||||
->assertSee('changed')
|
||||
->assertSee('Set')
|
||||
->assertDontSee('Hidden')
|
||||
->assertDontSee('old-secret')
|
||||
->assertDontSee('new-secret');
|
||||
});
|
||||
|
||||
it('renders added environment variables as set without exposing secret values', function () {
|
||||
$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,
|
||||
]);
|
||||
|
||||
Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
|
||||
->assertSee('API_TOKEN')
|
||||
->assertSee('From')
|
||||
->assertSee('Not set')
|
||||
->assertSee('To')
|
||||
->assertSee('Set')
|
||||
->assertDontSee('Hidden')
|
||||
->assertDontSee('new-secret');
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Application\Advanced;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function createApplicationForAdvancedStopGracePeriodTest(): Application
|
||||
{
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
return Application::create([
|
||||
'name' => 'stop-grace-period-test-app',
|
||||
'git_repository' => 'https://github.com/coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $server->standaloneDockers()->firstOrFail()->id,
|
||||
'destination_type' => $server->standaloneDockers()->firstOrFail()->getMorphClass(),
|
||||
]);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->actingAs(User::factory()->create());
|
||||
});
|
||||
|
||||
it('saves a valid stop grace period', function () {
|
||||
$application = createApplicationForAdvancedStopGracePeriodTest();
|
||||
|
||||
Livewire::test(Advanced::class, ['application' => $application])
|
||||
->set('stopGracePeriod', '300')
|
||||
->call('saveStopGracePeriod')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success');
|
||||
|
||||
expect($application->settings()->first()->stop_grace_period)->toBe(300);
|
||||
});
|
||||
|
||||
it('dispatches configuration changed when advanced settings are saved', function () {
|
||||
$application = createApplicationForAdvancedStopGracePeriodTest();
|
||||
|
||||
Livewire::test(Advanced::class, ['application' => $application])
|
||||
->set('includeSourceCommitInBuild', true)
|
||||
->call('submit')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('configurationChanged');
|
||||
});
|
||||
|
||||
it('clears the stop grace period when submitted empty', function () {
|
||||
$application = createApplicationForAdvancedStopGracePeriodTest();
|
||||
$application->settings->update(['stop_grace_period' => 300]);
|
||||
|
||||
Livewire::test(Advanced::class, ['application' => $application->fresh()])
|
||||
->set('stopGracePeriod', '')
|
||||
->call('saveStopGracePeriod')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('success');
|
||||
|
||||
expect($application->settings()->first()->stop_grace_period)->toBeNull();
|
||||
});
|
||||
|
||||
it('rejects invalid stop grace periods', function (string $value, string $rule) {
|
||||
$application = createApplicationForAdvancedStopGracePeriodTest();
|
||||
|
||||
Livewire::test(Advanced::class, ['application' => $application])
|
||||
->set('stopGracePeriod', $value)
|
||||
->call('saveStopGracePeriod')
|
||||
->assertHasErrors(['stopGracePeriod' => [$rule]]);
|
||||
|
||||
expect($application->settings()->first()->stop_grace_period)->toBeNull();
|
||||
})->with([
|
||||
'below minimum' => ['0', 'min'],
|
||||
'above maximum' => [(string) (MAX_STOP_GRACE_PERIOD_SECONDS + 1), 'max'],
|
||||
'malformed integer' => ['10abc', 'integer'],
|
||||
'decimal' => ['1.9', 'integer'],
|
||||
]);
|
||||
|
||||
it('uses one second deployment timeout in local only when stop grace period is unset', function () {
|
||||
config(['app.env' => 'local']);
|
||||
|
||||
$setting = new ApplicationSetting;
|
||||
|
||||
expect($setting->deploymentStopGracePeriodSeconds())->toBe(MIN_STOP_GRACE_PERIOD_SECONDS);
|
||||
|
||||
$setting->stop_grace_period = 10;
|
||||
|
||||
expect($setting->deploymentStopGracePeriodSeconds())->toBe(10);
|
||||
});
|
||||
|
||||
it('uses default deployment timeout outside local when stop grace period is unset', function () {
|
||||
config(['app.env' => 'production']);
|
||||
|
||||
$setting = new ApplicationSetting;
|
||||
|
||||
expect($setting->deploymentStopGracePeriodSeconds())->toBe(DEFAULT_STOP_GRACE_PERIOD_SECONDS);
|
||||
});
|
||||
@@ -299,6 +299,7 @@ it('creates ApplicationSetting with all fillable attributes', function () {
|
||||
'inject_build_args_to_dockerfile' => true,
|
||||
'include_source_commit_in_build' => true,
|
||||
'docker_images_to_keep' => 5,
|
||||
'stop_grace_period' => 300,
|
||||
]);
|
||||
|
||||
expect($setting->exists)->toBeTrue();
|
||||
@@ -309,6 +310,7 @@ it('creates ApplicationSetting with all fillable attributes', function () {
|
||||
expect($setting->custom_internal_name)->toBe('my-custom-app');
|
||||
expect($setting->is_spa)->toBeTrue();
|
||||
expect($setting->docker_images_to_keep)->toBe(5);
|
||||
expect($setting->stop_grace_period)->toBe(300);
|
||||
});
|
||||
|
||||
it('creates ServerSetting with all fillable attributes', function () {
|
||||
|
||||
@@ -38,14 +38,12 @@ describe('new application buildpack defaults', function () {
|
||||
test('public repository flow keeps railpack available after branch lookup', function () {
|
||||
Livewire::test(PublicGitRepository::class, ['type' => 'public'])
|
||||
->set('branchFound', true)
|
||||
->assertSeeInOrder(['Nixpacks', 'Railpack (Beta)'])
|
||||
->assertSee('Beta');
|
||||
->assertSeeInOrder(['Nixpacks', 'Railpack (Beta)']);
|
||||
});
|
||||
|
||||
test('deploy key repository flow shows railpack beta label in build pack selector', function () {
|
||||
test('deploy key repository flow shows railpack beta label in build pack selector without beta badge', function () {
|
||||
Livewire::test(GithubPrivateRepositoryDeployKey::class, ['type' => 'private-deploy-key'])
|
||||
->set('current_step', 'repository')
|
||||
->assertSee('Railpack (Beta)')
|
||||
->assertSee('Beta');
|
||||
->assertSee('Railpack (Beta)');
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ use App\Models\ApplicationSetting;
|
||||
|
||||
it('casts is_static to boolean when true', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = true;
|
||||
$setting->setRawAttributes(['is_static' => true]);
|
||||
|
||||
// Verify it's cast to boolean
|
||||
expect($setting->is_static)->toBeTrue()
|
||||
@@ -20,7 +20,7 @@ it('casts is_static to boolean when true', function () {
|
||||
|
||||
it('casts is_static to boolean when false', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = false;
|
||||
$setting->setRawAttributes(['is_static' => false]);
|
||||
|
||||
// Verify it's cast to boolean
|
||||
expect($setting->is_static)->toBeFalse()
|
||||
@@ -29,7 +29,7 @@ it('casts is_static to boolean when false', function () {
|
||||
|
||||
it('casts is_static from string "1" to boolean true', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = '1';
|
||||
$setting->setRawAttributes(['is_static' => '1']);
|
||||
|
||||
// Should cast string to boolean
|
||||
expect($setting->is_static)->toBeTrue()
|
||||
@@ -38,7 +38,7 @@ it('casts is_static from string "1" to boolean true', function () {
|
||||
|
||||
it('casts is_static from string "0" to boolean false', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = '0';
|
||||
$setting->setRawAttributes(['is_static' => '0']);
|
||||
|
||||
// Should cast string to boolean
|
||||
expect($setting->is_static)->toBeFalse()
|
||||
@@ -47,7 +47,7 @@ it('casts is_static from string "0" to boolean false', function () {
|
||||
|
||||
it('casts is_static from integer 1 to boolean true', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = 1;
|
||||
$setting->setRawAttributes(['is_static' => 1]);
|
||||
|
||||
// Should cast integer to boolean
|
||||
expect($setting->is_static)->toBeTrue()
|
||||
@@ -56,7 +56,7 @@ it('casts is_static from integer 1 to boolean true', function () {
|
||||
|
||||
it('casts is_static from integer 0 to boolean false', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->is_static = 0;
|
||||
$setting->setRawAttributes(['is_static' => 0]);
|
||||
|
||||
// Should cast integer to boolean
|
||||
expect($setting->is_static)->toBeFalse()
|
||||
@@ -103,3 +103,65 @@ it('casts all boolean fields correctly', function () {
|
||||
->and($casts[$field])->toBe('boolean');
|
||||
}
|
||||
});
|
||||
|
||||
it('casts stop_grace_period to integer', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$casts = $setting->getCasts();
|
||||
|
||||
expect($casts)->toHaveKey('stop_grace_period')
|
||||
->and($casts['stop_grace_period'])->toBe('integer');
|
||||
});
|
||||
|
||||
it('handles null stop_grace_period for default behavior', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = null;
|
||||
|
||||
expect($setting->stop_grace_period)->toBeNull();
|
||||
});
|
||||
|
||||
it('casts stop_grace_period from string to integer', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = '60';
|
||||
|
||||
expect($setting->stop_grace_period)->toBe(60)
|
||||
->and($setting->stop_grace_period)->toBeInt();
|
||||
});
|
||||
|
||||
it('casts stop_grace_period zero to integer (documents fallback trigger)', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = 0;
|
||||
|
||||
expect($setting->stop_grace_period)->toBe(0)
|
||||
->and($setting->stop_grace_period)->toBeInt();
|
||||
});
|
||||
|
||||
it('casts stop_grace_period negative value to integer (documents fallback trigger)', function () {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = -10;
|
||||
|
||||
expect($setting->stop_grace_period)->toBe(-10)
|
||||
->and($setting->stop_grace_period)->toBeInt();
|
||||
});
|
||||
|
||||
it('resolves valid stop grace periods', function (?int $storedValue, int $expectedValue) {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = $storedValue;
|
||||
|
||||
expect($setting->stopGracePeriodSeconds())->toBe($expectedValue);
|
||||
})->with([
|
||||
'minimum' => [MIN_STOP_GRACE_PERIOD_SECONDS, MIN_STOP_GRACE_PERIOD_SECONDS],
|
||||
'custom' => [300, 300],
|
||||
'maximum' => [MAX_STOP_GRACE_PERIOD_SECONDS, MAX_STOP_GRACE_PERIOD_SECONDS],
|
||||
]);
|
||||
|
||||
it('falls back to default stop grace period for invalid stored values', function (?int $storedValue) {
|
||||
$setting = new ApplicationSetting;
|
||||
$setting->stop_grace_period = $storedValue;
|
||||
|
||||
expect($setting->stopGracePeriodSeconds())->toBe(DEFAULT_STOP_GRACE_PERIOD_SECONDS);
|
||||
})->with([
|
||||
'null' => [null],
|
||||
'zero' => [0],
|
||||
'negative' => [-10],
|
||||
'above maximum' => [MAX_STOP_GRACE_PERIOD_SECONDS + 1],
|
||||
]);
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function snapshotTestApplication(array $attributes = []): Application
|
||||
{
|
||||
$team = Team::factory()->create();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
return Application::factory()->create(array_merge([
|
||||
'environment_id' => $environment->id,
|
||||
'status' => 'running:healthy',
|
||||
'fqdn' => 'https://example.com',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function markSnapshotTestApplicationDeployed(Application $application): ApplicationDeploymentQueue
|
||||
{
|
||||
$deployment = ApplicationDeploymentQueue::create([
|
||||
'application_id' => (string) $application->id,
|
||||
'deployment_uuid' => (string) Str::uuid(),
|
||||
'status' => 'finished',
|
||||
'commit' => 'HEAD',
|
||||
]);
|
||||
|
||||
$application->markDeploymentConfigurationApplied($deployment);
|
||||
|
||||
return $deployment->refresh();
|
||||
}
|
||||
|
||||
it('does not report preview deployment toggles as pending production configuration changes', function () {
|
||||
$application = snapshotTestApplication();
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->settings->update(['is_preview_deployments_enabled' => true]);
|
||||
|
||||
expect($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse();
|
||||
});
|
||||
|
||||
it('detects build-impacting changes', function () {
|
||||
$application = snapshotTestApplication();
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->update(['build_command' => 'pnpm build']);
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
|
||||
expect($diff->isChanged())->toBeTrue()
|
||||
->and($diff->requiresBuild())->toBeTrue()
|
||||
->and(collect($diff->changes())->pluck('label'))->toContain('Build command');
|
||||
});
|
||||
|
||||
it('detects redeploy-only domain changes', function () {
|
||||
$application = snapshotTestApplication();
|
||||
markSnapshotTestApplicationDeployed($application);
|
||||
|
||||
$application->update(['fqdn' => 'https://new.example.com']);
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
|
||||
expect($diff->isChanged())->toBeTrue()
|
||||
->and($diff->requiresBuild())->toBeFalse()
|
||||
->and(collect($diff->changes())->pluck('label'))->toContain('Domains');
|
||||
});
|
||||
|
||||
it('detects environment variable value changes without exposing secret values', function () {
|
||||
$application = snapshotTestApplication();
|
||||
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,
|
||||
]);
|
||||
markSnapshotTestApplicationDeployed($application->refresh());
|
||||
|
||||
$application->environment_variables()->where('key', 'API_TOKEN')->first()->update(['value' => 'new-secret']);
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
$change = collect($diff->changes())->firstWhere('label', 'API_TOKEN');
|
||||
|
||||
expect($change)->not->toBeNull()
|
||||
->and($change['display_summary'])->toBe('Changed')
|
||||
->and($change['old_display_value'])->toBe('Set')
|
||||
->and($change['new_display_value'])->toBe('Set')
|
||||
->and(json_encode($diff->toArray()))->not->toContain('old-secret')->not->toContain('new-secret');
|
||||
});
|
||||
|
||||
it('describes added environment variables as set without exposing secret values', function () {
|
||||
$application = snapshotTestApplication();
|
||||
markSnapshotTestApplicationDeployed($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,
|
||||
]);
|
||||
|
||||
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
|
||||
$change = collect($diff->changes())->firstWhere('label', 'API_TOKEN');
|
||||
|
||||
expect($change)->not->toBeNull()
|
||||
->and($change['display_summary'])->toBeNull()
|
||||
->and($change['old_display_value'])->toBe('Not set')
|
||||
->and($change['new_display_value'])->toBe('Set')
|
||||
->and(json_encode($diff->toArray()))->not->toContain('new-secret');
|
||||
});
|
||||
Reference in new issue
Block a user