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,
|
||||
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
|
||||
else
|
||||
{:error, :site_not_found} ->
|
||||
|
|
|
|||
|
|
@ -31,8 +31,15 @@ defmodule Plausible.Plugins.API.SharedLinks do
|
|||
{:ok, Plausible.Site.SharedLink.t()}
|
||||
def get_or_create(site, name, password \\ nil) do
|
||||
case get_by_name(site, name) do
|
||||
nil -> Plausible.Sites.create_shared_link(site, name, password)
|
||||
shared_link -> {:ok, shared_link}
|
||||
nil ->
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -369,13 +369,4 @@ defmodule Plausible.Segments do
|
|||
|
||||
"#{field} #{formatted_message}"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -14,15 +14,27 @@ defmodule Plausible.Site.SharedLink do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(link, attrs \\ %{}) do
|
||||
def changeset(link, attrs \\ %{}, opts \\ []) do
|
||||
link
|
||||
|> cast(attrs, [:slug, :password, :name])
|
||||
|> validate_required([:slug, :name])
|
||||
|> validate_special_name(opts)
|
||||
|> unique_constraint(:slug)
|
||||
|> unique_constraint(:name, name: :shared_links_site_id_name_index)
|
||||
|> hash_password()
|
||||
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
|
||||
case link.changes[:password] do
|
||||
nil ->
|
||||
|
|
|
|||
|
|
@ -6,15 +6,31 @@ defmodule Plausible.Sites do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
alias Plausible.Auth
|
||||
alias Plausible.Billing
|
||||
alias Plausible.Repo
|
||||
alias Plausible.Site
|
||||
alias Plausible.{Auth, Repo, Site, Teams, Billing}
|
||||
alias Plausible.Billing.Feature.SharedLinks
|
||||
alias Plausible.Site.SharedLink
|
||||
alias Plausible.Teams
|
||||
|
||||
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
|
||||
Repo.get_by(Site, domain: domain)
|
||||
end
|
||||
|
|
@ -338,17 +354,21 @@ defmodule Plausible.Sites do
|
|||
!!stats_start_date(site)
|
||||
end
|
||||
|
||||
def create_shared_link(site, name, password \\ nil) do
|
||||
changes =
|
||||
SharedLink.changeset(
|
||||
%SharedLink{
|
||||
site_id: site.id,
|
||||
slug: Nanoid.generate()
|
||||
},
|
||||
%{name: name, password: password}
|
||||
)
|
||||
def create_shared_link(site, name, opts \\ []) do
|
||||
password = Keyword.get(opts, :password)
|
||||
site = Plausible.Repo.preload(site, :team)
|
||||
skip_feature_check? = Keyword.get(opts, :skip_feature_check?, false)
|
||||
|
||||
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
|
||||
|
||||
def shared_link_url(site, link) do
|
||||
|
|
|
|||
|
|
@ -478,6 +478,7 @@ defmodule Plausible.Teams.Billing do
|
|||
"""
|
||||
def features_usage(team, site_ids \\ nil)
|
||||
|
||||
on_ee do
|
||||
def features_usage(nil, nil), do: []
|
||||
|
||||
def features_usage(%Teams.Team{} = team, nil) do
|
||||
|
|
@ -528,35 +529,38 @@ defmodule Plausible.Teams.Billing do
|
|||
end
|
||||
|
||||
def features_usage(nil, site_ids) when is_list(site_ids) do
|
||||
shared_links_usage_q =
|
||||
from l in Plausible.Site.SharedLink,
|
||||
where:
|
||||
l.site_id in ^site_ids and l.name not in ^Plausible.Sites.shared_link_special_names()
|
||||
|
||||
props_usage_q =
|
||||
from s in Plausible.Site,
|
||||
where: s.id in ^site_ids and fragment("cardinality(?) > 0", s.allowed_event_props)
|
||||
|
||||
funnels_usage_q = from f in "funnels", where: f.site_id in ^site_ids
|
||||
|
||||
revenue_goals_usage_q =
|
||||
from g in Plausible.Goal,
|
||||
where: g.site_id in ^site_ids and not is_nil(g.currency)
|
||||
|
||||
queries =
|
||||
on_ee do
|
||||
funnels_usage_q = from f in "funnels", where: f.site_id in ^site_ids
|
||||
site_segments_usage_q =
|
||||
from s in Plausible.Segments.Segment, where: s.site_id in ^site_ids and s.type == :site
|
||||
|
||||
[
|
||||
{Feature.SharedLinks, shared_links_usage_q},
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.Funnels, funnels_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q},
|
||||
{Feature.SiteSegments, Plausible.Segments.get_site_segments_usage_query(site_ids)}
|
||||
{Feature.SiteSegments, site_segments_usage_q}
|
||||
]
|
||||
else
|
||||
[
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
||||
]
|
||||
end
|
||||
|
||||
Enum.reduce(queries, [], fn {feature, query}, acc ->
|
||||
|> 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
|
||||
|
||||
defp query_team_member_emails(team, pending_ownership_site_ids, exclude_emails) do
|
||||
pending_owner_memberships_q =
|
||||
|
|
@ -613,7 +617,7 @@ defmodule Plausible.Teams.Billing do
|
|||
|
||||
case Plans.get_subscription_plan(team.subscription) do
|
||||
%EnterprisePlan{features: features} ->
|
||||
features ++ [SharedLinks]
|
||||
features
|
||||
|
||||
%Plan{features: features} ->
|
||||
features
|
||||
|
|
@ -625,7 +629,7 @@ defmodule Plausible.Teams.Billing do
|
|||
if Teams.on_trial?(team) do
|
||||
Feature.list() -- [SitesAPI]
|
||||
else
|
||||
[Goals, SharedLinks]
|
||||
[Goals]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -162,7 +162,14 @@ defmodule PlausibleWeb.SiteController do
|
|||
|
||||
def settings_visibility(conn, _params) do
|
||||
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
|
||||
|> render("settings_visibility.html",
|
||||
|
|
@ -576,10 +583,15 @@ defmodule PlausibleWeb.SiteController do
|
|||
def create_shared_link(conn, %{"shared_link" => link}) do
|
||||
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} ->
|
||||
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} ->
|
||||
conn
|
||||
|> assign(:skip_plausible_tracking, true)
|
||||
|
|
@ -609,7 +621,7 @@ defmodule PlausibleWeb.SiteController do
|
|||
changeset = Plausible.Site.SharedLink.changeset(shared_link, params)
|
||||
|
||||
case Repo.update(changeset) do
|
||||
{:ok, _created} ->
|
||||
{:ok, _updated} ->
|
||||
redirect(conn, to: Routes.site_path(conn, :settings_visibility, site.domain))
|
||||
|
||||
{:error, changeset} ->
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ defmodule PlausibleWeb.StatsController do
|
|||
alias Plausible.Stats.{Filters, Query}
|
||||
alias Plausible.Teams
|
||||
alias PlausibleWeb.Api
|
||||
alias Plausible.Billing.Feature.SharedLinks
|
||||
|
||||
plug(PlausibleWeb.Plugs.AuthorizeSiteAccess when action in [:stats, :csv_export])
|
||||
|
||||
|
|
@ -341,7 +342,30 @@ defmodule PlausibleWeb.StatsController do
|
|||
end
|
||||
|
||||
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
|
||||
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) ->
|
||||
current_user = conn.assigns[:current_user]
|
||||
site_role = get_fallback_site_role(conn)
|
||||
|
|
@ -379,15 +403,6 @@ defmodule PlausibleWeb.StatsController do
|
|||
load_dashboard_js: true,
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
No Shared Links configured for this site.
|
||||
</p>
|
||||
|
||||
<.table rows={@shared_links}>
|
||||
<.table rows={@shared_links} id="shared-links-table">
|
||||
<:thead>
|
||||
<.th hide_on_mobile>Name</.th>
|
||||
<.th>Link</.th>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,19 @@
|
|||
</svg>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<%= case @conn.assigns[:site_role] do %>
|
||||
<% role when role in [:owner, :billing] -> %>
|
||||
<%= case @conn.assigns do %>
|
||||
<% %{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">
|
||||
<p>
|
||||
This dashboard is locked because you don't have a valid subscription.
|
||||
|
|
@ -40,7 +48,7 @@
|
|||
Manage my subscription
|
||||
</.button_link>
|
||||
</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">
|
||||
<p>
|
||||
Owner of this site must upgrade their subscription plan in order to unlock the stats.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
defmodule Plausible.Billing.FeatureTest do
|
||||
alias Plausible.Billing.Feature.SiteSegments
|
||||
use Plausible.DataCase
|
||||
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
|
||||
test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan" do
|
||||
@v1_growth_plan_id "558018"
|
||||
@v5_growth_plan_id "910429"
|
||||
|
||||
describe "business features (for everyone)" do
|
||||
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)])
|
||||
|
|
@ -14,6 +27,15 @@ defmodule Plausible.Billing.FeatureTest do
|
|||
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)
|
||||
|
|
@ -24,80 +46,117 @@ defmodule Plausible.Billing.FeatureTest do
|
|||
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||
end
|
||||
|
||||
test "#{mod}.check_availability/1 returns error when site owner is on an old plan" do
|
||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
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
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.SharedLinks.check_availability/2 returns :ok when user is on an enterprise plan" do
|
||||
team = new_user() |> subscribe_to_enterprise_plan() |> team_of()
|
||||
assert :ok == Plausible.Billing.Feature.SharedLinks.check_availability(team)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on a business plan" do
|
||||
team = new_user() |> subscribe_to_business_plan() |> 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 old plan" do
|
||||
team = new_user() |> subscribe_to_plan(@v1_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
|
||||
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 == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
assert :ok == unquote(mod).check_availability(team)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an enterprise plan" do
|
||||
describe "business features (grandfathered Growth access before v4)" do
|
||||
for mod <- [Props, StatsAPI] 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: [Plausible.Billing.Feature.StatsAPI]
|
||||
)
|
||||
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [unquote(mod)])
|
||||
|> team_of()
|
||||
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
assert :ok == unquote(mod).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
|
||||
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} ==
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
assert {:error, :upgrade_required} == unquote(mod).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)
|
||||
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 "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)
|
||||
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
|
||||
|
||||
test "Plausible.Billing.Feature.Goals.check_availability/2 always returns :ok" do
|
||||
t1 = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
describe "growth features" do
|
||||
for mod <- [SharedLinks, SiteSegments] 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()
|
||||
|
||||
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
|
||||
|
||||
test "Goals.check_availability/2 always returns :ok" do
|
||||
t1 = new_user() |> subscribe_to_plan(@v1_growth_plan_id) |> team_of()
|
||||
t2 = new_user() |> subscribe_to_growth_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()
|
||||
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t1)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t2)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t3)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t4)
|
||||
assert :ok == Goals.check_availability(t1)
|
||||
assert :ok == Goals.check_availability(t2)
|
||||
assert :ok == Goals.check_availability(t3)
|
||||
assert :ok == Goals.check_availability(t4)
|
||||
end
|
||||
|
||||
for {mod, property} <- [
|
||||
{Plausible.Billing.Feature.Funnels, :funnels_enabled},
|
||||
{Plausible.Billing.Feature.Props, :props_enabled}
|
||||
{Funnels, :funnels_enabled},
|
||||
{Props, :props_enabled}
|
||||
] do
|
||||
test "#{mod}.toggle/2 toggles #{property} on and off" do
|
||||
user = new_user()
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
use Plausible.DataCase, async: true
|
||||
use Plausible
|
||||
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
|
||||
|
||||
on_ee do
|
||||
alias Plausible.Billing.Feature.Funnels
|
||||
alias Plausible.Billing.Feature.RevenueGoals
|
||||
alias Plausible.Billing.Feature.{Funnels, RevenueGoals, SitesAPI}
|
||||
end
|
||||
|
||||
@legacy_plan_id "558746"
|
||||
|
|
@ -471,6 +470,7 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
end
|
||||
end
|
||||
|
||||
on_ee do
|
||||
describe "features_usage/2" do
|
||||
test "returns an empty list for a user/site who does not use any feature" do
|
||||
assert [] == Plausible.Teams.Billing.features_usage(team_of(new_user()))
|
||||
|
|
@ -486,7 +486,6 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
assert [Props] == Plausible.Teams.Billing.features_usage(team)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
test "returns [Funnels] when user/site uses funnels" do
|
||||
user = new_user()
|
||||
site = new_site(owner: user)
|
||||
|
|
@ -509,7 +508,6 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(nil, [site.id])
|
||||
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(team)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns [StatsAPI] when user has a stats api key" do
|
||||
user = new_user(trial_expiry_date: Date.utc_today())
|
||||
|
|
@ -541,7 +539,6 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
assert [Props, StatsAPI] == Plausible.Teams.Billing.features_usage(team, site_ids)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
test "returns multiple features used by the user" do
|
||||
user = new_user()
|
||||
insert(:api_key, user: user)
|
||||
|
|
@ -563,20 +560,19 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
assert [Props, Funnels, RevenueGoals, StatsAPI] ==
|
||||
Plausible.Teams.Billing.features_usage(team)
|
||||
end
|
||||
end
|
||||
|
||||
test "accounts only for sites the user owns" do
|
||||
assert [] == Plausible.Teams.Billing.features_usage(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "allowed_features_for/1" do
|
||||
on_ee 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()
|
||||
|
||||
assert [Goals, Plausible.Billing.Feature.SharedLinks] ==
|
||||
Plausible.Teams.Billing.allowed_features_for(team)
|
||||
assert [Goals] == Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -605,7 +601,11 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
subscribe_to_enterprise_plan(user,
|
||||
monthly_pageview_limit: 100_000,
|
||||
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)
|
||||
|
|
@ -660,10 +660,7 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
|
||||
team = team_of(user)
|
||||
|
||||
assert [
|
||||
Plausible.Billing.Feature.StatsAPI,
|
||||
Plausible.Billing.Feature.SharedLinks
|
||||
] ==
|
||||
assert [Plausible.Billing.Feature.StatsAPI] ==
|
||||
Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
|
||||
|
|
@ -678,8 +675,7 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
|
||||
assert [
|
||||
Plausible.Billing.Feature.StatsAPI,
|
||||
Plausible.Billing.Feature.SitesAPI,
|
||||
Plausible.Billing.Feature.SharedLinks
|
||||
Plausible.Billing.Feature.SitesAPI
|
||||
] ==
|
||||
Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
|
|
@ -1059,4 +1055,31 @@ defmodule Plausible.Billing.QuotaTest do
|
|||
assert suggested_tier == :business
|
||||
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
|
||||
|
|
|
|||
|
|
@ -436,6 +436,39 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
|
|||
res = json_response(conn, 404)
|
||||
assert res["error"] == "Site could not be found"
|
||||
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
|
||||
|
||||
describe "PUT /api/v1/sites/goals" do
|
||||
|
|
|
|||
|
|
@ -691,6 +691,23 @@ defmodule PlausibleWeb.SiteControllerTest do
|
|||
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
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
|
|
@ -1614,6 +1631,39 @@ defmodule PlausibleWeb.SiteControllerTest do
|
|||
refute is_nil(link.password_hash)
|
||||
assert link.name == "New name"
|
||||
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
|
||||
|
||||
describe "GET /sites/:domain/shared-links/edit" do
|
||||
|
|
@ -1641,6 +1691,19 @@ defmodule PlausibleWeb.SiteControllerTest do
|
|||
|
||||
assert link.name == "Updated link name"
|
||||
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
|
||||
|
||||
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!()
|
||||
conn = get(conn, "/" <> locked_site.domain)
|
||||
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"
|
||||
end
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ defmodule PlausibleWeb.StatsControllerTest do
|
|||
|
||||
conn = get(conn, "/" <> locked_site.domain)
|
||||
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"
|
||||
end
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ defmodule PlausibleWeb.StatsControllerTest do
|
|||
|
||||
conn = get(conn, "/" <> locked_site.domain)
|
||||
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"
|
||||
assert resp =~ "Owner of this site must upgrade their subscription plan"
|
||||
end
|
||||
|
|
@ -207,7 +207,7 @@ defmodule PlausibleWeb.StatsControllerTest do
|
|||
locked_site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
|
||||
conn = get(build_conn(), "/" <> locked_site.domain)
|
||||
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"
|
||||
end
|
||||
|
||||
|
|
@ -1331,10 +1331,42 @@ defmodule PlausibleWeb.StatsControllerTest do
|
|||
|
||||
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")
|
||||
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
|
||||
conn = get(conn, "/share/example.com")
|
||||
assert response(conn, 404) =~ "nothing here"
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do
|
|||
"StatsAPI" => true,
|
||||
"SitesAPI" => true,
|
||||
"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
|
||||
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
|
||||
|
||||
describe "get /shared_links" do
|
||||
|
|
|
|||
Loading…
Reference in New Issue