analytics/test/plausible_web/plugins/api/controllers/goals_test.exs
hq1 6a2d7fc0f5
Merge Plugins.API.Router into main one (#3767)
* Merge `Plugins.API.Router` into main one

In order to get grafana metrics reported
See: https://github.com/akoutmos/prom_ex/issues/224

* Format
2024-02-12 10:44:32 +01:00

695 lines
21 KiB
Elixir

defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
use PlausibleWeb.PluginsAPICase, async: true
alias PlausibleWeb.Plugins.API.Schemas
describe "examples" do
test "Goal.CreateRequest.Revenue" do
assert_schema(
Schemas.Goal.CreateRequest.Revenue.schema().example,
"Goal.CreateRequest.Revenue",
spec()
)
end
test "Goal.CreateRequest.Pageview" do
assert_schema(
Schemas.Goal.CreateRequest.Pageview.schema().example,
"Goal.CreateRequest.Pageview",
spec()
)
end
test "Goal.CreateRequest.CustomEvent" do
assert_schema(
Schemas.Goal.CreateRequest.CustomEvent.schema().example,
"Goal.CreateRequest.CustomEvent",
spec()
)
end
end
describe "unauthorized calls" do
for {method, url} <- [
{:get, Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :index)},
{:get, Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :get, 1)},
{:put, Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create, %{})},
{:delete, Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :delete, 1)},
{:delete, Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :delete_bulk, %{})}
] 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 "business tier" do
@tag :full_build_only
test "fails on revenue goal creation attempt with insufficient plan", %{
site: site,
token: token,
conn: conn
} do
site = Plausible.Repo.preload(site, :owner)
insert(:growth_subscription, user: site.owner)
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload = %{
goal_type: "Goal.Revenue",
goal: %{event_name: "Purchase", currency: "EUR"}
}
assert_request_schema(payload, "Goal.CreateRequest.Revenue", spec())
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
|> json_response(402)
|> assert_schema("PaymentRequiredError", spec())
end
@tag :full_build_only
test "fails on bulk revenue goal creation attempt with insufficient plan", %{
site: site,
token: token,
conn: conn
} do
site = Plausible.Repo.preload(site, :owner)
insert(:growth_subscription, user: site.owner)
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload = %{
goals: [
%{
goal_type: "Goal.CustomEvent",
goal: %{event_name: "Signup"}
},
%{
goal_type: "Goal.Revenue",
goal: %{event_name: "Purchase", currency: "EUR"}
},
%{
goal_type: "Goal.Pageview",
goal: %{path: "/checkout"}
}
]
}
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
|> json_response(402)
|> assert_schema("PaymentRequiredError", spec())
end
end
describe "put /goals - create a single goal" do
test "validates input according to the schema", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, %{goal_type: "Goal.SomeTypo", goal: %{event_name: "Signup"}})
|> json_response(422)
|> assert_schema("UnprocessableEntityError", spec())
end
test "creates a custom event goal", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload = %{goal_type: "Goal.CustomEvent", goal: %{event_name: "Signup"}}
assert_request_schema(payload, "Goal.CreateRequest.CustomEvent", spec())
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
resp =
conn
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
resp.goals
|> List.first()
|> assert_schema("Goal.CustomEvent", spec())
[location] = get_resp_header(conn, "location")
assert location ==
Routes.plugins_api_goals_url(
PlausibleWeb.Endpoint,
:get,
List.first(resp.goals).goal.id
)
assert [%{event_name: "Signup"}] = Plausible.Goals.for_site(site)
end
@tag :full_build_only
test "creates a revenue goal", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload = %{
goal_type: "Goal.Revenue",
goal: %{event_name: "Purchase", currency: "EUR"}
}
assert_request_schema(payload, "Goal.CreateRequest.Revenue", spec())
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
resp =
conn
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
resp.goals
|> List.first()
|> assert_schema("Goal.Revenue", spec())
[location] = get_resp_header(conn, "location")
assert location ==
Routes.plugins_api_goals_url(
PlausibleWeb.Endpoint,
:get,
List.first(resp.goals).goal.id
)
assert [%{event_name: "Purchase", currency: :EUR}] = Plausible.Goals.for_site(site)
end
@tag :full_build_only
test "fails to create a revenue goal with unknown currency", %{
conn: conn,
token: token,
site: site
} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload = %{
goal_type: "Goal.Revenue",
goal: %{event_name: "Purchase", currency: "DFJKHJESFHYU"}
}
assert_request_schema(payload, "Goal.CreateRequest.Revenue", spec())
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
resp =
conn
|> json_response(422)
|> assert_schema("UnprocessableEntityError", spec())
assert [%{detail: "currency: is invalid"}] = resp.errors
end
@tag :full_build_only
test "edge case - revenue goal exists under the same name and different currency", %{
conn: conn,
token: token,
site: site
} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
{:ok, _} = Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "USD"})
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, %{goal_type: "Goal.Revenue", goal: %{event_name: "Purchase", currency: "EUR"}})
resp =
conn
|> json_response(422)
|> assert_schema("UnprocessableEntityError", spec())
assert [%{detail: "event_name: 'Purchase' (with currency: USD) has already been taken"}] =
resp.errors
end
test "creates a pageview goal", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload = %{goal_type: "Goal.Pageview", goal: %{path: "/checkout"}}
assert_request_schema(payload, "Goal.CreateRequest.Pageview", spec())
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
resp =
conn
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
resp.goals
|> List.first()
|> assert_schema("Goal.Pageview", spec())
[location] = get_resp_header(conn, "location")
assert location ==
Routes.plugins_api_goals_url(
PlausibleWeb.Endpoint,
:get,
List.first(resp.goals).goal.id
)
assert [%{page_path: "/checkout"}] = Plausible.Goals.for_site(site)
end
test "is idempotent", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
initial_conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
resp1 =
initial_conn
|> put(url, %{goal_type: "Goal.Pageview", goal: %{path: "/checkout"}})
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
resp1.goals
|> List.first()
|> assert_schema("Goal.Pageview", spec())
assert initial_conn
|> put(url, %{goal_type: "Goal.Pageview", goal: %{path: "/checkout"}})
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec()) == resp1
end
end
describe "put /goals - bulk creation" do
@tag :full_build_only
test "creates a goal of each type", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload = %{
goals: [
%{
goal_type: "Goal.CustomEvent",
goal: %{event_name: "Signup"}
},
%{
goal_type: "Goal.Revenue",
goal: %{event_name: "Purchase", currency: "EUR"}
},
%{
goal_type: "Goal.Pageview",
goal: %{path: "/checkout"}
}
]
}
assert_request_schema(payload, "Goal.CreateRequest.BulkGetOrCreate", spec())
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
resp =
conn
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
[l1, l2, l3] = get_resp_header(conn, "location")
assert l1 ==
Routes.plugins_api_goals_url(
PlausibleWeb.Endpoint,
:get,
Enum.at(resp.goals, 0).goal.id
)
assert l2 ==
Routes.plugins_api_goals_url(
PlausibleWeb.Endpoint,
:get,
Enum.at(resp.goals, 1).goal.id
)
assert l3 ==
Routes.plugins_api_goals_url(
PlausibleWeb.Endpoint,
:get,
Enum.at(resp.goals, 2).goal.id
)
assert Enum.count(resp.goals) == 3
assert [
%{page_path: "/checkout"},
%{event_name: "Purchase", currency: :EUR},
%{event_name: "Signup"}
] = Plausible.Goals.for_site(site)
end
test "no more than 8 goals can be created in bulk", %{conn: conn, token: token, site: site} do
# if this test fails due to implementation change, consider what to do with the pagination meta
# object returned in the response and also revise how funnels are created based on a list of goals
# - the funnels creation endpoint will likely reuse this schema's constraints
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
payload =
Enum.map(1..9, fn i ->
%{goal_type: "Goal.CustomEvent", goal: %{event_name: "Bulk Goal ##{i}"}}
end)
resp =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, %{
goals: payload
})
|> json_response(422)
|> assert_schema("UnprocessableEntityError", spec())
assert %Schemas.Error{
detail: "Array length 9 is larger than maxItems: 8"
} in resp.errors
end
@tag :full_build_only
test "is idempotent", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
initial_conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
payload = [
%{
goal_type: "Goal.CustomEvent",
goal: %{event_name: "Signup"}
},
%{
goal_type: "Goal.Revenue",
goal: %{event_name: "Purchase", currency: "EUR"}
},
%{
goal_type: "Goal.Pageview",
goal: %{path: "/checkout"}
}
]
initial_conn
|> put(url, %{goals: payload})
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
initial_conn
|> put(url, %{goals: payload})
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
end
@tag :full_build_only
test "edge case - revenue goals exist under the same name and different currency", %{
conn: conn,
token: token,
site: site
} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create)
initial_conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
payload1 = [%{goal_type: "Goal.Revenue", goal: %{event_name: "Purchase", currency: "EUR"}}]
payload2 = [%{goal_type: "Goal.Revenue", goal: %{event_name: "Purchase", currency: "USD"}}]
initial_conn
|> put(url, %{goals: payload1})
|> json_response(201)
|> assert_schema("Goal.ListResponse", spec())
resp =
initial_conn
|> put(url, %{goals: payload2})
|> json_response(422)
|> assert_schema("UnprocessableEntityError", spec())
assert [%{detail: "event_name: 'Purchase' (with currency: EUR) has already been taken"}] =
resp.errors
end
end
describe "get /goals/:id" do
test "validates input out of the box", %{conn: conn, token: token, site: site} do
url = Routes.plugins_api_goals_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
@tag :full_build_only
test "retrieves revenue goal by ID", %{conn: conn, site: site, token: token} do
{:ok, goal} =
Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :get, goal.id)
resp =
conn
|> authenticate(site.domain, token)
|> get(url)
|> json_response(200)
|> assert_schema("Goal", spec())
|> assert_schema("Goal.Revenue", spec())
assert resp.goal.id == goal.id
assert resp.goal_type == "Goal.Revenue"
assert resp.goal.display_name == "Purchase (EUR)"
end
test "retrieves pageview goal by ID", %{conn: conn, site: site, token: token} do
{:ok, goal} = Plausible.Goals.create(site, %{"page_path" => "/checkout"})
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :get, goal.id)
resp =
conn
|> authenticate(site.domain, token)
|> get(url)
|> json_response(200)
|> assert_schema("Goal", spec())
|> assert_schema("Goal.Pageview", spec())
assert resp.goal.id == goal.id
assert resp.goal_type == "Goal.Pageview"
assert resp.goal.display_name == "Visit /checkout"
end
test "retrieves custom event goal by ID", %{conn: conn, site: site, token: token} do
{:ok, goal} = Plausible.Goals.create(site, %{"event_name" => "Signup"})
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :get, goal.id)
resp =
conn
|> authenticate(site.domain, token)
|> get(url)
|> json_response(200)
|> assert_schema("Goal", spec())
|> assert_schema("Goal.CustomEvent", spec())
assert resp.goal.id == goal.id
assert resp.goal_type == "Goal.CustomEvent"
assert resp.goal.display_name == "Signup"
end
end
describe "get /goals" do
test "returns an empty goals list if there's none", %{
conn: conn,
token: token,
site: site
} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :index)
resp =
conn
|> authenticate(site.domain, token)
|> get(url)
|> json_response(200)
|> assert_schema("Goal.ListResponse", spec())
assert resp.goals == []
assert resp.meta.pagination.has_next_page == false
assert resp.meta.pagination.has_prev_page == false
assert resp.meta.pagination.links == %{}
end
@tag :full_build_only
test "returns a list of goals of each possible goal type", %{
conn: conn,
site: site,
token: token
} do
{:ok, g1} = Plausible.Goals.create(site, %{"event_name" => "Signup"})
{:ok, g2} = Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "EUR"})
{:ok, g3} = Plausible.Goals.create(site, %{"page_path" => "/checkout"})
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :index)
resp =
conn
|> authenticate(site.domain, token)
|> get(url)
|> json_response(200)
|> assert_schema("Goal.ListResponse", spec())
assert [checkout, purchase, signup] = resp.goals
assert checkout.goal.display_name == "Visit /checkout"
assert checkout.goal.path == "/checkout"
assert checkout.goal.id == g3.id
assert checkout.goal_type == "Goal.Pageview"
assert purchase.goal.display_name == "Purchase (EUR)"
assert purchase.goal.currency == "EUR"
assert purchase.goal.event_name == "Purchase"
assert purchase.goal.id == g2.id
assert purchase.goal_type == "Goal.Revenue"
assert signup.goal.display_name == "Signup"
assert signup.goal.event_name == "Signup"
assert signup.goal.id == g1.id
assert signup.goal_type == "Goal.CustomEvent"
end
test "returns a list of goals with pagination", %{conn: conn, site: site, token: token} do
for i <- 1..5 do
insert(:goal, site: site, event_name: "Goal #{i}")
end
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :index, limit: 2)
initial_conn = authenticate(conn, site.domain, token)
page1 =
initial_conn
|> get(url)
|> json_response(200)
|> assert_schema("Goal.ListResponse", spec())
assert [%{goal: %{event_name: "Goal 5"}}, %{goal: %{event_name: "Goal 4"}}] = page1.goals
assert page1.meta.pagination.has_next_page == true
assert page1.meta.pagination.has_prev_page == false
assert page1.meta.pagination.links.next
refute page1.meta.pagination.links[:prev]
page2 =
initial_conn
|> get(page1.meta.pagination.links.next.url)
|> json_response(200)
|> assert_schema("Goal.ListResponse", spec())
assert [%{goal: %{event_name: "Goal 3"}}, %{goal: %{event_name: "Goal 2"}}] = page2.goals
assert page2.meta.pagination.has_next_page == true
assert page2.meta.pagination.has_prev_page == true
assert page2.meta.pagination.links.next
assert page2.meta.pagination.links.prev
assert ^page1 =
initial_conn
|> get(page2.meta.pagination.links.prev.url)
|> json_response(200)
|> assert_schema("Goal.ListResponse", spec())
end
end
describe "delete /goals/:id" do
test "deletes goal by id", %{conn: conn, site: site, token: token} do
{:ok, %{id: goal_id}} =
Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "USD"})
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :delete, goal_id)
conn
|> authenticate(site.domain, token)
|> delete(url)
|> response(204)
refute Plausible.Repo.exists?(Plausible.Goal)
end
test "is idempotent", %{conn: conn, site: site, token: token} do
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :delete, 666)
conn
|> authenticate(site.domain, token)
|> delete(url)
|> response(204)
end
end
describe "delete - bulk" do
test "delete multiple goals", %{conn: conn, site: site, token: token} do
{:ok, g1} =
Plausible.Goals.create(site, %{"event_name" => "Purchase", "currency" => "USD"})
{:ok, g2} =
Plausible.Goals.create(site, %{"event_name" => "Signup"})
{:ok, g3} =
Plausible.Goals.create(site, %{"page_path" => "/home"})
url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :delete_bulk)
payload = %{
goal_ids: [
g1.id,
g2.id,
g3.id
]
}
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> delete(url, payload)
|> response(204)
refute Plausible.Repo.exists?(Plausible.Goal)
end
end
end