0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-14 13:58:53 +08:00
discourse/plugins/discourse-subscriptions/spec/requests/subscribe_controller_spec.rb
Arpit Jalan d55ea4fd0f
FIX: enforce published product allowlist for subscriptions (#40249)
Previously, subscription detail and checkout paths could accept Stripe
products or prices that were not published in Discourse.

This change checks the local product list before showing products,
creating purchases, or finalizing pending purchases, keeping checkout
behavior consistent with the public catalog.
2026-05-28 22:22:52 +05:30

885 lines
30 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe DiscourseSubscriptions::SubscribeController do
let(:user) { Fabricate(:user) }
let(:campaign_user) { Fabricate(:user) }
before do
SiteSetting.discourse_subscriptions_enabled = true
SiteSetting.discourse_subscriptions_secret_key = "secret-key"
end
context "when showing products" do
let(:product) do
{
id: "prodct_23456",
name: "Very Special Product",
metadata: {
description:
"Many people listened to my phone call with the Ukrainian President while it was being made",
repurchaseable: false,
},
otherstuff: true,
}
end
let(:prices) do
{
data: [
{
id: "plan_id123",
unit_amount: 1220,
currency: "aud",
recurring: {
interval: "year",
},
metadata: {
},
},
{
id: "plan_id234",
unit_amount: 1399,
currency: "usd",
recurring: {
interval: "year",
},
metadata: {
},
},
{
id: "plan_id678",
unit_amount: 1000,
currency: "aud",
recurring: {
interval: "week",
},
metadata: {
},
},
],
}
end
let(:product_ids) { ["prodct_23456"] }
before do
sign_in(user)
Fabricate(:product, external_id: "prodct_23456")
SiteSetting.discourse_subscriptions_public_key = "public-key"
end
describe "#index" do
let(:customer) do
Fabricate(:customer, product_id: product[:id], user_id: user.id, customer_id: "x")
end
it "gets products" do
::Stripe::Product
.expects(:list)
.with({ ids: product_ids, active: true }, DiscourseSubscriptions::Stripe.request_opts)
.returns(data: [product])
get "/s.json"
expect(response.parsed_body).to eq(
[
{
"id" => "prodct_23456",
"name" => "Very Special Product",
"description" =>
PrettyText.cook(
"Many people listened to my phone call with the Ukrainian President while it was being made",
),
"subscribed" => false,
"repurchaseable" => false,
},
],
)
end
it "is subscribed" do
Fabricate(:subscription, external_id: "sub_12345", customer_id: customer.id, status: nil)
::Stripe::Product
.expects(:list)
.with({ ids: product_ids, active: true }, DiscourseSubscriptions::Stripe.request_opts)
.returns(data: [product])
get "/s.json"
data = response.parsed_body
expect(data.first["subscribed"]).to eq true
end
it "is not subscribed" do
DiscourseSubscriptions::Customer.delete_all
::Stripe::Product
.expects(:list)
.with({ ids: product_ids, active: true }, DiscourseSubscriptions::Stripe.request_opts)
.returns(data: [product])
get "/s.json"
data = response.parsed_body
expect(data.first["subscribed"]).to eq false
end
end
describe "#get_contributors" do
before do
Fabricate(:product, external_id: "prod_campaign")
Fabricate(:customer, product_id: "prodct_23456", user_id: user.id, customer_id: "x")
Fabricate(
:customer,
product_id: "prod_campaign",
user_id: campaign_user.id,
customer_id: "y",
)
end
context "when not showing contributors" do
it "returns nothing if not set to show contributors" do
SiteSetting.discourse_subscriptions_campaign_show_contributors = false
get "/s/contributors.json"
data = response.parsed_body
expect(data).to be_empty
end
end
context "when showing contributors" do
before { SiteSetting.discourse_subscriptions_campaign_show_contributors = true }
it "filters users by campaign product if set" do
SiteSetting.discourse_subscriptions_campaign_product = "prod_campaign"
get "/s/contributors.json"
data = response.parsed_body
expect(data.first["id"]).to eq campaign_user.id
expect(data.length).to eq 1
end
it "shows all purchases if campaign product not set" do
SiteSetting.discourse_subscriptions_campaign_product = ""
get "/s/contributors.json"
data = response.parsed_body
expect(data.length).to eq 2
end
end
end
describe "#show" do
it "retrieves the product" do
Fabricate(:product, external_id: "prod_walterwhite")
::Stripe::Product
.expects(:retrieve)
.with("prod_walterwhite", DiscourseSubscriptions::Stripe.request_opts)
.returns(product)
::Stripe::Price
.expects(:list)
.with(
{ active: true, product: "prod_walterwhite" },
DiscourseSubscriptions::Stripe.request_opts,
)
.returns(prices)
get "/s/prod_walterwhite.json"
expect(response.parsed_body).to eq(
{
"product" => {
"id" => "prodct_23456",
"name" => "Very Special Product",
"description" =>
PrettyText.cook(
"Many people listened to my phone call with the Ukrainian President while it was being made",
),
"subscribed" => false,
"repurchaseable" => false,
},
"plans" => [
{
"currency" => "aud",
"id" => "plan_id123",
"recurring" => {
"interval" => "year",
},
"unit_amount" => 1220,
},
{
"currency" => "usd",
"id" => "plan_id234",
"recurring" => {
"interval" => "year",
},
"unit_amount" => 1399,
},
{
"currency" => "aud",
"id" => "plan_id678",
"recurring" => {
"interval" => "week",
},
"unit_amount" => 1000,
},
],
},
)
end
it "returns 404 for a product outside the allowlist" do
::Stripe::Product.expects(:retrieve).never
::Stripe::Price.expects(:list).never
get "/s/prod_hidden.json"
expect(response.status).to eq(404)
end
end
end
context "when creating subscriptions" do
context "when unauthenticated" do
it "does not create a subscription" do
::Stripe::Customer.expects(:create).never
::Stripe::Price.expects(:retrieve).never
::Stripe::Subscription.expects(:create).never
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
end
end
context "when authenticated" do
fab!(:published_product) { Fabricate(:product, external_id: "product_12345") }
before { sign_in(user) }
describe "#create" do
before do
::Stripe::Customer
.stubs(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "cus_1234")
end
it "creates a subscription without automatic_tax param" do
SiteSetting.discourse_subscriptions_enable_automatic_tax = false
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "product_12345",
metadata: {
group_name: "awesome",
trial_period_days: 0,
},
)
expected_subscription_params = {
customer: "cus_1234",
items: [{ price: "plan_1234" }],
metadata: {
user_id: user.id,
username: user.username_lower,
},
trial_period_days: 0,
promotion_code: nil,
}
::Stripe::Subscription
.expects(:create)
.with do |params, opts|
params == expected_subscription_params && !params.key?(:automatic_tax) &&
opts == DiscourseSubscriptions::Stripe.request_opts
end
.returns(status: "active", customer: "cus_1234")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count }
end
it "creates a subscription with automatic tax" do
SiteSetting.discourse_subscriptions_enable_automatic_tax = true
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "product_12345",
metadata: {
group_name: "awesome",
trial_period_days: 0,
},
)
expected_subscription_params = {
customer: "cus_1234",
items: [{ price: "plan_1234" }],
metadata: {
user_id: user.id,
username: user.username_lower,
},
trial_period_days: 0,
promotion_code: nil,
automatic_tax: {
enabled: true,
},
}
::Stripe::Subscription
.expects(:create)
.with(expected_subscription_params, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "active", customer: "cus_1234")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count }
end
it "rejects a plan outside the allowlist" do
::Stripe::Price
.expects(:retrieve)
.with("price_hidden", DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "prod_hidden",
metadata: {
group_name: "awesome",
trial_period_days: 0,
},
)
::Stripe::Customer.expects(:create).never
::Stripe::Subscription.expects(:create).never
expect {
post "/s/create.json", params: { plan: "price_hidden", source: "tok_1234" }
}.not_to change { DiscourseSubscriptions::Customer.count }
expect(response.status).to eq(404)
end
it "returns 422 on a one time payment subscription error" do
# It's possible that the invoice item doesn't get attached
# to the invoice. This means the invoice is paid, but for $0.00 with
# a pending invoice item.
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "one_time",
product: "product_12345",
metadata: {
group_name: "awesome",
},
)
::Stripe::InvoiceItem.expects(:create).with(
anything,
DiscourseSubscriptions::Stripe.request_opts,
)
::Stripe::Invoice
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "open", id: "in_123")
::Stripe::Invoice
.expects(:finalize_invoice)
.with(anything, anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", status: "paid", payment_intent: "pi_123")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.not_to change { DiscourseSubscriptions::Customer.count }
expect(response.status).to eq 422
end
it "creates a one time payment subscription without automatic tax" do
SiteSetting.discourse_subscriptions_enable_automatic_tax = false
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "one_time",
product: "product_12345",
metadata: {
group_name: "awesome",
},
)
::Stripe::InvoiceItem.expects(:create).with(
anything,
DiscourseSubscriptions::Stripe.request_opts,
)
expected_one_time_params = { customer: "cus_1234" }
::Stripe::Invoice
.expects(:create)
.with do |params, opts|
params == expected_one_time_params && !params.key?(:automatic_tax) &&
opts == DiscourseSubscriptions::Stripe.request_opts
end
.returns(status: "open", id: "in_123")
::Stripe::Invoice
.expects(:finalize_invoice)
.with(anything, anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", status: "open", payment_intent: "pi_123")
::Stripe::Invoice
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", status: "open", payment_intent: "pi_123")
::Stripe::PaymentIntent
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "successful")
::Stripe::Invoice
.expects(:pay)
.with(anything, anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "paid", customer: "cus_1234")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count }
end
it "creates a one time payment subscription" do
SiteSetting.discourse_subscriptions_enable_automatic_tax = true
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "one_time",
product: "product_12345",
metadata: {
group_name: "awesome",
},
)
::Stripe::InvoiceItem.expects(:create).with(
anything,
DiscourseSubscriptions::Stripe.request_opts,
)
expected_one_time_params = { customer: "cus_1234", automatic_tax: { enabled: true } }
::Stripe::Invoice
.expects(:create)
.with(expected_one_time_params, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "open", id: "in_123")
::Stripe::Invoice
.expects(:finalize_invoice)
.with(anything, anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", status: "open", payment_intent: "pi_123")
::Stripe::Invoice
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", status: "open", payment_intent: "pi_123")
::Stripe::PaymentIntent
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "successful")
::Stripe::Invoice
.expects(:pay)
.with(anything, anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "paid", customer: "cus_1234")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count }
end
it "creates a customer model" do
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(type: "recurring", product: "product_12345", metadata: {})
.twice
::Stripe::Subscription
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "active", customer: "cus_1234")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { DiscourseSubscriptions::Customer.count }
::Stripe::Customer.expects(:retrieve).with(
"cus_1234",
DiscourseSubscriptions::Stripe.request_opts,
)
expect {
post "/s/create.json", params: { plan: "plan_5678", source: "tok_5678" }
}.not_to change { DiscourseSubscriptions::Customer.count }
end
context "with customer name & address" do
it "creates a customer & subscription when a customer address is provided" do
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(type: "recurring", product: "product_12345", metadata: {})
::Stripe::Subscription
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "active", customer: "cus_1234")
expect {
post "/s/create.json",
params: {
plan: "plan_1234",
source: "tok_1234",
cardholder_name: "A. Customer",
cardholder_address: {
line1: "123 Main Street",
city: "Anywhere",
state: "VT",
country: "US",
postal_code: "12345",
},
}
}.to change { DiscourseSubscriptions::Customer.count }
end
end
context "with promo code" do
context "with invalid code" do
it "prevents use of invalid coupon codes" do
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "product_12345",
metadata: {
group_name: "awesome",
trial_period_days: 0,
},
)
::Stripe::PromotionCode
.expects(:list)
.with({ code: "invalid" }, DiscourseSubscriptions::Stripe.request_opts)
.returns(data: [])
post "/s/create.json",
params: {
plan: "plan_1234",
source: "tok_1234",
promo: "invalid",
}
data = response.parsed_body
expect(data["errors"]).not_to be_blank
end
end
context "with valid code" do
before do
::Stripe::PromotionCode
.expects(:list)
.with({ code: "123" }, DiscourseSubscriptions::Stripe.request_opts)
.returns(data: [{ id: "promo123", coupon: { id: "c123" } }])
end
it "applies promo code to recurring subscription" do
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "product_12345",
metadata: {
group_name: "awesome",
trial_period_days: 0,
},
)
expected_subscription_params = {
customer: "cus_1234",
items: [price: "plan_1234"],
metadata: {
user_id: user.id,
username: user.username_lower,
},
trial_period_days: 0,
promotion_code: "promo123",
}
::Stripe::Subscription
.expects(:create)
.with(expected_subscription_params, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "active", customer: "cus_1234")
expect {
post "/s/create.json",
params: {
plan: "plan_1234",
source: "tok_1234",
promo: "123",
}
}.to change { DiscourseSubscriptions::Customer.count }
end
it "applies promo code to one time purchase" do
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "one_time",
product: "product_12345",
metadata: {
group_name: "awesome",
},
)
::Stripe::Invoice
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "open", id: "in_123")
::Stripe::InvoiceItem.expects(:create).with(
{
customer: "cus_1234",
price: "plan_1234",
discounts: [{ coupon: "c123" }],
invoice: "in_123",
},
DiscourseSubscriptions::Stripe.request_opts,
)
::Stripe::Invoice
.expects(:finalize_invoice)
.with(anything, anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", status: "open", payment_intent: "pi_123")
::Stripe::Invoice
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", status: "open", payment_intent: "pi_123")
::Stripe::PaymentIntent
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "successful")
::Stripe::Invoice
.expects(:pay)
.with(anything, anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "paid", customer: "cus_1234")
expect {
post "/s/create.json",
params: {
plan: "plan_1234",
source: "tok_1234",
promo: "123",
}
}.to change { DiscourseSubscriptions::Customer.count }
end
end
end
end
describe "#finalize strong customer authenticated transaction" do
context "with subscription" do
it "finalizes the subscription" do
server_session["pending_subscription"] = {
transaction_id: "sub_1234",
plan_id: "plan_1234",
}
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "plan_1234", product: "product_12345", metadata: {})
::Stripe::Subscription
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "sub_123", customer: "cus_1234", status: "active")
expect { post "/s/finalize.json" }.to change { DiscourseSubscriptions::Customer.count }
end
end
context "with one-time payment" do
it "finalizes the one-time payment" do
server_session["pending_subscription"] = {
transaction_id: "in_1234",
plan_id: "plan_1234",
}
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "plan_1234", product: "product_12345", metadata: {})
::Stripe::Invoice
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "in_123", customer: "cus_1234", status: "paid")
expect { post "/s/finalize.json" }.to change { DiscourseSubscriptions::Customer.count }
end
end
it "returns 403 with no pending server session" do
post "/s/finalize.json"
expect(response.status).to eq(403)
end
it "prevents replay by rejecting a second finalize" do
server_session["pending_subscription"] = {
transaction_id: "sub_123",
plan_id: "plan_1234",
}
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "plan_1234", product: "product_12345", metadata: {})
::Stripe::Subscription
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "sub_123", customer: "cus_1234", status: "active")
post "/s/finalize.json"
expect(response.status).to eq(200)
post "/s/finalize.json"
expect(response.status).to eq(403)
end
it "rejects a pending plan outside the allowlist" do
server_session["pending_subscription"] = {
transaction_id: "sub_123",
plan_id: "price_hidden",
}
::Stripe::Price
.expects(:retrieve)
.with("price_hidden", DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "price_hidden", product: "prod_hidden", metadata: {})
::Stripe::Subscription.expects(:retrieve).never
expect { post "/s/finalize.json" }.not_to change {
DiscourseSubscriptions::Customer.count
}
expect(response.status).to eq(404)
end
end
describe "user groups" do
let(:group_name) { "group-123" }
let(:group) { Fabricate(:group, name: group_name) }
context "with unauthorized group" do
before do
::Stripe::Customer
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "cus_1234")
::Stripe::Subscription
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "active")
end
it "does not add the user to the admins group" do
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "product_12345",
metadata: {
group_name: "admins",
},
)
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
expect(user.admin).to eq false
end
it "does not add the user to other group" do
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "product_12345",
metadata: {
group_name: "other",
},
)
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
expect(user.groups).to be_empty
end
end
context "when plan has group in metadata" do
before do
::Stripe::Customer
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(id: "cus_1234")
::Stripe::Price
.expects(:retrieve)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(
type: "recurring",
product: "product_12345",
metadata: {
group_name: group_name,
},
)
end
it "does not add the user to the group when subscription fails" do
::Stripe::Subscription
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "failed")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.not_to change { group.users.count }
expect(user.groups).to be_empty
end
it "adds the user to the group when the subscription is active" do
::Stripe::Subscription
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "active")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { group.users.count }
expect(user.groups).not_to be_empty
end
it "adds the user to the group when the subscription is trialing" do
::Stripe::Subscription
.expects(:create)
.with(anything, DiscourseSubscriptions::Stripe.request_opts)
.returns(status: "trialing")
expect {
post "/s/create.json", params: { plan: "plan_1234", source: "tok_1234" }
}.to change { group.users.count }
expect(user.groups).not_to be_empty
end
end
end
end
end
end