mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +03:00
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:
parent
da0fa6c355
commit
615b6aef7d
27
lib/plausible/plugins/api/custom_props.ex
Normal file
27
lib/plausible/plugins/api/custom_props.ex
Normal 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
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
98
lib/plausible_web/plugins/api/controllers/custom_props.ex
Normal file
98
lib/plausible_web/plugins/api/controllers/custom_props.ex
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
27
lib/plausible_web/plugins/api/schemas/custom_prop.ex
Normal file
27
lib/plausible_web/plugins/api/schemas/custom_prop.ex
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
23
lib/plausible_web/plugins/api/views/custom_props.ex
Normal file
23
lib/plausible_web/plugins/api/views/custom_props.ex
Normal 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
|
249
test/plausible_web/plugins/api/controllers/custom_props_test.exs
Normal file
249
test/plausible_web/plugins/api/controllers/custom_props_test.exs
Normal 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
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user