discourse/plugins/discourse-patreon/spec/lib/api_spec.rb
Rafael dos Santos Silva bbc2fed22b
FIX: Patreon API pagination bugs causing missing pledges (#39058)
## Summary

- **Nil patron data crash**: Patreon V1 API can return pledge entries
where `relationships.patron.data` is null (deleted/deactivated
accounts). This caused `extract` to raise `NoMethodError`, preventing
`save!` from completing — leaving the pledge store stuck at stale data
(exactly 100 pledges from a previous sync). Fixed with `dig` for safe
nil traversal, skipping entries with missing IDs.
- **Double base URL**: Pagination `next` links from Patreon are absolute
URLs, but `Api.get` was prepending the base URL again
(`https://api.patreon.comhttps://api.patreon.com/...`). Fixed by
detecting absolute URIs and using them directly.
- **Non-incremental saves**: All pages were collected before a single
`save!`. If any page failed, nothing was persisted. Now each page is
saved incrementally — first page clears stale data, subsequent pages
append. Also handles the edge case where the API returns empty data by
explicitly clearing the store so ex-patrons don't keep group access.
2026-04-01 17:18:51 -03:00

68 lines
2.3 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Patreon::Api do
def stub_url(status, url)
content = { status: status, headers: { "Content-Type" => "application/json" }, body: "{}" }
stub_request(:get, url).to_return(content)
end
before { SiteSetting.stubs(patreon_enabled: true) }
context "with API v1" do
let(:url) do
"https://api.patreon.com/oauth2/api/current_user/campaigns?include=rewards,creator,goals,pledges&page%5Bcount%5D=100"
end
before { SiteSetting.patreon_api_version = "1" }
it "should add admin warning message for invalid api response" do
stub_url(401, url)
described_class.campaign_data
expect(ProblemCheckTracker[:access_token_invalid].blips).to eq(1)
end
it "should add warning log" do
stub_url(500, url)
Discourse.expects(:warn_exception).once
described_class.campaign_data
end
it "does not double the base URL when uri is already absolute" do
absolute_url =
"https://api.patreon.com/api/oauth2/api/campaigns/123/pledges?page%5Bcount%5D=100&sort=created"
stub_url(200, absolute_url)
result = described_class.get(absolute_url)
expect(result).to eq({})
expect(WebMock).to have_requested(:get, absolute_url)
end
end
context "with API v2" do
let(:url) do
"https://www.patreon.com/api/oauth2/v2/campaigns?include=tiers,creator&fields%5Bcampaign%5D=created_at,name,patron_count&fields%5Btier%5D=title,amount_cents,created_at"
end
before { SiteSetting.patreon_api_version = "2" }
it "should add admin warning message for invalid api response" do
stub_url(401, url)
described_class.campaign_data
expect(ProblemCheckTracker[:access_token_invalid].blips).to eq(1)
expect(AdminNotice.find_by(identifier: :access_token_invalid).message).to eq(
I18n.t("dashboard.problem.access_token_invalid", base_path: Discourse.base_path),
)
end
it "should not add admin warning message for valid api response" do
stub_url(200, url)
expect(ProblemCheckTracker[:access_token_invalid].blips).to eq(0)
end
it "should add warning log" do
stub_url(500, url)
Discourse.expects(:warn_exception).once
expect(described_class.campaign_data).to eq(error: I18n.t(described_class::INVALID_RESPONSE))
end
end
end