| defmodule Plausible.HelpScoutTest do |
| use Plausible.DataCase, async: true |
|
|
| @moduletag :ee_only |
|
|
| on_ee do |
| alias Plausible.Billing.Subscription |
| alias Plausible.HelpScout |
| alias Plausible.Repo |
|
|
| alias PlausibleWeb.Router.Helpers, as: Routes |
|
|
| require Plausible.Billing.Subscription.Status |
|
|
| @v4_business_monthly_plan_id "857105" |
| @v4_business_yearly_plan_id "857087" |
|
|
| describe "validate_signature/1" do |
| test "returns error on missing signature" do |
| conn = |
| :get |
| |> Plug.Test.conn("/?foo=one&bar=two&baz=three") |
| |> Plug.Conn.fetch_query_params() |
|
|
| assert {:error, :missing_signature} = HelpScout.validate_signature(conn) |
| end |
|
|
| test "returns error on invalid signature" do |
| conn = |
| :get |
| |> Plug.Test.conn("/?foo=one&bar=two&baz=three&X-HelpScout-Signature=invalid") |
| |> Plug.Conn.fetch_query_params() |
|
|
| assert {:error, :bad_signature} = HelpScout.validate_signature(conn) |
| end |
|
|
| test "passes for valid signature" do |
| signature_key = Application.fetch_env!(:plausible, HelpScout)[:signature_key] |
| data = ~s|{"foo":"one","bar":"two","baz":"three"}| |
|
|
| signature = |
| :hmac |
| |> :crypto.mac(:sha, signature_key, data) |
| |> Base.encode64() |
| |> URI.encode_www_form() |
|
|
| conn = |
| :get |
| |> Plug.Test.conn("/?foo=one&bar=two&baz=three&X-HelpScout-Signature=#{signature}") |
| |> Plug.Conn.fetch_query_params() |
|
|
| assert :ok = HelpScout.validate_signature(conn) |
| end |
| end |
|
|
| describe "get_details_for_customer/2" do |
| test "returns details for user on trial" do |
| %{email: email} = user = new_user(trial_expiry_date: Date.utc_today()) |
| stub_help_scout_requests(email) |
| team = team_of(user) |
|
|
| crm_url = Routes.customer_support_team_url(PlausibleWeb.Endpoint, :show, team.id) |
|
|
| assert {:ok, |
| %{ |
| status_link: ^crm_url, |
| status_label: "Trial", |
| plan_link: "#", |
| plan_label: "None", |
| sites_count: 0 |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user without trial or subscription" do |
| %{email: email} = new_user(trial_expiry_date: nil) |
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "None", |
| plan_link: "#", |
| plan_label: "None" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with trial expired" do |
| %{email: email} = |
| new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Expired trial", |
| plan_link: "#", |
| plan_label: "None" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with paid subscription on standard plan" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_plan(user, @v4_business_monthly_plan_id) |
|
|
| paddle_subscription_id = |
| user |
| |> team_of(with_subscription?: true) |
| |> Map.fetch!(:subscription) |
| |> Map.fetch!(:paddle_subscription_id) |
|
|
| stub_help_scout_requests(email) |
|
|
| plan_link = |
| "https://vendors.paddle.com/subscriptions/customers/manage/#{paddle_subscription_id}" |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Paid", |
| plan_link: ^plan_link, |
| plan_label: "10k Plan (€19 monthly)" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with paid subscription on standard yearly plan" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_plan(user, @v4_business_yearly_plan_id) |
|
|
| paddle_subscription_id = |
| user |
| |> team_of(with_subscription?: true) |
| |> Map.fetch!(:subscription) |
| |> Map.fetch!(:paddle_subscription_id) |
|
|
| stub_help_scout_requests(email) |
|
|
| plan_link = |
| "https://vendors.paddle.com/subscriptions/customers/manage/#{paddle_subscription_id}" |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Paid", |
| plan_link: ^plan_link, |
| plan_label: "10k Plan (€190 yearly)" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with paid subscription on free 10k plan" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_plan(user, "free_10k") |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Paid", |
| plan_link: _, |
| plan_label: "Free 10k" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with paid subscription on enterprise plan" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.StatsAPI] |
| ) |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Paid", |
| plan_link: _, |
| plan_label: "1M Enterprise Plan (€123 monthly)" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with paid subscription on yearly enterprise plan" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.StatsAPI], |
| billing_interval: :yearly |
| ) |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Paid", |
| plan_link: _, |
| plan_label: "1M Enterprise Plan (€123 yearly)" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with subscription pending cancellation" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_plan(user, @v4_business_monthly_plan_id, |
| status: Subscription.Status.deleted() |
| ) |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Pending cancellation", |
| plan_link: _, |
| plan_label: "10k Plan (€19 monthly)" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with canceled subscription" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_plan(user, @v4_business_monthly_plan_id, |
| status: Subscription.Status.deleted(), |
| next_bill_date: Date.add(Date.utc_today(), -1) |
| ) |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Canceled", |
| plan_link: _, |
| plan_label: "10k Plan (€19 monthly)" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with paused subscription" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| subscribe_to_plan(user, @v4_business_monthly_plan_id, |
| status: Subscription.Status.paused() |
| ) |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Paused", |
| plan_link: _, |
| plan_label: "10k Plan (€19 monthly)" |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns for user with locked site" do |
| user = %{email: email} = new_user(trial_expiry_date: Date.add(Date.utc_today(), -1)) |
|
|
| site = new_site(owner: user) |
| site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!() |
| subscribe_to_plan(user, @v4_business_monthly_plan_id) |
|
|
| stub_help_scout_requests(email) |
|
|
| assert {:ok, |
| %{ |
| status_link: _, |
| status_label: "Dashboard locked", |
| plan_link: _, |
| plan_label: "10k Plan (€19 monthly)", |
| sites_count: 1 |
| }} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns details for user via customer mapped email" do |
| %{email: email} = new_user(trial_expiry_date: Date.utc_today()) |
| stub_help_scout_requests("different.email.in.hs@example.com") |
| HelpScout.set_customer_mapping("500", email) |
|
|
| assert {:ok, %{status_label: "Trial"}} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns details for user via conversation mapped email" do |
| %{email: email} = new_user(trial_expiry_date: Date.utc_today()) |
| stub_help_scout_requests("different.email.in.hs@example.com") |
| HelpScout.set_conversation_mapping("1000", email) |
|
|
| assert {:ok, %{status_label: "Trial"}} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| for excluded_domain <- Plausible.HelpScout.excluded_email_domains() do |
| test "refuses to use customer mapped email when customer email is in #{excluded_domain}" do |
| %{email: email} = new_user(trial_expiry_date: Date.utc_today()) |
| stub_help_scout_requests("excluded.email@#{unquote(excluded_domain)}") |
| HelpScout.set_customer_mapping("500", email) |
|
|
| assert {:error, :excluded_email} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "uses conversation mapped email when customer email is in #{excluded_domain} " do |
| %{email: email} = new_user(trial_expiry_date: Date.utc_today()) |
| stub_help_scout_requests("excluded.email@#{unquote(excluded_domain)}") |
| HelpScout.set_conversation_mapping("1000", email) |
|
|
| assert {:ok, %{status_label: "Trial"}} = |
| HelpScout.get_details_for_customer("500", "1000") |
| end |
| end |
|
|
| test "returns error when no matching user found in database" do |
| new_user() |
|
|
| stub_help_scout_requests("another@example.com") |
|
|
| assert {:error, {:user_not_found, ["another@example.com"]}} = |
| HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns error when no customer found in Help Scout" do |
| Req.Test.stub(HelpScout, fn |
| %{request_path: "/v2/oauth2/token"} = conn -> |
| Req.Test.json(conn, %{ |
| "token_type" => "bearer", |
| "access_token" => "369dbb08be58430086d2f8bd832bc1eb", |
| "expires_in" => 172_800 |
| }) |
|
|
| %{request_path: "/v2/customers/500"} = conn -> |
| conn |
| |> Plug.Conn.put_status(404) |
| |> Req.Test.text("Not found") |
| end) |
|
|
| assert {:error, :not_found} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "returns error when found customer has no emails" do |
| Req.Test.stub(HelpScout, fn |
| %{request_path: "/v2/oauth2/token"} = conn -> |
| Req.Test.json(conn, %{ |
| "token_type" => "bearer", |
| "access_token" => "369dbb08be58430086d2f8bd832bc1eb", |
| "expires_in" => 172_800 |
| }) |
|
|
| %{request_path: "/v2/customers/500"} = conn -> |
| Req.Test.json(conn, %{ |
| "id" => 500, |
| "_embedded" => %{ |
| "emails" => [] |
| } |
| }) |
| end) |
|
|
| assert {:error, :not_found} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "uses existing access token when available" do |
| %{email: email} = new_user() |
| now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) |
|
|
| Repo.insert_all("help_scout_credentials", [ |
| [ |
| access_token: HelpScout.Vault.encrypt!("VerySecret"), |
| inserted_at: now, |
| updated_at: now |
| ] |
| ]) |
|
|
| Req.Test.stub(HelpScout, fn %{request_path: "/v2/customers/500"} = conn -> |
| Req.Test.json(conn, %{ |
| "id" => 500, |
| "_embedded" => %{ |
| "emails" => [ |
| %{ |
| "id" => 1, |
| "value" => email, |
| "type" => "home" |
| } |
| ] |
| } |
| }) |
| end) |
|
|
| assert {:ok, _} = HelpScout.get_details_for_customer("500", "1000") |
| end |
|
|
| test "refreshes token on expiry" do |
| %{email: email} = new_user() |
| now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) |
|
|
| Repo.insert_all("help_scout_credentials", [ |
| [ |
| access_token: HelpScout.Vault.encrypt!("VerySecretExpired"), |
| inserted_at: now, |
| updated_at: now |
| ] |
| ]) |
|
|
| Req.Test.stub(HelpScout, fn |
| %{request_path: "/v2/oauth2/token"} = conn -> |
| Req.Test.json(conn, %{ |
| "token_type" => "bearer", |
| "access_token" => "VerySecretNew", |
| "expires_in" => 172_800 |
| }) |
|
|
| %{request_path: "/v2/customers/500"} = conn -> |
| case Plug.Conn.get_req_header(conn, "authorization") do |
| ["Bearer VerySecretExpired"] -> |
| conn |
| |> Plug.Conn.put_status(401) |
| |> Req.Test.text("Token expired") |
|
|
| ["Bearer VerySecretNew"] -> |
| Req.Test.json(conn, %{ |
| "id" => 500, |
| "_embedded" => %{ |
| "emails" => [ |
| %{ |
| "id" => 1, |
| "value" => email, |
| "type" => "home" |
| } |
| ] |
| } |
| }) |
| end |
| end) |
|
|
| assert {:ok, _} = HelpScout.get_details_for_customer("500", "1000") |
| end |
| end |
|
|
| describe "get_details_for_emails/4" do |
| test "returns details for user and persists mappings" do |
| %{email: email} = user = new_user(trial_expiry_date: Date.utc_today()) |
|
|
| team = team_of(user) |
|
|
| crm_url = Routes.customer_support_team_url(PlausibleWeb.Endpoint, :show, team.id) |
|
|
| assert {:ok, |
| %{ |
| status_link: ^crm_url, |
| status_label: "Trial", |
| plan_link: "#", |
| plan_label: "None", |
| sites_count: 0 |
| }} = HelpScout.get_details_for_emails([email], "123", "1000", nil) |
|
|
| assert {:ok, [^email]} = HelpScout.lookup_customer_mapping("123") |
| assert {:ok, [^email]} = HelpScout.lookup_conversation_mapping("1000") |
| end |
|
|
| test "updates mappings if ones already exist" do |
| user = new_user() |
| %{email: new_email} = new_user() |
|
|
| HelpScout.set_customer_mapping("123", user.email) |
| HelpScout.set_conversation_mapping("1000", user.email) |
|
|
| assert {:ok, _} = HelpScout.get_details_for_emails([new_email], "123", "1000", nil) |
| assert {:ok, [^new_email]} = HelpScout.lookup_customer_mapping("123") |
| assert {:ok, [^new_email]} = HelpScout.lookup_conversation_mapping("1000") |
| end |
|
|
| test "picks the match with largest number of owned sites" do |
| user1 = new_user() |
| new_site(owner: user1) |
| add_guest(new_site(), user: user1, role: :viewer) |
| add_guest(new_site(), user: user1, role: :editor) |
|
|
| user2 = new_user() |
| new_site(owner: user2) |
| new_site(owner: user2) |
| team2 = team_of(user2) |
|
|
| crm_url = Routes.customer_support_team_url(PlausibleWeb.Endpoint, :show, team2.id) |
|
|
| assert {:ok, |
| %{ |
| status_link: ^crm_url, |
| status_label: "Trial", |
| plan_link: "#", |
| plan_label: "None", |
| sites_count: 2 |
| }} = |
| HelpScout.get_details_for_emails([user1.email, user2.email], "123", "1000", nil) |
|
|
| user2_email = user2.email |
| assert {:ok, [^user2_email]} = HelpScout.lookup_customer_mapping("123") |
| end |
|
|
| test "does not persist the mapping when there's no match" do |
| assert {:error, {:user_not_found, ["does.not.exist@example.com"]}} = |
| HelpScout.get_details_for_emails( |
| ["does.not.exist@example.com"], |
| "123", |
| "1000", |
| nil |
| ) |
|
|
| assert {:error, :not_found} = HelpScout.lookup_customer_mapping("123") |
| end |
| end |
|
|
| describe "search_users/3" do |
| test "lists matching users by email or site domain ordered by site counts" do |
| user1 = new_user(email: "user1@match.example.com") |
|
|
| user2 = new_user(email: "user2@match.example.com") |
| new_site(owner: user2) |
|
|
| user3 = new_user(email: "user3@umatched.example.com") |
| new_site(owner: user3) |
| new_site(domain: "big.match.example.com/hit", owner: user3) |
|
|
| |
| new_site(domain: "consolidated.example.com", owner: user3, consolidated: true) |
|
|
| assert HelpScout.search_users("match.example.co", "123", "1000") == [ |
| %{email: user3.email, sites_count: 2}, |
| %{email: user2.email, sites_count: 1}, |
| %{email: user1.email, sites_count: 0} |
| ] |
| end |
|
|
| test "clears existing customer and conversation mappings" do |
| new_user(email: "user@match.example.com") |
|
|
| HelpScout.set_customer_mapping("123", "some@email.com") |
| HelpScout.set_conversation_mapping("1000", "some@email.com") |
|
|
| assert [_] = HelpScout.search_users("match.example.co", "123", "1000") |
|
|
| assert {:error, :not_found} = HelpScout.lookup_customer_mapping("123") |
| assert {:error, :not_found} = HelpScout.lookup_conversation_mapping("1000") |
| end |
| end |
|
|
| defp stub_help_scout_requests(email) do |
| Req.Test.stub(HelpScout, fn |
| %{request_path: "/v2/oauth2/token"} = conn -> |
| Req.Test.json(conn, %{ |
| "token_type" => "bearer", |
| "access_token" => "369dbb08be58430086d2f8bd832bc1eb", |
| "expires_in" => 172_800 |
| }) |
|
|
| %{request_path: "/v2/customers/500"} = conn -> |
| Req.Test.json(conn, %{ |
| "id" => 500, |
| "_embedded" => %{ |
| "emails" => [ |
| %{ |
| "id" => 1, |
| "value" => email, |
| "type" => "home" |
| } |
| ] |
| } |
| }) |
| end) |
| end |
| end |
| end |
|
|