Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
commit
af6077b9c3
455 changed files with 78366 additions and 0 deletions
281
test/tarakan_web/controllers/webhooks/stripe_controller_test.exs
Normal file
281
test/tarakan_web/controllers/webhooks/stripe_controller_test.exs
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
defmodule TarakanWeb.Webhooks.StripeControllerTest do
|
||||
use TarakanWeb.ConnCase, async: true
|
||||
|
||||
alias Tarakan.Accounts.Scope
|
||||
alias Tarakan.Market.Bounty
|
||||
alias Tarakan.Repo
|
||||
|
||||
@secret "whsec_test_secret"
|
||||
|
||||
defp fiat_bounty do
|
||||
repository = listed_github_repository_fixture()
|
||||
sponsor = active_account_fixture() |> fund_credits(1_000)
|
||||
|
||||
{bounty, _checkout_url} = fiat_bounty_fixture(Scope.for_account(sponsor), repository)
|
||||
bounty
|
||||
end
|
||||
|
||||
defp signed_post(conn, payload, opts \\ []) do
|
||||
timestamp = Keyword.get(opts, :timestamp, System.system_time(:second))
|
||||
secret = Keyword.get(opts, :secret, @secret)
|
||||
|
||||
signature =
|
||||
:crypto.mac(:hmac, :sha256, secret, "#{timestamp}.#{payload}")
|
||||
|> Base.encode16(case: :lower)
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("stripe-signature", "t=#{timestamp},v1=#{signature}")
|
||||
|> post(~p"/webhooks/stripe", payload)
|
||||
end
|
||||
|
||||
defp checkout_completed_payload(session_id) do
|
||||
Jason.encode!(%{
|
||||
"id" => "evt_1",
|
||||
"type" => "checkout.session.completed",
|
||||
"data" => %{"object" => %{"id" => session_id, "payment_intent" => "pi_1"}}
|
||||
})
|
||||
end
|
||||
|
||||
test "a validly signed checkout.session.completed funds the bounty", %{conn: conn} do
|
||||
bounty = fiat_bounty()
|
||||
|
||||
conn = signed_post(conn, checkout_completed_payload(bounty.stripe_checkout_session_id))
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
assert Repo.get!(Bounty, bounty.id).status == "open"
|
||||
end
|
||||
|
||||
defp checkout_payload(session_id, type, extra) do
|
||||
Jason.encode!(%{
|
||||
"id" => "evt_1",
|
||||
"type" => type,
|
||||
"data" => %{"object" => Map.merge(%{"id" => session_id}, extra)}
|
||||
})
|
||||
end
|
||||
|
||||
test "a completed but unpaid session does not fund the bounty", %{conn: conn} do
|
||||
bounty = fiat_bounty()
|
||||
|
||||
# Delayed-notification methods complete the session before the money
|
||||
# arrives. Funding here would let work be claimed against nothing.
|
||||
conn =
|
||||
signed_post(
|
||||
conn,
|
||||
checkout_payload(
|
||||
bounty.stripe_checkout_session_id,
|
||||
"checkout.session.completed",
|
||||
%{"payment_status" => "unpaid"}
|
||||
)
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
|
||||
end
|
||||
|
||||
test "the later async payment confirmation funds it", %{conn: conn} do
|
||||
bounty = fiat_bounty()
|
||||
|
||||
conn =
|
||||
signed_post(
|
||||
conn,
|
||||
checkout_payload(
|
||||
bounty.stripe_checkout_session_id,
|
||||
"checkout.session.completed",
|
||||
%{"payment_status" => "unpaid"}
|
||||
)
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> recycle()
|
||||
|> signed_post(
|
||||
checkout_payload(
|
||||
bounty.stripe_checkout_session_id,
|
||||
"checkout.session.async_payment_succeeded",
|
||||
%{}
|
||||
)
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
assert Repo.get!(Bounty, bounty.id).status == "open"
|
||||
end
|
||||
|
||||
test "a failed async payment leaves the bounty unfunded", %{conn: conn} do
|
||||
bounty = fiat_bounty()
|
||||
|
||||
conn =
|
||||
signed_post(
|
||||
conn,
|
||||
checkout_payload(
|
||||
bounty.stripe_checkout_session_id,
|
||||
"checkout.session.async_payment_failed",
|
||||
%{}
|
||||
)
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
|
||||
end
|
||||
|
||||
test "an invalid signature is rejected", %{conn: conn} do
|
||||
bounty = fiat_bounty()
|
||||
|
||||
conn =
|
||||
signed_post(conn, checkout_completed_payload(bounty.stripe_checkout_session_id),
|
||||
secret: "whsec_wrong"
|
||||
)
|
||||
|
||||
assert json_response(conn, 400) == %{"error" => "invalid_signature"}
|
||||
assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
|
||||
end
|
||||
|
||||
test "a stale timestamp is rejected", %{conn: conn} do
|
||||
bounty = fiat_bounty()
|
||||
|
||||
conn =
|
||||
signed_post(conn, checkout_completed_payload(bounty.stripe_checkout_session_id),
|
||||
timestamp: System.system_time(:second) - 600
|
||||
)
|
||||
|
||||
assert json_response(conn, 400) == %{"error" => "timestamp_outside_tolerance"}
|
||||
assert Repo.get!(Bounty, bounty.id).status == "pending_funding"
|
||||
end
|
||||
|
||||
test "a missing signature header is rejected", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post(~p"/webhooks/stripe", checkout_completed_payload("cs_test_whatever"))
|
||||
|
||||
assert json_response(conn, 400) == %{"error" => "missing_signature"}
|
||||
end
|
||||
|
||||
test "a tampered body is rejected", %{conn: conn} do
|
||||
timestamp = System.system_time(:second)
|
||||
|
||||
signature =
|
||||
:crypto.mac(
|
||||
:hmac,
|
||||
:sha256,
|
||||
@secret,
|
||||
"#{timestamp}.{\"type\":\"checkout.session.completed\"}"
|
||||
)
|
||||
|> Base.encode16(case: :lower)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("stripe-signature", "t=#{timestamp},v1=#{signature}")
|
||||
|> post(~p"/webhooks/stripe", checkout_completed_payload("cs_test_tampered"))
|
||||
|
||||
assert json_response(conn, 400) == %{"error" => "invalid_signature"}
|
||||
end
|
||||
|
||||
test "unknown events are acknowledged and ignored", %{conn: conn} do
|
||||
payload = Jason.encode!(%{"id" => "evt_2", "type" => "payment_intent.created", "data" => %{}})
|
||||
|
||||
conn = signed_post(conn, payload)
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
end
|
||||
|
||||
test "funding an unknown session still returns 200", %{conn: conn} do
|
||||
conn = signed_post(conn, checkout_completed_payload("cs_test_unknown"))
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
end
|
||||
|
||||
describe "subscription events" do
|
||||
defp subscription_event_payload(type, object) do
|
||||
Jason.encode!(%{"id" => "evt_sub", "type" => type, "data" => %{"object" => object}})
|
||||
end
|
||||
|
||||
defp checkout_subscription_payload(account) do
|
||||
subscription_event_payload("checkout.session.completed", %{
|
||||
"id" => "cs_test_sub",
|
||||
"mode" => "subscription",
|
||||
"client_reference_id" => Integer.to_string(account.id),
|
||||
"customer" => "cus_webhook_1",
|
||||
"subscription" => "sub_webhook_1",
|
||||
"metadata" => %{"plan" => "team"}
|
||||
})
|
||||
end
|
||||
|
||||
test "a signed subscription checkout activates the account's subscription", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
|
||||
conn = signed_post(conn, checkout_subscription_payload(account))
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
|
||||
subscription = Tarakan.Billing.subscription(account)
|
||||
assert subscription.status == "active"
|
||||
assert subscription.plan == "team"
|
||||
assert subscription.stripe_subscription_id == "sub_webhook_1"
|
||||
end
|
||||
|
||||
test "customer.subscription.updated syncs the subscription", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
|
||||
conn = signed_post(conn, checkout_subscription_payload(account))
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
|
||||
payload =
|
||||
subscription_event_payload("customer.subscription.updated", %{
|
||||
"id" => "sub_webhook_1",
|
||||
"status" => "active",
|
||||
"current_period_end" => 1_900_000_000,
|
||||
"items" => %{"data" => [%{"price" => %{"id" => "price_enterprise_test"}}]}
|
||||
})
|
||||
|
||||
conn = signed_post(build_conn(), payload)
|
||||
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
|
||||
subscription = Tarakan.Billing.subscription(account)
|
||||
assert subscription.plan == "enterprise"
|
||||
assert DateTime.to_unix(subscription.current_period_end) == 1_900_000_000
|
||||
end
|
||||
|
||||
test "invoice.payment_failed and customer.subscription.deleted move the status", %{
|
||||
conn: conn
|
||||
} do
|
||||
account = account_fixture()
|
||||
|
||||
conn = signed_post(conn, checkout_subscription_payload(account))
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
|
||||
failed =
|
||||
subscription_event_payload("invoice.payment_failed", %{
|
||||
"subscription" => "sub_webhook_1"
|
||||
})
|
||||
|
||||
conn = signed_post(build_conn(), failed)
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
assert Tarakan.Billing.subscription(account).status == "past_due"
|
||||
|
||||
deleted =
|
||||
subscription_event_payload("customer.subscription.deleted", %{
|
||||
"id" => "sub_webhook_1"
|
||||
})
|
||||
|
||||
conn = signed_post(build_conn(), deleted)
|
||||
assert json_response(conn, 200) == %{"received" => true}
|
||||
assert Tarakan.Billing.subscription(account).status == "canceled"
|
||||
end
|
||||
|
||||
test "subscription events require a valid signature", %{conn: conn} do
|
||||
account = account_fixture()
|
||||
|
||||
conn =
|
||||
signed_post(conn, checkout_subscription_payload(account), secret: "whsec_wrong")
|
||||
|
||||
assert json_response(conn, 400) == %{"error" => "invalid_signature"}
|
||||
assert Tarakan.Billing.subscription(account) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue