| defmodule PlausibleWeb.Api.ExternalSitesControllerTest do |
| @moduledoc """ |
| Tests for the following endpoints |
| |
| GET /api/v1/sites/teams |
| |
| GET /api/v1/sites/guests |
| PUT /api/v1/sites/guests |
| DELETE /api/v1/sites/guests |
| |
| PUT /api/v1/sites/shared-links |
| |
| GET /api/v1/custom-props |
| PUT /api/v1/sites/custom-props |
| DELETE /api/v1/sites/custom-props/:property |
| |
| GET /api/v1/goals |
| PUT /api/v1/sites/goals |
| DELETE /api/v1/sites/goals/:goal_id |
| |
| Site CRUD endpoints tests are in ExternalSitesControllerSitesCrudApiTest |
| """ |
| use PlausibleWeb.ConnCase, async: false |
| use Plausible.Repo |
| use Bamboo.Test |
|
|
| on_ee do |
| setup :create_user |
|
|
| setup %{conn: conn, user: user} do |
| api_key = insert(:api_key, user: user, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
| {:ok, api_key: api_key, conn: conn} |
| end |
|
|
| describe "GET /api/v1/sites/teams" do |
| test "shows empty list when user is not a member of any team", %{conn: conn} do |
| conn = get(conn, "/api/v1/sites/teams") |
|
|
| assert json_response(conn, 200) == %{ |
| "teams" => [], |
| "meta" => %{ |
| "before" => nil, |
| "after" => nil, |
| "limit" => 100 |
| } |
| } |
| end |
|
|
| test "shows list of teams user is a member of with api availability reflecting team state", |
| %{conn: conn, user: user} do |
| user |> subscribe_to_growth_plan() |
|
|
| personal_team = team_of(user) |
|
|
| owner1 = |
| new_user( |
| trial_expiry_date: Date.add(Date.utc_today(), -1), |
| team: [name: "Team Without Stats API"] |
| ) |
| |> subscribe_to_enterprise_plan(features: []) |
|
|
| team_without_stats = owner1 |> team_of() |> Plausible.Teams.complete_setup() |
| add_member(team_without_stats, user: user, role: :editor) |
| owner2 = new_user(team: [name: "Team With Stats API"]) |
| team_with_stats = owner2 |> team_of() |> Plausible.Teams.complete_setup() |
| add_member(team_with_stats, user: user, role: :owner) |
|
|
| conn = get(conn, "/api/v1/sites/teams") |
|
|
| assert json_response(conn, 200) == %{ |
| "teams" => [ |
| %{ |
| "id" => team_with_stats.identifier, |
| "name" => "Team With Stats API", |
| "api_available" => true |
| }, |
| %{ |
| "id" => team_without_stats.identifier, |
| "name" => "Team Without Stats API", |
| "api_available" => false |
| }, |
| %{ |
| "id" => personal_team.identifier, |
| "name" => "My personal sites", |
| "api_available" => false |
| } |
| ], |
| "meta" => %{ |
| "before" => nil, |
| "after" => nil, |
| "limit" => 100 |
| } |
| } |
| end |
|
|
| test "shows only one team for team scoped key", %{conn: conn, user: user} do |
| user |> subscribe_to_business_plan() |
|
|
| personal_team = team_of(user) |
|
|
| another_team = new_site().team |> Plausible.Teams.complete_setup() |
| add_member(another_team, user: user, role: :admin) |
|
|
| api_key = insert(:api_key, user: user, team: personal_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = get(conn, "/api/v1/sites/teams") |
|
|
| assert json_response(conn, 200) == %{ |
| "teams" => [ |
| %{ |
| "id" => personal_team.identifier, |
| "name" => "My personal sites", |
| "api_available" => true |
| } |
| ], |
| "meta" => %{ |
| "before" => nil, |
| "after" => nil, |
| "limit" => 100 |
| } |
| } |
| end |
| end |
|
|
| describe "PUT /api/v1/sites/shared-links" do |
| setup :create_site |
|
|
| setup %{user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [ |
| Plausible.Billing.Feature.SharedLinks, |
| Plausible.Billing.Feature.SitesAPI |
| ] |
| ) |
|
|
| :ok |
| end |
|
|
| test "can add a shared link to a site", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: site.domain, |
| name: "WordPress" |
| }) |
|
|
| res = json_response(conn, 200) |
| assert res["name"] == "WordPress" |
| assert String.starts_with?(res["url"], "http://") |
| end |
|
|
| test "can add a shared link to a site using the old site id after domain change", %{ |
| conn: conn, |
| site: site |
| } do |
| old_domain = site.domain |
| new_domain = "new.example.com" |
|
|
| Plausible.Site.Domain.change(site, new_domain) |
|
|
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: old_domain, |
| name: "WordPress" |
| }) |
|
|
| res = json_response(conn, 200) |
| assert res["name"] == "WordPress" |
| assert String.starts_with?(res["url"], "http://") |
| end |
|
|
| test "is idempotent find or create op", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: site.domain, |
| name: "WordPress" |
| }) |
|
|
| %{"url" => url} = json_response(conn, 200) |
|
|
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: site.domain, |
| name: "WordPress" |
| }) |
|
|
| assert %{"url" => ^url} = json_response(conn, 200) |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do |
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: site.domain, |
| name: "WordPress" |
| }) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "returns 400 when site id missing", %{conn: conn} do |
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| name: "WordPress" |
| }) |
|
|
| res = json_response(conn, 400) |
|
|
| assert res["error"] == |
| "Missing site ID. Please provide the required site_id parameter with your request." |
| end |
|
|
| test "returns 404 when site id is non existent", %{conn: conn} do |
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| name: "WordPress", |
| site_id: "bad" |
| }) |
|
|
| res = json_response(conn, 404) |
| assert res["error"] == "Site could not be found" |
| end |
|
|
| @tag :capture_log |
| test "returns 404 when api key owner does not have permissions to create a shared link", %{ |
| conn: conn, |
| user: user |
| } do |
| site = new_site() |
|
|
| add_guest(site, user: user, role: :viewer) |
|
|
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: site.domain, |
| name: "WordPress" |
| }) |
|
|
| res = json_response(conn, 402) |
| assert res["error"] =~ "API key does not have access to Sites API" |
| end |
|
|
| test "fails to create without access to SharedLinks feature", %{ |
| conn: conn, |
| site: site, |
| user: user |
| } do |
| subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.SitesAPI]) |
|
|
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: site.domain, |
| name: "My Link" |
| }) |
|
|
| res = json_response(conn, 402) |
|
|
| assert res["error"] == "Your current subscription plan does not include Shared Links" |
| end |
|
|
| for special_name <- Plausible.Sites.shared_link_special_names() do |
| test "fails to create with the special '#{special_name}' name intended for Plugins API", |
| %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/shared-links", %{ |
| site_id: site.domain, |
| name: unquote(special_name) |
| }) |
|
|
| res = json_response(conn, 400) |
|
|
| assert res["error"] == "This name is reserved. Please choose another one" |
| end |
| end |
| end |
|
|
| describe "PUT /api/v1/sites/custom-props" do |
| setup :create_site |
|
|
| setup %{user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [ |
| Plausible.Billing.Feature.Props, |
| Plausible.Billing.Feature.SitesAPI |
| ] |
| ) |
|
|
| :ok |
| end |
|
|
| test "can add a custom property to a site", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1" |
| }) |
|
|
| res = json_response(conn, 200) |
|
|
| assert res["created"] == true |
|
|
| assert Repo.reload!(site).allowed_event_props == ["prop1"] |
| end |
|
|
| test "can add a custom prop using old site_id after domain change", %{ |
| conn: conn, |
| site: site |
| } do |
| old_domain = site.domain |
| new_domain = "new.example.com" |
|
|
| Plausible.Site.Domain.change(site, new_domain) |
|
|
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: old_domain, |
| property: "prop1" |
| }) |
|
|
| res = json_response(conn, 200) |
|
|
| assert res["created"] == true |
|
|
| assert Repo.reload!(site).allowed_event_props == ["prop1"] |
| end |
|
|
| test "is idempotent", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1" |
| }) |
|
|
| assert %{"created" => true} = json_response(conn, 200) |
|
|
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1" |
| }) |
|
|
| assert %{"created" => true} = json_response(conn, 200) |
|
|
| assert Repo.reload!(site).allowed_event_props == ["prop1"] |
| end |
|
|
| test "fails to add a custom prop with too long name", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: String.duplicate("a", Plausible.Props.max_prop_key_length() + 1) |
| }) |
|
|
| assert %{"error" => "Parameter `property` is too long"} = json_response(conn, 400) |
| end |
|
|
| test "fails to add a custom prop when props list is too long", %{conn: conn, site: site} do |
| max_props = Plausible.Props.max_props() |
|
|
| Enum.reduce(1..max_props, site, fn idx, site -> |
| {:ok, site} = Plausible.Props.allow(site, "prop#{idx}") |
| site |
| end) |
|
|
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop#{max_props + 1}" |
| }) |
|
|
| assert %{"error" => "Can't add any more custom properties"} = json_response(conn, 400) |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do |
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1" |
| }) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "returns 400 when site id missing", %{conn: conn} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| property: "prop1" |
| }) |
|
|
| res = json_response(conn, 400) |
|
|
| assert res["error"] == |
| "Missing site ID. Please provide the required site_id parameter with your request." |
| end |
|
|
| test "returns 404 when site id is non existent", %{conn: conn} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| property: "prop1", |
| site_id: "bad" |
| }) |
|
|
| res = json_response(conn, 404) |
| assert res["error"] == "Site could not be found" |
| end |
|
|
| @tag :capture_log |
| test "returns 404 when api key owner does not have permissions to create a goal", %{ |
| conn: conn, |
| user: user |
| } do |
| site = new_site() |
|
|
| add_guest(site, user: user, role: :viewer) |
|
|
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1" |
| }) |
|
|
| res = json_response(conn, 402) |
| assert res["error"] =~ "API key does not have access to Sites API" |
| end |
|
|
| test "returns 400 when property missing", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain |
| }) |
|
|
| res = json_response(conn, 400) |
| assert res["error"] == "Parameter `property` is required to create a custom property" |
| end |
| end |
|
|
| describe "PUT /api/v1/sites/goals" do |
| setup :create_site |
|
|
| setup %{user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.SitesAPI] |
| ) |
|
|
| :ok |
| end |
|
|
| test "can add a goal as event to a site", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| resp = json_response(conn, 200) |
|
|
| assert_matches ^strict_map(%{ |
| "custom_props" => ^strict_map(%{}), |
| "display_name" => "Signup", |
| "domain" => ^site.domain, |
| "event_name" => "Signup", |
| "goal_type" => "event", |
| "id" => ^any(:integer), |
| "page_path" => nil |
| }) = resp |
| end |
|
|
| test "can add a goal as page to a site", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "page", |
| page_path: "/signup" |
| }) |
|
|
| resp = json_response(conn, 200) |
|
|
| assert_matches ^strict_map(%{ |
| "custom_props" => ^strict_map(%{}), |
| "display_name" => "Visit /signup", |
| "domain" => ^site.domain, |
| "event_name" => nil, |
| "goal_type" => "page", |
| "id" => ^any(:integer), |
| "page_path" => "/signup" |
| }) = resp |
| end |
|
|
| test "can add a goal using old site_id after domain change", %{conn: conn, site: site} do |
| old_domain = site.domain |
| new_domain = "new.example.com" |
|
|
| Plausible.Site.Domain.change(site, new_domain) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: old_domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| res = json_response(conn, 200) |
| assert res["goal_type"] == "event" |
| assert res["event_name"] == "Signup" |
| assert res["display_name"] == "Signup" |
| assert res["domain"] == new_domain |
| end |
|
|
| test "can add a goal as event with display name", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup", |
| display_name: "Customer Acquired" |
| }) |
|
|
| res = json_response(conn, 200) |
| assert res["goal_type"] == "event" |
| assert res["event_name"] == "Signup" |
| assert res["display_name"] == "Customer Acquired" |
| assert res["domain"] == site.domain |
| end |
|
|
| test "can add a goal as page with display name", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "page", |
| page_path: "/foo", |
| display_name: "Visit the foo page" |
| }) |
|
|
| res = json_response(conn, 200) |
| assert res["goal_type"] == "page" |
| assert res["display_name"] == "Visit the foo page" |
| assert res["page_path"] == "/foo" |
| assert res["domain"] == site.domain |
| end |
|
|
| test "is idempotent find or create op", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| %{"id" => goal_id} = json_response(conn, 200) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| assert %{"id" => ^goal_id} = json_response(conn, 200) |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do |
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "returns 400 when site id missing", %{conn: conn} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| res = json_response(conn, 400) |
|
|
| assert res["error"] == |
| "Missing site ID. Please provide the required site_id parameter with your request." |
| end |
|
|
| test "returns 404 when site id is non existent", %{conn: conn} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| goal_type: "event", |
| event_name: "Signup", |
| site_id: "bad" |
| }) |
|
|
| res = json_response(conn, 404) |
| assert res["error"] == "Site could not be found" |
| end |
|
|
| @tag :capture_log |
| test "returns 404 when api key owner does not have permissions to create a goal", %{ |
| conn: conn, |
| user: user |
| } do |
| site = new_site() |
|
|
| add_guest(site, user: user, role: :viewer) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| res = json_response(conn, 402) |
| assert res["error"] =~ "API key does not have access to Sites API" |
| end |
|
|
| test "returns 400 when goal type missing", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| event_name: "Signup" |
| }) |
|
|
| res = json_response(conn, 400) |
| assert res["error"] == "Parameter `goal_type` is required to create a goal" |
| end |
|
|
| test "returns 400 when goal event name missing", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event" |
| }) |
|
|
| res = json_response(conn, 400) |
| assert res["error"] == "Parameter `event_name` is required to create a goal" |
| end |
|
|
| test "returns 400 when goal page path missing", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "page" |
| }) |
|
|
| res = json_response(conn, 400) |
| assert res["error"] == "Parameter `page_path` is required to create a goal" |
| end |
|
|
| test "fails when max goals per site limit reached", %{conn: conn, site: site} do |
| for i <- 1..10, do: {:ok, _} = Plausible.Goals.create(site, %{"event_name" => "G#{i}"}) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| res = json_response(conn, 400) |
|
|
| assert res["error"] =~ "Maximum number of goals reached" |
| end |
|
|
| test "can create a goal with custom_props", %{conn: conn, site: site, user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.SitesAPI, Plausible.Billing.Feature.Props] |
| ) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Purchase", |
| custom_props: %{"product" => "enterprise", "tier" => "premium"} |
| }) |
|
|
| res = json_response(conn, 200) |
| assert res["goal_type"] == "event" |
| assert res["event_name"] == "Purchase" |
| assert res["custom_props"] == %{"product" => "enterprise", "tier" => "premium"} |
| end |
|
|
| test "custom_props defaults to empty map if not provided", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| res = json_response(conn, 200) |
| assert res["custom_props"] == %{} |
| end |
|
|
| test "fails when custom_props exceeds max of 3", %{conn: conn, site: site, user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.SitesAPI, Plausible.Billing.Feature.Props] |
| ) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Purchase", |
| custom_props: %{"a" => "1", "b" => "2", "c" => "3", "d" => "4"} |
| }) |
|
|
| res = json_response(conn, 400) |
| assert res["error"] =~ "use at most 3 properties per goal" |
| end |
|
|
| test "fails when custom_props has null values", %{conn: conn, site: site, user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.SitesAPI, Plausible.Billing.Feature.Props] |
| ) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Purchase", |
| custom_props: %{"product" => nil} |
| }) |
|
|
| res = json_response(conn, 400) |
| assert res["error"] =~ "must be a map with string keys and string values" |
| end |
|
|
| test "fails when custom_props has non-string values", %{conn: conn, site: site, user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.SitesAPI, Plausible.Billing.Feature.Props] |
| ) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Purchase", |
| custom_props: %{"count" => 42} |
| }) |
|
|
| res = json_response(conn, 400) |
| assert res["error"] =~ "must be a map with string keys and string values" |
| end |
|
|
| @tag :ee_only |
| test "fails without Props feature when custom_props is non-empty", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Purchase", |
| custom_props: %{"product" => "enterprise"} |
| }) |
|
|
| res = json_response(conn, 402) |
| assert res["error"] == "Your current subscription plan does not include Custom Properties" |
| end |
| end |
|
|
| describe "DELETE /api/v1/sites/custom-props/:property" do |
| setup :create_site |
|
|
| setup %{user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [ |
| Plausible.Billing.Feature.Props, |
| Plausible.Billing.Feature.SitesAPI |
| ] |
| ) |
|
|
| :ok |
| end |
|
|
| test "deletes a custom property", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1" |
| }) |
|
|
| assert %{"created" => true} = json_response(conn, 200) |
|
|
| conn = |
| delete(conn, "/api/v1/sites/custom-props/prop1", %{ |
| site_id: site.domain |
| }) |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
|
|
| assert Repo.reload!(site).allowed_event_props == [] |
| end |
|
|
| test "deletes a custom property with slash in name", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1/key/with/slashes" |
| }) |
|
|
| assert %{"created" => true} = json_response(conn, 200) |
|
|
| conn = |
| delete(conn, "/api/v1/sites/custom-props/prop1/key/with/slashes", %{ |
| site_id: site.domain |
| }) |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
|
|
| assert Repo.reload!(site).allowed_event_props == [] |
| end |
|
|
| test "deletes a custom prop using old site_id after domain change", %{ |
| conn: conn, |
| site: site |
| } do |
| old_domain = site.domain |
| new_domain = "new.example.com" |
|
|
| Plausible.Site.Domain.change(site, new_domain) |
|
|
| conn = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: new_domain, |
| property: "prop1" |
| }) |
|
|
| assert %{"created" => true} = json_response(conn, 200) |
|
|
| conn = |
| delete(conn, "/api/v1/sites/custom-props/prop1", %{ |
| site_id: old_domain |
| }) |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
| assert Repo.reload!(site).allowed_event_props == [] |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do |
| conn1 = |
| put(conn, "/api/v1/sites/custom-props", %{ |
| site_id: site.domain, |
| property: "prop1" |
| }) |
|
|
| assert %{"created" => true} = json_response(conn1, 200) |
|
|
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| delete(conn, "/api/v1/sites/custom-props/prop1", %{ |
| site_id: site.domain |
| }) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "handles non-existent custom prop gracefully", %{conn: conn, site: site} do |
| conn = |
| delete(conn, "/api/v1/sites/custom-props/none", %{ |
| site_id: site.domain |
| }) |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
| end |
|
|
| @tag :capture_log |
| test "cannot delete a custom prop belongs to a site that the user does not own", %{ |
| conn: conn, |
| user: user |
| } do |
| site = new_site() |
| add_guest(site, user: user, role: :viewer) |
|
|
| conn = |
| delete(conn, "/api/v1/sites/custom-props/prop1", %{ |
| site_id: site.domain |
| }) |
|
|
| assert %{"error" => error} = json_response(conn, 402) |
| assert error =~ "API key does not have access to Sites API" |
| end |
|
|
| test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do |
| {:ok, site} = Plausible.Props.allow(site, "prop1") |
| api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) |
|
|
| conn = |
| conn |
| |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| delete(conn, "/api/v1/sites/custom-props/prop1", %{ |
| site_id: site.domain |
| }) |
|
|
| assert json_response(conn, 401) == %{ |
| "error" => |
| "Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested." |
| } |
| end |
| end |
|
|
| describe "DELETE /api/v1/sites/goals/:goal_id" do |
| setup :create_site |
|
|
| setup %{user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [ |
| Plausible.Billing.Feature.SitesAPI |
| ] |
| ) |
|
|
| :ok |
| end |
|
|
| test "delete a goal by its id", %{conn: conn, site: site} do |
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| %{"id" => goal_id} = json_response(conn, 200) |
|
|
| conn = |
| delete(conn, "/api/v1/sites/goals/#{goal_id}", %{ |
| site_id: site.domain |
| }) |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
| end |
|
|
| test "delete a goal using old site_id after domain change", %{conn: conn, site: site} do |
| old_domain = site.domain |
| new_domain = "new.example.com" |
|
|
| Plausible.Site.Domain.change(site, new_domain) |
|
|
| conn = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: new_domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| %{"id" => goal_id} = json_response(conn, 200) |
|
|
| conn = |
| delete(conn, "/api/v1/sites/goals/#{goal_id}", %{ |
| site_id: old_domain |
| }) |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do |
| conn1 = |
| put(conn, "/api/v1/sites/goals", %{ |
| site_id: site.domain, |
| goal_type: "event", |
| event_name: "Signup" |
| }) |
|
|
| %{"id" => goal_id} = json_response(conn1, 200) |
|
|
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| delete(conn, "/api/v1/sites/goals/#{goal_id}", %{ |
| site_id: site.domain |
| }) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "is 404 when goal cannot be found", %{conn: conn, site: site} do |
| conn = |
| delete(conn, "/api/v1/sites/goals/0", %{ |
| site_id: site.domain |
| }) |
|
|
| assert json_response(conn, 404) == %{"error" => "Goal could not be found"} |
| end |
|
|
| @tag :capture_log |
| test "cannot delete a goal belongs to a site that the user does not own", %{ |
| conn: conn, |
| user: user |
| } do |
| site = new_site() |
| add_guest(site, user: user, role: :viewer) |
|
|
| conn = |
| delete(conn, "/api/v1/sites/goals/1", %{ |
| site_id: site.domain |
| }) |
|
|
| assert %{"error" => error} = json_response(conn, 402) |
| assert error =~ "API key does not have access to Sites API" |
| end |
|
|
| test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do |
| api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) |
|
|
| conn = |
| conn |
| |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| delete(conn, "/api/v1/sites/goals/1", %{ |
| site_id: site.domain |
| }) |
|
|
| assert json_response(conn, 401) == %{ |
| "error" => |
| "Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested." |
| } |
| end |
| end |
|
|
| describe "GET /api/v1/sites/guests" do |
| test "returns empty when there are no guests for site", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
| conn = get(conn, "/api/v1/sites/guests?site_id=#{site.domain}") |
|
|
| assert json_response(conn, 200) == %{ |
| "guests" => [], |
| "meta" => %{ |
| "before" => nil, |
| "after" => nil, |
| "limit" => 100 |
| } |
| } |
| end |
|
|
| test "returns guests when present", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| guest1 = add_guest(site, site: site, role: :editor) |
| guest2 = add_guest(site, site: site, role: :viewer) |
| guest3 = invite_guest(site, "third@example.com", inviter: user, role: :viewer) |
|
|
| conn = get(conn, "/api/v1/sites/guests?site_id=#{site.domain}") |
|
|
| assert json_response(conn, 200) == %{ |
| "guests" => [ |
| %{"email" => guest2.email, "status" => "accepted", "role" => "viewer"}, |
| %{"email" => guest1.email, "status" => "accepted", "role" => "editor"}, |
| %{ |
| "email" => guest3.team_invitation.email, |
| "status" => "invited", |
| "role" => "viewer" |
| } |
| ], |
| "meta" => %{ |
| "before" => nil, |
| "after" => nil, |
| "limit" => 100 |
| } |
| } |
| end |
|
|
| test "returns guests paginated", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| %{email: guest1_email} = add_guest(site, site: site, role: :editor) |
| %{email: guest2_email} = add_guest(site, site: site, role: :viewer) |
| invite_guest(site, "third@example.com", inviter: user, role: :viewer) |
|
|
| conn1 = get(conn, "/api/v1/sites/guests?site_id=#{site.domain}&limit=2") |
|
|
| assert %{ |
| "guests" => [ |
| %{"email" => ^guest2_email, "status" => "accepted", "role" => "viewer"}, |
| %{"email" => ^guest1_email, "status" => "accepted", "role" => "editor"} |
| ], |
| "meta" => %{ |
| "before" => nil, |
| "after" => after_cursor, |
| "limit" => 2 |
| } |
| } = json_response(conn1, 200) |
|
|
| conn2 = |
| get( |
| conn, |
| "/api/v1/sites/guests?site_id=#{site.domain}&limit=2&after=#{after_cursor}" |
| ) |
|
|
| assert %{ |
| "guests" => [ |
| %{ |
| "email" => "third@example.com", |
| "status" => "invited", |
| "role" => "viewer" |
| } |
| ], |
| "meta" => %{ |
| "before" => before_cursor, |
| "after" => nil, |
| "limit" => 2 |
| } |
| } = |
| json_response(conn2, 200) |
|
|
| assert is_binary(before_cursor) |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user} do |
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| site = new_site(owner: user) |
|
|
| _guest = add_guest(site, site: site, role: :editor) |
|
|
| conn = get(conn, "/api/v1/sites/guests?site_id=#{site.domain}") |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
| end |
|
|
| describe "PUT /api/v1/sites/guests" do |
| setup %{user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [ |
| Plausible.Billing.Feature.SitesAPI |
| ] |
| ) |
|
|
| :ok |
| end |
|
|
| test "creates new invitation", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| conn = |
| put(conn, "/api/v1/sites/guests?site_id=#{site.domain}", %{ |
| "role" => "viewer", |
| "email" => "test@example.com" |
| }) |
|
|
| assert json_response(conn, 200) == %{ |
| "status" => "invited", |
| "email" => "test@example.com", |
| "role" => "viewer" |
| } |
|
|
| assert_email_delivered_with( |
| to: [nil: "test@example.com"], |
| subject: ~r/You've been invited to #{site.domain}/ |
| ) |
| end |
|
|
| test "is idempotent", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| conn1 = |
| put(conn, "/api/v1/sites/guests?site_id=#{site.domain}", %{ |
| "role" => "viewer", |
| "email" => "test@example.com" |
| }) |
|
|
| assert_email_delivered_with(to: [nil: "test@example.com"]) |
| assert json_response(conn1, 200) |
|
|
| conn2 = |
| put(conn, "/api/v1/sites/guests?site_id=#{site.domain}", %{ |
| "role" => "editor", |
| "email" => "test@example.com" |
| }) |
|
|
| assert %{"role" => "viewer", "status" => "invited"} = json_response(conn2, 200) |
|
|
| assert %{memberships: [_], invitations: [%{role: :viewer}]} = |
| Plausible.Sites.list_people(site) |
|
|
| assert_no_emails_delivered() |
| end |
|
|
| test "is idempotent when membership already present", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
| guest = new_user(email: "guest@example.com") |
|
|
| add_guest(site, role: :viewer, user: guest) |
|
|
| conn = |
| put(conn, "/api/v1/sites/guests?site_id=#{site.domain}", %{ |
| "role" => "editor", |
| "email" => "guest@example.com" |
| }) |
|
|
| assert %{"role" => "viewer", "status" => "accepted"} = json_response(conn, 200) |
|
|
| assert %{ |
| memberships: [%{user: _}, %{user: %{email: "guest@example.com"}}], |
| invitations: [] |
| } = |
| Plausible.Sites.list_people(site) |
|
|
| assert_no_emails_delivered() |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = |
| put(conn, "/api/v1/sites/guests?site_id=#{site.domain}", %{ |
| "role" => "viewer", |
| "email" => "test@example.com" |
| }) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "fails for unknown role", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| conn = |
| put(conn, "/api/v1/sites/guests?site_id=#{site.domain}", %{ |
| "role" => "owner", |
| "email" => "test@example.com" |
| }) |
|
|
| assert %{"error" => error} = json_response(conn, 400) |
|
|
| assert error =~ |
| "Parameter `role` is required to create guest. Possible values: `viewer` or `editor`" |
|
|
| assert_no_emails_delivered() |
| end |
| end |
|
|
| describe "DELETE /api/v1/sites/guests" do |
| setup %{user: user} do |
| subscribe_to_enterprise_plan(user, |
| features: [Plausible.Billing.Feature.SitesAPI] |
| ) |
|
|
| :ok |
| end |
|
|
| test "no-op when nothing to delete", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| conn = delete(conn, "/api/v1/sites/guests/test@example.com?site_id=#{site.domain}") |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
| end |
|
|
| test "deletes invitation", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| invite_guest(site, "invite@example.com", inviter: user, role: :viewer) |
|
|
| assert %{invitations: [_]} = Plausible.Sites.list_people(site) |
|
|
| conn = delete(conn, "/api/v1/sites/guests/invite@example.com?site_id=#{site.domain}") |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
|
|
| assert %{invitations: []} = Plausible.Sites.list_people(site) |
| end |
|
|
| test "deletes guest membership", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| guest = new_user(email: "guest@example.com") |
| add_guest(site, role: :viewer, user: guest) |
|
|
| assert %{memberships: [_, _]} = Plausible.Sites.list_people(site) |
|
|
| conn = delete(conn, "/api/v1/sites/guests/#{guest.email}?site_id=#{site.domain}") |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
|
|
| assert %{memberships: [_]} = Plausible.Sites.list_people(site) |
| end |
|
|
| test "is idempotent", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| invite_guest(site, "third@example.com", inviter: user, role: :viewer) |
|
|
| assert %{invitations: [_]} = Plausible.Sites.list_people(site) |
|
|
| conn1 = delete(conn, "/api/v1/sites/guests/third@example.com?site_id=#{site.domain}") |
| assert json_response(conn1, 200) == %{"deleted" => true} |
|
|
| conn2 = delete(conn, "/api/v1/sites/guests/third@example.com?site_id=#{site.domain}") |
| assert json_response(conn2, 200) == %{"deleted" => true} |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| conn = delete(conn, "/api/v1/sites/guests/test@example.com?site_id=#{site.domain}") |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "won't delete non-guest membership", %{conn: conn, user: user} do |
| site = new_site(owner: user) |
|
|
| assert %{memberships: [_]} = Plausible.Sites.list_people(site) |
|
|
| conn = delete(conn, "/api/v1/sites/guests/#{user.email}?site_id=#{site.domain}") |
|
|
| assert json_response(conn, 200) == %{"deleted" => true} |
|
|
| assert %{memberships: [_]} = Plausible.Sites.list_people(site) |
| end |
| end |
|
|
| describe "GET /api/v1/custom-props" do |
| setup :create_site |
|
|
| test "returns empty when there are no custom props for site", %{conn: conn, site: site} do |
| conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain) |
|
|
| assert json_response(conn, 200) == %{"custom_properties" => []} |
| end |
|
|
| test "returns custom props when present", %{conn: conn, site: site} do |
| {:ok, site} = Plausible.Props.allow(site, "prop1") |
| {:ok, site} = Plausible.Props.allow(site, "prop2") |
| Plausible.Props.allow(site, "prop3") |
|
|
| conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain) |
|
|
| assert json_response(conn, 200) == %{ |
| "custom_properties" => [ |
| %{"property" => "prop1"}, |
| %{"property" => "prop2"}, |
| %{"property" => "prop3"} |
| ] |
| } |
| end |
|
|
| @tag :capture_log |
| test "returns custom props for site where user is viewer", %{site: site} do |
| viewer = new_user() |
| add_guest(site, user: viewer, role: :viewer) |
|
|
| {:ok, site} = Plausible.Props.allow(site, "prop1") |
|
|
| api_key = insert(:api_key, user: viewer, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(build_conn(), "authorization", "Bearer #{api_key.key}") |
|
|
| conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain) |
|
|
| assert %{"custom_properties" => [%{"property" => "prop1"}]} = json_response(conn, 200) |
| end |
|
|
| test "lists custom props for user with read-only scope", %{ |
| conn: conn, |
| user: user, |
| site: site |
| } do |
| {:ok, site} = Plausible.Props.allow(site, "prop1") |
| api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) |
|
|
| conn = |
| conn |
| |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") |
| |> get("/api/v1/sites/custom-props?site_id=" <> site.domain) |
|
|
| assert %{"custom_properties" => [%{"property" => "prop1"}]} = json_response(conn, 200) |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do |
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| _goal = insert(:goal, %{site: site, page_path: "/login"}) |
|
|
| conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "returns error when `site_id` parameter is missing", %{conn: conn} do |
| conn = get(conn, "/api/v1/sites/custom-props") |
|
|
| assert json_response(conn, 400) == %{ |
| "error" => |
| "Missing site ID. Please provide the required site_id parameter with your request." |
| } |
| end |
|
|
| test "returns error when `site_id` parameter is invalid", %{conn: conn} do |
| conn = get(conn, "/api/v1/sites/custom-props?site_id=does.not.exist") |
|
|
| assert json_response(conn, 404) == %{ |
| "error" => "Site could not be found" |
| } |
| end |
|
|
| @tag :capture_log |
| test "returns error when user is not a member of the site", %{conn: conn} do |
| site = new_site() |
|
|
| conn = get(conn, "/api/v1/sites/custom-props?site_id=" <> site.domain) |
|
|
| assert %{"error" => error} = json_response(conn, 401) |
| assert(error =~ "Invalid API key") |
| end |
| end |
|
|
| describe "GET /api/v1/goals" do |
| setup :create_site |
|
|
| test "returns empty when there are no goals for site", %{conn: conn, site: site} do |
| conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) |
|
|
| assert json_response(conn, 200) == %{ |
| "goals" => [], |
| "meta" => %{ |
| "before" => nil, |
| "after" => nil, |
| "limit" => 100 |
| } |
| } |
| end |
|
|
| test "returns goals when present", %{conn: conn, site: site} do |
| goal1 = insert(:goal, %{site: site, page_path: "/login"}) |
| goal2 = insert(:goal, %{site: site, event_name: "Signup"}) |
| goal3 = insert(:goal, %{site: site, event_name: "Purchase", currency: "USD"}) |
|
|
| conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) |
|
|
| assert json_response(conn, 200) == %{ |
| "goals" => [ |
| %{ |
| "id" => goal3.id, |
| "display_name" => "Purchase", |
| "goal_type" => "event", |
| "event_name" => "Purchase", |
| "page_path" => nil, |
| "custom_props" => %{} |
| }, |
| %{ |
| "id" => goal2.id, |
| "display_name" => "Signup", |
| "goal_type" => "event", |
| "event_name" => "Signup", |
| "page_path" => nil, |
| "custom_props" => %{} |
| }, |
| %{ |
| "id" => goal1.id, |
| "display_name" => "Visit /login", |
| "goal_type" => "page", |
| "event_name" => nil, |
| "page_path" => "/login", |
| "custom_props" => %{} |
| } |
| ], |
| "meta" => %{ |
| "before" => nil, |
| "after" => nil, |
| "limit" => 100 |
| } |
| } |
| end |
|
|
| test "returns goals with custom_props", %{conn: conn, site: site} do |
| goal = |
| insert(:goal, %{ |
| site: site, |
| event_name: "Purchase", |
| custom_props: %{"product" => "enterprise", "tier" => "premium"} |
| }) |
|
|
| conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) |
|
|
| goal_id = goal.id |
|
|
| assert %{ |
| "goals" => [ |
| %{ |
| "id" => ^goal_id, |
| "custom_props" => %{"product" => "enterprise", "tier" => "premium"} |
| } |
| ] |
| } = json_response(conn, 200) |
| end |
|
|
| @tag :capture_log |
| test "returns goals for site where user is viewer", %{site: site} do |
| viewer = new_user() |
| add_guest(site, user: viewer, role: :viewer) |
|
|
| %{id: goal_id} = insert(:goal, %{site: site, event_name: "Signup"}) |
|
|
| api_key = insert(:api_key, user: viewer, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(build_conn(), "authorization", "Bearer #{api_key.key}") |
|
|
| conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) |
|
|
| assert %{"goals" => [%{"id" => ^goal_id}]} = json_response(conn, 200) |
| end |
|
|
| test "handles pagination correctly", %{conn: conn, site: site} do |
| %{id: goal1_id} = insert(:goal, %{site: site, page_path: "/login"}) |
| %{id: goal2_id} = insert(:goal, %{site: site, event_name: "Signup"}) |
| %{id: goal3_id} = insert(:goal, %{site: site, event_name: "Purchase", currency: "USD"}) |
|
|
| conn1 = get(conn, "/api/v1/sites/goals?limit=2&site_id=" <> site.domain) |
|
|
| assert %{ |
| "goals" => [ |
| %{"id" => ^goal3_id}, |
| %{"id" => ^goal2_id} |
| ], |
| "meta" => %{ |
| "before" => nil, |
| "after" => after_cursor, |
| "limit" => 2 |
| } |
| } = json_response(conn1, 200) |
|
|
| conn2 = |
| get(conn, "/api/v1/sites/goals?limit=2&after=#{after_cursor}&site_id=" <> site.domain) |
|
|
| assert %{ |
| "goals" => [ |
| %{"id" => ^goal1_id} |
| ], |
| "meta" => %{ |
| "before" => before_cursor, |
| "after" => nil, |
| "limit" => 2 |
| } |
| } = json_response(conn2, 200) |
|
|
| assert is_binary(before_cursor) |
| end |
|
|
| test "lists goals for user with read-only scope", %{conn: conn, user: user, site: site} do |
| %{id: goal_id} = insert(:goal, %{site: site, page_path: "/login"}) |
| api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) |
|
|
| conn = |
| conn |
| |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") |
| |> get("/api/v1/sites/goals?site_id=" <> site.domain) |
|
|
| assert %{"goals" => [%{"id" => ^goal_id}]} = json_response(conn, 200) |
| end |
|
|
| test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do |
| another_team = new_user() |> subscribe_to_business_plan() |> team_of() |
| add_member(another_team, user: user, role: :admin) |
| api_key = insert(:api_key, user: user, team: another_team, scopes: ["sites:provision:*"]) |
| conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") |
|
|
| _goal = insert(:goal, %{site: site, page_path: "/login"}) |
|
|
| conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) |
|
|
| res = json_response(conn, 401) |
| assert res["error"] =~ "Invalid API key" |
| end |
|
|
| test "returns error when `site_id` parameter is missing", %{conn: conn} do |
| conn = get(conn, "/api/v1/sites/goals") |
|
|
| assert json_response(conn, 400) == %{ |
| "error" => |
| "Missing site ID. Please provide the required site_id parameter with your request." |
| } |
| end |
|
|
| test "returns error when `site_id` parameter is invalid", %{conn: conn} do |
| conn = get(conn, "/api/v1/sites/goals?site_id=does.not.exist") |
|
|
| assert json_response(conn, 404) == %{ |
| "error" => "Site could not be found" |
| } |
| end |
|
|
| @tag :capture_log |
| test "returns error when user is not a member of the site", %{conn: conn} do |
| site = new_site() |
|
|
| conn = get(conn, "/api/v1/sites/goals?site_id=" <> site.domain) |
|
|
| assert %{"error" => error} = json_response(conn, 401) |
| assert error =~ "Invalid API key" |
| end |
| end |
| end |
| end |
|
|