mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-03 16:50:30 +08:00
I wanted a better way to show a live/up-to-date status of a PR than what the https://github.com/discourse/github-status-theme provided. First, I wanted it to work on any PR, including ones in private repositories. Second, I wanted to include the approved status, which requires a 2nd call to GitHub's API. Third, I wanted something that was less "intrusive" than the "shield-like" status, so I opted out to use the icons & color that GitHub uses (but maybe that's a bad idea). After a first PoC that was a dump proxy to handle authentication for private repositories, the idea to use org webhooks was raised and turned out to be much lighter and easier than figuring out a proper caching strategy. So this adds support for receiving `pull_request` and `pull_request_review` webhooks events to schedule a background job that will rebake posts and chat messages that have a link to the mentioned PR. The GitHub PR onebox has been updated to query and parse the review state so we can have a better icon. This also adds the `Chat::MessageLink` model (that is the #chat version of `TopicLink`) to help better keep track of links posted in chat. **BEFORE** <img width="1525" height="1356" alt="BEFORE" src="https://github.com/user-attachments/assets/02f66137-9f4f-4a91-b321-73816161e204" /> **AFTER** <img width="1525" height="1356" alt="AFTER" src="https://github.com/user-attachments/assets/2164b7ef-8cd0-4cec-aea8-936a76a94fb2" /> Internal ref - t/17138
151 lines
5 KiB
Ruby
151 lines
5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe DiscourseGithub::WebhooksController do
|
|
let(:webhook_secret) { "test_secret_123" }
|
|
let(:pr_url) { "https://github.com/discourse/discourse/pull/123" }
|
|
|
|
let(:pull_request_payload) do
|
|
{ action: "closed", pull_request: { html_url: pr_url, merged: true } }
|
|
end
|
|
|
|
let(:pull_request_review_payload) do
|
|
{ action: "submitted", review: { state: "approved" }, pull_request: { html_url: pr_url } }
|
|
end
|
|
|
|
def sign_payload(payload, secret)
|
|
body = payload.to_json
|
|
signature = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, body)
|
|
[body, signature]
|
|
end
|
|
|
|
before do
|
|
SiteSetting.enable_discourse_github_plugin = true
|
|
SiteSetting.github_webhook_secret = webhook_secret
|
|
end
|
|
|
|
describe "#github" do
|
|
context "when plugin is disabled" do
|
|
before { SiteSetting.enable_discourse_github_plugin = false }
|
|
|
|
it "returns 404" do
|
|
body, signature = sign_payload(pull_request_payload, webhook_secret)
|
|
post "/discourse-github/webhooks/github",
|
|
params: body,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "pull_request",
|
|
"X-Hub-Signature-256" => signature,
|
|
}
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
context "with invalid signature" do
|
|
it "returns 403" do
|
|
body, _signature = sign_payload(pull_request_payload, webhook_secret)
|
|
post "/discourse-github/webhooks/github",
|
|
params: body,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "pull_request",
|
|
"X-Hub-Signature-256" => "sha256=invalid",
|
|
}
|
|
expect(response.status).to eq(403)
|
|
end
|
|
end
|
|
|
|
context "with missing signature" do
|
|
it "returns 403" do
|
|
post "/discourse-github/webhooks/github",
|
|
params: pull_request_payload.to_json,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "pull_request",
|
|
}
|
|
expect(response.status).to eq(403)
|
|
end
|
|
end
|
|
|
|
context "with missing webhook secret setting" do
|
|
before { SiteSetting.github_webhook_secret = "" }
|
|
|
|
it "returns 403" do
|
|
body, signature = sign_payload(pull_request_payload, webhook_secret)
|
|
post "/discourse-github/webhooks/github",
|
|
params: body,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "pull_request",
|
|
"X-Hub-Signature-256" => signature,
|
|
}
|
|
expect(response.status).to eq(403)
|
|
end
|
|
end
|
|
|
|
context "with valid signature" do
|
|
it "enqueues rebake job for pull_request event" do
|
|
body, signature = sign_payload(pull_request_payload, webhook_secret)
|
|
|
|
expect_enqueued_with(job: :rebake_github_pr_posts, args: { pr_url: pr_url }) do
|
|
post "/discourse-github/webhooks/github",
|
|
params: body,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "pull_request",
|
|
"X-Hub-Signature-256" => signature,
|
|
}
|
|
end
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "enqueues rebake job for pull_request_review event" do
|
|
body, signature = sign_payload(pull_request_review_payload, webhook_secret)
|
|
|
|
expect_enqueued_with(job: :rebake_github_pr_posts, args: { pr_url: pr_url }) do
|
|
post "/discourse-github/webhooks/github",
|
|
params: body,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "pull_request_review",
|
|
"X-Hub-Signature-256" => signature,
|
|
}
|
|
end
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "ignores other event types" do
|
|
payload = { action: "created", issue: { html_url: "https://github.com/org/repo/issues/1" } }
|
|
body, signature = sign_payload(payload, webhook_secret)
|
|
|
|
post "/discourse-github/webhooks/github",
|
|
params: body,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "issues",
|
|
"X-Hub-Signature-256" => signature,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(Jobs::RebakeGithubPrPosts.jobs.size).to eq(0)
|
|
end
|
|
|
|
it "handles missing PR URL gracefully" do
|
|
payload = { action: "closed", pull_request: {} }
|
|
body, signature = sign_payload(payload, webhook_secret)
|
|
|
|
post "/discourse-github/webhooks/github",
|
|
params: body,
|
|
headers: {
|
|
"CONTENT_TYPE" => "application/json",
|
|
"X-GitHub-Event" => "pull_request",
|
|
"X-Hub-Signature-256" => signature,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(Jobs::RebakeGithubPrPosts.jobs.size).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
end
|