instance(RemoteCommandRunner::class, new class implements RemoteCommandRunner { public string $script = ''; public function run(Server $server, string $script): string { $this->script = $script; return "image_digest=postgres:18@sha256:resolveddigest\n"; } }); $service = Service::factory()->for(serviceDigestServer())->create([ 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'version' => '18', 'version_track' => '18', 'driver_name' => 'postgres.18', 'credentials' => [ 'user' => 'keystone', 'password' => 'secret', 'db' => 'keystone', ], ]); expect(app(ResolveServiceImageDigest::class)->execute($service))->toBe('sha256:resolveddigest'); }); it('pulls the image before failing digest resolution when it is not present locally', function () { $runner = new class implements RemoteCommandRunner { public string $script = ''; public function run(Server $server, string $script): string { $this->script = $script; return 'image_digest=valkey/valkey:8@sha256:pulleddigest'; } }; app()->instance(RemoteCommandRunner::class, $runner); $service = Service::factory()->for(serviceDigestServer())->create([ 'category' => ServiceCategory::CACHE, 'type' => ServiceType::VALKEY, 'version' => '8', 'version_track' => '8', 'driver_name' => 'valkey.8', ]); expect(app(ResolveServiceImageDigest::class)->execute($service))->toBe('sha256:pulleddigest') ->and($runner->script)->toContain('docker pull "$image"'); }); it('short circuits when the resolved image is already a sha256 digest', function () { $service = Service::factory()->for(serviceDigestServer())->create([ 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'version' => '18', 'version_track' => '18', 'driver_name' => 'postgres.18', 'credentials' => [ 'user' => 'keystone', 'password' => 'secret', 'db' => 'keystone', ], 'available_image_digest' => 'sha256:precomputed', ]); expect(app(ResolveServiceImageDigest::class)->execute($service))->toBe('sha256:precomputed'); }); it('falls back to the raw output when the digest line is not present', function () { app()->instance(RemoteCommandRunner::class, new class implements RemoteCommandRunner { public function run(Server $server, string $script): string { return 'postgres:18@sha256:fallbackdigest'; } }); $service = Service::factory()->for(serviceDigestServer())->create([ 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'version' => '18', 'version_track' => '18', 'driver_name' => 'postgres.18', 'credentials' => ['user' => 'u', 'password' => 'p', 'db' => 'd'], ]); expect(app(ResolveServiceImageDigest::class)->execute($service))->toBe('sha256:fallbackdigest'); }); it('throws when the service has no server to query', function () { $service = Service::factory()->create([ 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'driver_name' => 'postgres.18', 'credentials' => ['user' => 'u', 'password' => 'p', 'db' => 'd'], 'server_id' => null, ]); expect(fn () => app(ResolveServiceImageDigest::class)->execute($service)) ->toThrow(RuntimeException::class, 'must have a target server'); }); it('throws when the remote output does not yield a digest', function () { app()->instance(RemoteCommandRunner::class, new class implements RemoteCommandRunner { public function run(Server $server, string $script): string { return 'no digest at all'; } }); $service = Service::factory()->for(serviceDigestServer())->create([ 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'driver_name' => 'postgres.18', 'credentials' => ['user' => 'u', 'password' => 'p', 'db' => 'd'], ]); expect(fn () => app(ResolveServiceImageDigest::class)->execute($service)) ->toThrow(RuntimeException::class, 'Unable to resolve image digest'); }); function serviceDigestServer(): Server { $organisation = Organisation::factory()->create(); $provider = Provider::factory()->forOrganisation($organisation)->create(); $network = Network::create([ 'organisation_id' => $organisation->id, 'provider_id' => $provider->id, 'name' => 'test-network', 'ip_range' => '10.0.0.0/24', ]); return Server::factory() ->forOrganisation($organisation->id) ->forProvider($provider->id) ->forNetwork($network->id) ->create(); }