Starter Tier: Shared Link Feature Gates (#5474)
* feature gate creating shared links * feature gate GET shared_link * stop granting shared links access in code + organize tests * allow GET shared_link for WP * prevent shared link creation with special name unless created by the Plugins API, the name WordPress - Shared Dashboard will be considered reserved. * do not render special shared links in site settings > visibility * remove hardcoded special name from test * add function doc for special names * prevent updates to special name as well * warn about losing access to shared links * make features_usage return empty list on ce * Update lib/plausible/sites.ex Co-authored-by: hq1 <hq@mtod.org> * move special name check to changeset * fix tests --------- Co-authored-by: hq1 <hq@mtod.org>
This commit is contained in:
parent
efc55e323d
commit
4e5093f86c
|
|
@ -300,6 +300,13 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||||
name: link.name,
|
name: link.name,
|
||||||
url: Sites.shared_link_url(site, link)
|
url: Sites.shared_link_url(site, link)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{msg, _} = changeset.errors[:name]
|
||||||
|
H.bad_request(conn, msg)
|
||||||
|
|
||||||
|
{:error, :upgrade_required} ->
|
||||||
|
H.payment_required(conn, "Your current subscription plan does not include Shared Links")
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:error, :site_not_found} ->
|
{:error, :site_not_found} ->
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,15 @@ defmodule Plausible.Plugins.API.SharedLinks do
|
||||||
{:ok, Plausible.Site.SharedLink.t()}
|
{:ok, Plausible.Site.SharedLink.t()}
|
||||||
def get_or_create(site, name, password \\ nil) do
|
def get_or_create(site, name, password \\ nil) do
|
||||||
case get_by_name(site, name) do
|
case get_by_name(site, name) do
|
||||||
nil -> Plausible.Sites.create_shared_link(site, name, password)
|
nil ->
|
||||||
shared_link -> {:ok, shared_link}
|
Plausible.Sites.create_shared_link(site, name,
|
||||||
|
password: password,
|
||||||
|
skip_feature_check?: true,
|
||||||
|
skip_special_name_check?: true
|
||||||
|
)
|
||||||
|
|
||||||
|
shared_link ->
|
||||||
|
{:ok, shared_link}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -369,13 +369,4 @@ defmodule Plausible.Segments do
|
||||||
|
|
||||||
"#{field} #{formatted_message}"
|
"#{field} #{formatted_message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_site_segments_usage_query(list(pos_integer())) :: Ecto.Query.t()
|
|
||||||
def get_site_segments_usage_query(site_ids) do
|
|
||||||
from(segment in Segment,
|
|
||||||
as: :segment,
|
|
||||||
where: segment.type == :site,
|
|
||||||
where: segment.site_id in ^site_ids
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,27 @@ defmodule Plausible.Site.SharedLink do
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(link, attrs \\ %{}) do
|
def changeset(link, attrs \\ %{}, opts \\ []) do
|
||||||
link
|
link
|
||||||
|> cast(attrs, [:slug, :password, :name])
|
|> cast(attrs, [:slug, :password, :name])
|
||||||
|> validate_required([:slug, :name])
|
|> validate_required([:slug, :name])
|
||||||
|
|> validate_special_name(opts)
|
||||||
|> unique_constraint(:slug)
|
|> unique_constraint(:slug)
|
||||||
|> unique_constraint(:name, name: :shared_links_site_id_name_index)
|
|> unique_constraint(:name, name: :shared_links_site_id_name_index)
|
||||||
|> hash_password()
|
|> hash_password()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp validate_special_name(changeset, opts) do
|
||||||
|
name = get_change(changeset, :name)
|
||||||
|
|
||||||
|
if name not in Plausible.Sites.shared_link_special_names() ||
|
||||||
|
Keyword.get(opts, :skip_special_name_check?, false) do
|
||||||
|
changeset
|
||||||
|
else
|
||||||
|
changeset |> add_error(:name, "This name is reserved. Please choose another one")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp hash_password(link) do
|
defp hash_password(link) do
|
||||||
case link.changes[:password] do
|
case link.changes[:password] do
|
||||||
nil ->
|
nil ->
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,31 @@ defmodule Plausible.Sites do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Plausible.Auth
|
alias Plausible.{Auth, Repo, Site, Teams, Billing}
|
||||||
alias Plausible.Billing
|
alias Plausible.Billing.Feature.SharedLinks
|
||||||
alias Plausible.Repo
|
|
||||||
alias Plausible.Site
|
|
||||||
alias Plausible.Site.SharedLink
|
alias Plausible.Site.SharedLink
|
||||||
alias Plausible.Teams
|
|
||||||
|
|
||||||
require Plausible.Site.UserPreference
|
require Plausible.Site.UserPreference
|
||||||
|
|
||||||
|
@shared_link_special_names ["WordPress - Shared Dashboard"]
|
||||||
|
@doc """
|
||||||
|
Special shared link names are used to distinguish between those
|
||||||
|
created by the Plugins API, and those created in any other way
|
||||||
|
(i.e. via Sites API or in the Dashboard Site Settings UI).
|
||||||
|
|
||||||
|
The intent is to give our WP plugin the ability to display an
|
||||||
|
embedded dashboard even when the user's subscription does not
|
||||||
|
support the shared links feature.
|
||||||
|
|
||||||
|
A shared link with a special name can only be created via the
|
||||||
|
plugins API, and it will not show up under the list of shared
|
||||||
|
links in Site Settings > Visibility.
|
||||||
|
|
||||||
|
Once created with the special name, the link will be accessible
|
||||||
|
even when the team does not have access to SharedLinks feature.
|
||||||
|
"""
|
||||||
|
def shared_link_special_names(), do: @shared_link_special_names
|
||||||
|
|
||||||
def get_by_domain(domain) do
|
def get_by_domain(domain) do
|
||||||
Repo.get_by(Site, domain: domain)
|
Repo.get_by(Site, domain: domain)
|
||||||
end
|
end
|
||||||
|
|
@ -338,17 +354,21 @@ defmodule Plausible.Sites do
|
||||||
!!stats_start_date(site)
|
!!stats_start_date(site)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_shared_link(site, name, password \\ nil) do
|
def create_shared_link(site, name, opts \\ []) do
|
||||||
changes =
|
password = Keyword.get(opts, :password)
|
||||||
SharedLink.changeset(
|
site = Plausible.Repo.preload(site, :team)
|
||||||
%SharedLink{
|
skip_feature_check? = Keyword.get(opts, :skip_feature_check?, false)
|
||||||
site_id: site.id,
|
|
||||||
slug: Nanoid.generate()
|
|
||||||
},
|
|
||||||
%{name: name, password: password}
|
|
||||||
)
|
|
||||||
|
|
||||||
Repo.insert(changes)
|
if not skip_feature_check? and SharedLinks.check_availability(site.team) != :ok do
|
||||||
|
{:error, :upgrade_required}
|
||||||
|
else
|
||||||
|
%SharedLink{site_id: site.id, slug: Nanoid.generate()}
|
||||||
|
|> SharedLink.changeset(
|
||||||
|
%{name: name, password: password},
|
||||||
|
Keyword.take(opts, [:skip_special_name_check?])
|
||||||
|
)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def shared_link_url(site, link) do
|
def shared_link_url(site, link) do
|
||||||
|
|
|
||||||
|
|
@ -478,84 +478,88 @@ defmodule Plausible.Teams.Billing do
|
||||||
"""
|
"""
|
||||||
def features_usage(team, site_ids \\ nil)
|
def features_usage(team, site_ids \\ nil)
|
||||||
|
|
||||||
def features_usage(nil, nil), do: []
|
on_ee do
|
||||||
|
def features_usage(nil, nil), do: []
|
||||||
|
|
||||||
def features_usage(%Teams.Team{} = team, nil) do
|
def features_usage(%Teams.Team{} = team, nil) do
|
||||||
owned_site_ids = Teams.owned_sites_ids(team)
|
owned_site_ids = Teams.owned_sites_ids(team)
|
||||||
features_usage(team, owned_site_ids)
|
features_usage(team, owned_site_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
def features_usage(%Teams.Team{} = team, owned_site_ids) when is_list(owned_site_ids) do
|
def features_usage(%Teams.Team{} = team, owned_site_ids) when is_list(owned_site_ids) do
|
||||||
site_scoped_feature_usage = features_usage(nil, owned_site_ids)
|
site_scoped_feature_usage = features_usage(nil, owned_site_ids)
|
||||||
|
|
||||||
stats_api_used? =
|
stats_api_used? =
|
||||||
Repo.exists?(
|
Repo.exists?(
|
||||||
from tm in Plausible.Teams.Membership,
|
from tm in Plausible.Teams.Membership,
|
||||||
as: :team_membership,
|
as: :team_membership,
|
||||||
where: tm.team_id == ^team.id,
|
where: tm.team_id == ^team.id,
|
||||||
where:
|
where:
|
||||||
exists(
|
exists(
|
||||||
from ak in Plausible.Auth.ApiKey,
|
from ak in Plausible.Auth.ApiKey,
|
||||||
where: ak.user_id == parent_as(:team_membership).user_id
|
where: ak.user_id == parent_as(:team_membership).user_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
site_scoped_feature_usage =
|
site_scoped_feature_usage =
|
||||||
if stats_api_used? do
|
if stats_api_used? do
|
||||||
site_scoped_feature_usage ++ [Feature.StatsAPI]
|
site_scoped_feature_usage ++ [Feature.StatsAPI]
|
||||||
|
else
|
||||||
|
site_scoped_feature_usage
|
||||||
|
end
|
||||||
|
|
||||||
|
sites_api_used? =
|
||||||
|
Repo.exists?(
|
||||||
|
from tm in Plausible.Teams.Membership,
|
||||||
|
as: :team_membership,
|
||||||
|
where: tm.team_id == ^team.id,
|
||||||
|
where:
|
||||||
|
exists(
|
||||||
|
from ak in Plausible.Auth.ApiKey,
|
||||||
|
where: ak.user_id == parent_as(:team_membership).user_id,
|
||||||
|
where: "sites:provision:*" in ak.scopes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if sites_api_used? do
|
||||||
|
site_scoped_feature_usage ++ [SitesAPI]
|
||||||
else
|
else
|
||||||
site_scoped_feature_usage
|
site_scoped_feature_usage
|
||||||
end
|
end
|
||||||
|
|
||||||
sites_api_used? =
|
|
||||||
Repo.exists?(
|
|
||||||
from tm in Plausible.Teams.Membership,
|
|
||||||
as: :team_membership,
|
|
||||||
where: tm.team_id == ^team.id,
|
|
||||||
where:
|
|
||||||
exists(
|
|
||||||
from ak in Plausible.Auth.ApiKey,
|
|
||||||
where: ak.user_id == parent_as(:team_membership).user_id,
|
|
||||||
where: "sites:provision:*" in ak.scopes
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if sites_api_used? do
|
|
||||||
site_scoped_feature_usage ++ [SitesAPI]
|
|
||||||
else
|
|
||||||
site_scoped_feature_usage
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def features_usage(nil, site_ids) when is_list(site_ids) do
|
def features_usage(nil, site_ids) when is_list(site_ids) do
|
||||||
props_usage_q =
|
shared_links_usage_q =
|
||||||
from s in Plausible.Site,
|
from l in Plausible.Site.SharedLink,
|
||||||
where: s.id in ^site_ids and fragment("cardinality(?) > 0", s.allowed_event_props)
|
where:
|
||||||
|
l.site_id in ^site_ids and l.name not in ^Plausible.Sites.shared_link_special_names()
|
||||||
|
|
||||||
revenue_goals_usage_q =
|
props_usage_q =
|
||||||
from g in Plausible.Goal,
|
from s in Plausible.Site,
|
||||||
where: g.site_id in ^site_ids and not is_nil(g.currency)
|
where: s.id in ^site_ids and fragment("cardinality(?) > 0", s.allowed_event_props)
|
||||||
|
|
||||||
queries =
|
funnels_usage_q = from f in "funnels", where: f.site_id in ^site_ids
|
||||||
on_ee do
|
|
||||||
funnels_usage_q = from f in "funnels", where: f.site_id in ^site_ids
|
|
||||||
|
|
||||||
[
|
revenue_goals_usage_q =
|
||||||
{Feature.Props, props_usage_q},
|
from g in Plausible.Goal,
|
||||||
{Feature.Funnels, funnels_usage_q},
|
where: g.site_id in ^site_ids and not is_nil(g.currency)
|
||||||
{Feature.RevenueGoals, revenue_goals_usage_q},
|
|
||||||
{Feature.SiteSegments, Plausible.Segments.get_site_segments_usage_query(site_ids)}
|
|
||||||
]
|
|
||||||
else
|
|
||||||
[
|
|
||||||
{Feature.Props, props_usage_q},
|
|
||||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
Enum.reduce(queries, [], fn {feature, query}, acc ->
|
site_segments_usage_q =
|
||||||
if Repo.exists?(query), do: acc ++ [feature], else: acc
|
from s in Plausible.Segments.Segment, where: s.site_id in ^site_ids and s.type == :site
|
||||||
end)
|
|
||||||
|
[
|
||||||
|
{Feature.SharedLinks, shared_links_usage_q},
|
||||||
|
{Feature.Props, props_usage_q},
|
||||||
|
{Feature.Funnels, funnels_usage_q},
|
||||||
|
{Feature.RevenueGoals, revenue_goals_usage_q},
|
||||||
|
{Feature.SiteSegments, site_segments_usage_q}
|
||||||
|
]
|
||||||
|
|> Enum.reduce([], fn {feature, query}, acc ->
|
||||||
|
if Repo.exists?(query), do: acc ++ [feature], else: acc
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
def features_usage(_team, _site_ids), do: []
|
||||||
end
|
end
|
||||||
|
|
||||||
defp query_team_member_emails(team, pending_ownership_site_ids, exclude_emails) do
|
defp query_team_member_emails(team, pending_ownership_site_ids, exclude_emails) do
|
||||||
|
|
@ -613,7 +617,7 @@ defmodule Plausible.Teams.Billing do
|
||||||
|
|
||||||
case Plans.get_subscription_plan(team.subscription) do
|
case Plans.get_subscription_plan(team.subscription) do
|
||||||
%EnterprisePlan{features: features} ->
|
%EnterprisePlan{features: features} ->
|
||||||
features ++ [SharedLinks]
|
features
|
||||||
|
|
||||||
%Plan{features: features} ->
|
%Plan{features: features} ->
|
||||||
features
|
features
|
||||||
|
|
@ -625,7 +629,7 @@ defmodule Plausible.Teams.Billing do
|
||||||
if Teams.on_trial?(team) do
|
if Teams.on_trial?(team) do
|
||||||
Feature.list() -- [SitesAPI]
|
Feature.list() -- [SitesAPI]
|
||||||
else
|
else
|
||||||
[Goals, SharedLinks]
|
[Goals]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,14 @@ defmodule PlausibleWeb.SiteController do
|
||||||
|
|
||||||
def settings_visibility(conn, _params) do
|
def settings_visibility(conn, _params) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
shared_links = Repo.all(from(l in Plausible.Site.SharedLink, where: l.site_id == ^site.id))
|
|
||||||
|
shared_links =
|
||||||
|
Repo.all(
|
||||||
|
from(l in Plausible.Site.SharedLink,
|
||||||
|
where:
|
||||||
|
l.site_id == ^site.id and l.name not in ^Plausible.Sites.shared_link_special_names()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> render("settings_visibility.html",
|
|> render("settings_visibility.html",
|
||||||
|
|
@ -576,10 +583,15 @@ defmodule PlausibleWeb.SiteController do
|
||||||
def create_shared_link(conn, %{"shared_link" => link}) do
|
def create_shared_link(conn, %{"shared_link" => link}) do
|
||||||
site = conn.assigns[:site]
|
site = conn.assigns[:site]
|
||||||
|
|
||||||
case Sites.create_shared_link(site, link["name"], link["password"]) do
|
case Sites.create_shared_link(site, link["name"], password: link["password"]) do
|
||||||
{:ok, _created} ->
|
{:ok, _created} ->
|
||||||
redirect(conn, to: Routes.site_path(conn, :settings_visibility, site.domain))
|
redirect(conn, to: Routes.site_path(conn, :settings_visibility, site.domain))
|
||||||
|
|
||||||
|
{:error, :upgrade_required} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Your current subscription plan does not include Shared Links")
|
||||||
|
|> redirect(to: Routes.site_path(conn, :settings_visibility, site.domain))
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
conn
|
conn
|
||||||
|> assign(:skip_plausible_tracking, true)
|
|> assign(:skip_plausible_tracking, true)
|
||||||
|
|
@ -609,7 +621,7 @@ defmodule PlausibleWeb.SiteController do
|
||||||
changeset = Plausible.Site.SharedLink.changeset(shared_link, params)
|
changeset = Plausible.Site.SharedLink.changeset(shared_link, params)
|
||||||
|
|
||||||
case Repo.update(changeset) do
|
case Repo.update(changeset) do
|
||||||
{:ok, _created} ->
|
{:ok, _updated} ->
|
||||||
redirect(conn, to: Routes.site_path(conn, :settings_visibility, site.domain))
|
redirect(conn, to: Routes.site_path(conn, :settings_visibility, site.domain))
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ defmodule PlausibleWeb.StatsController do
|
||||||
alias Plausible.Stats.{Filters, Query}
|
alias Plausible.Stats.{Filters, Query}
|
||||||
alias Plausible.Teams
|
alias Plausible.Teams
|
||||||
alias PlausibleWeb.Api
|
alias PlausibleWeb.Api
|
||||||
|
alias Plausible.Billing.Feature.SharedLinks
|
||||||
|
|
||||||
plug(PlausibleWeb.Plugs.AuthorizeSiteAccess when action in [:stats, :csv_export])
|
plug(PlausibleWeb.Plugs.AuthorizeSiteAccess when action in [:stats, :csv_export])
|
||||||
|
|
||||||
|
|
@ -341,7 +342,30 @@ defmodule PlausibleWeb.StatsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_shared_link(conn, shared_link) do
|
defp render_shared_link(conn, shared_link) do
|
||||||
|
shared_links_feature_access? =
|
||||||
|
SharedLinks.check_availability(shared_link.site.team) == :ok or
|
||||||
|
shared_link.name in Plausible.Sites.shared_link_special_names()
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
|
Teams.locked?(shared_link.site.team) ->
|
||||||
|
owners = Plausible.Repo.preload(shared_link.site, :owners)
|
||||||
|
|
||||||
|
render(conn, "site_locked.html",
|
||||||
|
owners: owners,
|
||||||
|
site: shared_link.site,
|
||||||
|
dogfood_page_path: "/share/:dashboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
not shared_links_feature_access? ->
|
||||||
|
owners = Plausible.Repo.preload(shared_link.site, :owners)
|
||||||
|
|
||||||
|
render(conn, "site_locked.html",
|
||||||
|
only_shared_link_access_missing?: true,
|
||||||
|
owners: owners,
|
||||||
|
site: shared_link.site,
|
||||||
|
dogfood_page_path: "/share/:dashboard"
|
||||||
|
)
|
||||||
|
|
||||||
not Teams.locked?(shared_link.site.team) ->
|
not Teams.locked?(shared_link.site.team) ->
|
||||||
current_user = conn.assigns[:current_user]
|
current_user = conn.assigns[:current_user]
|
||||||
site_role = get_fallback_site_role(conn)
|
site_role = get_fallback_site_role(conn)
|
||||||
|
|
@ -379,15 +403,6 @@ defmodule PlausibleWeb.StatsController do
|
||||||
load_dashboard_js: true,
|
load_dashboard_js: true,
|
||||||
hide_footer?: if(ce?(), do: embedded?, else: embedded? || site_role != :public)
|
hide_footer?: if(ce?(), do: embedded?, else: embedded? || site_role != :public)
|
||||||
)
|
)
|
||||||
|
|
||||||
Teams.locked?(shared_link.site.team) ->
|
|
||||||
owners = Plausible.Repo.preload(shared_link.site, :owners)
|
|
||||||
|
|
||||||
render(conn, "site_locked.html",
|
|
||||||
owners: owners,
|
|
||||||
site: shared_link.site,
|
|
||||||
dogfood_page_path: "/share/:dashboard"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
No Shared Links configured for this site.
|
No Shared Links configured for this site.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<.table rows={@shared_links}>
|
<.table rows={@shared_links} id="shared-links-table">
|
||||||
<:thead>
|
<:thead>
|
||||||
<.th hide_on_mobile>Name</.th>
|
<.th hide_on_mobile>Name</.th>
|
||||||
<.th>Link</.th>
|
<.th>Link</.th>
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,19 @@
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="mt-6 text-center text-2xl leading-6 font-medium text-gray-900 dark:text-gray-200">
|
<h3 class="mt-6 text-center text-2xl leading-6 font-medium text-gray-900 dark:text-gray-200">
|
||||||
Dashboard locked
|
<%= if @conn.assigns[:only_shared_link_access_missing?] do %>
|
||||||
|
Shared Link Unavailable
|
||||||
|
<% else %>
|
||||||
|
Dashboard Locked
|
||||||
|
<% end %>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<%= case @conn.assigns[:site_role] do %>
|
<%= case @conn.assigns do %>
|
||||||
<% role when role in [:owner, :billing] -> %>
|
<% %{only_shared_link_access_missing?: true} -> %>
|
||||||
|
<p class="mt-6 text-gray-600 dark:text-gray-300 text-center">
|
||||||
|
This shared link is locked because the owner of the site does not have access to the Shared Links feature. To restore it, the owner must upgrade to a suitable plan.
|
||||||
|
</p>
|
||||||
|
<% %{site_role: role} when role in [:owner, :billing] -> %>
|
||||||
<div class="mt-3 text-gray-600 dark:text-gray-300 text-center">
|
<div class="mt-3 text-gray-600 dark:text-gray-300 text-center">
|
||||||
<p>
|
<p>
|
||||||
This dashboard is locked because you don't have a valid subscription.
|
This dashboard is locked because you don't have a valid subscription.
|
||||||
|
|
@ -40,7 +48,7 @@
|
||||||
Manage my subscription
|
Manage my subscription
|
||||||
</.button_link>
|
</.button_link>
|
||||||
</div>
|
</div>
|
||||||
<% role when role in [:admin, :viewer, :editor] -> %>
|
<% %{site_role: role} when role in [:admin, :viewer, :editor] -> %>
|
||||||
<div class="mt-3 text-gray-600 dark:text-gray-300 text-center">
|
<div class="mt-3 text-gray-600 dark:text-gray-300 text-center">
|
||||||
<p>
|
<p>
|
||||||
Owner of this site must upgrade their subscription plan in order to unlock the stats.
|
Owner of this site must upgrade their subscription plan in order to unlock the stats.
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,162 @@
|
||||||
defmodule Plausible.Billing.FeatureTest do
|
defmodule Plausible.Billing.FeatureTest do
|
||||||
|
alias Plausible.Billing.Feature.SiteSegments
|
||||||
use Plausible.DataCase
|
use Plausible.DataCase
|
||||||
use Plausible.Teams.Test
|
use Plausible.Teams.Test
|
||||||
|
|
||||||
@v1_plan_id "558018"
|
alias Plausible.Billing.Feature.{
|
||||||
|
Goals,
|
||||||
|
SiteSegments,
|
||||||
|
SharedLinks,
|
||||||
|
Funnels,
|
||||||
|
RevenueGoals,
|
||||||
|
StatsAPI,
|
||||||
|
Props
|
||||||
|
}
|
||||||
|
|
||||||
for mod <- [Plausible.Billing.Feature.Funnels, Plausible.Billing.Feature.RevenueGoals] do
|
@v1_growth_plan_id "558018"
|
||||||
test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan" do
|
@v5_growth_plan_id "910429"
|
||||||
team =
|
|
||||||
new_user()
|
|
||||||
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [unquote(mod)])
|
|
||||||
|> team_of()
|
|
||||||
|
|
||||||
assert :ok == unquote(mod).check_availability(team)
|
describe "business features (for everyone)" do
|
||||||
end
|
for mod <- [Funnels, RevenueGoals] do
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan that supports #{mod}" do
|
||||||
|
team =
|
||||||
|
new_user()
|
||||||
|
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [unquote(mod)])
|
||||||
|
|> team_of()
|
||||||
|
|
||||||
test "#{mod}.check_availability/1 returns :ok when site owner is on a business plan" do
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
team = new_user() |> subscribe_to_business_plan() |> team_of()
|
end
|
||||||
assert :ok == unquote(mod).check_availability(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "#{mod}.check_availability/1 returns error when site owner is on a growth plan" do
|
test "#{mod}.check_availability/1 returns error when site owner is on a enterprise plan does not support #{mod}" do
|
||||||
team = new_user() |> subscribe_to_growth_plan() |> team_of()
|
team =
|
||||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
new_user()
|
||||||
end
|
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [Goals])
|
||||||
|
|> team_of()
|
||||||
|
|
||||||
test "#{mod}.check_availability/1 returns error when site owner is on an old plan" do
|
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
end
|
||||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when site owner is on a business plan" do
|
||||||
|
team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns error when site owner is on a growth plan" do
|
||||||
|
team = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||||
|
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns error when site owner is on an old v1 plan" do
|
||||||
|
team = new_user() |> subscribe_to_plan(@v1_growth_plan_id) |> team_of()
|
||||||
|
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when user is on trial" do
|
||||||
|
team = new_user(trial_expiry_date: Date.utc_today()) |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Plausible.Billing.Feature.SharedLinks.check_availability/2 returns :ok when user is on an enterprise plan" do
|
describe "business features (grandfathered Growth access before v4)" do
|
||||||
team = new_user() |> subscribe_to_enterprise_plan() |> team_of()
|
for mod <- [Props, StatsAPI] do
|
||||||
assert :ok == Plausible.Billing.Feature.SharedLinks.check_availability(team)
|
test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan that supports #{mod}" do
|
||||||
|
team =
|
||||||
|
new_user()
|
||||||
|
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [unquote(mod)])
|
||||||
|
|> team_of()
|
||||||
|
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns error when site owner is on a enterprise plan does not support #{mod}" do
|
||||||
|
team =
|
||||||
|
new_user()
|
||||||
|
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [Goals])
|
||||||
|
|> team_of()
|
||||||
|
|
||||||
|
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when site owner is on a business plan" do
|
||||||
|
team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns error when site owner is on a new growth plan" do
|
||||||
|
team = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||||
|
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when site owner is on an old plan" do
|
||||||
|
team = new_user() |> subscribe_to_plan(@v1_growth_plan_id) |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when user is on trial" do
|
||||||
|
team = new_user(trial_expiry_date: Date.utc_today()) |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on a business plan" do
|
describe "growth features" do
|
||||||
team = new_user() |> subscribe_to_business_plan() |> team_of()
|
for mod <- [SharedLinks, SiteSegments] do
|
||||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan that supports #{mod}" do
|
||||||
|
team =
|
||||||
|
new_user()
|
||||||
|
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [unquote(mod)])
|
||||||
|
|> team_of()
|
||||||
|
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns error when site owner is on a enterprise plan does not support #{mod}" do
|
||||||
|
team =
|
||||||
|
new_user()
|
||||||
|
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [Goals])
|
||||||
|
|> team_of()
|
||||||
|
|
||||||
|
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when site owner is on a business plan" do
|
||||||
|
team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when site owner is on a growth plan" do
|
||||||
|
team = new_user() |> subscribe_to_plan(@v5_growth_plan_id) |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns error when site owner is on a starter plan" do
|
||||||
|
team = new_user() |> subscribe_to_starter_plan() |> team_of()
|
||||||
|
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#{mod}.check_availability/1 returns :ok when user is on trial" do
|
||||||
|
team = new_user(trial_expiry_date: Date.utc_today()) |> team_of()
|
||||||
|
assert :ok == unquote(mod).check_availability(team)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an old plan" do
|
test "Goals.check_availability/2 always returns :ok" do
|
||||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
t1 = new_user() |> subscribe_to_plan(@v1_growth_plan_id) |> team_of()
|
||||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on trial" do
|
|
||||||
team = new_user(trial_expiry_date: Date.utc_today()) |> team_of()
|
|
||||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an enterprise plan" do
|
|
||||||
team =
|
|
||||||
new_user()
|
|
||||||
|> subscribe_to_enterprise_plan(
|
|
||||||
paddle_plan_id: "123321",
|
|
||||||
features: [Plausible.Billing.Feature.StatsAPI]
|
|
||||||
)
|
|
||||||
|> team_of()
|
|
||||||
|
|
||||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
@tag :ee_only
|
|
||||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns error when user is on a growth plan" do
|
|
||||||
team = new_user() |> subscribe_to_growth_plan() |> team_of()
|
|
||||||
|
|
||||||
assert {:error, :upgrade_required} ==
|
|
||||||
Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
@tag :ee_only
|
|
||||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns error when user trial hasn't started and was created after the business tier launch" do
|
|
||||||
_user = new_user(trial_expiry_date: nil)
|
|
||||||
|
|
||||||
assert {:error, :upgrade_required} ==
|
|
||||||
Plausible.Billing.Feature.StatsAPI.check_availability(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "Plausible.Billing.Feature.Props.check_availability/1 applies grandfathering to old plans" do
|
|
||||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
|
||||||
assert :ok == Plausible.Billing.Feature.Props.check_availability(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "Plausible.Billing.Feature.Goals.check_availability/2 always returns :ok" do
|
|
||||||
t1 = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
|
||||||
t2 = new_user() |> subscribe_to_growth_plan() |> team_of()
|
t2 = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||||
t3 = new_user() |> subscribe_to_business_plan() |> team_of()
|
t3 = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||||
t4 = new_user() |> subscribe_to_enterprise_plan(paddle_plan_id: "123321") |> team_of()
|
t4 = new_user() |> subscribe_to_enterprise_plan(paddle_plan_id: "123321") |> team_of()
|
||||||
|
|
||||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t1)
|
assert :ok == Goals.check_availability(t1)
|
||||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t2)
|
assert :ok == Goals.check_availability(t2)
|
||||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t3)
|
assert :ok == Goals.check_availability(t3)
|
||||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t4)
|
assert :ok == Goals.check_availability(t4)
|
||||||
end
|
end
|
||||||
|
|
||||||
for {mod, property} <- [
|
for {mod, property} <- [
|
||||||
{Plausible.Billing.Feature.Funnels, :funnels_enabled},
|
{Funnels, :funnels_enabled},
|
||||||
{Plausible.Billing.Feature.Props, :props_enabled}
|
{Props, :props_enabled}
|
||||||
] do
|
] do
|
||||||
test "#{mod}.toggle/2 toggles #{property} on and off" do
|
test "#{mod}.toggle/2 toggles #{property} on and off" do
|
||||||
user = new_user()
|
user = new_user()
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
use Plausible.DataCase, async: true
|
use Plausible.DataCase, async: true
|
||||||
use Plausible
|
use Plausible
|
||||||
alias Plausible.Billing.{Quota, Plans}
|
alias Plausible.Billing.{Quota, Plans}
|
||||||
alias Plausible.Billing.Feature.{Goals, Props, SitesAPI, StatsAPI, SharedLinks}
|
alias Plausible.Billing.Feature.{Goals, Props, StatsAPI, SharedLinks}
|
||||||
|
|
||||||
use Plausible.Teams.Test
|
use Plausible.Teams.Test
|
||||||
|
|
||||||
on_ee do
|
on_ee do
|
||||||
alias Plausible.Billing.Feature.Funnels
|
alias Plausible.Billing.Feature.{Funnels, RevenueGoals, SitesAPI}
|
||||||
alias Plausible.Billing.Feature.RevenueGoals
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@legacy_plan_id "558746"
|
@legacy_plan_id "558746"
|
||||||
|
|
@ -471,22 +470,22 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "features_usage/2" do
|
on_ee do
|
||||||
test "returns an empty list for a user/site who does not use any feature" do
|
describe "features_usage/2" do
|
||||||
assert [] == Plausible.Teams.Billing.features_usage(team_of(new_user()))
|
test "returns an empty list for a user/site who does not use any feature" do
|
||||||
assert [] == Plausible.Teams.Billing.features_usage(nil, [new_site().id])
|
assert [] == Plausible.Teams.Billing.features_usage(team_of(new_user()))
|
||||||
end
|
assert [] == Plausible.Teams.Billing.features_usage(nil, [new_site().id])
|
||||||
|
end
|
||||||
|
|
||||||
test "returns [Props] when user/site uses custom props" do
|
test "returns [Props] when user/site uses custom props" do
|
||||||
user = new_user()
|
user = new_user()
|
||||||
site = new_site(owner: user, allowed_event_props: ["dummy"])
|
site = new_site(owner: user, allowed_event_props: ["dummy"])
|
||||||
team = team_of(user)
|
team = team_of(user)
|
||||||
|
|
||||||
assert [Props] == Plausible.Teams.Billing.features_usage(nil, [site.id])
|
assert [Props] == Plausible.Teams.Billing.features_usage(nil, [site.id])
|
||||||
assert [Props] == Plausible.Teams.Billing.features_usage(team)
|
assert [Props] == Plausible.Teams.Billing.features_usage(team)
|
||||||
end
|
end
|
||||||
|
|
||||||
on_ee do
|
|
||||||
test "returns [Funnels] when user/site uses funnels" do
|
test "returns [Funnels] when user/site uses funnels" do
|
||||||
user = new_user()
|
user = new_user()
|
||||||
site = new_site(owner: user)
|
site = new_site(owner: user)
|
||||||
|
|
@ -509,39 +508,37 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(nil, [site.id])
|
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(nil, [site.id])
|
||||||
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(team)
|
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(team)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "returns [StatsAPI] when user has a stats api key" do
|
test "returns [StatsAPI] when user has a stats api key" do
|
||||||
user = new_user(trial_expiry_date: Date.utc_today())
|
user = new_user(trial_expiry_date: Date.utc_today())
|
||||||
team = team_of(user)
|
team = team_of(user)
|
||||||
insert(:api_key, user: user)
|
insert(:api_key, user: user)
|
||||||
|
|
||||||
assert [StatsAPI] == Plausible.Teams.Billing.features_usage(team)
|
assert [StatsAPI] == Plausible.Teams.Billing.features_usage(team)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns [SitesAPI] when user has a Sites API enabled api key" do
|
test "returns [SitesAPI] when user has a Sites API enabled api key" do
|
||||||
user =
|
user =
|
||||||
new_user()
|
new_user()
|
||||||
|> subscribe_to_enterprise_plan(features: [StatsAPI, SitesAPI])
|
|> subscribe_to_enterprise_plan(features: [StatsAPI, SitesAPI])
|
||||||
|
|
||||||
team = team_of(user)
|
team = team_of(user)
|
||||||
|
|
||||||
insert(:api_key, user: user, scopes: ["sites:provision:*"])
|
insert(:api_key, user: user, scopes: ["sites:provision:*"])
|
||||||
|
|
||||||
assert [StatsAPI, SitesAPI] == Plausible.Teams.Billing.features_usage(team)
|
assert [StatsAPI, SitesAPI] == Plausible.Teams.Billing.features_usage(team)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns feature usage based on a user and a custom list of site_ids" do
|
test "returns feature usage based on a user and a custom list of site_ids" do
|
||||||
user = new_user(trial_expiry_date: Date.utc_today())
|
user = new_user(trial_expiry_date: Date.utc_today())
|
||||||
team = team_of(user)
|
team = team_of(user)
|
||||||
insert(:api_key, user: user)
|
insert(:api_key, user: user)
|
||||||
site_using_props = new_site(allowed_event_props: ["dummy"])
|
site_using_props = new_site(allowed_event_props: ["dummy"])
|
||||||
|
|
||||||
site_ids = [site_using_props.id]
|
site_ids = [site_using_props.id]
|
||||||
assert [Props, StatsAPI] == Plausible.Teams.Billing.features_usage(team, site_ids)
|
assert [Props, StatsAPI] == Plausible.Teams.Billing.features_usage(team, site_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
on_ee do
|
|
||||||
test "returns multiple features used by the user" do
|
test "returns multiple features used by the user" do
|
||||||
user = new_user()
|
user = new_user()
|
||||||
insert(:api_key, user: user)
|
insert(:api_key, user: user)
|
||||||
|
|
@ -563,10 +560,10 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
assert [Props, Funnels, RevenueGoals, StatsAPI] ==
|
assert [Props, Funnels, RevenueGoals, StatsAPI] ==
|
||||||
Plausible.Teams.Billing.features_usage(team)
|
Plausible.Teams.Billing.features_usage(team)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
test "accounts only for sites the user owns" do
|
test "accounts only for sites the user owns" do
|
||||||
assert [] == Plausible.Teams.Billing.features_usage(nil)
|
assert [] == Plausible.Teams.Billing.features_usage(nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -575,8 +572,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
test "users with expired trials have no access to subscription features" do
|
test "users with expired trials have no access to subscription features" do
|
||||||
team = new_user(trial_expiry_date: ~D[2023-01-01]) |> team_of()
|
team = new_user(trial_expiry_date: ~D[2023-01-01]) |> team_of()
|
||||||
|
|
||||||
assert [Goals, Plausible.Billing.Feature.SharedLinks] ==
|
assert [Goals] == Plausible.Teams.Billing.allowed_features_for(team)
|
||||||
Plausible.Teams.Billing.allowed_features_for(team)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -605,7 +601,11 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
subscribe_to_enterprise_plan(user,
|
subscribe_to_enterprise_plan(user,
|
||||||
monthly_pageview_limit: 100_000,
|
monthly_pageview_limit: 100_000,
|
||||||
site_limit: 500,
|
site_limit: 500,
|
||||||
features: [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.Funnels]
|
features: [
|
||||||
|
Plausible.Billing.Feature.StatsAPI,
|
||||||
|
Plausible.Billing.Feature.Funnels,
|
||||||
|
Plausible.Billing.Feature.SharedLinks
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
team = team_of(user)
|
team = team_of(user)
|
||||||
|
|
@ -660,10 +660,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
|
|
||||||
team = team_of(user)
|
team = team_of(user)
|
||||||
|
|
||||||
assert [
|
assert [Plausible.Billing.Feature.StatsAPI] ==
|
||||||
Plausible.Billing.Feature.StatsAPI,
|
|
||||||
Plausible.Billing.Feature.SharedLinks
|
|
||||||
] ==
|
|
||||||
Plausible.Teams.Billing.allowed_features_for(team)
|
Plausible.Teams.Billing.allowed_features_for(team)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -678,8 +675,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
Plausible.Billing.Feature.StatsAPI,
|
Plausible.Billing.Feature.StatsAPI,
|
||||||
Plausible.Billing.Feature.SitesAPI,
|
Plausible.Billing.Feature.SitesAPI
|
||||||
Plausible.Billing.Feature.SharedLinks
|
|
||||||
] ==
|
] ==
|
||||||
Plausible.Teams.Billing.allowed_features_for(team)
|
Plausible.Teams.Billing.allowed_features_for(team)
|
||||||
end
|
end
|
||||||
|
|
@ -1059,4 +1055,31 @@ defmodule Plausible.Billing.QuotaTest do
|
||||||
assert suggested_tier == :business
|
assert suggested_tier == :business
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "feature usage and ensuring access" do
|
||||||
|
@describetag :ee_only
|
||||||
|
setup [:create_user, :create_site]
|
||||||
|
|
||||||
|
test "subscribing to Starter plan when using shared links", %{user: user, site: site} do
|
||||||
|
insert(:shared_link, site: site)
|
||||||
|
|
||||||
|
usage = team_of(user) |> Plausible.Teams.Billing.quota_usage(with_features: true)
|
||||||
|
plan = Plausible.Billing.Plans.find(@v5_10m_starter_plan_id)
|
||||||
|
|
||||||
|
assert {:error, {:unavailable_features, [SharedLinks]}} =
|
||||||
|
Quota.ensure_feature_access(usage, plan)
|
||||||
|
end
|
||||||
|
|
||||||
|
for special_name <- Plausible.Sites.shared_link_special_names() do
|
||||||
|
test "having a shared link with the name '#{special_name}' does not count as using shared links",
|
||||||
|
%{user: user, site: site} do
|
||||||
|
insert(:shared_link, site: site, name: unquote(special_name))
|
||||||
|
|
||||||
|
usage = team_of(user) |> Plausible.Teams.Billing.quota_usage(with_features: true)
|
||||||
|
plan = Plausible.Billing.Plans.find(@v5_10m_starter_plan_id)
|
||||||
|
|
||||||
|
assert :ok = Quota.ensure_feature_access(usage, plan)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -436,6 +436,39 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
||||||
res = json_response(conn, 404)
|
res = json_response(conn, 404)
|
||||||
assert res["error"] == "Site could not be found"
|
assert res["error"] == "Site could not be found"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fails to create without access to SharedLinks feature", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site,
|
||||||
|
user: user
|
||||||
|
} do
|
||||||
|
subscribe_to_enterprise_plan(user, features: [Plausible.Billing.Feature.SitesAPI])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
put(conn, "/api/v1/sites/shared-links", %{
|
||||||
|
site_id: site.domain,
|
||||||
|
name: "My Link"
|
||||||
|
})
|
||||||
|
|
||||||
|
res = json_response(conn, 402)
|
||||||
|
|
||||||
|
assert res["error"] == "Your current subscription plan does not include Shared Links"
|
||||||
|
end
|
||||||
|
|
||||||
|
for special_name <- Plausible.Sites.shared_link_special_names() do
|
||||||
|
test "fails to create with the special '#{special_name}' name intended for Plugins API",
|
||||||
|
%{conn: conn, site: site} do
|
||||||
|
conn =
|
||||||
|
put(conn, "/api/v1/sites/shared-links", %{
|
||||||
|
site_id: site.domain,
|
||||||
|
name: unquote(special_name)
|
||||||
|
})
|
||||||
|
|
||||||
|
res = json_response(conn, 400)
|
||||||
|
|
||||||
|
assert res["error"] == "This name is reserved. Please choose another one"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "PUT /api/v1/sites/goals" do
|
describe "PUT /api/v1/sites/goals" do
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,23 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /:domain/settings/visibility" do
|
||||||
|
setup [:create_user, :log_in, :create_site]
|
||||||
|
|
||||||
|
test "does not render shared links with special names", %{conn: conn, site: site} do
|
||||||
|
for special_name <- Plausible.Sites.shared_link_special_names() do
|
||||||
|
insert(:shared_link, name: special_name, site: site)
|
||||||
|
|
||||||
|
html =
|
||||||
|
conn
|
||||||
|
|> get("/#{site.domain}/settings/visibility")
|
||||||
|
|> html_response(200)
|
||||||
|
|
||||||
|
refute text_of_element(html, "#shared-links-table") =~ special_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "GET /:domain/settings/goals" do
|
describe "GET /:domain/settings/goals" do
|
||||||
setup [:create_user, :log_in, :create_site]
|
setup [:create_user, :log_in, :create_site]
|
||||||
|
|
||||||
|
|
@ -1614,6 +1631,39 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||||
refute is_nil(link.password_hash)
|
refute is_nil(link.password_hash)
|
||||||
assert link.name == "New name"
|
assert link.name == "New name"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fails to create when subscription plan doesn't support the shared links feature", %{
|
||||||
|
conn: conn,
|
||||||
|
user: user,
|
||||||
|
site: site
|
||||||
|
} do
|
||||||
|
subscribe_to_starter_plan(user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
post(conn, "/sites/#{site.domain}/shared-links", %{
|
||||||
|
"shared_link" => %{"name" => "Something"}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert redirected_to(conn, 302) == Routes.site_path(conn, :settings_visibility, site.domain)
|
||||||
|
|
||||||
|
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
|
||||||
|
"Your current subscription plan does not include Shared Links"
|
||||||
|
|
||||||
|
refute Repo.exists?(Plausible.Site.SharedLink)
|
||||||
|
end
|
||||||
|
|
||||||
|
for special_name <- Plausible.Sites.shared_link_special_names() do
|
||||||
|
test "fails to create with the special '#{special_name}' name intended for Plugins API",
|
||||||
|
%{conn: conn, site: site} do
|
||||||
|
conn =
|
||||||
|
post(conn, "/sites/#{site.domain}/shared-links", %{
|
||||||
|
"shared_link" => %{"name" => unquote(special_name)}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert html_response(conn, 200) =~ "This name is reserved. Please choose another one"
|
||||||
|
refute Repo.exists?(Plausible.Site.SharedLink)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /sites/:domain/shared-links/edit" do
|
describe "GET /sites/:domain/shared-links/edit" do
|
||||||
|
|
@ -1641,6 +1691,19 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||||
|
|
||||||
assert link.name == "Updated link name"
|
assert link.name == "Updated link name"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
for special_name <- Plausible.Sites.shared_link_special_names() do
|
||||||
|
test "fails to change link name to #{special_name}", %{conn: conn, site: site} do
|
||||||
|
link = insert(:shared_link, site: site)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
put(conn, "/sites/#{site.domain}/shared-links/#{link.slug}", %{
|
||||||
|
"shared_link" => %{"name" => unquote(special_name)}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert html_response(conn, 200) =~ "This name is reserved. Please choose another one"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "DELETE /sites/:domain/shared-links/:slug" do
|
describe "DELETE /sites/:domain/shared-links/:slug" do
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ defmodule PlausibleWeb.StatsControllerTest do
|
||||||
locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
|
locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
|
||||||
conn = get(conn, "/" <> locked_site.domain)
|
conn = get(conn, "/" <> locked_site.domain)
|
||||||
resp = html_response(conn, 200)
|
resp = html_response(conn, 200)
|
||||||
assert resp =~ "Dashboard locked"
|
assert resp =~ "Dashboard Locked"
|
||||||
assert resp =~ "Please subscribe to the appropriate tier with the link below"
|
assert resp =~ "Please subscribe to the appropriate tier with the link below"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -185,7 +185,7 @@ defmodule PlausibleWeb.StatsControllerTest do
|
||||||
|
|
||||||
conn = get(conn, "/" <> locked_site.domain)
|
conn = get(conn, "/" <> locked_site.domain)
|
||||||
resp = html_response(conn, 200)
|
resp = html_response(conn, 200)
|
||||||
assert resp =~ "Dashboard locked"
|
assert resp =~ "Dashboard Locked"
|
||||||
assert resp =~ "Please subscribe to the appropriate tier with the link below"
|
assert resp =~ "Please subscribe to the appropriate tier with the link below"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -197,7 +197,7 @@ defmodule PlausibleWeb.StatsControllerTest do
|
||||||
|
|
||||||
conn = get(conn, "/" <> locked_site.domain)
|
conn = get(conn, "/" <> locked_site.domain)
|
||||||
resp = html_response(conn, 200)
|
resp = html_response(conn, 200)
|
||||||
assert resp =~ "Dashboard locked"
|
assert resp =~ "Dashboard Locked"
|
||||||
refute resp =~ "Please subscribe to the appropriate tier with the link below"
|
refute resp =~ "Please subscribe to the appropriate tier with the link below"
|
||||||
assert resp =~ "Owner of this site must upgrade their subscription plan"
|
assert resp =~ "Owner of this site must upgrade their subscription plan"
|
||||||
end
|
end
|
||||||
|
|
@ -207,7 +207,7 @@ defmodule PlausibleWeb.StatsControllerTest do
|
||||||
locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
|
locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
|
||||||
conn = get(build_conn(), "/" <> locked_site.domain)
|
conn = get(build_conn(), "/" <> locked_site.domain)
|
||||||
resp = html_response(conn, 200)
|
resp = html_response(conn, 200)
|
||||||
assert resp =~ "Dashboard locked"
|
assert resp =~ "Dashboard Locked"
|
||||||
assert resp =~ "You can check back later or contact the site owner"
|
assert resp =~ "You can check back later or contact the site owner"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1331,10 +1331,42 @@ defmodule PlausibleWeb.StatsControllerTest do
|
||||||
|
|
||||||
conn = get(conn, "/share/test-site.com/?auth=#{link.slug}")
|
conn = get(conn, "/share/test-site.com/?auth=#{link.slug}")
|
||||||
|
|
||||||
assert html_response(conn, 200) =~ "Dashboard locked"
|
assert html_response(conn, 200) =~ "Dashboard Locked"
|
||||||
refute String.contains?(html_response(conn, 200), "Back to my sites")
|
refute String.contains?(html_response(conn, 200), "Back to my sites")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "shows locked page if shared link is locked due to insufficient team subscription", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
site = new_site(domain: "test-site.com")
|
||||||
|
link = insert(:shared_link, site: site)
|
||||||
|
|
||||||
|
insert(:starter_subscription, team: site.team)
|
||||||
|
|
||||||
|
conn = get(conn, "/share/test-site.com/?auth=#{link.slug}")
|
||||||
|
|
||||||
|
assert html_response(conn, 200) =~ "Shared Link Unavailable"
|
||||||
|
refute String.contains?(html_response(conn, 200), "Back to my sites")
|
||||||
|
end
|
||||||
|
|
||||||
|
for special_name <- Plausible.Sites.shared_link_special_names() do
|
||||||
|
test "shows dashboard if team subscription insufficient but shared link name is '#{special_name}'",
|
||||||
|
%{conn: conn} do
|
||||||
|
site = new_site(domain: "test-site.com")
|
||||||
|
link = insert(:shared_link, site: site, name: unquote(special_name))
|
||||||
|
|
||||||
|
insert(:starter_subscription, team: site.team)
|
||||||
|
|
||||||
|
html =
|
||||||
|
conn
|
||||||
|
|> get("/share/test-site.com/?auth=#{link.slug}")
|
||||||
|
|> html_response(200)
|
||||||
|
|
||||||
|
assert element_exists?(html, @react_container)
|
||||||
|
refute html =~ "Shared Link Unavailable"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "renders 404 not found when no auth parameter supplied", %{conn: conn} do
|
test "renders 404 not found when no auth parameter supplied", %{conn: conn} do
|
||||||
conn = get(conn, "/share/example.com")
|
conn = get(conn, "/share/example.com")
|
||||||
assert response(conn, 404) =~ "nothing here"
|
assert response(conn, 404) =~ "nothing here"
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do
|
||||||
"StatsAPI" => true,
|
"StatsAPI" => true,
|
||||||
"SitesAPI" => true,
|
"SitesAPI" => true,
|
||||||
"SiteSegments" => false,
|
"SiteSegments" => false,
|
||||||
"SharedLinks" => true
|
"SharedLinks" => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,58 @@ defmodule PlausibleWeb.Plugins.API.Controllers.SharedLinksTest do
|
||||||
|
|
||||||
assert %{errors: [%{detail: "Missing field: shared_link"}]} = resp
|
assert %{errors: [%{detail: "Missing field: shared_link"}]} = resp
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "skips shared links feature access check", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site,
|
||||||
|
token: token
|
||||||
|
} do
|
||||||
|
insert(:starter_subscription, team: site.team)
|
||||||
|
|
||||||
|
url = Routes.plugins_api_shared_links_url(PlausibleWeb.Endpoint, :create)
|
||||||
|
|
||||||
|
resp =
|
||||||
|
authenticate(conn, site.domain, token)
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put(url, %{
|
||||||
|
shared_link: %{
|
||||||
|
name: "My Shared Link"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|> json_response(201)
|
||||||
|
|> assert_schema("SharedLink", spec())
|
||||||
|
|
||||||
|
assert resp.shared_link.name == "My Shared Link"
|
||||||
|
|
||||||
|
assert resp.shared_link.href =~
|
||||||
|
"http://localhost:8000/share/#{URI.encode_www_form(site.domain)}?auth="
|
||||||
|
end
|
||||||
|
|
||||||
|
for special_name <- Plausible.Sites.shared_link_special_names() do
|
||||||
|
test "can create shared link with the reserved '#{special_name}' name", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site,
|
||||||
|
token: token
|
||||||
|
} do
|
||||||
|
url = Routes.plugins_api_shared_links_url(PlausibleWeb.Endpoint, :create)
|
||||||
|
|
||||||
|
resp =
|
||||||
|
authenticate(conn, site.domain, token)
|
||||||
|
|> put_req_header("content-type", "application/json")
|
||||||
|
|> put(url, %{
|
||||||
|
shared_link: %{
|
||||||
|
name: unquote(special_name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|> json_response(201)
|
||||||
|
|> assert_schema("SharedLink", spec())
|
||||||
|
|
||||||
|
assert resp.shared_link.name == unquote(special_name)
|
||||||
|
|
||||||
|
assert resp.shared_link.href =~
|
||||||
|
"http://localhost:8000/share/#{URI.encode_www_form(site.domain)}?auth="
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get /shared_links" do
|
describe "get /shared_links" do
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue