mirror of
https://github.com/plausible/analytics.git
synced 2024-12-22 09:01:40 +03:00
Implement Stats API feature gate (#3411)
* Include ApiKey functions in Auth context * Make feature notice work without %Site{} Previously the extra feature notice required a %Site{} in order to check the owner plan. However, not every feature is scoped by site, for example the Stats API. For features like this, a %User{} is required, and not a %Site{}. This commit replaces the `:site` param with `:billable_user`, which is common to both site and user-scoped features. * Add stats_api to the list of extra features * Limit API Key creation based on user plan
This commit is contained in:
parent
192aefc493
commit
c0fe2a3996
@ -2,6 +2,8 @@ defmodule Plausible.Auth.ApiKey do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@type t() :: %__MODULE__{}
|
||||
|
||||
@required [:user_id, :name]
|
||||
@optional [:key, :scopes, :hourly_request_limit]
|
||||
schema "api_keys" do
|
||||
@ -22,7 +24,7 @@ defmodule Plausible.Auth.ApiKey do
|
||||
schema
|
||||
|> cast(attrs, @required ++ @optional)
|
||||
|> validate_required(@required)
|
||||
|> generate_key()
|
||||
|> maybe_put_key()
|
||||
|> process_key()
|
||||
|> unique_constraint(:key_hash, error_key: :key)
|
||||
end
|
||||
@ -50,12 +52,12 @@ defmodule Plausible.Auth.ApiKey do
|
||||
|
||||
def process_key(changeset), do: changeset
|
||||
|
||||
defp generate_key(changeset) do
|
||||
if !changeset.changes[:key] do
|
||||
key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64)
|
||||
change(changeset, key: key)
|
||||
else
|
||||
defp maybe_put_key(changeset) do
|
||||
if get_change(changeset, :key) do
|
||||
changeset
|
||||
else
|
||||
key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64)
|
||||
put_change(changeset, :key, key)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -125,4 +125,42 @@ defmodule Plausible.Auth do
|
||||
|> Ecto.assoc(:enterprise_plan)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@spec create_api_key(Auth.User.t(), String.t(), String.t()) ::
|
||||
{:ok, Auth.ApiKey.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_api_key(user, name, key) do
|
||||
params = %{name: name, user_id: user.id, key: key}
|
||||
changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, params)
|
||||
|
||||
with :ok <- Plausible.Billing.Feature.StatsAPI.check_availability(user),
|
||||
do: Repo.insert(changeset)
|
||||
end
|
||||
|
||||
@spec delete_api_key(Auth.User.t(), integer()) :: :ok | {:error, :not_found}
|
||||
def delete_api_key(user, id) do
|
||||
query = from(api_key in Auth.ApiKey, where: api_key.id == ^id and api_key.user_id == ^user.id)
|
||||
|
||||
case Repo.delete_all(query) do
|
||||
{1, _} -> :ok
|
||||
{0, _} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_api_key(String.t()) :: {:ok, Auth.ApiKey.t()} | {:error, :invalid_api_key}
|
||||
def find_api_key(raw_key) do
|
||||
hashed_key = Auth.ApiKey.do_hash(raw_key)
|
||||
|
||||
query =
|
||||
from(api_key in Auth.ApiKey,
|
||||
join: user in assoc(api_key, :user),
|
||||
where: api_key.key_hash == ^hashed_key,
|
||||
preload: [user: user]
|
||||
)
|
||||
|
||||
if found = Repo.one(query) do
|
||||
{:ok, found}
|
||||
else
|
||||
{:error, :invalid_api_key}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -47,8 +47,8 @@ defmodule Plausible.Billing.Feature do
|
||||
@doc """
|
||||
Checks whether the site owner or the user plan includes the given feature.
|
||||
"""
|
||||
@callback check_availability(Plausible.Site.t() | Plausible.Auth.User.t()) ::
|
||||
:ok | {:error, :upgrade_required}
|
||||
@callback check_availability(Plausible.Auth.User.t()) ::
|
||||
:ok | {:error, :upgrade_required} | {:error, :not_implemented}
|
||||
|
||||
@features [
|
||||
Plausible.Billing.Feature.Funnels,
|
||||
@ -77,22 +77,17 @@ defmodule Plausible.Billing.Feature do
|
||||
def toggle_field, do: Keyword.get(unquote(opts), :toggle_field)
|
||||
|
||||
@impl true
|
||||
def enabled?(site) do
|
||||
def enabled?(%Plausible.Site{} = site) do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
|
||||
cond do
|
||||
check_availability(site) !== :ok -> false
|
||||
check_availability(site.owner) !== :ok -> false
|
||||
is_nil(toggle_field()) -> true
|
||||
true -> Map.get(site, toggle_field())
|
||||
true -> Map.fetch!(site, toggle_field())
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def check_availability(site_or_user)
|
||||
|
||||
def check_availability(%Plausible.Site{} = site) do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
check_availability(site.owner)
|
||||
end
|
||||
|
||||
def check_availability(%Plausible.Auth.User{} = user) do
|
||||
extra_feature = Keyword.get(unquote(opts), :extra_feature)
|
||||
|
||||
@ -105,9 +100,10 @@ defmodule Plausible.Billing.Feature do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def toggle(site, opts \\ []) do
|
||||
def toggle(%Plausible.Site{} = site, opts \\ []) do
|
||||
with key when not is_nil(key) <- toggle_field(),
|
||||
:ok <- check_availability(site) do
|
||||
site <- Plausible.Repo.preload(site, :owner),
|
||||
:ok <- check_availability(site.owner) do
|
||||
override = Keyword.get(opts, :override)
|
||||
toggle = if is_boolean(override), do: override, else: !Map.fetch!(site, toggle_field())
|
||||
|
||||
@ -119,8 +115,6 @@ defmodule Plausible.Billing.Feature do
|
||||
{:error, :upgrade_required} -> {:error, :upgrade_required}
|
||||
end
|
||||
end
|
||||
|
||||
defoverridable check_availability: 1, toggle: 1, toggle: 2
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -154,3 +148,10 @@ defmodule Plausible.Billing.Feature.Props do
|
||||
toggle_field: :props_enabled,
|
||||
extra_feature: :props
|
||||
end
|
||||
|
||||
defmodule Plausible.Billing.Feature.StatsAPI do
|
||||
@moduledoc false
|
||||
use Plausible.Billing.Feature,
|
||||
display_name: "Stats API",
|
||||
extra_feature: :stats_api
|
||||
end
|
||||
|
@ -31,7 +31,7 @@ defmodule Plausible.Billing.Plans do
|
||||
alias Plausible.Billing.{Subscription, Plan, EnterprisePlan}
|
||||
alias Plausible.Auth.User
|
||||
|
||||
@available_features ["props", "revenue_goals", "funnels"]
|
||||
@available_features ["props", "revenue_goals", "funnels", "stats_api"]
|
||||
|
||||
for f <- [
|
||||
:plans_v1,
|
||||
|
@ -162,7 +162,7 @@ defmodule Plausible.Billing.Quota do
|
||||
end)
|
||||
end
|
||||
|
||||
@all_features [:props, :revenue_goals, :funnels]
|
||||
@all_features [:props, :revenue_goals, :funnels, :stats_api]
|
||||
@doc """
|
||||
Returns a list of extra features the user can use. Trial users have the
|
||||
ability to use all features during their trial.
|
||||
|
@ -214,7 +214,8 @@ defmodule Plausible.Goals do
|
||||
|
||||
defp maybe_check_feature_access(site, changeset) do
|
||||
if Ecto.Changeset.get_field(changeset, :currency) do
|
||||
Plausible.Billing.Feature.RevenueGoals.check_availability(site)
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
Plausible.Billing.Feature.RevenueGoals.check_availability(site.owner)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
|
@ -24,7 +24,8 @@ defmodule Plausible.Props do
|
||||
data to be dropped or lost.
|
||||
"""
|
||||
def allow(site, prop_or_props) do
|
||||
with :ok <- Plausible.Billing.Feature.Props.check_availability(site) do
|
||||
with site <- Plausible.Repo.preload(site, :owner),
|
||||
:ok <- Plausible.Billing.Feature.Props.check_availability(site.owner) do
|
||||
site
|
||||
|> allow_changeset(prop_or_props)
|
||||
|> Plausible.Repo.update()
|
||||
|
@ -7,7 +7,7 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
alias Plausible.Billing.Subscription
|
||||
|
||||
attr(:site, Plausible.Site, required: true)
|
||||
attr(:billable_user, Plausible.Auth.User, required: true)
|
||||
attr(:current_user, Plausible.Auth.User, required: true)
|
||||
attr(:feature_mod, :atom, required: true, values: Plausible.Billing.Feature.list())
|
||||
attr(:grandfathered?, :boolean, default: false)
|
||||
@ -17,8 +17,8 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
|
||||
def extra_feature_notice(assigns) do
|
||||
private_preview? = not FunWithFlags.enabled?(:business_tier, for: assigns.current_user)
|
||||
owner? = assigns.current_user.id == assigns.site.owner.id
|
||||
has_access? = assigns.feature_mod.check_availability(assigns.site.owner) == :ok
|
||||
display_upgrade_link? = assigns.current_user.id == assigns.billable_user.id
|
||||
has_access? = assigns.feature_mod.check_availability(assigns.billable_user) == :ok
|
||||
|
||||
message =
|
||||
cond do
|
||||
@ -28,10 +28,10 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
private_preview? ->
|
||||
"#{assigns.feature_mod.display_name()} is an upcoming premium functionality that's free-to-use during the private preview. Pricing will be announced soon."
|
||||
|
||||
Plausible.Billing.on_trial?(assigns.site.owner) ->
|
||||
Plausible.Billing.on_trial?(assigns.billable_user) ->
|
||||
"#{assigns.feature_mod.display_name()} is part of the Plausible Business plan. You can access it during your trial, but you'll need to subscribe to the Business plan to retain access after the trial ends."
|
||||
|
||||
not has_access? && owner? ->
|
||||
not has_access? && display_upgrade_link? ->
|
||||
~H"""
|
||||
<%= @feature_mod.display_name() %> is part of the Plausible Business plan. To get access to it, please
|
||||
<.link class="underline" href={Routes.billing_path(PlausibleWeb.Endpoint, :upgrade)}>
|
||||
@ -39,7 +39,7 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
</.link> to the Business plan.
|
||||
"""
|
||||
|
||||
not has_access? && not owner? ->
|
||||
not has_access? && not display_upgrade_link? ->
|
||||
"#{assigns.feature_mod.display_name()} is part of the Plausible Business plan. To get access to it, please reach out to the site owner to upgrade your subscription to the Business plan."
|
||||
|
||||
true ->
|
||||
|
@ -14,7 +14,7 @@ defmodule PlausibleWeb.Components.Site.Feature do
|
||||
assigns =
|
||||
assigns
|
||||
|> assign(:current_setting, assigns.feature_mod.enabled?(assigns.site))
|
||||
|> assign(:disabled?, assigns.feature_mod.check_availability(assigns.site) !== :ok)
|
||||
|> assign(:disabled?, assigns.feature_mod.check_availability(assigns.site.owner) !== :ok)
|
||||
|
||||
~H"""
|
||||
<div>
|
||||
|
@ -414,8 +414,7 @@ defmodule PlausibleWeb.AuthController do
|
||||
end
|
||||
|
||||
def new_api_key(conn, _params) do
|
||||
key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64)
|
||||
changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, %{key: key})
|
||||
changeset = Auth.ApiKey.changeset(%Auth.ApiKey{})
|
||||
|
||||
render(conn, "new_api_key.html",
|
||||
changeset: changeset,
|
||||
@ -423,12 +422,8 @@ defmodule PlausibleWeb.AuthController do
|
||||
)
|
||||
end
|
||||
|
||||
def create_api_key(conn, %{"api_key" => key_params}) do
|
||||
api_key = %Auth.ApiKey{user_id: conn.assigns[:current_user].id}
|
||||
key_params = Map.delete(key_params, "user_id")
|
||||
changeset = Auth.ApiKey.changeset(api_key, key_params)
|
||||
|
||||
case Repo.insert(changeset) do
|
||||
def create_api_key(conn, %{"api_key" => %{"name" => name, "key" => key}}) do
|
||||
case Auth.create_api_key(conn.assigns.current_user, name, key) do
|
||||
{:ok, _api_key} ->
|
||||
conn
|
||||
|> put_flash(:success, "API key created successfully")
|
||||
@ -443,18 +438,17 @@ defmodule PlausibleWeb.AuthController do
|
||||
end
|
||||
|
||||
def delete_api_key(conn, %{"id" => id}) do
|
||||
query =
|
||||
from(k in Auth.ApiKey,
|
||||
where: k.id == ^id and k.user_id == ^conn.assigns[:current_user].id
|
||||
)
|
||||
case Auth.delete_api_key(conn.assigns.current_user, id) do
|
||||
:ok ->
|
||||
conn
|
||||
|> put_flash(:success, "API key revoked successfully")
|
||||
|> redirect(to: "/settings#api-keys")
|
||||
|
||||
query
|
||||
|> Repo.one!()
|
||||
|> Repo.delete!()
|
||||
|
||||
conn
|
||||
|> put_flash(:success, "API key revoked successfully")
|
||||
|> redirect(to: "/settings#api-keys")
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_flash(:error, "Could not find API Key to delete")
|
||||
|> redirect(to: "/settings#api-keys")
|
||||
end
|
||||
end
|
||||
|
||||
def delete_me(conn, params) do
|
||||
|
@ -210,7 +210,9 @@ defmodule PlausibleWeb.SiteController do
|
||||
end
|
||||
|
||||
def settings_goals(conn, _params) do
|
||||
site = conn.assigns[:site] |> Repo.preload(:custom_domain)
|
||||
site = Repo.preload(conn.assigns[:site], [:custom_domain, :owner])
|
||||
owner = Plausible.Users.with_subscription(site.owner)
|
||||
site = Map.put(site, :owner, owner)
|
||||
|
||||
conn
|
||||
|> render("settings_goals.html",
|
||||
|
@ -29,7 +29,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
site = Map.put(site, :owner, owner)
|
||||
|
||||
has_access_to_revenue_goals? =
|
||||
Plausible.Billing.Feature.RevenueGoals.check_availability(site) == :ok
|
||||
Plausible.Billing.Feature.RevenueGoals.check_availability(owner) == :ok
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
@ -193,7 +193,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do
|
||||
|
||||
<span x-show="active">
|
||||
<PlausibleWeb.Components.Billing.extra_feature_notice
|
||||
site={@site}
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.RevenueGoals}
|
||||
size={:xs}
|
||||
|
@ -1,7 +1,7 @@
|
||||
defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||
import Plug.Conn
|
||||
use Plausible.Repo
|
||||
alias Plausible.Auth.ApiKey
|
||||
alias Plausible.Auth
|
||||
alias Plausible.Sites
|
||||
alias PlausibleWeb.Api.Helpers, as: H
|
||||
|
||||
@ -11,7 +11,7 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||
|
||||
def call(conn, _opts) do
|
||||
with {:ok, token} <- get_bearer_token(conn),
|
||||
{:ok, api_key} <- find_api_key(token),
|
||||
{:ok, api_key} <- Auth.find_api_key(token),
|
||||
:ok <- check_api_key_rate_limit(api_key),
|
||||
{:ok, site} <- verify_access(api_key, conn.params["site_id"]) do
|
||||
Plausible.OpenTelemetry.add_site_attributes(site)
|
||||
@ -41,6 +41,12 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||
"Invalid API key or site ID. Please make sure you're using a valid API key with access to the site you've requested."
|
||||
)
|
||||
|
||||
{:error, :upgrade_required} ->
|
||||
H.payment_required(
|
||||
conn,
|
||||
"#{Plausible.Billing.Feature.StatsAPI.display_name()} is part of the Plausible Business plan. To get access to this feature, please upgrade your account."
|
||||
)
|
||||
|
||||
{:error, :site_locked} ->
|
||||
H.payment_required(
|
||||
conn,
|
||||
@ -61,10 +67,20 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||
is_super_admin? = Plausible.Auth.is_super_admin?(api_key.user_id)
|
||||
|
||||
cond do
|
||||
is_super_admin? -> {:ok, site}
|
||||
Sites.locked?(site) -> {:error, :site_locked}
|
||||
is_member? -> {:ok, site}
|
||||
true -> {:error, :invalid_api_key}
|
||||
is_super_admin? ->
|
||||
{:ok, site}
|
||||
|
||||
Sites.locked?(site) ->
|
||||
{:error, :site_locked}
|
||||
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(api_key.user) !== :ok ->
|
||||
{:error, :upgrade_required}
|
||||
|
||||
is_member? ->
|
||||
{:ok, site}
|
||||
|
||||
true ->
|
||||
{:error, :invalid_api_key}
|
||||
end
|
||||
|
||||
nil ->
|
||||
@ -83,12 +99,6 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||
end
|
||||
end
|
||||
|
||||
defp find_api_key(token) do
|
||||
hashed_key = ApiKey.do_hash(token)
|
||||
found_key = Repo.get_by(ApiKey, key_hash: hashed_key)
|
||||
if found_key, do: {:ok, found_key}, else: {:error, :invalid_api_key}
|
||||
end
|
||||
|
||||
@one_hour 60 * 60 * 1000
|
||||
defp check_api_key_rate_limit(api_key) do
|
||||
case Hammer.check_rate("api_request:#{api_key.id}", @one_hour, api_key.hourly_request_limit) do
|
||||
|
@ -316,10 +316,15 @@
|
||||
id="api-keys"
|
||||
class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-indigo-100 rounded rounded-t-none shadow-md dark:bg-gray-800 dark:border-indigo-500"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100">API keys</h2>
|
||||
|
||||
<h2 class="text-xl font-black dark:text-gray-100">API Keys</h2>
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<PlausibleWeb.Components.Billing.extra_feature_notice
|
||||
billable_user={@current_user}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.StatsAPI}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col mt-6">
|
||||
<div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
@ -370,7 +375,13 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= link("+ New API key", to: "/settings/api-keys/new", class: "button mt-4") %>
|
||||
<.link
|
||||
:if={Plausible.Billing.Feature.StatsAPI.check_availability(@current_user) == :ok}
|
||||
href={Routes.auth_path(@conn, :new_api_key)}
|
||||
class="button mt-4"
|
||||
>
|
||||
+ New API Key
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -388,7 +399,7 @@
|
||||
</p>
|
||||
|
||||
<%= if @subscription && @subscription.status == Plausible.Billing.Subscription.Status.active() do %>
|
||||
<span class="mt-6 bg-gray-300 button dark:bg-gray-600 hover:shadow-none hover:bg-gray-300">
|
||||
<span class="mt-6 bg-gray-300 button dark:bg-gray-600 hover:shadow-none hover:bg-gray-300 cursor-not-allowed">
|
||||
Delete my account
|
||||
</span>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<PlausibleWeb.Components.Billing.extra_feature_notice
|
||||
site={@site}
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.Funnels}
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<PlausibleWeb.Components.Billing.extra_feature_notice
|
||||
site={@site}
|
||||
billable_user={@site.owner}
|
||||
current_user={@current_user}
|
||||
feature_mod={Plausible.Billing.Feature.Props}
|
||||
grandfathered?
|
||||
|
@ -6,7 +6,7 @@
|
||||
"yearly_product_id":"572810",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -15,7 +15,7 @@
|
||||
"yearly_product_id":"590752",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -24,7 +24,7 @@
|
||||
"yearly_product_id":"597486",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -33,7 +33,7 @@
|
||||
"yearly_product_id":"597488",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -42,7 +42,7 @@
|
||||
"yearly_product_id":"597643",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -51,7 +51,7 @@
|
||||
"yearly_product_id":"597310",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -60,7 +60,7 @@
|
||||
"yearly_product_id":"597312",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -69,7 +69,7 @@
|
||||
"yearly_product_id":"642354",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -78,7 +78,7 @@
|
||||
"yearly_product_id":"642356",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -87,6 +87,6 @@
|
||||
"yearly_product_id":"650653",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
}
|
||||
]
|
||||
|
@ -6,7 +6,7 @@
|
||||
"yearly_product_id":"653232",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -15,7 +15,7 @@
|
||||
"yearly_product_id":"653234",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -24,7 +24,7 @@
|
||||
"yearly_product_id":"653236",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -33,7 +33,7 @@
|
||||
"yearly_product_id":"653239",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -42,7 +42,7 @@
|
||||
"yearly_product_id":"653242",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -51,7 +51,7 @@
|
||||
"yearly_product_id":"653254",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -60,7 +60,7 @@
|
||||
"yearly_product_id":"653256",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -69,7 +69,7 @@
|
||||
"yearly_product_id":"653257",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -78,7 +78,7 @@
|
||||
"yearly_product_id":"653258",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -87,6 +87,6 @@
|
||||
"yearly_product_id":"653259",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
}
|
||||
]
|
||||
|
@ -6,7 +6,7 @@
|
||||
"yearly_product_id":"749343",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -15,7 +15,7 @@
|
||||
"yearly_product_id":"749345",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -24,7 +24,7 @@
|
||||
"yearly_product_id":"749347",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -33,7 +33,7 @@
|
||||
"yearly_product_id":"749349",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -42,7 +42,7 @@
|
||||
"yearly_product_id":"749352",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -51,7 +51,7 @@
|
||||
"yearly_product_id":"749355",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -60,7 +60,7 @@
|
||||
"yearly_product_id":"749357",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -69,6 +69,6 @@
|
||||
"yearly_product_id":"749359",
|
||||
"site_limit":50,
|
||||
"team_member_limit":"unlimited",
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
}
|
||||
]
|
||||
|
@ -78,7 +78,7 @@
|
||||
"yearly_product_id":"change-me-b749343",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -87,7 +87,7 @@
|
||||
"yearly_product_id":"change-me-b749345",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -96,7 +96,7 @@
|
||||
"yearly_product_id":"change-me-b749347",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -105,7 +105,7 @@
|
||||
"yearly_product_id":"change-me-b749349",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -114,7 +114,7 @@
|
||||
"yearly_product_id":"change-me-b749352",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -123,7 +123,7 @@
|
||||
"yearly_product_id":"change-me-b749355",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -132,7 +132,7 @@
|
||||
"yearly_product_id":"change-me-b749357",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -141,6 +141,6 @@
|
||||
"yearly_product_id":"change-me-b749359",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props","revenue_goals","funnels"]
|
||||
"extra_features":["props","revenue_goals","funnels","stats_api"]
|
||||
}
|
||||
]
|
||||
|
@ -6,7 +6,7 @@
|
||||
"yearly_product_id":"63859",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -15,7 +15,7 @@
|
||||
"yearly_product_id":"63860",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -24,7 +24,7 @@
|
||||
"yearly_product_id":"63861",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -33,7 +33,7 @@
|
||||
"yearly_product_id":"63862",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -42,7 +42,7 @@
|
||||
"yearly_product_id":"63863",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -51,7 +51,7 @@
|
||||
"yearly_product_id":"63864",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -60,7 +60,7 @@
|
||||
"yearly_product_id":"63865",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"growth",
|
||||
@ -69,7 +69,7 @@
|
||||
"yearly_product_id":"63866",
|
||||
"site_limit":10,
|
||||
"team_member_limit":5,
|
||||
"extra_features":["props"]
|
||||
"extra_features":["props","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -78,7 +78,7 @@
|
||||
"yearly_product_id":"63867",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -87,7 +87,7 @@
|
||||
"yearly_product_id":"63868",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -96,7 +96,7 @@
|
||||
"yearly_product_id":"63869",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -105,7 +105,7 @@
|
||||
"yearly_product_id":"63870",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -114,7 +114,7 @@
|
||||
"yearly_product_id":"63871",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -123,7 +123,7 @@
|
||||
"yearly_product_id":"63872",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -132,7 +132,7 @@
|
||||
"yearly_product_id":"63873",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
},
|
||||
{
|
||||
"kind":"business",
|
||||
@ -141,6 +141,6 @@
|
||||
"yearly_product_id":"63874",
|
||||
"site_limit":50,
|
||||
"team_member_limit":50,
|
||||
"extra_features":["props", "revenue_goals", "funnels"]
|
||||
"extra_features":["props", "revenue_goals", "funnels","stats_api"]
|
||||
}
|
||||
]
|
||||
|
@ -2,6 +2,8 @@ defmodule Plausible.AuthTest do
|
||||
use Plausible.DataCase, async: true
|
||||
alias Plausible.Auth
|
||||
|
||||
@v4_growth_plan_id "change-me-749342"
|
||||
|
||||
describe "user_completed_setup?" do
|
||||
test "is false if user does not have any sites" do
|
||||
user = insert(:user)
|
||||
@ -49,4 +51,50 @@ defmodule Plausible.AuthTest do
|
||||
refute Auth.enterprise_configured?(user_without_plan)
|
||||
refute Auth.enterprise_configured?(nil)
|
||||
end
|
||||
|
||||
describe "create_api_key/3" do
|
||||
test "creates a new api key" do
|
||||
user = insert(:user)
|
||||
key = Ecto.UUID.generate()
|
||||
assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(user, "my new key", key)
|
||||
end
|
||||
|
||||
test "errors when key already exists" do
|
||||
u1 = insert(:user)
|
||||
u2 = insert(:user)
|
||||
key = Ecto.UUID.generate()
|
||||
assert {:ok, %Auth.ApiKey{}} = Auth.create_api_key(u1, "my new key", key)
|
||||
assert {:error, changeset} = Auth.create_api_key(u2, "my other key", key)
|
||||
|
||||
assert changeset.errors[:key] ==
|
||||
{"has already been taken",
|
||||
[constraint: :unique, constraint_name: "api_keys_key_hash_index"]}
|
||||
end
|
||||
|
||||
test "returns error when user is on a growth plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_growth_plan_id))
|
||||
|
||||
assert {:error, :upgrade_required} =
|
||||
Auth.create_api_key(user, "my new key", Ecto.UUID.generate())
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_api_key/2" do
|
||||
test "deletes the record" do
|
||||
user = insert(:user)
|
||||
assert {:ok, api_key} = Auth.create_api_key(user, "my new key", Ecto.UUID.generate())
|
||||
assert :ok = Auth.delete_api_key(user, api_key.id)
|
||||
refute Plausible.Repo.reload(api_key)
|
||||
end
|
||||
|
||||
test "returns error when api key does not exist or does not belong to user" do
|
||||
me = insert(:user)
|
||||
|
||||
other_user = insert(:user)
|
||||
{:ok, other_api_key} = Auth.create_api_key(other_user, "my new key", Ecto.UUID.generate())
|
||||
|
||||
assert {:error, :not_found} = Auth.delete_api_key(me, other_api_key.id)
|
||||
assert {:error, :not_found} = Auth.delete_api_key(me, -1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -13,45 +13,62 @@ defmodule Plausible.Billing.FeatureTest do
|
||||
subscription: build(:subscription, paddle_plan_id: "123321")
|
||||
)
|
||||
|
||||
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
|
||||
assert :ok == unquote(mod).check_availability(user)
|
||||
assert :ok == unquote(mod).check_availability(site)
|
||||
end
|
||||
|
||||
test "#{mod}.check_availability/1 returns :ok when site owner is on a business plan" do
|
||||
user =
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_business_plan_id))
|
||||
|
||||
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
|
||||
assert :ok == unquote(mod).check_availability(user)
|
||||
assert :ok == unquote(mod).check_availability(site)
|
||||
end
|
||||
|
||||
test "#{mod}.check_availability/1 returns error when site owner is on a growth plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_growth_plan_id))
|
||||
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(user)
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(site)
|
||||
end
|
||||
|
||||
test "#{mod}.check_availability/1 returns error when site owner is on an old plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(user)
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(site)
|
||||
end
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on a business plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_business_plan_id))
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an old plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on trial" do
|
||||
user = insert(:user)
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an enterprise plan" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
|
||||
subscription: build(:subscription, paddle_plan_id: "123321")
|
||||
)
|
||||
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns error when user is on a growth plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_growth_plan_id))
|
||||
|
||||
assert {:error, :upgrade_required} ==
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.Props.check_availability/1 applies grandfathering to old plans" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
|
||||
assert :ok == Plausible.Billing.Feature.Props.check_availability(user)
|
||||
assert :ok == Plausible.Billing.Feature.Props.check_availability(site)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.Goals.check_availability/2 always returns :ok" do
|
||||
|
@ -436,9 +436,9 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
user_on_v2 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
|
||||
user_on_v3 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_plan_id))
|
||||
|
||||
assert [:props] == Quota.extra_features_limit(user_on_v1)
|
||||
assert [:props] == Quota.extra_features_limit(user_on_v2)
|
||||
assert [:props] == Quota.extra_features_limit(user_on_v3)
|
||||
assert [:props, :stats_api] == Quota.extra_features_limit(user_on_v1)
|
||||
assert [:props, :stats_api] == Quota.extra_features_limit(user_on_v2)
|
||||
assert [:props, :stats_api] == Quota.extra_features_limit(user_on_v3)
|
||||
end
|
||||
|
||||
test "returns an empty list when user is on free_10k plan" do
|
||||
@ -459,12 +459,12 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
_subscription =
|
||||
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
|
||||
|
||||
assert [:props, :revenue_goals, :funnels] == Quota.extra_features_limit(user)
|
||||
assert [:props, :revenue_goals, :funnels, :stats_api] == Quota.extra_features_limit(user)
|
||||
end
|
||||
|
||||
test "returns all extra features when user in on trial" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 7))
|
||||
assert [:props, :revenue_goals, :funnels] == Quota.extra_features_limit(user)
|
||||
assert [:props, :revenue_goals, :funnels, :stats_api] == Quota.extra_features_limit(user)
|
||||
end
|
||||
|
||||
test "returns previous plan limits for enterprise users who have not paid yet" do
|
||||
@ -474,7 +474,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
subscription: build(:subscription, paddle_plan_id: @v1_plan_id)
|
||||
)
|
||||
|
||||
assert [:props] == Quota.extra_features_limit(user)
|
||||
assert [:props, :stats_api] == Quota.extra_features_limit(user)
|
||||
end
|
||||
|
||||
test "returns trial limits for enterprise users who have not upgraded yet and are on trial" do
|
||||
@ -484,7 +484,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
subscription: nil
|
||||
)
|
||||
|
||||
assert [:props, :revenue_goals, :funnels] == Quota.extra_features_limit(user)
|
||||
assert [:props, :revenue_goals, :funnels, :stats_api] == Quota.extra_features_limit(user)
|
||||
end
|
||||
|
||||
test "returns all extra features for enterprise customers" do
|
||||
@ -494,7 +494,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
subscription: build(:subscription, paddle_plan_id: "123321")
|
||||
)
|
||||
|
||||
assert [:props, :revenue_goals, :funnels] == Quota.extra_features_limit(user)
|
||||
assert [:props, :revenue_goals, :funnels, :stats_api] == Quota.extra_features_limit(user)
|
||||
end
|
||||
|
||||
test "returns all extra features for enterprise customers who are due to change a plan" do
|
||||
@ -505,7 +505,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
)
|
||||
|
||||
insert(:enterprise_plan, user_id: user.id, paddle_plan_id: "new-paddle-plan-id")
|
||||
assert [:props, :revenue_goals, :funnels] == Quota.extra_features_limit(user)
|
||||
assert [:props, :revenue_goals, :funnels, :stats_api] == Quota.extra_features_limit(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -8,10 +8,9 @@ defmodule PlausibleWeb.Components.BillingTest do
|
||||
|
||||
test "extra_feature_notice/1 renders a message when user is on trial" do
|
||||
me = insert(:user)
|
||||
site = :site |> insert(members: [me]) |> Plausible.Repo.preload(:owner)
|
||||
|
||||
assert render_component(&Billing.extra_feature_notice/1,
|
||||
site: site,
|
||||
billable_user: me,
|
||||
current_user: me,
|
||||
feature_mod: Plausible.Billing.Feature.Props
|
||||
) =~
|
||||
@ -20,11 +19,10 @@ defmodule PlausibleWeb.Components.BillingTest do
|
||||
|
||||
test "extra_feature_notice/1 renders an upgrade link when user is the site owner and does not have access to the feature" do
|
||||
me = insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_growth_plan_id))
|
||||
site = :site |> insert(members: [me]) |> Plausible.Repo.preload(:owner)
|
||||
|
||||
rendered =
|
||||
render_component(&Billing.extra_feature_notice/1,
|
||||
site: site,
|
||||
billable_user: me,
|
||||
current_user: me,
|
||||
feature_mod: Plausible.Billing.Feature.Props
|
||||
)
|
||||
@ -38,19 +36,9 @@ defmodule PlausibleWeb.Components.BillingTest do
|
||||
me = insert(:user)
|
||||
owner = insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_growth_plan_id))
|
||||
|
||||
site =
|
||||
:site
|
||||
|> insert(
|
||||
memberships: [
|
||||
build(:site_membership, user: owner, role: :owner),
|
||||
build(:site_membership, user: me, role: :admin)
|
||||
]
|
||||
)
|
||||
|> Plausible.Repo.preload(:owner)
|
||||
|
||||
rendered =
|
||||
render_component(&Billing.extra_feature_notice/1,
|
||||
site: site,
|
||||
billable_user: owner,
|
||||
current_user: me,
|
||||
feature_mod: Plausible.Billing.Feature.Funnels
|
||||
)
|
||||
@ -63,11 +51,10 @@ defmodule PlausibleWeb.Components.BillingTest do
|
||||
|
||||
test "extra_feature_notice/1 does not render a notice when the user has access to the feature" do
|
||||
me = insert(:user, subscription: build(:subscription, paddle_plan_id: @v4_business_plan_id))
|
||||
site = :site |> insert(members: [me]) |> Plausible.Repo.preload(:owner)
|
||||
|
||||
rendered =
|
||||
render_component(&Billing.extra_feature_notice/1,
|
||||
site: site,
|
||||
billable_user: me,
|
||||
current_user: me,
|
||||
feature_mod: Plausible.Billing.Feature.Funnels
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do
|
||||
use PlausibleWeb.ConnCase
|
||||
@v4_growth_plan_id "change-me-749342"
|
||||
|
||||
setup [:create_user, :create_api_key]
|
||||
|
||||
@ -149,6 +150,23 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do
|
||||
})
|
||||
end
|
||||
|
||||
test "returns HTTP 402 when user is on a growth plan", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
api_key: api_key
|
||||
} do
|
||||
insert(:subscription, user: user, paddle_plan_id: @v4_growth_plan_id)
|
||||
site = insert(:site, members: [user])
|
||||
|
||||
conn
|
||||
|> with_api_key(api_key)
|
||||
|> get("/api/v1/stats/aggregate", %{"site_id" => site.domain, "metrics" => "pageviews"})
|
||||
|> assert_error(
|
||||
402,
|
||||
"Stats API is part of the Plausible Business plan. To get access to this feature, please upgrade your account."
|
||||
)
|
||||
end
|
||||
|
||||
defp with_api_key(conn, api_key) do
|
||||
Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key}")
|
||||
end
|
||||
|
@ -978,10 +978,8 @@ defmodule PlausibleWeb.AuthControllerTest do
|
||||
|> ApiKey.changeset(%{"name" => "other user's key"})
|
||||
|> Repo.insert()
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
delete(conn, "/settings/api-keys/#{api_key.id}")
|
||||
end
|
||||
|
||||
conn = delete(conn, "/settings/api-keys/#{api_key.id}")
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Could not find API Key to delete"
|
||||
assert Repo.get(ApiKey, api_key.id)
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user