Plugins API exentsions (custom props, bulk goal delete, goal creation => ListResponse always) (#3593)

* End polymorphic response in goals create

Being part of the v3 spec, this isn't well
(or at all) supported by OpenAPI generators.

Always respond with `Goal.ListResponse`

* Implement `PUT /custom_props`

* Implement bulk `DELETE /goals`

* Expose API for (bulk-)disabling custom props

* Add controller typespecs

* Delegate list wrapping to `Plausible.API.*`
This commit is contained in:
hq1 2023-12-06 12:33:33 +01:00 committed by GitHub
parent da0fa6c355
commit 615b6aef7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 667 additions and 86 deletions

View File

@ -0,0 +1,27 @@
defmodule Plausible.Plugins.API.CustomProps do
@moduledoc """
Plugins API context module for Custom Props.
All high level Custom Props operations should be implemented here.
"""
@spec enable(Plausible.Site.t(), String.t() | [String.t()]) ::
{:ok, [String.t()]} | {:error, :upgrade_required | Ecto.Changeset.t()}
def enable(site, prop_or_props) do
case Plausible.Props.allow(site, prop_or_props) do
{:ok, site} ->
{:ok, site.allowed_event_props}
error ->
error
end
end
@spec disable(Plausible.Site.t(), String.t() | [String.t()]) ::
:ok | {:error, Ecto.Changeset.t()}
def disable(site, prop_or_props) do
case Plausible.Props.disallow(site, prop_or_props) do
{:ok, _site} -> :ok
error -> error
end
end
end

View File

@ -42,6 +42,19 @@ defmodule Plausible.Plugins.API.Goals do
|> Repo.one()
end
@spec delete(Plausible.Site.t(), [pos_integer()] | pos_integer()) :: :ok
def delete(site, id_or_ids) do
Plausible.Repo.transaction(fn ->
id_or_ids
|> List.wrap()
|> Enum.each(fn id when is_integer(id) ->
Plausible.Goals.delete(id, site)
end)
end)
:ok
end
defp get_query(site) do
from g in Plausible.Goal,
where: g.site_id == ^site.id,

View File

@ -39,18 +39,18 @@ defmodule Plausible.Props do
changeset(site, new_props)
end
@spec disallow(Plausible.Site.t(), prop()) ::
@spec disallow(Plausible.Site.t(), [prop()] | prop()) ::
{:ok, Plausible.Site.t()} | {:error, Ecto.Changeset.t()}
@doc """
Removes a previously allowed prop key from the allow list. This means this
Removes previously allowed prop key(s) from the allow list. This means this
prop key won't be included in ClickHouse queries. This doesn't drop any
ClickHouse data, nor affects ingestion.
"""
def disallow(site, prop) do
def disallow(site, prop_or_props) do
allowed_event_props = site.allowed_event_props || []
site
|> changeset(allowed_event_props -- [prop])
|> changeset(allowed_event_props -- List.wrap(prop_or_props))
|> Plausible.Repo.update()
end

View File

@ -0,0 +1,98 @@
defmodule PlausibleWeb.Plugins.API.Controllers.CustomProps do
@moduledoc """
Controller for the CustomProp resource under Plugins API
"""
use PlausibleWeb, :plugins_api_controller
operation(:enable,
id: "CustomProp.GetOrEnable",
summary: "Get or enable CustomProp(s)",
request_body:
{"CustomProp enable params", "application/json", Schemas.CustomProp.EnableRequest},
responses: %{
created: {"CustomProp", "application/json", Schemas.CustomProp.ListResponse},
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized},
payment_required: {"Payment required", "application/json", Schemas.PaymentRequired},
unprocessable_entity:
{"Unprocessable entity", "application/json", Schemas.UnprocessableEntity}
}
)
@spec enable(Plug.Conn.t(), map()) :: Plug.Conn.t()
def enable(
%{private: %{open_api_spex: %{body_params: body_params}}} = conn,
_params
) do
site = conn.assigns.authorized_site
prop_or_props =
case body_params do
%{custom_props: props} ->
Enum.map(props, & &1.custom_prop.key)
%{custom_prop: %{key: prop}} ->
prop
end
case API.CustomProps.enable(site, prop_or_props) do
{:ok, enabled_props} ->
conn
|> put_view(Views.CustomProp)
|> put_status(:created)
|> render("index.json", props: enabled_props, authorized_site: site)
{:error, :upgrade_required} ->
payment_required(conn)
{:error, changeset} ->
Errors.error(conn, 422, changeset)
end
end
operation(:disable,
id: "CustomProp.DisableBulk",
summary: "Disable CustomProp(s)",
request_body:
{"CustomProp disable params", "application/json", Schemas.CustomProp.DisableRequest},
responses: %{
no_content: {"NoContent", nil, nil},
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized},
payment_required: {"Payment required", "application/json", Schemas.PaymentRequired},
unprocessable_entity:
{"Unprocessable entity", "application/json", Schemas.UnprocessableEntity}
}
)
@spec disable(Plug.Conn.t(), map()) :: Plug.Conn.t()
def disable(
%{private: %{open_api_spex: %{body_params: body_params}}} = conn,
_params
) do
site = conn.assigns.authorized_site
prop_or_props =
case body_params do
%{custom_props: props} ->
Enum.map(props, & &1.custom_prop.key)
%{custom_prop: %{key: prop}} ->
prop
end
case API.CustomProps.disable(site, prop_or_props) do
:ok ->
send_resp(conn, :no_content, "")
{:error, changeset} ->
Errors.error(conn, 422, changeset)
end
end
defp payment_required(conn) do
Errors.error(
conn,
402,
"#{Plausible.Billing.Feature.Props.display_name()} is part of the Plausible Business plan. To get access to this feature, please upgrade your account."
)
end
end

View File

@ -4,6 +4,49 @@ defmodule PlausibleWeb.Plugins.API.Controllers.Goals do
"""
use PlausibleWeb, :plugins_api_controller
operation(:create,
id: "Goal.GetOrCreate",
summary: "Get or create Goal",
request_body: {"Goal params", "application/json", Schemas.Goal.CreateRequest},
responses: %{
created: {"Goal", "application/json", Schemas.Goal.ListResponse},
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized},
payment_required: {"Payment required", "application/json", Schemas.PaymentRequired},
unprocessable_entity:
{"Unprocessable entity", "application/json", Schemas.UnprocessableEntity}
}
)
def create(
%{private: %{open_api_spex: %{body_params: body_params}}} = conn,
_params
) do
site = conn.assigns.authorized_site
goal_or_goals =
case body_params do
%{goals: goals} -> goals
%{goal: _} = single_goal -> single_goal
end
case API.Goals.create(site, goal_or_goals) do
{:ok, goals} ->
location_headers = Enum.map(goals, &{"location", goals_url(base_uri(), :get, &1.id)})
conn
|> prepend_resp_headers(location_headers)
|> put_view(Views.Goal)
|> put_status(:created)
|> render("index.json", goals: goals, authorized_site: site)
{:error, :upgrade_required} ->
payment_required(conn)
{:error, changeset} ->
Errors.error(conn, 422, changeset)
end
end
operation(:index,
summary: "Retrieve Goals",
parameters: [
@ -25,70 +68,6 @@ defmodule PlausibleWeb.Plugins.API.Controllers.Goals do
}
)
operation(:create,
id: "Goal.GetOrCreate",
summary: "Get or create Goal",
request_body: {"Goal params", "application/json", Schemas.Goal.CreateRequest},
responses: %{
created:
{"Goal", "application/json",
%OpenApiSpex.Schema{
oneOf: [Schemas.Goal.ListResponse, Schemas.Goal]
}},
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized},
payment_required: {"Payment required", "application/json", Schemas.PaymentRequired},
unprocessable_entity:
{"Unprocessable entity", "application/json", Schemas.UnprocessableEntity}
}
)
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create(
%{private: %{open_api_spex: %{body_params: %{goal: _} = goal}}} = conn,
_params
) do
site = conn.assigns.authorized_site
case API.Goals.create(site, goal) do
{:ok, [goal]} ->
conn
|> put_view(Views.Goal)
|> put_status(:created)
|> put_resp_header("location", goals_url(base_uri(), :get, goal.id))
|> render("goal.json", goal: goal, authorized_site: site)
{:error, :upgrade_required} ->
payment_required(conn)
{:error, changeset} ->
Errors.error(conn, 422, changeset)
end
end
def create(
%{private: %{open_api_spex: %{body_params: %{goals: goals}}}} = conn,
_params
) do
site = conn.assigns.authorized_site
case API.Goals.create(site, goals) do
{:ok, goals} ->
location_headers = Enum.map(goals, &{"location", goals_url(base_uri(), :get, &1.id)})
conn
|> prepend_resp_headers(location_headers)
|> put_view(Views.Goal)
|> put_status(:created)
|> render("index.json", goals: goals, authorized_site: site)
{:error, :upgrade_required} ->
payment_required(conn)
{:error, changeset} ->
Errors.error(conn, 422, changeset)
end
end
@spec index(Plug.Conn.t(), %{}) :: Plug.Conn.t()
def index(conn, _params) do
{:ok, pagination} = API.Goals.get_goals(conn.assigns.authorized_site, conn.query_params)
@ -156,13 +135,27 @@ defmodule PlausibleWeb.Plugins.API.Controllers.Goals do
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
def delete(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _params) do
case Plausible.Goals.delete(id, conn.assigns.authorized_site) do
:ok ->
send_resp(conn, :no_content, "")
:ok = API.Goals.delete(conn.assigns.authorized_site, id)
send_resp(conn, :no_content, "")
end
{:error, :not_found} ->
send_resp(conn, :no_content, "")
end
operation(:delete_bulk,
id: "Goal.DeleteBulk",
summary: "Delete Goals in bulk",
request_body: {"Goal params", "application/json", Schemas.Goal.DeleteBulkRequest},
responses: %{
no_content: {"NoContent", nil, nil},
unauthorized: {"Unauthorized", "application/json", Schemas.Unauthorized}
}
)
@spec delete_bulk(Plug.Conn.t(), map()) :: Plug.Conn.t()
def delete_bulk(
%{private: %{open_api_spex: %{body_params: %{goal_ids: goal_ids}}}} = conn,
_params
) do
:ok = API.Goals.delete(conn.assigns.authorized_site, goal_ids)
send_resp(conn, :no_content, "")
end
defp payment_required(conn) do

View File

@ -26,6 +26,11 @@ defmodule PlausibleWeb.Plugins.API.Router do
get("/goals", Goals, :index)
get("/goals/:id", Goals, :get)
put("/goals", Goals, :create)
delete("/goals/:id", Goals, :delete)
delete("/goals", Goals, :delete_bulk)
put("/custom_props", CustomProps, :enable)
delete("/custom_props", CustomProps, :disable)
end
end

View File

@ -0,0 +1,27 @@
defmodule PlausibleWeb.Plugins.API.Schemas.CustomProp do
@moduledoc """
OpenAPI schema for Goal
"""
use PlausibleWeb, :open_api_schema
OpenApiSpex.schema(%{
title: "CustomProp",
description: "Custom Property object",
type: :object,
required: [:custom_prop],
properties: %{
custom_prop: %Schema{
type: :object,
required: [:key],
properties: %{
key: %Schema{type: :string, description: "Custom Property Key"}
}
}
},
example: %{
custom_prop: %{
key: "author"
}
}
})
end

View File

@ -0,0 +1,29 @@
defmodule PlausibleWeb.Plugins.API.Schemas.CustomProp.DisableRequest do
@moduledoc """
OpenAPI schema for Custom Property disable request
"""
use PlausibleWeb, :open_api_schema
OpenApiSpex.schema(%{
title: "CustomProp.DisableRequest",
description: "Custom Property disable params",
type: :object,
oneOf: [
%Schema{
title: "CustomProp.DisableRequest.BulkDisable",
type: :object,
description: "Bulk Custom Property disable request",
required: [:custom_props],
properties: %{
custom_props: %Schema{
type: :array,
minItems: 1,
items: Schemas.CustomProp
}
}
},
Schemas.CustomProp
],
example: %{custom_props: [%{custom_prop: %{key: "author"}}]}
})
end

View File

@ -0,0 +1,29 @@
defmodule PlausibleWeb.Plugins.API.Schemas.CustomProp.EnableRequest do
@moduledoc """
OpenAPI schema for Custom Property creation request
"""
use PlausibleWeb, :open_api_schema
OpenApiSpex.schema(%{
title: "CustomProp.EnableRequest",
description: "Custom Property enable params",
type: :object,
oneOf: [
%Schema{
title: "CustomProp.EnableRequest.BulkEnable",
type: :object,
description: "Bulk Custom Property enable request",
required: [:custom_props],
properties: %{
custom_props: %Schema{
type: :array,
minItems: 1,
items: Schemas.CustomProp
}
}
},
Schemas.CustomProp
],
example: %{custom_props: [%{custom_prop: %{key: "author"}}]}
})
end

View File

@ -0,0 +1,19 @@
defmodule PlausibleWeb.Plugins.API.Schemas.CustomProp.ListResponse do
@moduledoc """
OpenAPI schema for SharedLink list response
"""
use PlausibleWeb, :open_api_schema
OpenApiSpex.schema(%{
title: "CustomProp.ListResponse",
description: "Custom Props list response",
type: :object,
required: [:custom_props],
properties: %{
custom_props: %Schema{
items: Schemas.CustomProp,
type: :array
}
}
})
end

View File

@ -0,0 +1,22 @@
defmodule PlausibleWeb.Plugins.API.Schemas.Goal.DeleteBulkRequest do
@moduledoc """
OpenAPI schema for bulk Goal deletion request
"""
use Plausible.Funnel.Const
use PlausibleWeb, :open_api_schema
OpenApiSpex.schema(%{
title: "Goal.DeleteBulkRequest",
description: "Goal deletion params",
type: :object,
required: [:goal_ids],
properties: %{
goal_ids: %Schema{
type: :array,
minItems: 1,
maxItems: Funnel.Const.max_steps(),
items: %Schema{type: :integer, description: "Goal ID"}
}
}
})
end

View File

@ -0,0 +1,23 @@
defmodule PlausibleWeb.Plugins.API.Views.CustomProp do
@moduledoc """
View for rendering Custom Props in the Plugins API
"""
use PlausibleWeb, :plugins_api_view
def render("index.json", %{props: props}) do
%{
custom_props: render_many(props, __MODULE__, "custom_prop.json")
}
end
def render("custom_prop.json", %{
custom_prop: custom_prop
}) do
%{
custom_prop: %{
key: custom_prop
}
}
end
end

View File

@ -0,0 +1,249 @@
defmodule PlausibleWeb.Plugins.API.Controllers.CustomPropsTest do
use PlausibleWeb.PluginsAPICase, async: true
alias PlausibleWeb.Plugins.API.Schemas
describe "examples" do
test "CustomProp" do
assert_schema(
Schemas.CustomProp.schema().example,
"CustomProp",
spec()
)
end
test "CustomProp.CreateRequest" do
assert_schema(
Schemas.CustomProp.EnableRequest.schema().example,
"CustomProp.EnableRequest",
spec()
)
end
end
describe "unauthorized calls" do
for {method, url} <- [
{:put, Routes.custom_props_url(base_uri(), :enable)},
{:delete, Routes.custom_props_url(base_uri(), :disable)}
] 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
@describetag :full_build_only
test "fails on custom prop enable attempt with insufficient plan", %{
site: site,
token: token,
conn: conn
} do
site = Plausible.Repo.preload(site, :owner)
FunWithFlags.enable(:business_tier, for_actor: site.owner)
insert(:growth_subscription, user: site.owner)
url = Routes.custom_props_url(base_uri(), :enable)
payload = %{
custom_prop: %{key: "author"}
}
assert_request_schema(payload, "CustomProp.EnableRequest", 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 on bulk prop enable attempt with insufficient plan", %{
site: site,
token: token,
conn: conn
} do
site = Plausible.Repo.preload(site, :owner)
FunWithFlags.enable(:business_tier, for_actor: site.owner)
insert(:growth_subscription, user: site.owner)
url = Routes.custom_props_url(base_uri(), :enable)
payload = %{
custom_props: [
%{
custom_prop: %{key: "author"}
},
%{
custom_prop: %{key: "category"}
}
]
}
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 /custom_prop - enable single prop" do
test "validates input according to the schema", %{conn: conn, token: token, site: site} do
url = Routes.custom_props_url(base_uri(), :enable)
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, %{custom_prop: %{typo: "author"}})
|> json_response(422)
|> assert_schema("UnprocessableEntityError", spec())
end
test "enables single custom prop", %{conn: conn, token: token, site: site} do
url = Routes.custom_props_url(base_uri(), :enable)
payload = %{
custom_prop: %{key: "author"}
}
assert_request_schema(payload, "CustomProp.EnableRequest", spec())
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
resp =
conn
|> json_response(201)
|> assert_schema("CustomProp.ListResponse", spec())
resp.custom_props
|> List.first()
|> assert_schema("CustomProp", spec())
assert "author" in Plausible.Repo.reload!(site).allowed_event_props
end
test "is idempotent", %{conn: conn, token: token, site: site} do
url = Routes.custom_props_url(base_uri(), :enable)
initial_conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
resp1 =
initial_conn
|> put(
url,
%{
custom_prop: %{key: "author"}
}
)
|> json_response(201)
|> assert_schema("CustomProp.ListResponse", spec())
resp1.custom_props
|> List.first()
|> assert_schema("CustomProp", spec())
assert initial_conn
|> put(
url,
%{
custom_prop: %{key: "author"}
}
)
|> json_response(201)
|> assert_schema("CustomProp.ListResponse", spec()) == resp1
end
end
describe "put /custom_props - bulk creation" do
test "creates many custom props", %{conn: conn, token: token, site: site} do
url = Routes.custom_props_url(base_uri(), :enable)
payload = %{
custom_props: [
%{
custom_prop: %{key: "author"}
},
%{
custom_prop: %{key: "rating"}
},
%{
custom_prop: %{key: "category"}
}
]
}
assert_request_schema(payload, "CustomProp.EnableRequest.BulkEnable", spec())
conn =
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> put(url, payload)
resp =
conn
|> json_response(201)
|> assert_schema("CustomProp.ListResponse", spec())
assert Enum.count(resp.custom_props) == 3
assert [
"author",
"rating",
"category"
] = Plausible.Repo.reload!(site).allowed_event_props
end
end
describe "delete /custom_props" do
test "disable one prop", %{conn: conn, site: site, token: token} do
{:ok, ["author"]} = Plausible.Plugins.API.CustomProps.enable(site, "author")
url = Routes.custom_props_url(base_uri(), :enable)
payload = %{custom_prop: %{key: "author"}}
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> delete(url, payload)
|> response(204)
assert Plausible.Repo.reload!(site).allowed_event_props == []
end
test "disable many props", %{conn: conn, site: site, token: token} do
{:ok, [_, _, _]} =
Plausible.Plugins.API.CustomProps.enable(site, ["author", "category", "third"])
url = Routes.custom_props_url(base_uri(), :enable)
payload = %{
custom_props: [
%{custom_prop: %{key: "author"}},
%{custom_prop: %{key: "category"}}
]
}
conn
|> authenticate(site.domain, token)
|> put_req_header("content-type", "application/json")
|> delete(url, payload)
|> response(204)
assert Plausible.Repo.reload!(site).allowed_event_props == ["third"]
end
end
end

View File

@ -33,7 +33,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
{:get, Routes.goals_url(base_uri(), :index)},
{:get, Routes.goals_url(base_uri(), :get, 1)},
{:put, Routes.goals_url(base_uri(), :create, %{})},
{:delete, Routes.goals_url(base_uri(), :delete, 1)}
{:delete, Routes.goals_url(base_uri(), :delete, 1)},
{:delete, Routes.goals_url(base_uri(), :delete_bulk, %{})}
] do
test "unauthorized call: #{method} #{url}", %{conn: conn} do
conn
@ -138,13 +139,16 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
resp =
conn
|> json_response(201)
|> assert_schema("Goal", spec())
|> assert_schema("Goal.CustomEvent", spec())
|> assert_schema("Goal.ListResponse", spec())
resp.goals
|> List.first()
|> assert_schema("Goal.CustomEvent", spec())
[location] = get_resp_header(conn, "location")
assert location ==
Routes.goals_url(base_uri(), :get, resp.goal.id)
Routes.goals_url(base_uri(), :get, List.first(resp.goals).goal.id)
assert [%{event_name: "Signup"}] = Plausible.Goals.for_site(site)
end
@ -169,13 +173,16 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
resp =
conn
|> json_response(201)
|> assert_schema("Goal", spec())
|> assert_schema("Goal.Revenue", spec())
|> assert_schema("Goal.ListResponse", spec())
resp.goals
|> List.first()
|> assert_schema("Goal.Revenue", spec())
[location] = get_resp_header(conn, "location")
assert location ==
Routes.goals_url(base_uri(), :get, resp.goal.id)
Routes.goals_url(base_uri(), :get, List.first(resp.goals).goal.id)
assert [%{event_name: "Purchase", currency: :EUR}] = Plausible.Goals.for_site(site)
end
@ -249,13 +256,16 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
resp =
conn
|> json_response(201)
|> assert_schema("Goal", spec())
|> assert_schema("Goal.Pageview", spec())
|> assert_schema("Goal.ListResponse", spec())
resp.goals
|> List.first()
|> assert_schema("Goal.Pageview", spec())
[location] = get_resp_header(conn, "location")
assert location ==
Routes.goals_url(base_uri(), :get, resp.goal.id)
Routes.goals_url(base_uri(), :get, List.first(resp.goals).goal.id)
assert [%{page_path: "/checkout"}] = Plausible.Goals.for_site(site)
end
@ -272,12 +282,16 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
initial_conn
|> put(url, %{goal_type: "Goal.Pageview", goal: %{path: "/checkout"}})
|> json_response(201)
|> assert_schema("Goal.Pageview", spec())
|> 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.Pageview", spec()) == resp1
|> assert_schema("Goal.ListResponse", spec()) == resp1
end
end
@ -606,6 +620,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
|> 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
@ -617,4 +633,35 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do
|> 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.goals_url(base_uri(), :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