ubicloud/spec/routes/web/webhook/github_spec.rb
Enes Cakir 4385c4a6cb Add a prog to destroy GitHub installation
GithubInstallation has repositories and runners that must be destroyed
when the installation is destroyed. Given that they have strands, their
removal is asynchronous. However, the installation must be removed
synchronously when we receive a webhook from GitHub or when the user
attempts to delete the project. Currently, it fails if the installation
has any repositories or runners.

Since the installation doesn't have a strand, I've created an
operational strand to facilitate its destruction. In this prog, we
initially delete the installation from the GitHub API. This ensures that
GitHub will no longer send us new webhook events. Then it destroys the
repositories and runners, then proceed to destroy the installation. It
bypasses deregistration of the runner to ensure destroy it immediately,
even if it's currently running a job.

We may also need to manually clean up the installation due to fraudulent
activities. This prog will assist with these operations.
2024-10-03 17:37:42 +03:00

170 lines
6.6 KiB
Ruby

# frozen_string_literal: true
require_relative "../spec_helper"
RSpec.describe Clover, "github" do
let(:installation) do
prj = Project.create_with_id(name: "test-project")
GithubInstallation.create_with_id(installation_id: 123, name: "test-user", type: "User", project_id: prj.id)
end
let(:runner) { GithubRunner.create_with_id(installation_id: installation.id, label: "ubicloud", repository_name: "my-repo", runner_id: 123, vm_id: "46683a25-acb1-4371-afe9-d39f303e44b4") }
before do
allow(Config).to receive(:github_app_webhook_secret).and_return("secret")
end
it "fails if no signature header" do
page.driver.post("/webhook/github")
expect(page.status_code).to eq(401)
end
it "fails if signature digest tampered" do
page.driver.post("/webhook/github", {}, {"HTTP_X_HUB_SIGNATURE_256" => "sha256=1234567890"})
expect(page.status_code).to eq(401)
end
it "fails if unexpected event" do
send_webhook("repository", {})
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "Unhandled event"}}.to_json)
end
context "when installation event" do
it "fails if unexpected action" do
send_webhook("installation", {action: "created", installation: {id: 1234}})
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "Unhandled installation action"}}.to_json)
end
it "fails if installation not exists when receive deleted action" do
send_webhook("installation", {action: "deleted", installation: {id: 1234}})
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "Unregistered installation"}}.to_json)
end
it "destroys installation when receive deleted action" do
expect(Prog::Github::DestroyGithubInstallation).to receive(:assemble).with(installation)
send_webhook("installation", {action: "deleted", installation: {id: installation.installation_id}})
expect(page.status_code).to eq(200)
expect(page.body).to eq({message: "GithubInstallation[#{installation.ubid}] deleted"}.to_json)
end
end
context "when workflow_job event" do
it "fails if installation not exists" do
send_webhook("workflow_job", workflow_job_payload(action: "queued", installation_id: 789))
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "Unregistered installation"}}.to_json)
end
it "fails if label not matched" do
send_webhook("workflow_job", workflow_job_payload(action: "queued", workflow_job: workflow_job_object(label: "other")))
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "Unmatched label"}}.to_json)
end
it "fails if workflow job is empty" do
expect(Clog).to receive(:emit).at_least(:once).and_call_original
send_webhook("workflow_job", workflow_job_payload(action: "queued", workflow_job: nil))
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "No workflow_job in the payload"}}.to_json)
end
it "creates runner when receive queued action" do
st = instance_double(Strand, id: runner.id)
expect(Prog::Vm::GithubRunner).to receive(:assemble).with(installation, repository_name: "my-repo", label: "ubicloud", default_branch: "main").and_return(st)
send_webhook("workflow_job", workflow_job_payload(action: "queued"))
expect(page.status_code).to eq(200)
expect(page.body).to eq({message: "GithubRunner[#{runner.ubid}] created"}.to_json)
end
it "fails if not queued and runner_id is empty" do
send_webhook("workflow_job", workflow_job_payload(action: "waiting", workflow_job: workflow_job_object(runner_id: nil)))
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "A workflow_job without runner_id"}}.to_json)
end
it "fails if runner not exists" do
send_webhook("workflow_job", workflow_job_payload(action: "in_progress", workflow_job: workflow_job_object(runner_id: 789)))
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "Unregistered runner"}}.to_json)
end
it "updates job details of runner when receive in_progress action" do
expect(Clog).to receive(:emit).with("runner_started")
expect(runner).to receive(:vm).and_return(instance_double(Vm, ubid: "vm-ubid", arch: "x64", cores: 2, vm_host: nil, pool_id: nil)).at_least(:once)
expect(GithubRunner).to receive(:first).and_return(runner)
send_webhook("workflow_job", workflow_job_payload(action: "in_progress", workflow_job: workflow_job_object(runner_id: runner.runner_id)))
expect(page.status_code).to eq(200)
expect(page.body).to eq({message: "GithubRunner[#{runner.ubid}] picked job 232323"}.to_json)
expect(runner.reload.workflow_job["id"]).to eq(232323)
end
it "destroys runner when receive completed action" do
Strand.create(prog: "Vm::GithubRunner", label: "start") { _1.id = runner.id }
send_webhook("workflow_job", workflow_job_payload(action: "completed", workflow_job: workflow_job_object(runner_id: runner.runner_id)))
expect(page.status_code).to eq(200)
expect(page.body).to eq({message: "GithubRunner[#{runner.ubid}] deleted"}.to_json)
expect(SemSnap.new(runner.id).set?("destroy")).to be true
end
it "fails if unexpected action" do
expect(GithubRunner).to receive(:first).and_return(runner)
send_webhook("workflow_job", workflow_job_payload(action: "approved"))
expect(page.status_code).to eq(200)
expect(page.body).to eq({error: {message: "Unhandled workflow_job action"}}.to_json)
end
end
def workflow_job_object(runner_id: 123, label: "ubicloud")
{
id: 232323,
runner_id: runner_id,
labels: [label],
name: "test workflow job name",
job_name: "test job name",
run_id: 7777777,
workflow_name: "test workflow name",
head_branch: "test head branch",
created_at: "2024-04-24T16:02:42Z",
started_at: "2024-04-24T16:03:40Z"
}
end
def workflow_job_payload(action:, installation_id: installation.installation_id, repository_name: "my-repo", workflow_job: workflow_job_object)
{
action: action,
installation: {id: installation_id},
repository: {full_name: repository_name, default_branch: "main"},
workflow_job: workflow_job
}
end
def send_webhook(event, data)
data_json = data.to_json
page.driver.post("/webhook/github",
data_json,
{
"Content-Type" => "application/json",
"HTTP_X_GITHUB_EVENT" => event,
"HTTP_X_HUB_SIGNATURE_256" => "sha256=#{OpenSSL::HMAC.hexdigest("sha256", "secret", data_json)}"
})
end
end