Két dolog volt beépítve az engine-be, ami nem az övé. A **képtár** eddig `WarpEngine::Image` volt, pedig a modell teljesen általános: a teletypegames-ben a tagok arcképét is ez hordozza, nem csak a katalógus borítóit. Az `Image` modell, a feltöltött fájlok, az `/api/image/:id` végpont és az admin oldal ezért átkerült a hosztba, az engine pedig adapteren szól hozzá (`WarpEngine::Images`): `url_for` adja a katalógus JSON `imageUrl`-jét, `select_options` a software-form képválasztóját, `build_from_upload` a "tölts fel új képet" ágat. Az alapértelmezés az `Image` osztály, tehát a default útvonal bitre a régi. A `SoftwareImage` (a katalógus-kapcsolat) maradt az engine-ben, és **az `images` tábla nem mozdult**: az engine csak abbahagyta a létrehozását, a generátor írja meg hoszt-kódként. A **CI** eddig végig Woodpecker volt: kliens, aláírás-ellenőrzés, pipeline-receptek, repo-szinkron, secret-kiosztás. Mindez egy adapter mögé került (`WarpEngine.ci`), a Woodpecker-implementáció pedig az engine-ben maradt `WarpEngine::CI::Woodpecker` néven — kliens, adapter, httpsig-ellenőrző és a platformonkénti pipeline-receptek, mert a YAML-dialektus a szolgáltatóé. Az engine saját kódja már nem nevez szolgáltatót: `CI::Repo` és `CI::Run` értékeket kap, `CI::ConnectionError`/`ApiError`/`NotConfigured` hibákat dob, a `Pipeline` pedig `remote_repo_id`-t ad a történelmi `woodpecker_repo_id` kolumna fölött (a tábla itt sem mozdult). `c.ci_adapter = :none` azt jelenti, hogy ez a hoszt nem buildel: az `/api/ci/*` 503, a `/build/config` elutasít, az admin akciók elbújnak. Mindkét seam a hoszt initializerében van kimondva, nem alapértelmezésre hagyva — a hoszt megnevezi, mi a képtára és mi a CI-ja. Törés a 0.6-hoz képest: `image_container_path`, `image_owners`, `ci_platforms`, `ci_update_server`, `ci_extension_public_key(_url)`, `woodpecker_url`, `woodpecker_api_token`, `woodpecker_repo_owner` és a `WarpEngine.woodpecker_configured?` megszűnt; a helyük `c.image_class_name` / `c.image_adapter` és `c.ci_adapter`. Az `/api/ci/*` `latest_run`/`trigger` válasza a normalizált `CI::Run` alakot adja (number, status, branch, message, createdAt, url), a `pipelines` lista pedig `repo_id`-t is közöl a megtartott `woodpecker_repo_id` mellett. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
116 lines
3.5 KiB
Ruby
116 lines
3.5 KiB
Ruby
require "rails_helper"
|
|
|
|
RSpec.describe WarpEngine::SecretSyncService do
|
|
let(:ci) { instance_double(WarpEngine::CI::Woodpecker::Adapter) }
|
|
let(:service) { described_class.new(ci: ci) }
|
|
|
|
before do
|
|
allow(WarpEngine.config).to receive(:application_token_source).and_return(:database)
|
|
allow(WarpEngine.config).to receive(:application_token_owner_class).and_return("TestOwner")
|
|
end
|
|
|
|
describe "#provision" do
|
|
it "hands the token to the provider for every pipeline" do
|
|
repo = create(:pipeline)
|
|
allow(ci).to receive(:secret_set)
|
|
|
|
result = service.provision("plaintoken", pipelines: [ repo ])
|
|
|
|
expect(result[:synced]).to eq([ repo ])
|
|
expect(ci).to have_received(:secret_set).with(
|
|
repo.remote_repo_id, name: "application_token", value: "plaintoken"
|
|
)
|
|
end
|
|
|
|
it "records failed repos without raising" do
|
|
repo = create(:pipeline)
|
|
allow(ci).to receive(:secret_set).and_raise(
|
|
WarpEngine::CI::ConnectionError, "unreachable"
|
|
)
|
|
|
|
result = service.provision("tok", pipelines: [ repo ])
|
|
|
|
expect(result[:synced]).to be_empty
|
|
expect(result[:failed].size).to eq(1)
|
|
end
|
|
end
|
|
|
|
describe "#deprovision" do
|
|
it "deletes secrets from all relevant repos" do
|
|
token = create(:application_token)
|
|
sw = create(:software, name: "game1", owner: token.owner)
|
|
repo = create(:pipeline, :with_software, software: sw)
|
|
|
|
allow(ci).to receive(:secret_delete)
|
|
|
|
service.deprovision(token)
|
|
|
|
expect(ci).to have_received(:secret_delete).with(repo.remote_repo_id, "application_token")
|
|
end
|
|
end
|
|
|
|
describe "#rotate" do
|
|
it "creates new token, provisions, revokes old" do
|
|
token = create(:application_token)
|
|
sw = create(:software, name: "game1", owner: token.owner)
|
|
repo = create(:pipeline, :with_software, software: sw)
|
|
|
|
allow(ci).to receive(:secret_set)
|
|
|
|
result = service.rotate(token)
|
|
|
|
expect(result[:rotated]).to be true
|
|
expect(result[:new_token]).to be_a(WarpEngine::ApplicationToken)
|
|
expect(token.reload.deleted_at).to be_present
|
|
end
|
|
|
|
it "rolls back if all repos fail" do
|
|
token = create(:application_token)
|
|
sw = create(:software, name: "game1", owner: token.owner)
|
|
create(:pipeline, :with_software, software: sw)
|
|
|
|
allow(ci).to receive(:secret_set).and_raise(
|
|
WarpEngine::CI::ConnectionError, "down"
|
|
)
|
|
|
|
result = service.rotate(token)
|
|
|
|
expect(result[:rotated]).to be false
|
|
expect(token.reload.deleted_at).to be_nil
|
|
end
|
|
|
|
it "returns no-op when no repos exist" do
|
|
token = create(:application_token)
|
|
|
|
result = service.rotate(token)
|
|
|
|
expect(result[:rotated]).to be false
|
|
expect(result[:reason]).to eq("no pipelines")
|
|
end
|
|
end
|
|
|
|
describe "#pipelines_for_token" do
|
|
it "returns all active repos for unrestricted tokens" do
|
|
token = create(:application_token, :unrestricted)
|
|
repo1 = create(:pipeline)
|
|
create(:pipeline, :inactive)
|
|
|
|
repos = service.pipelines_for_token(token)
|
|
|
|
expect(repos).to eq([ repo1 ])
|
|
end
|
|
|
|
it "returns only owner's repos for scoped tokens" do
|
|
token = create(:application_token)
|
|
own_sw = create(:software, name: "mine", owner: token.owner)
|
|
other_sw = create(:software, name: "theirs", owner: create(:test_owner))
|
|
own_repo = create(:pipeline, :with_software, software: own_sw)
|
|
create(:pipeline, :with_software, software: other_sw)
|
|
|
|
repos = service.pipelines_for_token(token)
|
|
|
|
expect(repos).to eq([ own_repo ])
|
|
end
|
|
end
|
|
end
|