mirror of
https://github.com/discourse/discourse.git
synced 2026-08-09 21:45:25 +08:00
This replaces the old ember-cli build with a modern Rolldown build. In local testing, this provides an 80% improvement in build times, while remaining 100% backwards compatible for themes and plugins. As part of this move, we have decided to stop using a proxy in front of Discourse for development. Development should now be done directly against the Rails server. `bin/ember-cli -u` has been replaced with `bin/dev`. This will launch Rails on `:3000`, and will run the rolldown build in the background. Log output from both processes will be shown with an appropriate prefix. You should visit `:3000` in your browser. `:4200` will no longer serve anything. To help with migration, `bin/ember-cli` is now a backwards-compatible shim. It will print help information, and will launch a lightweight server on `:4200` with instructions to move to `:3000`. If you prefer to launch Rails and the JS build as separate commands, you can still do that. Rails boot commands are unchanged, and the rolldown development builder can be run using `bin/dev --only ember`. https://meta.discourse.org/t/403908 --------- Co-authored-by: Jarek Radosz <jarek@cvx.dev> Co-authored-by: Chris Manson <chris@manson.ie>
595 lines
19 KiB
Ruby
Vendored
595 lines
19 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe DiscourseSubscriptions::HooksController do
|
|
before do
|
|
SiteSetting.discourse_subscriptions_webhook_secret = "zascharoo"
|
|
SiteSetting.discourse_subscriptions_secret_key = "secret-key"
|
|
SiteSetting.discourse_subscriptions_enabled = true
|
|
end
|
|
|
|
it "rejects webhooks when webhook secret is blank" do
|
|
SiteSetting.discourse_subscriptions_webhook_secret = ""
|
|
post "/s/hooks.json", params: "{}", headers: { HTTP_STRIPE_SIGNATURE: "t=1,v1=abc" }
|
|
expect(response.status).to eq 403
|
|
end
|
|
|
|
it "constructs a webhook event" do
|
|
payload = "we-want-a-shrubbery"
|
|
headers = { HTTP_STRIPE_SIGNATURE: "stripe-webhook-signature" }
|
|
|
|
::Stripe::Webhook
|
|
.expects(:construct_event)
|
|
.with("we-want-a-shrubbery", "stripe-webhook-signature", "zascharoo")
|
|
.returns(type: "something")
|
|
|
|
post "/s/hooks.json", params: payload, headers: headers
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
describe "event types" do
|
|
let(:user) { Fabricate(:user) }
|
|
let(:customer) do
|
|
Fabricate(:customer, customer_id: "c_575768", product_id: "p_8654", user_id: user.id)
|
|
end
|
|
let!(:subscription) do
|
|
Fabricate(:subscription, external_id: "sub_12345", customer_id: customer.id, status: nil)
|
|
end
|
|
let(:group) { Fabricate(:group, name: "subscribers-group") }
|
|
let(:client_reference_id) do
|
|
user.signed_id(
|
|
expires_in: DiscourseSubscriptions::CHECKOUT_SESSION_USER_REFERENCE_EXPIRES_IN,
|
|
purpose: DiscourseSubscriptions::CHECKOUT_SESSION_USER_REFERENCE_PURPOSE,
|
|
)
|
|
end
|
|
|
|
let(:event_data) do
|
|
{
|
|
object: {
|
|
customer: customer.customer_id,
|
|
plan: {
|
|
product: customer.product_id,
|
|
metadata: {
|
|
group_name: group.name,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
end
|
|
|
|
let(:customer_subscription_deleted_data) do
|
|
{
|
|
object: {
|
|
id: subscription.external_id,
|
|
customer: customer.customer_id,
|
|
plan: {
|
|
product: customer.product_id,
|
|
metadata: {
|
|
group_name: group.name,
|
|
},
|
|
},
|
|
status: "canceled",
|
|
},
|
|
}
|
|
end
|
|
|
|
let(:checkout_session_completed_data) do
|
|
{
|
|
object: {
|
|
id: "cs_test_a1ENei5A9TGOaEketyV5qweiQR5CyJWHT5j8T3HheQY3uah3RxzKttVUKZ",
|
|
object: "checkout.session",
|
|
customer: customer.customer_id,
|
|
customer_email: user.email,
|
|
client_reference_id: client_reference_id,
|
|
invoice: "in_1P9b7iEYXaQnncSh81AQtuHD",
|
|
metadata: {
|
|
},
|
|
mode: "subscription",
|
|
payment_status: "paid",
|
|
status: "complete",
|
|
submit_type: nil,
|
|
subscription: "sub_1P9b7iEYXaQnncSh3H3G9d2Y",
|
|
success_url: "http://localhost:3000/my/billing/subscriptions",
|
|
url: nil,
|
|
},
|
|
}
|
|
end
|
|
|
|
let(:checkout_session_completed_data_one_off) do
|
|
{
|
|
object: {
|
|
id: "cs_test_a1ENei5A9TGOaEketyV5qweiQR5CyJWHT5j8T3HheQY3uah3RxzKttVUKZ",
|
|
object: "checkout.session",
|
|
customer: nil,
|
|
customer_email: user.email,
|
|
client_reference_id: client_reference_id,
|
|
invoice: nil,
|
|
metadata: {
|
|
},
|
|
mode: "subscription",
|
|
payment_intent: "pi_3PsohkGHcn",
|
|
payment_status: "paid",
|
|
status: "complete",
|
|
submit_type: nil,
|
|
subscription: nil,
|
|
success_url: "http://localhost:3000/my/billing/subscriptions",
|
|
url: nil,
|
|
},
|
|
}
|
|
end
|
|
|
|
let(:checkout_session_completed_bad_data) do
|
|
{
|
|
object: {
|
|
id: "cs_test_a1ENei5A9TGOaEketyV5qweiQR5CyJWHT5j8T3HheQY3uah3RxzKttVUKZ",
|
|
object: "checkout.session",
|
|
customer: nil,
|
|
customer_email: nil,
|
|
invoice: "in_1P9b7iEYXaQnncSh81AQtuHD",
|
|
metadata: {
|
|
},
|
|
mode: "subscription",
|
|
payment_status: "paid",
|
|
status: "complete",
|
|
submit_type: nil,
|
|
subscription: nil,
|
|
success_url: "http://localhost:3000/my/billing/subscriptions",
|
|
url: nil,
|
|
},
|
|
}
|
|
end
|
|
|
|
let(:list_line_items_data) do
|
|
{
|
|
data: [
|
|
{
|
|
id: "li_1P9YlFEYXaQnncShFBl7r0na",
|
|
object: "item",
|
|
description: "Exclusive Access",
|
|
price: {
|
|
id: "price_1OrmlvEYXaQnncShNahrpKvA",
|
|
metadata: {
|
|
group_name: group.name,
|
|
trial_period_days: "0",
|
|
},
|
|
nickname: "EA1",
|
|
product: "prod_PhB6IpGhEX14Hi",
|
|
},
|
|
},
|
|
],
|
|
}
|
|
end
|
|
|
|
describe "checkout.session.completed" do
|
|
before do
|
|
event = { type: "checkout.session.completed", data: checkout_session_completed_data }
|
|
::Stripe::Checkout::Session
|
|
.stubs(:list_line_items)
|
|
.with(
|
|
checkout_session_completed_data[:object][:id],
|
|
{ limit: 1 },
|
|
DiscourseSubscriptions::Stripe.request_opts,
|
|
)
|
|
.returns(list_line_items_data)
|
|
|
|
::Stripe::Subscription
|
|
.stubs(:update)
|
|
.with(
|
|
checkout_session_completed_data[:object][:subscription],
|
|
{ metadata: { user_id: user.id, username: user.username } },
|
|
DiscourseSubscriptions::Stripe.request_opts,
|
|
)
|
|
.returns(
|
|
{
|
|
id: checkout_session_completed_data[:object][:subscription],
|
|
object: "subscription",
|
|
metadata: {
|
|
user_id: user.id.to_s,
|
|
username: user.username,
|
|
},
|
|
},
|
|
)
|
|
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "is successful" do
|
|
post "/s/hooks.json"
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
it "accepts a secondary email that belongs to the signed user" do
|
|
secondary_email = "secondary-#{user.id}@example.com"
|
|
::UserEmail.create!(user: user, email: secondary_email, primary: false)
|
|
data = checkout_session_completed_data.deep_dup
|
|
data[:object][:customer_email] = secondary_email
|
|
::Stripe::Webhook.stubs(:construct_event).returns(
|
|
type: "checkout.session.completed",
|
|
data: data,
|
|
)
|
|
|
|
expect { post "/s/hooks.json" }.to change { user.reload.groups.count }.by(1)
|
|
|
|
aggregate_failures do
|
|
expect(response.status).to eq(200)
|
|
expect(group.reload.users).to contain_exactly(user)
|
|
expect(DiscourseSubscriptions::Customer.order(:id).last.user_id).to eq(user.id)
|
|
end
|
|
end
|
|
|
|
describe "completing the subscription" do
|
|
it "adds the user to the group when completing the transaction" do
|
|
expect { post "/s/hooks.json" }.to change { user.groups.count }.by(1)
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
end
|
|
|
|
it "does not create records or add the user to the group when payment_status is not paid" do
|
|
unpaid_data = checkout_session_completed_data.deep_dup
|
|
unpaid_data[:object][:payment_status] = "unpaid"
|
|
event = { type: "checkout.session.completed", data: unpaid_data }
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
|
|
expect { post "/s/hooks.json" }.not_to change { user.groups.count }
|
|
expect(response.status).to eq(200)
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed with unsigned metadata user id" do
|
|
fab!(:other_user, :user)
|
|
|
|
before do
|
|
data = checkout_session_completed_data.deep_dup
|
|
data[:object][:metadata] = { user_id: other_user.id }
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session
|
|
.stubs(:list_line_items)
|
|
.with(
|
|
checkout_session_completed_data[:object][:id],
|
|
{ limit: 1 },
|
|
DiscourseSubscriptions::Stripe.request_opts,
|
|
)
|
|
.returns(list_line_items_data)
|
|
|
|
::Stripe::Subscription.stubs(:update).returns({})
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "ignores unsigned metadata" do
|
|
expect { post "/s/hooks.json" }.to change { user.reload.groups.count }.by(1)
|
|
|
|
aggregate_failures do
|
|
expect(response.status).to eq(200)
|
|
expect(group.reload.users).to contain_exactly(user)
|
|
expect(DiscourseSubscriptions::Customer.order(:id).last.user_id).to eq(user.id)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed with conflicting email" do
|
|
fab!(:other_user, :user)
|
|
|
|
before do
|
|
data = checkout_session_completed_data.deep_dup
|
|
data[:object][:customer_email] = other_user.email
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "rejects the mismatched checkout email" do
|
|
expect { post "/s/hooks.json" }.not_to change { DiscourseSubscriptions::Customer.count }
|
|
|
|
aggregate_failures do
|
|
expect(response.status).to eq(422)
|
|
expect(group.reload.users).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed without customer and with conflicting email" do
|
|
fab!(:other_user, :user)
|
|
|
|
before do
|
|
data = checkout_session_completed_data_one_off.deep_dup
|
|
data[:object][:customer_email] = other_user.email
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Customer.expects(:create).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "rejects before creating a Stripe customer" do
|
|
expect { post "/s/hooks.json" }.not_to change { DiscourseSubscriptions::Customer.count }
|
|
|
|
aggregate_failures do
|
|
expect(response.status).to eq(422)
|
|
expect(group.reload.users).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed without a trusted user reference" do
|
|
before do
|
|
data = checkout_session_completed_data.deep_dup
|
|
data[:object].delete(:client_reference_id)
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "does not bind by customer email" do
|
|
expect { post "/s/hooks.json" }.not_to change { DiscourseSubscriptions::Customer.count }
|
|
|
|
aggregate_failures do
|
|
expect(response.status).to eq(422)
|
|
expect(group.reload.users).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed with an invalid user reference" do
|
|
before do
|
|
data = checkout_session_completed_data.deep_dup
|
|
data[:object][:client_reference_id] = "tampered-reference"
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "does not fall back to customer email" do
|
|
expect { post "/s/hooks.json" }.not_to change { DiscourseSubscriptions::Customer.count }
|
|
|
|
aggregate_failures do
|
|
expect(response.status).to eq(422)
|
|
expect(group.reload.users).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed without customer email" do
|
|
before do
|
|
data = checkout_session_completed_data.deep_dup
|
|
data[:object][:customer_email] = nil
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "keeps rejecting the event" do
|
|
expect { post "/s/hooks.json" }.not_to change { DiscourseSubscriptions::Customer.count }
|
|
|
|
aggregate_failures do
|
|
expect(response.status).to eq(422)
|
|
expect(group.reload.users).to be_empty
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed with bad data" do
|
|
before do
|
|
data = checkout_session_completed_bad_data.deep_dup
|
|
data[:object][:client_reference_id] = client_reference_id
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Customer.expects(:create).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "is returns 422" do
|
|
post "/s/hooks.json"
|
|
expect(response.status).to eq 422
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed for one-off purchase" do
|
|
before do
|
|
event = {
|
|
type: "checkout.session.completed",
|
|
data: checkout_session_completed_data_one_off,
|
|
}
|
|
::Stripe::Checkout::Session
|
|
.stubs(:list_line_items)
|
|
.with(
|
|
checkout_session_completed_data[:object][:id],
|
|
{ limit: 1 },
|
|
DiscourseSubscriptions::Stripe.request_opts,
|
|
)
|
|
.returns(list_line_items_data)
|
|
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
::Stripe::Customer
|
|
.stubs(:create)
|
|
.with({ email: user.email }, DiscourseSubscriptions::Stripe.request_opts)
|
|
.returns(id: "cus_1234")
|
|
end
|
|
|
|
it "is returns 200" do
|
|
expect { post "/s/hooks.json" }.to change { user.groups.count }.by(1)
|
|
expect(response.status).to eq 200
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed with anonymous user" do
|
|
before do
|
|
data = checkout_session_completed_bad_data.deep_dup
|
|
data[:object][:customer_email] = "anonymous@example.com"
|
|
data[:object][:client_reference_id] = client_reference_id
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Customer.expects(:create).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "is returns 422" do
|
|
post "/s/hooks.json"
|
|
expect(response.status).to eq 422
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.completed with no customer email or customer" do
|
|
before do
|
|
data = checkout_session_completed_bad_data.deep_dup
|
|
data[:object][:customer_email] = nil
|
|
data[:object][:client_reference_id] = client_reference_id
|
|
event = { type: "checkout.session.completed", data: data }
|
|
|
|
::Stripe::Checkout::Session.expects(:list_line_items).never
|
|
::Stripe::Customer.expects(:create).never
|
|
::Stripe::Subscription.expects(:update).never
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "is returns 422" do
|
|
post "/s/hooks.json"
|
|
expect(response.status).to eq 422
|
|
end
|
|
end
|
|
|
|
describe "customer.subscription.updated" do
|
|
before do
|
|
event = { type: "customer.subscription.updated", data: event_data }
|
|
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
end
|
|
|
|
it "is successful" do
|
|
post "/s/hooks.json"
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
describe "completing the subscription" do
|
|
it "does not add the user to the group" do
|
|
event_data[:object][:status] = "incomplete"
|
|
event_data[:previous_attributes] = { status: "incomplete" }
|
|
|
|
expect { post "/s/hooks.json" }.not_to change { user.groups.count }
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
it "does not add the user to the group" do
|
|
event_data[:object][:status] = "incomplete"
|
|
event_data[:previous_attributes] = { status: "something-else" }
|
|
|
|
expect { post "/s/hooks.json" }.not_to change { user.groups.count }
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
it "adds the user to the group when completing the transaction" do
|
|
event_data[:object][:status] = "complete"
|
|
event_data[:previous_attributes] = { status: "incomplete" }
|
|
|
|
expect { post "/s/hooks.json" }.to change { user.groups.count }.by(1)
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
it "adds the user to the group when status is active" do
|
|
event_data[:object][:status] = "active"
|
|
|
|
expect { post "/s/hooks.json" }.to change { user.groups.count }.by(1)
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "customer.subscription.deleted" do
|
|
before do
|
|
event = { type: "customer.subscription.deleted", data: customer_subscription_deleted_data }
|
|
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
|
|
group.add(user)
|
|
end
|
|
|
|
it "deletes the customer" do
|
|
expect { post "/s/hooks.json" }.to change {
|
|
DiscourseSubscriptions::Subscription.where(status: "canceled").count
|
|
}.by(+1)
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
|
|
it "removes the user from the group" do
|
|
expect { post "/s/hooks.json" }.to change { user.groups.count }.by(-1)
|
|
|
|
expect(response.status).to eq 200
|
|
end
|
|
end
|
|
|
|
describe "checkout.session.async_payment_succeeded" do
|
|
before do
|
|
event = {
|
|
type: "checkout.session.async_payment_succeeded",
|
|
data: checkout_session_completed_data,
|
|
}
|
|
::Stripe::Webhook.stubs(:construct_event).returns(event)
|
|
::Stripe::Checkout::Session
|
|
.stubs(:list_line_items)
|
|
.with(
|
|
checkout_session_completed_data[:object][:id],
|
|
{ limit: 1 },
|
|
DiscourseSubscriptions::Stripe.request_opts,
|
|
)
|
|
.returns(list_line_items_data)
|
|
::Stripe::Subscription
|
|
.stubs(:update)
|
|
.with(
|
|
checkout_session_completed_data[:object][:subscription],
|
|
{ metadata: { user_id: user.id, username: user.username } },
|
|
DiscourseSubscriptions::Stripe.request_opts,
|
|
)
|
|
.returns(
|
|
{
|
|
id: checkout_session_completed_data[:object][:subscription],
|
|
object: "subscription",
|
|
metadata: {
|
|
user_id: user.id.to_s,
|
|
username: user.username,
|
|
},
|
|
},
|
|
)
|
|
end
|
|
|
|
it "creates customer and subscription records and adds the user to the group" do
|
|
post "/s/hooks.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(user.groups).to include(group)
|
|
|
|
expect(
|
|
DiscourseSubscriptions::Customer.exists?(
|
|
user_id: user.id,
|
|
customer_id: customer.customer_id,
|
|
product_id: "prod_PhB6IpGhEX14Hi",
|
|
),
|
|
).to eq(true)
|
|
expect(
|
|
DiscourseSubscriptions::Subscription.exists?(
|
|
customer_id: DiscourseSubscriptions::Customer.last.id,
|
|
external_id: "sub_1P9b7iEYXaQnncSh3H3G9d2Y",
|
|
),
|
|
).to eq(true)
|
|
end
|
|
end
|
|
end
|
|
end
|