| defmodule PlausibleWeb.Api.PaddleControllerTest do |
| use PlausibleWeb.ConnCase, async: true |
| use Plausible.Repo |
|
|
| import Mox |
| setup :verify_on_exit! |
|
|
| @webhook_body %{ |
| "alert_id" => "16173800", |
| "alert_name" => "subscription_created", |
| "cancel_url" => |
| "https://checkout.paddle.com/subscription/cancel?user=1032746&subscription=1869424&hash=eyJpdiI6Ik5XSnhkT1k2RSticStZcEVYZ2FEeTVZTHVSaTNpcFM4XC9aMFJNSjh6TFNrPSIsInZhbHVlIjoiWTMzd3dENGQySTc0UjVPM0U3dzVDUWlBMEQ3dGVzM2lyODRPczBVcjdBRXlncmQzRUVmQjhnZVhpTzJIRjFtNW41MmZOVmJNVUJ4empmeXgyUno1Z3c9PSIsIm1hYyI6ImFjNDMyNDIxNmNmMWNiNmViMmFlNGVkMzQ3ZjYyYTQ5ZWI2YTEyMDQ4YjFhNTEyMjAxNzNlNjEwOWU4OTVhOWMifQ%3D%3D", |
| "checkout_id" => "38111668-chre6449b9c3cc8-f473e48a86", |
| "currency" => "USD", |
| "email" => "josh@joshuae.com", |
| "event_time" => "2019-08-20 21:44:54", |
| "marketing_consent" => 0, |
| "next_bill_date" => "2019-09-20", |
| "passthrough" => "235", |
| "quantity" => "1", |
| "status" => "active", |
| "subscription_id" => "1869424", |
| "subscription_plan_id" => "558018", |
| "unit_price" => "6.00", |
| "update_url" => |
| "https://checkout.paddle.com/subscription/update?user=1032746&subscription=1869424&hash=eyJpdiI6IkNUS2VZQlRxcFA5MVlEXC9Oa1ZwRDBNaGN4VVwvaU1RU2srWXc0bU1tQndyTT0iLCJ2YWx1ZSI6ImtTeU1ESkxWcEVrTDFmKzkyZ0FaSFo2Q0VNK3A2XC9NdEU4S2tGVFE2blJicGxBQzZ1XC9mMG1PcUo3MWV2OGY4YURjT1UxY2hpeHh5SFhlMmhXaFpoalE9PSIsIm1hYyI6ImZkNGU5MTg3YzQxZWYxNDJjNDkyMWFkYmZhZjIyMGQ3ZGI2YTVmMTcxZGViY2VkNzI0ZjNlMDRkZTgwNTEwMzUifQ%3D%3D", |
| "user_id" => "1032746", |
| "p_signature" => |
| "qfqKA3dI9d60uie9IORcvkHYV+rd1UaCu/f5kh4miTkeIQNimgusQG8pS1OHobCvN/OktwKCjFcbIwoa4nakOOWGroHJ8FjLJHBK4g1uI37Bp6l73dNl8mB4dNGW1M+atkz7ag6pETRIdEKCmC5tV9afN5CvbcqRV1lsj/x2fAsjAe/sQkmAP1jbDXOMEuHqkWssSB7Q+NGHHLHuNQ67m7YFBnZSgYzLeLMEApkZClJn0j6MokUVjW37ISn5eA5FlUbT7s6Kph54roRzLIpYvC+ff/n6ae2Iu1OsORxRBg4Uv8dqqjqBKXlv84/OB80U89yMIbRw/pbHD6+zF4FxgNV7nk2bjgK2V6h55AOuhJHHUMb4XX9R8i8iG1FOlNJaTwbhkIkvQF3q7nEItKCqizn+l4tFQ9MUcrjw8jytDznbOnSlmNhtcDVlnvXNDaSPkEA7AyR6c+BiZV/Y6I3y8sr8h/F/cBM3OPTwfdKK34jyWW4LRn15nSxq2kjH3SyLPEpTJUMdcRGAgBZc06E4lENU2x22E/JKG5BRi1aDs5OFQtrjYi2hOTI0dyPF3OLNeZcCgBCKBmKq5XIf1T0RPFWAWtKkzXhl/QH+4feNATb9/i6k5xKeUJf0ltWzsI5x84kvsC/m05hn/AuBDmcZGkVnDLXrqttR+zDXY6P1euE=" |
| } |
|
|
| describe "webhook verification" do |
| test "is verified when signature is correct", %{conn: conn} do |
| insert(:user, id: 235) |
|
|
| |
| assert_raise RuntimeError, ~r/Invalid passthrough sent via Paddle/, fn -> |
| post(conn, Routes.paddle_path(conn, :webhook), @webhook_body) |
| end |
| end |
|
|
| test "not verified when signature is corrupted", %{conn: conn} do |
| corrupted = Map.put(@webhook_body, "p_signature", Base.encode64("123 fake signature")) |
| conn = post(conn, Routes.paddle_path(conn, :webhook), corrupted) |
| assert conn.status == 400 |
| end |
| end |
|
|
| describe "fetching currency" do |
| test "retrieves successfully", %{conn: conn} do |
| expect_get_prices_response(get_prices_body("USD")) |
|
|
| conn = get(conn, Routes.paddle_path(conn, :currency)) |
| assert_receive :paddle_queried |
| assert json_response(conn, 200) == %{"currency" => "$"} |
| end |
|
|
| test "caches per ip", %{conn: initial_conn} do |
| expect_get_prices_response(get_prices_body("USD")) |
|
|
| conn = get(initial_conn, Routes.paddle_path(initial_conn, :currency)) |
| assert json_response(conn, 200) == %{"currency" => "$"} |
| assert_receive :paddle_queried |
|
|
| expect_get_prices_response(get_prices_body("GBP")) |
|
|
| conn = get(initial_conn, Routes.paddle_path(initial_conn, :currency)) |
| assert json_response(conn, 200) == %{"currency" => "$"} |
| refute_receive :paddle_queried |
|
|
| new_ip = |
| Plug.Conn.put_req_header(initial_conn, "x-forwarded-for", random_ip()) |
|
|
| conn = get(new_ip, Routes.paddle_path(initial_conn, :currency)) |
| assert json_response(conn, 200) == %{"currency" => "£"} |
| assert_receive :paddle_queried |
| end |
|
|
| test "falls back to EUR when paddle fails to respond", %{conn: conn} do |
| expect_get_prices_response(%{"response" => %{}}) |
|
|
| conn = get(conn, Routes.paddle_path(conn, :currency)) |
| assert_receive :paddle_queried |
| assert json_response(conn, 200) == %{"currency" => "€"} |
| end |
|
|
| test "does not cache failed fetches", %{conn: initial_conn} do |
| expect_get_prices_response(%{"response" => %{}}) |
|
|
| conn = get(initial_conn, Routes.paddle_path(initial_conn, :currency)) |
| assert json_response(conn, 200) == %{"currency" => "€"} |
|
|
| expect_get_prices_response(get_prices_body("USD")) |
|
|
| conn = get(initial_conn, Routes.paddle_path(initial_conn, :currency)) |
| assert json_response(conn, 200) == %{"currency" => "$"} |
| end |
|
|
| defp expect_get_prices_response(body) do |
| test = self() |
|
|
| expect( |
| Plausible.HTTPClient.Mock, |
| :get, |
| fn "https://checkout.paddle.com/api/2.0/prices" <> query, _, _ -> |
| assert query =~ "product_ids=857097" |
|
|
| send(test, :paddle_queried) |
|
|
| {:ok, |
| %Finch.Response{ |
| status: 200, |
| headers: [{"content-type", "application/json"}], |
| body: body |
| }} |
| end |
| ) |
| end |
|
|
| defp get_prices_body(currency) do |
| %{ |
| "response" => %{ |
| "customer_country" => "PL", |
| "products" => [ |
| %{ |
| "applied_coupon" => [], |
| "currency" => currency, |
| "list_price" => %{"gross" => 49.0, "net" => 49.0, "tax" => 0.0}, |
| "price" => %{"gross" => 49.0, "net" => 49.0, "tax" => 0.0}, |
| "product_id" => 857_097, |
| "product_title" => "random", |
| "subscription" => %{ |
| "frequency" => 1, |
| "interval" => "month", |
| "list_price" => %{"gross" => 49.0, "net" => 49.0, "tax" => 0.0}, |
| "price" => %{"gross" => 49.0, "net" => 49.0, "tax" => 0.0}, |
| "trial_days" => 0 |
| }, |
| "vendor_set_prices_included_tax" => false |
| } |
| ] |
| }, |
| "success" => true |
| } |
| end |
| end |
| end |
|
|