mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +03:00
Check for limits and features used by site before transferring ownership (#3445)
* Simplify team_member_usage query * Check limits before transferring ownership * Extract invite creation to dedicated service module * Simplify team member usage query * Remove unnecessary distinct clause * Delegate CreateInvitation via Memberships
This commit is contained in:
parent
a226773da1
commit
ad05af08a4
@ -108,27 +108,34 @@ defmodule Plausible.Billing.Quota do
|
||||
with the user's sites.
|
||||
"""
|
||||
def team_member_usage(user) do
|
||||
Plausible.Repo.aggregate(team_member_usage_query(user), :count)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def team_member_usage_query(user, site \\ nil) do
|
||||
owned_sites_query = owned_sites_query(user)
|
||||
|
||||
owned_sites_query =
|
||||
if site do
|
||||
where(owned_sites_query, [os], os.site_id == ^site.id)
|
||||
else
|
||||
owned_sites_query
|
||||
end
|
||||
|
||||
team_members_query =
|
||||
from os in subquery(owned_sites_query(user)),
|
||||
from os in subquery(owned_sites_query),
|
||||
inner_join: sm in Plausible.Site.Membership,
|
||||
on: sm.site_id == os.site_id,
|
||||
inner_join: u in assoc(sm, :user),
|
||||
select: %{email: u.email}
|
||||
where: sm.role != :owner,
|
||||
select: u.email
|
||||
|
||||
invitations_and_team_members_query =
|
||||
from i in Plausible.Auth.Invitation,
|
||||
inner_join: os in subquery(owned_sites_query(user)),
|
||||
on: i.site_id == os.site_id,
|
||||
where: i.role != :owner,
|
||||
select: %{email: i.email},
|
||||
union: ^team_members_query
|
||||
|
||||
query =
|
||||
from itm in subquery(invitations_and_team_members_query),
|
||||
where: itm.email != ^user.email,
|
||||
select: count(itm.email, :distinct)
|
||||
|
||||
Plausible.Repo.one(query)
|
||||
from i in Plausible.Auth.Invitation,
|
||||
inner_join: os in subquery(owned_sites_query),
|
||||
on: i.site_id == os.site_id,
|
||||
where: i.role != :owner,
|
||||
select: i.email,
|
||||
union: ^team_members_query
|
||||
end
|
||||
|
||||
@spec features_usage(Plausible.Auth.User.t()) :: [atom()]
|
||||
|
@ -82,7 +82,11 @@ defmodule Plausible.SiteAdmin do
|
||||
|
||||
if new_owner do
|
||||
{:ok, _} =
|
||||
Plausible.Sites.bulk_transfer_ownership(sites, inviter, new_owner.email,
|
||||
Plausible.Site.Memberships.bulk_create_invitation(
|
||||
sites,
|
||||
inviter,
|
||||
new_owner.email,
|
||||
:owner,
|
||||
check_permissions: false
|
||||
)
|
||||
|
||||
@ -100,7 +104,7 @@ defmodule Plausible.SiteAdmin do
|
||||
new_owner = Plausible.Auth.find_user_by(email: email)
|
||||
|
||||
if new_owner do
|
||||
case Plausible.Sites.bulk_transfer_ownership_direct(sites, new_owner) do
|
||||
case Plausible.Site.Memberships.bulk_transfer_ownership_direct(sites, new_owner) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, :transfer_to_self} -> {:error, "User is already an owner of one of the sites"}
|
||||
end
|
||||
|
@ -14,6 +14,14 @@ defmodule Plausible.Site.Memberships do
|
||||
defdelegate reject_invitation(invitation_id, user), to: Memberships.RejectInvitation
|
||||
defdelegate remove_invitation(invitation_id, site), to: Memberships.RemoveInvitation
|
||||
|
||||
defdelegate create_invitation(site, inviter, invitee_email, role),
|
||||
to: Memberships.CreateInvitation
|
||||
|
||||
defdelegate bulk_create_invitation(sites, inviter, invitee_email, role, opts),
|
||||
to: Memberships.CreateInvitation
|
||||
|
||||
defdelegate bulk_transfer_ownership_direct(sites, new_owner), to: Memberships.CreateInvitation
|
||||
|
||||
@spec any?(Auth.User.t()) :: boolean()
|
||||
def any?(user) do
|
||||
user
|
||||
|
181
lib/plausible/site/memberships/create_invitation.ex
Normal file
181
lib/plausible/site/memberships/create_invitation.ex
Normal file
@ -0,0 +1,181 @@
|
||||
defmodule Plausible.Site.Memberships.CreateInvitation do
|
||||
@moduledoc """
|
||||
Service for inviting new or existing users to a sites, including ownershhip
|
||||
transfers.
|
||||
"""
|
||||
|
||||
alias Plausible.Auth.{User, Invitation}
|
||||
alias Plausible.{Site, Sites, Site.Membership}
|
||||
alias Plausible.Billing.Quota
|
||||
import Ecto.Query
|
||||
|
||||
@type invite_error() ::
|
||||
Ecto.Changeset.t()
|
||||
| :already_a_member
|
||||
| :transfer_to_self
|
||||
| {:over_limit, non_neg_integer()}
|
||||
| :forbidden
|
||||
| :upgrade_required
|
||||
|
||||
@spec create_invitation(Site.t(), User.t(), String.t(), atom()) ::
|
||||
{:ok, Invitation.t()} | {:error, invite_error()}
|
||||
@doc """
|
||||
Invites a new team member to the given site. Returns a
|
||||
%Plausible.Auth.Invitation{} struct and sends the invitee an email to accept
|
||||
this invitation.
|
||||
|
||||
The inviter must have enough permissions to invite the new team member,
|
||||
otherwise this function returns `{:error, :forbidden}`.
|
||||
|
||||
If the new team member role is `:owner`, this function handles the invitation
|
||||
as an ownership transfer and requires the inviter to be the owner of the site.
|
||||
"""
|
||||
def create_invitation(site, inviter, invitee_email, role) do
|
||||
Plausible.Repo.transaction(fn ->
|
||||
do_invite(site, inviter, invitee_email, role)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec bulk_transfer_ownership_direct([Site.t()], User.t()) ::
|
||||
{:ok, [Membership.t()]} | {:error, invite_error()}
|
||||
def bulk_transfer_ownership_direct(sites, new_owner) do
|
||||
Plausible.Repo.transaction(fn ->
|
||||
for site <- sites do
|
||||
with site <- Plausible.Repo.preload(site, :owner),
|
||||
:ok <- ensure_transfer_valid(site, new_owner, :owner),
|
||||
{:ok, membership} <- Site.Memberships.transfer_ownership(site, new_owner) do
|
||||
membership
|
||||
else
|
||||
{:error, error} -> Plausible.Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec bulk_create_invitation([Site.t()], User.t(), String.t(), atom(), Keyword.t()) ::
|
||||
{:ok, [Invitation.t()]} | {:error, invite_error()}
|
||||
def bulk_create_invitation(sites, inviter, invitee_email, role, opts \\ []) do
|
||||
Plausible.Repo.transaction(fn ->
|
||||
for site <- sites do
|
||||
do_invite(site, inviter, invitee_email, role, opts)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_invite(site, inviter, invitee_email, role, opts \\ []) do
|
||||
attrs = %{email: invitee_email, role: role, site_id: site.id, inviter_id: inviter.id}
|
||||
|
||||
with site <- Plausible.Repo.preload(site, :owner),
|
||||
:ok <- check_invitation_permissions(site, inviter, role, opts),
|
||||
:ok <- check_team_member_limit(site, role),
|
||||
invitee <- Plausible.Auth.find_user_by(email: invitee_email),
|
||||
:ok <- ensure_transfer_valid(site, invitee, role),
|
||||
:ok <- ensure_new_membership(site, invitee, role),
|
||||
%Ecto.Changeset{} = changeset <- Invitation.new(attrs),
|
||||
{:ok, invitation} <- Plausible.Repo.insert(changeset) do
|
||||
send_invitation_email(invitation, invitee)
|
||||
invitation
|
||||
else
|
||||
{:error, cause} -> Plausible.Repo.rollback(cause)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_invitation_permissions(site, inviter, requested_role, opts) do
|
||||
check_permissions? = Keyword.get(opts, :check_permissions, true)
|
||||
|
||||
if check_permissions? do
|
||||
required_roles = if requested_role == :owner, do: [:owner], else: [:admin, :owner]
|
||||
|
||||
membership_query =
|
||||
from(m in Membership,
|
||||
where: m.user_id == ^inviter.id and m.site_id == ^site.id and m.role in ^required_roles
|
||||
)
|
||||
|
||||
if Plausible.Repo.exists?(membership_query), do: :ok, else: {:error, :forbidden}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp send_invitation_email(invitation, invitee) do
|
||||
invitation = Plausible.Repo.preload(invitation, [:site, :inviter])
|
||||
|
||||
email =
|
||||
case {invitee, invitation.role} do
|
||||
{invitee, :owner} -> PlausibleWeb.Email.ownership_transfer_request(invitation, invitee)
|
||||
{nil, _role} -> PlausibleWeb.Email.new_user_invitation(invitation)
|
||||
{%User{}, _role} -> PlausibleWeb.Email.existing_user_invitation(invitation)
|
||||
end
|
||||
|
||||
Plausible.Mailer.send(email)
|
||||
end
|
||||
|
||||
defp within_team_member_limit_after_transfer?(site, new_owner) do
|
||||
limit = Quota.team_member_limit(new_owner)
|
||||
|
||||
current_usage = Quota.team_member_usage(new_owner)
|
||||
site_usage = Plausible.Repo.aggregate(Quota.team_member_usage_query(site.owner, site), :count)
|
||||
usage_after_transfer = current_usage + site_usage
|
||||
|
||||
Quota.within_limit?(usage_after_transfer, limit)
|
||||
end
|
||||
|
||||
defp within_site_limit_after_transfer?(new_owner) do
|
||||
limit = Quota.site_limit(new_owner)
|
||||
usage_after_transfer = Quota.site_usage(new_owner) + 1
|
||||
|
||||
Quota.within_limit?(usage_after_transfer, limit)
|
||||
end
|
||||
|
||||
defp has_access_to_site_features?(site, new_owner) do
|
||||
features_to_check = [
|
||||
Plausible.Billing.Feature.Props,
|
||||
Plausible.Billing.Feature.RevenueGoals,
|
||||
Plausible.Billing.Feature.Funnels
|
||||
]
|
||||
|
||||
Enum.all?(features_to_check, fn feature ->
|
||||
if feature.enabled?(site), do: feature.check_availability(new_owner) == :ok, else: true
|
||||
end)
|
||||
end
|
||||
|
||||
defp ensure_transfer_valid(%Site{} = site, %User{} = new_owner, :owner) do
|
||||
cond do
|
||||
Sites.role(new_owner.id, site) == :owner -> {:error, :transfer_to_self}
|
||||
not within_team_member_limit_after_transfer?(site, new_owner) -> {:error, :upgrade_required}
|
||||
not within_site_limit_after_transfer?(new_owner) -> {:error, :upgrade_required}
|
||||
not has_access_to_site_features?(site, new_owner) -> {:error, :upgrade_required}
|
||||
true -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_transfer_valid(_site, _invitee, _role) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_new_membership(_site, _invitee, :owner) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_new_membership(site, invitee, _role) do
|
||||
if invitee && Sites.is_member?(invitee.id, site) do
|
||||
{:error, :already_a_member}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_team_member_limit(_site, :owner) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp check_team_member_limit(site, _role) do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
limit = Quota.team_member_limit(site.owner)
|
||||
usage = Quota.team_member_usage(site.owner)
|
||||
|
||||
if Quota.within_limit?(usage, limit),
|
||||
do: :ok,
|
||||
else: {:error, {:over_limit, limit}}
|
||||
end
|
||||
end
|
@ -1,15 +1,7 @@
|
||||
defmodule Plausible.Sites do
|
||||
alias Plausible.{Repo, Site, Site.SharedLink, Auth.User, Billing.Quota}
|
||||
alias PlausibleWeb.Email
|
||||
alias Plausible.{Repo, Site, Site.SharedLink, Billing.Quota}
|
||||
import Ecto.Query
|
||||
|
||||
@type invite_error() ::
|
||||
Ecto.Changeset.t()
|
||||
| :already_a_member
|
||||
| :transfer_to_self
|
||||
| {:over_limit, non_neg_integer()}
|
||||
| :forbidden
|
||||
|
||||
def get_by_domain(domain) do
|
||||
Repo.get_by(Site, domain: domain)
|
||||
end
|
||||
@ -47,139 +39,6 @@ defmodule Plausible.Sites do
|
||||
end
|
||||
end
|
||||
|
||||
@spec bulk_transfer_ownership_direct([Site.t()], Plausible.Auth.User.t()) ::
|
||||
{:ok, [Plausible.Site.Membership.t()]} | {:error, invite_error()}
|
||||
def bulk_transfer_ownership_direct(sites, new_owner) do
|
||||
Repo.transaction(fn ->
|
||||
for site <- sites do
|
||||
with :ok <- ensure_transfer_valid(site, new_owner, :owner),
|
||||
{:ok, membership} <- Site.Memberships.transfer_ownership(site, new_owner) do
|
||||
membership
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec bulk_transfer_ownership(
|
||||
[Site.t()],
|
||||
Plausible.Auth.User.t(),
|
||||
String.t(),
|
||||
Keyword.t()
|
||||
) :: {:ok, [Plausible.Auth.Invitation.t()]} | {:error, invite_error()}
|
||||
def bulk_transfer_ownership(sites, inviter, invitee_email, opts \\ []) do
|
||||
Repo.transaction(fn ->
|
||||
for site <- sites do
|
||||
do_invite(site, inviter, invitee_email, :owner, opts)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec invite(Site.t(), Plausible.Auth.User.t(), String.t(), atom()) ::
|
||||
{:ok, Plausible.Auth.Invitation.t()} | {:error, invite_error()}
|
||||
@doc """
|
||||
Invites a new team member to the given site. Returns a
|
||||
%Plausible.Auth.Invitation{} struct and sends the invitee an email to accept
|
||||
this invitation.
|
||||
|
||||
The inviter must have enough permissions to invite the new team member,
|
||||
otherwise this function returns `{:error, :forbidden}`.
|
||||
|
||||
If the new team member role is `:owner`, this function handles the invitation
|
||||
as an ownership transfer and requires the inviter to be the owner of the site.
|
||||
"""
|
||||
def invite(site, inviter, invitee_email, role) do
|
||||
Repo.transaction(fn ->
|
||||
do_invite(site, inviter, invitee_email, role)
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_invite(site, inviter, invitee_email, role, opts \\ []) do
|
||||
attrs = %{email: invitee_email, role: role, site_id: site.id, inviter_id: inviter.id}
|
||||
|
||||
with :ok <- check_invitation_permissions(site, inviter, role, opts),
|
||||
:ok <- check_team_member_limit(site, role),
|
||||
invitee <- Plausible.Auth.find_user_by(email: invitee_email),
|
||||
:ok <- ensure_transfer_valid(site, invitee, role),
|
||||
:ok <- ensure_new_membership(site, invitee, role),
|
||||
%Ecto.Changeset{} = changeset <- Plausible.Auth.Invitation.new(attrs),
|
||||
{:ok, invitation} <- Repo.insert(changeset) do
|
||||
send_invitation_email(invitation, invitee)
|
||||
invitation
|
||||
else
|
||||
{:error, cause} -> Repo.rollback(cause)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_invitation_permissions(site, inviter, requested_role, opts) do
|
||||
check_permissions? = Keyword.get(opts, :check_permissions, true)
|
||||
|
||||
if check_permissions? do
|
||||
required_roles = if requested_role == :owner, do: [:owner], else: [:admin, :owner]
|
||||
|
||||
membership_query =
|
||||
from(m in Plausible.Site.Membership,
|
||||
where: m.user_id == ^inviter.id and m.site_id == ^site.id and m.role in ^required_roles
|
||||
)
|
||||
|
||||
if Repo.exists?(membership_query), do: :ok, else: {:error, :forbidden}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp send_invitation_email(invitation, invitee) do
|
||||
invitation = Repo.preload(invitation, [:site, :inviter])
|
||||
|
||||
email =
|
||||
case {invitee, invitation.role} do
|
||||
{invitee, :owner} -> Email.ownership_transfer_request(invitation, invitee)
|
||||
{nil, _role} -> Email.new_user_invitation(invitation)
|
||||
{%User{}, _role} -> Email.existing_user_invitation(invitation)
|
||||
end
|
||||
|
||||
Plausible.Mailer.send(email)
|
||||
end
|
||||
|
||||
defp ensure_transfer_valid(site, invitee, :owner) do
|
||||
if invitee && role(invitee.id, site) == :owner do
|
||||
{:error, :transfer_to_self}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_transfer_valid(_site, _invitee, _role) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_new_membership(_site, _invitee, :owner) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_new_membership(site, invitee, _role) do
|
||||
if invitee && is_member?(invitee.id, site) do
|
||||
{:error, :already_a_member}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_team_member_limit(_site, :owner) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp check_team_member_limit(site, _role) do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
limit = Quota.team_member_limit(site.owner)
|
||||
usage = Quota.team_member_usage(site.owner)
|
||||
|
||||
if Quota.within_limit?(usage, limit),
|
||||
do: :ok,
|
||||
else: {:error, {:over_limit, limit}}
|
||||
end
|
||||
|
||||
@spec stats_start_date(Plausible.Site.t()) :: Date.t() | nil
|
||||
@doc """
|
||||
Returns the date of the first event of the given site, or `nil` if the site
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
use PlausibleWeb, :controller
|
||||
use Plausible.Repo
|
||||
alias Plausible.Sites
|
||||
alias Plausible.Site.Membership
|
||||
alias Plausible.Site.{Membership, Memberships}
|
||||
|
||||
@only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership]
|
||||
|
||||
@ -39,7 +39,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
site_domain = conn.assigns[:site].domain
|
||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
|
||||
|
||||
case Sites.invite(site, conn.assigns.current_user, email, role) do
|
||||
case Memberships.create_invitation(site, conn.assigns.current_user, email, role) do
|
||||
{:ok, invitation} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
@ -98,7 +98,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
site_domain = conn.assigns[:site].domain
|
||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
|
||||
|
||||
case Sites.invite(site, conn.assigns.current_user, email, :owner) do
|
||||
case Memberships.create_invitation(site, conn.assigns.current_user, email, :owner) do
|
||||
{:ok, _invitation} ->
|
||||
conn
|
||||
|> put_flash(:success, "Site transfer request has been sent to #{email}")
|
||||
@ -111,6 +111,16 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
|> put_flash(:error, "Can't transfer ownership to existing owner")
|
||||
|> redirect(to: Routes.site_path(conn, :settings_people, site.domain))
|
||||
|
||||
{:error, :upgrade_required} ->
|
||||
conn
|
||||
|> put_flash(:ttl, :timer.seconds(5))
|
||||
|> put_flash(:error_title, "Transfer error")
|
||||
|> put_flash(
|
||||
:error,
|
||||
"The site you're trying to transfer exceeds the invitee's subscription plan. To proceed, please contact us at hello@plausible.io for further assistance."
|
||||
)
|
||||
|> redirect(to: Routes.site_path(conn, :settings_people, site.domain))
|
||||
|
||||
{:error, changeset} ->
|
||||
errors = Plausible.ChangesetHelpers.traverse_errors(changeset)
|
||||
|
||||
|
376
test/plausible/site/memberships/create_invitation_test.exs
Normal file
376
test/plausible/site/memberships/create_invitation_test.exs
Normal file
@ -0,0 +1,376 @@
|
||||
defmodule Plausible.Site.Memberships.CreateInvitationTest do
|
||||
alias Plausible.Site.Memberships.CreateInvitation
|
||||
use Plausible.DataCase
|
||||
use Bamboo.Test
|
||||
|
||||
describe "create_invitation/4" do
|
||||
test "creates an invitation" do
|
||||
inviter = insert(:user)
|
||||
invitee = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||
end
|
||||
|
||||
test "returns validation errors" do
|
||||
inviter = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:error, changeset} = CreateInvitation.create_invitation(site, inviter, "", :viewer)
|
||||
assert {"can't be blank", _} = changeset.errors[:email]
|
||||
end
|
||||
|
||||
test "returns error when user is already a member" do
|
||||
inviter = insert(:user)
|
||||
invitee = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: inviter, role: :owner),
|
||||
build(:site_membership, user: invitee, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :already_a_member} =
|
||||
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||
|
||||
assert {:error, :already_a_member} =
|
||||
CreateInvitation.create_invitation(site, inviter, inviter.email, :viewer)
|
||||
end
|
||||
|
||||
test "sends invitation email for existing users" do
|
||||
[inviter, invitee] = insert_list(2, :user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: invitee.email],
|
||||
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
||||
)
|
||||
end
|
||||
|
||||
test "sends invitation email for new users" do
|
||||
inviter = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :viewer)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: "vini@plausible.test"],
|
||||
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
||||
)
|
||||
end
|
||||
|
||||
test "returns error when owner is over their team member limit" do
|
||||
[owner, inviter, invitee] = insert_list(3, :user)
|
||||
|
||||
memberships =
|
||||
[
|
||||
build(:site_membership, user: owner, role: :owner),
|
||||
build(:site_membership, user: inviter, role: :admin)
|
||||
] ++ build_list(4, :site_membership)
|
||||
|
||||
site = insert(:site, memberships: memberships)
|
||||
|
||||
assert {:error, {:over_limit, 5}} =
|
||||
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
|
||||
end
|
||||
|
||||
test "sends ownership transfer email when inviter role is owner" do
|
||||
inviter = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :owner)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: "vini@plausible.test"],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site.domain}"
|
||||
)
|
||||
end
|
||||
|
||||
test "only allows owners to transfer ownership" do
|
||||
inviter = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: build(:user), role: :owner),
|
||||
build(:site_membership, user: inviter, role: :admin)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :forbidden} =
|
||||
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :owner)
|
||||
end
|
||||
|
||||
test "allows ownership transfer to existing site members" do
|
||||
inviter = insert(:user)
|
||||
invitee = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: inviter, role: :owner),
|
||||
build(:site_membership, user: invitee, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
CreateInvitation.create_invitation(site, inviter, invitee.email, :owner)
|
||||
end
|
||||
|
||||
test "does not allow transferring ownership to existing owner" do
|
||||
inviter = insert(:user, email: "vini@plausible.test")
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: inviter, role: :owner)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :transfer_to_self} =
|
||||
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :owner)
|
||||
end
|
||||
|
||||
test "does not check for limits when transferring ownership" do
|
||||
inviter = insert(:user)
|
||||
|
||||
memberships =
|
||||
[build(:site_membership, user: inviter, role: :owner)] ++ build_list(5, :site_membership)
|
||||
|
||||
site = insert(:site, memberships: memberships)
|
||||
|
||||
assert {:ok, _invitation} =
|
||||
CreateInvitation.create_invitation(
|
||||
site,
|
||||
inviter,
|
||||
"newowner@plausible.test",
|
||||
:owner
|
||||
)
|
||||
end
|
||||
|
||||
test "does not allow viewers to invite users" do
|
||||
inviter = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: build(:user), role: :owner),
|
||||
build(:site_membership, user: inviter, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :forbidden} =
|
||||
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :viewer)
|
||||
end
|
||||
|
||||
test "allows admins to invite other admins" do
|
||||
inviter = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: build(:user), role: :owner),
|
||||
build(:site_membership, user: inviter, role: :admin)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :admin)
|
||||
end
|
||||
|
||||
test "does not allow transferring ownership when site does not fit the new owner subscription" do
|
||||
old_owner = insert(:user, subscription: build(:business_subscription))
|
||||
new_owner = insert(:user, subscription: build(:growth_subscription))
|
||||
|
||||
site_with_too_many_team_members =
|
||||
insert(:site,
|
||||
memberships:
|
||||
[build(:site_membership, user: old_owner, role: :owner)] ++
|
||||
build_list(6, :site_membership, role: :admin)
|
||||
)
|
||||
|
||||
site_using_premium_features =
|
||||
insert(:site,
|
||||
memberships: [build(:site_membership, user: old_owner, role: :owner)],
|
||||
props_enabled: true,
|
||||
funnels_enabled: true
|
||||
)
|
||||
|
||||
assert {:error, :upgrade_required} =
|
||||
CreateInvitation.create_invitation(
|
||||
site_with_too_many_team_members,
|
||||
old_owner,
|
||||
new_owner.email,
|
||||
:owner
|
||||
)
|
||||
|
||||
assert {:error, :upgrade_required} =
|
||||
CreateInvitation.create_invitation(
|
||||
site_using_premium_features,
|
||||
old_owner,
|
||||
new_owner.email,
|
||||
:owner
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "bulk_create_invitation/5" do
|
||||
test "initiates ownership transfer for multiple sites in one action" do
|
||||
admin_user = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 =
|
||||
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
||||
|
||||
site2 =
|
||||
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
||||
|
||||
assert {:ok, _} =
|
||||
CreateInvitation.bulk_create_invitation(
|
||||
[site1, site2],
|
||||
admin_user,
|
||||
new_owner.email,
|
||||
:owner
|
||||
)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
||||
)
|
||||
|
||||
assert Repo.exists?(
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where:
|
||||
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
||||
)
|
||||
)
|
||||
|
||||
assert_invitation_exists(site1, new_owner.email, :owner)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
||||
)
|
||||
|
||||
assert_invitation_exists(site2, new_owner.email, :owner)
|
||||
end
|
||||
|
||||
test "initiates ownership transfer for multiple sites in one action skipping permission checks" do
|
||||
superadmin_user = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 = insert(:site)
|
||||
site2 = insert(:site)
|
||||
|
||||
assert {:ok, _} =
|
||||
CreateInvitation.bulk_create_invitation(
|
||||
[site1, site2],
|
||||
superadmin_user,
|
||||
new_owner.email,
|
||||
:owner,
|
||||
check_permissions: false
|
||||
)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
||||
)
|
||||
|
||||
assert Repo.exists?(
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where:
|
||||
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
||||
)
|
||||
)
|
||||
|
||||
assert_invitation_exists(site1, new_owner.email, :owner)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
||||
)
|
||||
|
||||
assert_invitation_exists(site2, new_owner.email, :owner)
|
||||
end
|
||||
end
|
||||
|
||||
describe "bulk_transfer_ownership_direct/2" do
|
||||
test "transfers ownership for multiple sites in one action" do
|
||||
current_owner = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 =
|
||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||
|
||||
site2 =
|
||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||
|
||||
assert {:ok, _} = CreateInvitation.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site1.id,
|
||||
user_id: new_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site2.id,
|
||||
user_id: new_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site1.id,
|
||||
user_id: current_owner.id,
|
||||
role: :admin
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site2.id,
|
||||
user_id: current_owner.id,
|
||||
role: :admin
|
||||
)
|
||||
end
|
||||
|
||||
test "returns error when user is already an owner for one of the sites" do
|
||||
current_owner = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 =
|
||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||
|
||||
site2 = insert(:site, memberships: [build(:site_membership, user: new_owner, role: :owner)])
|
||||
|
||||
assert {:error, :transfer_to_self} =
|
||||
CreateInvitation.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site1.id,
|
||||
user_id: current_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site2.id,
|
||||
user_id: new_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_invitation_exists(site, email, role) do
|
||||
assert Repo.exists?(
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where: i.site_id == ^site.id and i.email == ^email and i.role == ^role
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
@ -1,6 +1,5 @@
|
||||
defmodule Plausible.SitesTest do
|
||||
use Plausible.DataCase
|
||||
use Bamboo.Test
|
||||
alias Plausible.Sites
|
||||
|
||||
describe "is_member?" do
|
||||
@ -68,310 +67,6 @@ defmodule Plausible.SitesTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "invite/4" do
|
||||
test "creates an invitation" do
|
||||
inviter = insert(:user)
|
||||
invitee = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
Sites.invite(site, inviter, invitee.email, :viewer)
|
||||
end
|
||||
|
||||
test "returns validation errors" do
|
||||
inviter = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:error, changeset} = Sites.invite(site, inviter, "", :viewer)
|
||||
assert {"can't be blank", _} = changeset.errors[:email]
|
||||
end
|
||||
|
||||
test "returns error when user is already a member" do
|
||||
inviter = insert(:user)
|
||||
invitee = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: inviter, role: :owner),
|
||||
build(:site_membership, user: invitee, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :already_a_member} = Sites.invite(site, inviter, invitee.email, :viewer)
|
||||
assert {:error, :already_a_member} = Sites.invite(site, inviter, inviter.email, :viewer)
|
||||
end
|
||||
|
||||
test "sends invitation email for existing users" do
|
||||
[inviter, invitee] = insert_list(2, :user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
Sites.invite(site, inviter, invitee.email, :viewer)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: invitee.email],
|
||||
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
||||
)
|
||||
end
|
||||
|
||||
test "sends invitation email for new users" do
|
||||
inviter = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
Sites.invite(site, inviter, "vini@plausible.test", :viewer)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: "vini@plausible.test"],
|
||||
subject: "[Plausible Analytics] You've been invited to #{site.domain}"
|
||||
)
|
||||
end
|
||||
|
||||
test "returns error when owner is over their team member limit" do
|
||||
[owner, inviter, invitee] = insert_list(3, :user)
|
||||
|
||||
memberships =
|
||||
[
|
||||
build(:site_membership, user: owner, role: :owner),
|
||||
build(:site_membership, user: inviter, role: :admin)
|
||||
] ++ build_list(4, :site_membership)
|
||||
|
||||
site = insert(:site, memberships: memberships)
|
||||
assert {:error, {:over_limit, 5}} = Sites.invite(site, inviter, invitee.email, :viewer)
|
||||
end
|
||||
|
||||
test "sends ownership transfer email when inviter role is owner" do
|
||||
inviter = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: inviter, role: :owner)])
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
Sites.invite(site, inviter, "vini@plausible.test", :owner)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: "vini@plausible.test"],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site.domain}"
|
||||
)
|
||||
end
|
||||
|
||||
test "only allows owners to transfer ownership" do
|
||||
inviter = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: build(:user), role: :owner),
|
||||
build(:site_membership, user: inviter, role: :admin)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :forbidden} = Sites.invite(site, inviter, "vini@plausible.test", :owner)
|
||||
end
|
||||
|
||||
test "allows ownership transfer to existing site members" do
|
||||
inviter = insert(:user)
|
||||
invitee = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: inviter, role: :owner),
|
||||
build(:site_membership, user: invitee, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
Sites.invite(site, inviter, invitee.email, :owner)
|
||||
end
|
||||
|
||||
test "does not allow transferring ownership to existing owner" do
|
||||
inviter = insert(:user, email: "vini@plausible.test")
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: inviter, role: :owner)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :transfer_to_self} =
|
||||
Sites.invite(site, inviter, "vini@plausible.test", :owner)
|
||||
end
|
||||
|
||||
test "does not check for limits when transferring ownership" do
|
||||
inviter = insert(:user)
|
||||
|
||||
memberships =
|
||||
[build(:site_membership, user: inviter, role: :owner)] ++ build_list(5, :site_membership)
|
||||
|
||||
site = insert(:site, memberships: memberships)
|
||||
assert {:ok, _invitation} = Sites.invite(site, inviter, "newowner@plausible.test", :owner)
|
||||
end
|
||||
|
||||
test "does not allow viewers to invite users" do
|
||||
inviter = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: build(:user), role: :owner),
|
||||
build(:site_membership, user: inviter, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:error, :forbidden} = Sites.invite(site, inviter, "vini@plausible.test", :viewer)
|
||||
end
|
||||
|
||||
test "allows admins to invite other admins" do
|
||||
inviter = insert(:user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: build(:user), role: :owner),
|
||||
build(:site_membership, user: inviter, role: :admin)
|
||||
]
|
||||
)
|
||||
|
||||
assert {:ok, %Plausible.Auth.Invitation{}} =
|
||||
Sites.invite(site, inviter, "vini@plausible.test", :admin)
|
||||
end
|
||||
end
|
||||
|
||||
describe "bulk_transfer_ownership/4" do
|
||||
test "initiates ownership transfer for multiple sites in one action" do
|
||||
admin_user = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 =
|
||||
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
||||
|
||||
site2 =
|
||||
insert(:site, memberships: [build(:site_membership, user: admin_user, role: :owner)])
|
||||
|
||||
assert {:ok, _} = Sites.bulk_transfer_ownership([site1, site2], admin_user, new_owner.email)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
||||
)
|
||||
|
||||
assert Repo.exists?(
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where:
|
||||
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
||||
)
|
||||
)
|
||||
|
||||
assert_invitation_exists(site1, new_owner.email, :owner)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
||||
)
|
||||
|
||||
assert_invitation_exists(site2, new_owner.email, :owner)
|
||||
end
|
||||
|
||||
test "initiates ownership transfer for multiple sites in one action skipping permission checks" do
|
||||
superadmin_user = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 = insert(:site)
|
||||
site2 = insert(:site)
|
||||
|
||||
assert {:ok, _} =
|
||||
Sites.bulk_transfer_ownership([site1, site2], superadmin_user, new_owner.email,
|
||||
check_permissions: false
|
||||
)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site1.domain}"
|
||||
)
|
||||
|
||||
assert Repo.exists?(
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where:
|
||||
i.site_id == ^site1.id and i.email == ^new_owner.email and i.role == :owner
|
||||
)
|
||||
)
|
||||
|
||||
assert_invitation_exists(site1, new_owner.email, :owner)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [nil: new_owner.email],
|
||||
subject: "[Plausible Analytics] Request to transfer ownership of #{site2.domain}"
|
||||
)
|
||||
|
||||
assert_invitation_exists(site2, new_owner.email, :owner)
|
||||
end
|
||||
end
|
||||
|
||||
describe "bulk_transfer_ownership_direct/2" do
|
||||
test "transfers ownership for multiple sites in one action" do
|
||||
current_owner = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 =
|
||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||
|
||||
site2 =
|
||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||
|
||||
assert {:ok, _} = Sites.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site1.id,
|
||||
user_id: new_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site2.id,
|
||||
user_id: new_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site1.id,
|
||||
user_id: current_owner.id,
|
||||
role: :admin
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site2.id,
|
||||
user_id: current_owner.id,
|
||||
role: :admin
|
||||
)
|
||||
end
|
||||
|
||||
test "returns error when user is already an owner for one of the sites" do
|
||||
current_owner = insert(:user)
|
||||
new_owner = insert(:user)
|
||||
|
||||
site1 =
|
||||
insert(:site, memberships: [build(:site_membership, user: current_owner, role: :owner)])
|
||||
|
||||
site2 = insert(:site, memberships: [build(:site_membership, user: new_owner, role: :owner)])
|
||||
|
||||
assert {:error, :transfer_to_self} =
|
||||
Sites.bulk_transfer_ownership_direct([site1, site2], new_owner)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site1.id,
|
||||
user_id: current_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
assert Repo.get_by(Plausible.Site.Membership,
|
||||
site_id: site2.id,
|
||||
user_id: new_owner.id,
|
||||
role: :owner
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_for_user/2" do
|
||||
test "get site for super_admin" do
|
||||
user1 = insert(:user)
|
||||
@ -386,12 +81,4 @@ defmodule Plausible.SitesTest do
|
||||
assert %{id: ^site_id} = Sites.get_for_user(user2.id, domain, [:super_admin])
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_invitation_exists(site, email, role) do
|
||||
assert Repo.exists?(
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where: i.site_id == ^site.id and i.email == ^email and i.role == ^role
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user