diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index f7048d8838..fbc2b7e1c3 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -54,8 +54,7 @@ class LocalFileVolume extends BaseModel return; } - $fileVolume->load(['service']); - dispatch(new ServerStorageSaveJob($fileVolume)); + ServerStorageSaveJob::dispatch($fileVolume)->afterCommit(); }); static::deleting(function (LocalFileVolume $fileVolume): void { diff --git a/tests/Feature/FileStorageParserStateTest.php b/tests/Feature/FileStorageParserStateTest.php index bb39ad1d49..8673fe802b 100644 --- a/tests/Feature/FileStorageParserStateTest.php +++ b/tests/Feature/FileStorageParserStateTest.php @@ -1,5 +1,6 @@ content)->toBeNull() ->and($fileVolume->is_directory)->toBeTrue(); }); + +it('queues a new application mount once without loading its service relation', function () { + $application = makeComposeApplication(DATA_DIR_COMPOSE); + + applicationParser($application); + applicationParser($application); + + Bus::assertDispatchedTimes(ServerStorageSaveJob::class, 1); + Bus::assertDispatched(ServerStorageSaveJob::class, function (ServerStorageSaveJob $job): bool { + return ! $job->localFileVolume->relationLoaded('service') && $job->afterCommit === true; + }); +}); + +it('queues a new service mount once without loading its service relation', function () { + [$service] = makeComposeService(DATA_DIR_COMPOSE); + + serviceParser($service); + serviceParser($service); + + Bus::assertDispatchedTimes(ServerStorageSaveJob::class, 1); + Bus::assertDispatched(ServerStorageSaveJob::class, function (ServerStorageSaveJob $job): bool { + return ! $job->localFileVolume->relationLoaded('service') && $job->afterCommit === true; + }); +}); diff --git a/tests/Feature/ServerStorageSaveDispatchTest.php b/tests/Feature/ServerStorageSaveDispatchTest.php new file mode 100644 index 0000000000..edf4f82c7a --- /dev/null +++ b/tests/Feature/ServerStorageSaveDispatchTest.php @@ -0,0 +1,99 @@ + true, '--no-interaction' => true]); + + config([ + 'app.maintenance.store' => 'array', + 'cache.default' => 'array', + 'queue.default' => 'sync', + 'constants.ssh.mux_enabled' => false, + ]); + + $team = Team::factory()->create(); + $key = PrivateKey::factory()->create(['team_id' => $team->id]); + $server = Server::factory()->create(['team_id' => $team->id, 'private_key_id' => $key->id]); + $destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail(); + $project = Project::factory()->create(['team_id' => $team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $this->application = Application::factory()->create([ + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + + Process::fake(fn ($process) => Process::result( + output: str_contains($process->command, 'realpath -m') ? 'OK' : 'NOK' + )); + + $this->processedSaves = 0; + Event::listen(JobProcessed::class, function (JobProcessed $event): void { + if ($event->job->resolveName() === ServerStorageSaveJob::class) { + $this->processedSaves++; + } + }); +}); + +function createStorageSaveTestMount(Application $application): LocalFileVolume +{ + return LocalFileVolume::create([ + 'fs_path' => application_configuration_dir().'/'.$application->uuid.'/data', + 'mount_path' => '/app/data', + 'is_directory' => true, + 'resource_id' => $application->id, + 'resource_type' => $application->getMorphClass(), + ]); +} + +it('runs the storage save after its mount is committed', function () { + DB::beginTransaction(); + $volume = createStorageSaveTestMount($this->application); + + expect($this->processedSaves)->toBe(0); + Process::assertNotRan(fn ($process) => str_contains($process->command, 'mkdir -p')); + + DB::commit(); + + expect($this->processedSaves)->toBe(1) + ->and($volume->exists)->toBeTrue(); + Process::assertRan(fn ($process) => str_contains($process->command, 'mkdir -p')); +}); + +it('does not run the storage save after its mount is rolled back', function () { + DB::beginTransaction(); + createStorageSaveTestMount($this->application); + DB::rollBack(); + + expect($this->processedSaves)->toBe(0) + ->and(LocalFileVolume::query()->count())->toBe(0); + Process::assertNotRan(fn ($process) => str_contains($process->command, 'mkdir -p')); +}); + +it('writes a new file mount immediately when there is no transaction', function () { + LocalFileVolume::create([ + 'fs_path' => application_configuration_dir().'/'.$this->application->uuid.'/config.env', + 'mount_path' => '/app/config.env', + 'content' => 'KEY=value', + 'is_directory' => false, + 'resource_id' => $this->application->id, + 'resource_type' => $this->application->getMorphClass(), + ]); + + expect($this->processedSaves)->toBe(1); + Process::assertRan(fn ($process) => str_contains($process->command, base64_encode('KEY=value'))); +});