analytics/test/plausible_web/plugins/api/controllers/funnels_test.exs

407 lines
12 KiB
Elixir

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