| defmodule PlausibleWeb.Plugins.API.Controllers.FunnelsTest do |
| use PlausibleWeb.PluginsAPICase, async: true |
|
|
| @moduletag :ee_only |
|
|
| on_ee do |
| alias PlausibleWeb.Plugins.API.Schemas |
|
|
| describe "examples" do |
| test "Funnel" do |
| assert_schema( |
| Schemas.Funnel.schema().example, |
| "Funnel", |
| spec() |
| ) |
| end |
|
|
| test "Funnel.CreateRequest" do |
| assert_schema( |
| Schemas.Funnel.CreateRequest.schema().example, |
| "Funnel.CreateRequest", |
| spec() |
| ) |
| end |
| end |
|
|
| describe "unauthorized calls" do |
| for {method, url} <- [ |
| {:get, Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :index)}, |
| {:get, Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :get, 1)}, |
| {:put, Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create, %{})} |
| ] do |
| test "unauthorized call: #{method} #{url}", %{conn: conn} do |
| conn |
| |> unquote(method)(unquote(url)) |
| |> json_response(401) |
| |> assert_schema("UnauthorizedError", spec()) |
| end |
| end |
| end |
|
|
| describe "get /funnels/:id" do |
| test "validates input out of the box", %{conn: conn, token: token, site: site} do |
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :get, "hello") |
|
|
| resp = |
| conn |
| |> authenticate(site.domain, token) |
| |> get(url) |
| |> json_response(422) |
| |> assert_schema("UnprocessableEntityError", spec()) |
|
|
| assert %{errors: [%{detail: "Invalid integer. Got: string"}]} = resp |
| end |
|
|
| test "retrieves no funnel on non-existing ID", %{conn: conn, token: token, site: site} do |
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :get, 9999) |
|
|
| resp = |
| conn |
| |> authenticate(site.domain, token) |
| |> get(url) |
| |> json_response(404) |
| |> assert_schema("NotFoundError", spec()) |
|
|
| assert %{errors: [%{detail: "Plugins API: resource not found"}]} = resp |
| end |
|
|
| test "retrieves funnel by ID", %{conn: conn, site: site, token: token} do |
| {:ok, g1} = Plausible.Goals.create(site, %{"page_path" => "/product/123"}) |
|
|
| {:ok, g2} = |
| Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"}) |
|
|
| {:ok, g3} = Plausible.Goals.create(site, %{"event_name" => "FiveStarReview"}) |
|
|
| {:ok, funnel} = |
| Plausible.Funnels.create( |
| site, |
| "Peek & buy", |
| [g1, g2, g3] |
| ) |
|
|
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :get, funnel.id) |
|
|
| resp = |
| conn |
| |> authenticate(site.domain, token) |
| |> get(url) |
| |> json_response(200) |
|
|
| schema = assert_schema(resp, "Funnel", spec()) |
|
|
| assert schema.funnel.id == funnel.id |
| assert schema.funnel.name == "Peek & buy" |
| [_, _, _] = schema.funnel.steps |
| [s1, s2, s3] = resp |> Map.fetch!("funnel") |> Map.fetch!("steps") |
|
|
| assert_schema(s1, "Goal.Pageview", spec()) |
| assert_schema(s2, "Goal.Revenue", spec()) |
| assert_schema(s3, "Goal.CustomEvent", spec()) |
| end |
| end |
|
|
| describe "get /funnels" do |
| test "returns an empty funnels list if there's none", %{ |
| conn: conn, |
| token: token, |
| site: site |
| } do |
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :index) |
|
|
| resp = |
| conn |
| |> authenticate(site.domain, token) |
| |> get(url) |
| |> json_response(200) |
| |> assert_schema("Funnel.ListResponse", spec()) |
|
|
| assert resp.funnels == [] |
| assert resp.meta.pagination.has_next_page == false |
| assert resp.meta.pagination.has_prev_page == false |
| assert resp.meta.pagination.links == %{} |
| end |
|
|
| test "retrieves all funnels", %{conn: conn, site: site, token: token} do |
| {:ok, g1} = Plausible.Goals.create(site, %{"page_path" => "/product/123"}) |
|
|
| {:ok, g2} = |
| Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"}) |
|
|
| {:ok, g3} = Plausible.Goals.create(site, %{"event_name" => "FiveStarReview"}) |
|
|
| for i <- 1..3 do |
| {:ok, _} = |
| Plausible.Funnels.create( |
| site, |
| "Funnel #{i}", |
| [g1, g2, g3] |
| ) |
| end |
|
|
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :index) |
|
|
| resp = |
| conn |
| |> authenticate(site.domain, token) |
| |> get(url) |
| |> json_response(200) |
| |> assert_schema("Funnel.ListResponse", spec()) |
|
|
| assert Enum.count(resp.funnels) == 3 |
| end |
|
|
| test "retrieves funnels with pagination", %{conn: conn, site: site, token: token} do |
| {:ok, g1} = Plausible.Goals.create(site, %{"page_path" => "/product/123"}) |
|
|
| {:ok, g2} = |
| Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"}) |
|
|
| {:ok, g3} = Plausible.Goals.create(site, %{"event_name" => "FiveStarReview"}) |
|
|
| initial_order = Enum.shuffle([g1, g2, g3]) |
|
|
| for i <- 1..3 do |
| {:ok, _} = |
| Plausible.Funnels.create( |
| site, |
| "Funnel #{i}", |
| initial_order |
| ) |
| end |
|
|
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :index, limit: 2) |
| initial_conn = authenticate(conn, site.domain, token) |
|
|
| page1 = |
| initial_conn |
| |> get(url) |
| |> json_response(200) |
| |> assert_schema("Funnel.ListResponse", spec()) |
|
|
| assert Enum.count(page1.funnels) == 2 |
| assert page1.meta.pagination.has_next_page == true |
| assert page1.meta.pagination.has_prev_page == false |
|
|
| assert [%{funnel: %{steps: steps}}, %{funnel: %{steps: steps}}] = page1.funnels |
| assert Enum.map(steps, & &1.goal.id) == Enum.map(initial_order, & &1.id) |
|
|
| page2 = |
| initial_conn |
| |> get(page1.meta.pagination.links.next.url) |
| |> json_response(200) |
| |> assert_schema("Funnel.ListResponse", spec()) |
|
|
| assert Enum.count(page2.funnels) == 1 |
| assert page2.meta.pagination.has_next_page == false |
| assert page2.meta.pagination.has_prev_page == true |
| end |
| end |
|
|
| describe "put /funnels - funnel creation" do |
| test "creates a funnel including its goals", %{conn: conn, token: token, site: site} do |
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create) |
|
|
| payload = %{ |
| funnel: %{ |
| name: "My Test Funnel", |
| steps: [ |
| %{ |
| goal_type: "Goal.CustomEvent", |
| goal: %{event_name: "Signup"} |
| }, |
| %{ |
| goal_type: "Goal.Pageview", |
| goal: %{path: "/checkout"} |
| }, |
| %{ |
| goal_type: "Goal.Revenue", |
| goal: %{event_name: "Purchase", currency: "EUR"} |
| } |
| ] |
| } |
| } |
|
|
| assert_request_schema(payload, "Funnel.CreateRequest", spec()) |
|
|
| conn = |
| conn |
| |> authenticate(site.domain, token) |
| |> put_req_header("content-type", "application/json") |
| |> put(url, payload) |
|
|
| resp = |
| conn |
| |> json_response(201) |
| |> assert_schema("Funnel", spec()) |
|
|
| [location] = get_resp_header(conn, "location") |
|
|
| assert location == |
| Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :get, resp.funnel.id) |
|
|
| funnel = Plausible.Funnels.get(site, resp.funnel.id) |
|
|
| assert funnel.name == resp.funnel.name |
| assert funnel.site_id == site.id |
| assert Enum.count(funnel.steps) == 3 |
| end |
|
|
| test "fails for insufficient plan", %{conn: conn, token: token, site: site} do |
| [owner | _] = Plausible.Repo.preload(site, :owners).owners |
| subscribe_to_growth_plan(owner) |
|
|
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create) |
|
|
| payload = %{ |
| funnel: %{ |
| name: "My Test Funnel", |
| steps: [ |
| %{ |
| goal_type: "Goal.CustomEvent", |
| goal: %{event_name: "Signup"} |
| }, |
| %{ |
| goal_type: "Goal.Pageview", |
| goal: %{path: "/checkout"} |
| } |
| ] |
| } |
| } |
|
|
| assert_request_schema(payload, "Funnel.CreateRequest", spec()) |
|
|
| conn |
| |> authenticate(site.domain, token) |
| |> put_req_header("content-type", "application/json") |
| |> put(url, payload) |
| |> json_response(402) |
| |> assert_schema("PaymentRequiredError", spec()) |
| end |
|
|
| test "fails with only one step - guarded by the schema", %{ |
| conn: conn, |
| token: token, |
| site: site |
| } do |
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create) |
|
|
| payload = %{ |
| funnel: %{ |
| name: "My Test Funnel", |
| steps: [ |
| %{ |
| goal_type: "Goal.CustomEvent", |
| goal: %{event_name: "Signup"} |
| } |
| ] |
| } |
| } |
|
|
| resp = |
| conn |
| |> authenticate(site.domain, token) |
| |> put_req_header("content-type", "application/json") |
| |> put(url, payload) |
| |> json_response(422) |
| |> assert_schema("UnprocessableEntityError", spec()) |
|
|
| assert %{errors: [%{detail: "Array length 1 is smaller than minItems: 2"}]} = resp |
| end |
|
|
| test "is idempotent on full creation", %{conn: conn, token: token, site: site} do |
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create) |
|
|
| {:ok, _g1} = |
| Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"}) |
|
|
| payload = %{ |
| funnel: %{ |
| name: "My Test Funnel", |
| steps: [ |
| %{ |
| goal_type: "Goal.CustomEvent", |
| goal: %{event_name: "Signup"} |
| }, |
| %{ |
| goal_type: "Goal.Pageview", |
| goal: %{path: "/checkout"} |
| }, |
| %{ |
| goal_type: "Goal.Revenue", |
| goal: %{event_name: "Purchase", currency: "EUR"} |
| } |
| ] |
| } |
| } |
|
|
| assert_request_schema(payload, "Funnel.CreateRequest", spec()) |
|
|
| initial_conn = |
| conn |
| |> authenticate(site.domain, token) |
| |> put_req_header("content-type", "application/json") |
|
|
| resp1 = |
| initial_conn |
| |> put(url, payload) |
| |> json_response(201) |
| |> assert_schema("Funnel", spec()) |
|
|
| resp2 = |
| initial_conn |
| |> put(url, payload) |
| |> json_response(201) |
| |> assert_schema("Funnel", spec()) |
|
|
| assert resp1.funnel == resp2.funnel |
| end |
|
|
| test "edge case - different currency goal already exists", %{ |
| conn: conn, |
| token: token, |
| site: site |
| } do |
| url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create) |
|
|
| {:ok, _g1} = |
| Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "USD"}) |
|
|
| payload = %{ |
| funnel: %{ |
| name: "My Test Funnel", |
| steps: [ |
| %{ |
| goal_type: "Goal.CustomEvent", |
| goal: %{event_name: "Signup"} |
| }, |
| %{ |
| goal_type: "Goal.Pageview", |
| goal: %{path: "/checkout"} |
| }, |
| %{ |
| goal_type: "Goal.Revenue", |
| goal: %{event_name: "Purchase", currency: "EUR"} |
| } |
| ] |
| } |
| } |
|
|
| assert_request_schema(payload, "Funnel.CreateRequest", spec()) |
|
|
| resp = |
| conn |
| |> authenticate(site.domain, token) |
| |> put_req_header("content-type", "application/json") |
| |> put(url, payload) |
| |> json_response(422) |
| |> assert_schema("UnprocessableEntityError", spec()) |
|
|
| assert [%{detail: "event_name: 'Purchase' (with currency: USD) has already been taken"}] = |
| resp.errors |
| end |
| end |
| end |
| end |
|
|