diff --git a/lib/plausible/teams/invitation.ex b/lib/plausible/teams/invitation.ex
index 5a5722a09fd9..71d8a977b19c 100644
--- a/lib/plausible/teams/invitation.ex
+++ b/lib/plausible/teams/invitation.ex
@@ -7,12 +7,14 @@ defmodule Plausible.Teams.Invitation do
import Ecto.Changeset
+ @roles Plausible.Teams.Membership.roles()
+
@type t() :: %__MODULE__{}
schema "team_invitations" do
field :invitation_id, :string
field :email, :string
- field :role, Ecto.Enum, values: [:guest, :viewer, :editor, :admin, :owner]
+ field :role, Ecto.Enum, values: @roles
belongs_to :inviter, Plausible.Auth.User
belongs_to :team, Plausible.Teams.Team
@@ -22,6 +24,8 @@ defmodule Plausible.Teams.Invitation do
timestamps()
end
+ def roles(), do: @roles
+
def changeset(team, opts) do
email = Keyword.fetch!(opts, :email)
role = Keyword.fetch!(opts, :role)
diff --git a/lib/plausible/teams/invitations.ex b/lib/plausible/teams/invitations.ex
index ff064e001092..9f5129ca12f3 100644
--- a/lib/plausible/teams/invitations.ex
+++ b/lib/plausible/teams/invitations.ex
@@ -87,7 +87,11 @@ defmodule Plausible.Teams.Invitations do
end
end
- def invite(site, invitee_email, role, inviter) do
+ def invite(%Teams.Team{} = team, invitee_email, role, inviter) do
+ create_team_invitation(team, invitee_email, inviter, role: role)
+ end
+
+ def invite(%Plausible.Site{} = site, invitee_email, role, inviter) do
site = Teams.load_for_site(site)
if role == :owner do
@@ -376,7 +380,27 @@ defmodule Plausible.Teams.Invitations do
end
@doc false
- def check_invitation_permissions(site, inviter, invitation_role, opts) do
+ def check_invitation_permissions(%Teams.Team{} = team, inviter, invitation_role, opts) do
+ check_permissions? = Keyword.get(opts, :check_permissions, true)
+
+ if check_permissions? do
+ case Teams.Memberships.team_role(team, inviter) do
+ {:ok, :owner} when invitation_role == :owner ->
+ :ok
+
+ {:ok, inviter_role}
+ when inviter_role in [:owner, :admin] and invitation_role != :owner ->
+ :ok
+
+ _ ->
+ {:error, :forbidden}
+ end
+ else
+ :ok
+ end
+ end
+
+ def check_invitation_permissions(%Plausible.Site{} = site, inviter, invitation_role, opts) do
check_permissions? = Keyword.get(opts, :check_permissions, true)
if check_permissions? do
@@ -411,11 +435,19 @@ defmodule Plausible.Teams.Invitations do
end
@doc false
- def ensure_new_membership(_site, nil, _role), do: :ok
+ def ensure_new_membership(_site_or_team, nil, _role), do: :ok
+
+ def ensure_new_membership(_site_or_team, _invitee, :owner), do: :ok
- def ensure_new_membership(_site, _invitee, :owner), do: :ok
+ def ensure_new_membership(%Teams.Team{} = team, invitee, _role) do
+ case Teams.Memberships.team_role(team, invitee) do
+ {:ok, :guest} -> :ok
+ {:error, :not_a_member} -> :ok
+ {:ok, _} -> {:error, :already_a_member}
+ end
+ end
- def ensure_new_membership(site, invitee, _role) do
+ def ensure_new_membership(%Plausible.Site{} = site, invitee, _role) do
if Teams.Memberships.site_role(site, invitee) == {:error, :not_a_member} do
:ok
else
@@ -439,12 +471,13 @@ defmodule Plausible.Teams.Invitations do
defp create_team_invitation(team, invitee_email, inviter, opts \\ []) do
now = NaiveDateTime.utc_now(:second)
+ role = Keyword.get(opts, :role, :guest)
result =
Ecto.Multi.new()
|> Ecto.Multi.put(
:changeset,
- Teams.Invitation.changeset(team, email: invitee_email, role: :guest, inviter: inviter)
+ Teams.Invitation.changeset(team, email: invitee_email, role: role, inviter: inviter)
)
|> Ecto.Multi.run(:ensure_no_site_transfers, fn _repo, %{changeset: changeset} ->
ensure_no_site_transfers(changeset, opts[:ensure_no_site_transfers_for], invitee_email)
@@ -452,10 +485,19 @@ defmodule Plausible.Teams.Invitations do
|> Ecto.Multi.insert(
:team_invitation,
& &1.changeset,
- on_conflict: [set: [updated_at: now]],
+ on_conflict: [set: [updated_at: now, role: role]],
conflict_target: [:team_id, :email],
returning: true
)
+ |> Ecto.Multi.run(:prune_guest_entries, fn _repo, %{team_invitation: team_invitation} ->
+ if team_invitation.role != :guest do
+ team_invitation
+ |> Ecto.assoc(:guest_invitations)
+ |> Repo.delete_all()
+ end
+
+ {:ok, nil}
+ end)
|> Repo.transaction()
case result do
@@ -493,6 +535,26 @@ defmodule Plausible.Teams.Invitations do
Plausible.Mailer.send(email)
end
+ def send_invitation_email(%Teams.Invitation{} = team_invitation, invitee) do
+ email =
+ if invitee do
+ PlausibleWeb.Email.existing_user_team_invitation(
+ team_invitation.email,
+ team_invitation.team,
+ team_invitation.inviter
+ )
+ else
+ PlausibleWeb.Email.new_user_team_invitation(
+ team_invitation.email,
+ team_invitation.invitation_id,
+ team_invitation.team,
+ team_invitation.inviter
+ )
+ end
+
+ Plausible.Mailer.send(email)
+ end
+
def send_invitation_email(%Teams.GuestInvitation{} = guest_invitation, invitee) do
team_invitation = guest_invitation.team_invitation
diff --git a/lib/plausible/teams/invitations/invite_to_team.ex b/lib/plausible/teams/invitations/invite_to_team.ex
new file mode 100644
index 000000000000..c20dddcab540
--- /dev/null
+++ b/lib/plausible/teams/invitations/invite_to_team.ex
@@ -0,0 +1,53 @@
+defmodule Plausible.Teams.Invitations.InviteToTeam do
+ @moduledoc """
+ Service for inviting new or existing users to team.
+ """
+
+ alias Plausible.Teams
+ alias Plausible.Repo
+
+ @valid_roles Plausible.Teams.Invitation.roles() -- [:guest]
+ @valid_roles @valid_roles ++ Enum.map(@valid_roles, &to_string/1)
+
+ def invite(team, inviter, invitee_email, role, opts \\ [])
+
+ def invite(team, inviter, invitee_email, role, opts) when role in @valid_roles do
+ with team <- Repo.preload(team, [:owner]),
+ :ok <-
+ Teams.Invitations.check_invitation_permissions(
+ team,
+ inviter,
+ role,
+ opts
+ ),
+ :ok <-
+ Teams.Invitations.check_team_member_limit(
+ team,
+ role,
+ invitee_email
+ ),
+ invitee = Plausible.Auth.find_user_by(email: invitee_email),
+ :ok <-
+ Teams.Invitations.ensure_new_membership(
+ team,
+ invitee,
+ role
+ ),
+ {:ok, invitation} <-
+ Teams.Invitations.invite(team, invitee_email, role, inviter) do
+ send_invitation_email(invitation, invitee)
+
+ {:ok, invitation}
+ end
+ end
+
+ def invite(_team, _inviter, _invitee_email, role, _opts) do
+ raise "Invalid role passed: #{inspect(role)}"
+ end
+
+ defp send_invitation_email(invitation, invitee) do
+ invitation
+ |> Repo.preload([:team, :inviter])
+ |> Teams.Invitations.send_invitation_email(invitee)
+ end
+end
diff --git a/lib/plausible/teams/membership.ex b/lib/plausible/teams/membership.ex
index afc740565def..efb4f7def3f6 100644
--- a/lib/plausible/teams/membership.ex
+++ b/lib/plausible/teams/membership.ex
@@ -7,10 +7,12 @@ defmodule Plausible.Teams.Membership do
import Ecto.Changeset
+ @roles [:guest, :viewer, :editor, :admin, :owner]
+
@type t() :: %__MODULE__{}
schema "team_memberships" do
- field :role, Ecto.Enum, values: [:guest, :viewer, :editor, :admin, :owner]
+ field :role, Ecto.Enum, values: @roles
belongs_to :user, Plausible.Auth.User
belongs_to :team, Plausible.Teams.Team
@@ -20,6 +22,8 @@ defmodule Plausible.Teams.Membership do
timestamps()
end
+ def roles(), do: @roles
+
def changeset(team, user, role) do
%__MODULE__{}
|> change()
diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex
index 1a38fb8c7e9f..af0bc5f8a1da 100644
--- a/lib/plausible_web/email.ex
+++ b/lib/plausible_web/email.ex
@@ -263,6 +263,29 @@ defmodule PlausibleWeb.Email do
)
end
+ def new_user_team_invitation(email, invitation_id, team, inviter) do
+ priority_email()
+ |> to(email)
+ |> tag("new-user-team-invitation")
+ |> subject("[#{Plausible.product_name()}] You've been invited to \"#{team.name}\" team")
+ |> render("new_user_team_invitation.html",
+ invitation_id: invitation_id,
+ team: team,
+ inviter: inviter
+ )
+ end
+
+ def existing_user_team_invitation(email, team, inviter) do
+ priority_email()
+ |> to(email)
+ |> tag("existing-user-team-invitation")
+ |> subject("[#{Plausible.product_name()}] You've been invited to \"#{team.name}\" team")
+ |> render("existing_user_team_invitation.html",
+ team: team,
+ inviter: inviter
+ )
+ end
+
def ownership_transfer_request(email, invitation_id, site, inviter, new_owner_account) do
priority_email()
|> to(email)
diff --git a/lib/plausible_web/live/register_form.ex b/lib/plausible_web/live/register_form.ex
index dc7a4aea79cc..60a9ba0bdce3 100644
--- a/lib/plausible_web/live/register_form.ex
+++ b/lib/plausible_web/live/register_form.ex
@@ -284,7 +284,7 @@ defmodule PlausibleWeb.Live.RegisterForm do
|> Map.put("email", invitation.email)
|> Auth.User.new()
- with_team? = invitation.role == :owner
+ with_team? = invitation.type == :site_transfer
add_user(socket, user, with_team?: with_team?)
else
@@ -349,6 +349,8 @@ defmodule PlausibleWeb.Live.RegisterForm do
defp find_by_id_unified(invitation_or_transfer_id) do
result =
with {:error, :invitation_not_found} <-
+ find_team_invitation_by_id_unified(invitation_or_transfer_id),
+ {:error, :invitation_not_found} <-
find_invitation_by_id_unified(invitation_or_transfer_id) do
find_transfer_by_id_unified(invitation_or_transfer_id)
end
@@ -359,6 +361,25 @@ defmodule PlausibleWeb.Live.RegisterForm do
end
end
+ defp find_team_invitation_by_id_unified(id) do
+ invitation =
+ Teams.Invitation
+ |> Repo.get_by(invitation_id: id)
+ |> Repo.preload(:inviter)
+
+ case invitation do
+ nil ->
+ {:error, :invitation_not_found}
+
+ team_invitation ->
+ {:ok,
+ %{
+ type: :team_invitation,
+ email: team_invitation.email
+ }}
+ end
+ end
+
defp find_invitation_by_id_unified(id) do
invitation =
Teams.GuestInvitation
@@ -372,7 +393,7 @@ defmodule PlausibleWeb.Live.RegisterForm do
guest_invitation ->
{:ok,
%{
- role: guest_invitation.role,
+ type: :guest_invitation,
email: guest_invitation.team_invitation.email
}}
end
@@ -391,7 +412,7 @@ defmodule PlausibleWeb.Live.RegisterForm do
transfer ->
{:ok,
%{
- role: :owner,
+ type: :site_transfer,
email: transfer.email
}}
end
diff --git a/lib/plausible_web/templates/email/existing_user_team_invitation.html.heex b/lib/plausible_web/templates/email/existing_user_team_invitation.html.heex
new file mode 100644
index 000000000000..bde0c94b911b
--- /dev/null
+++ b/lib/plausible_web/templates/email/existing_user_team_invitation.html.heex
@@ -0,0 +1,3 @@
+<%= @inviter.email %> has invited you to the "<%= @team.name %>" team on <%= Plausible.product_name() %>.
+Click here to view and respond to the invitation. The invitation
+will expire 48 hours after this email is sent.
diff --git a/lib/plausible_web/templates/email/new_user_team_invitation.html.heex b/lib/plausible_web/templates/email/new_user_team_invitation.html.heex
new file mode 100644
index 000000000000..c6e8a0122ce2
--- /dev/null
+++ b/lib/plausible_web/templates/email/new_user_team_invitation.html.heex
@@ -0,0 +1,10 @@
+<%= @inviter.email %> has invited you to join the "<%= @team.name %>" team on <%= Plausible.product_name() %>.
+Click here to create your account. The link is valid for 48 hours after this email is sent.
+
+Plausible is a lightweight and open-source website analytics tool. We hope you like our simple and ethical approach to tracking website visitors.
diff --git a/test/plausible/teams/invitations/invite_to_team_test.exs b/test/plausible/teams/invitations/invite_to_team_test.exs
new file mode 100644
index 000000000000..e75d944eaa78
--- /dev/null
+++ b/test/plausible/teams/invitations/invite_to_team_test.exs
@@ -0,0 +1,220 @@
+defmodule Plausible.Teams.Invitations.InviteToTeamTest do
+ use Plausible.DataCase
+ use Plausible
+ use Bamboo.Test
+ use Plausible.Teams.Test
+
+ alias Plausible.Repo
+ alias Plausible.Teams
+ alias Plausible.Teams.Invitations.InviteToTeam
+
+ @subject_prefix if ee?(), do: "[Plausible Analytics] ", else: "[Plausible CE] "
+
+ describe "invite/4,5" do
+ for role <- Teams.Invitation.roles() -- [:guest] do
+ test "creates an invitation with role #{role} for existing user" do
+ inviter = new_user()
+ invitee = new_user()
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+
+ assert {:ok, %Plausible.Teams.Invitation{} = team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, unquote(role))
+
+ assert team_invitation.team_id == team.id
+ assert team_invitation.role == unquote(role)
+ assert team_invitation.email == invitee.email
+ assert team_invitation.inviter_id == inviter.id
+ assert is_binary(team_invitation.invitation_id)
+ assert [] = Repo.preload(team_invitation, :guest_invitations).guest_invitations
+
+ assert_email_delivered_with(
+ to: [nil: invitee.email],
+ subject: @subject_prefix <> "You've been invited to \"#{team.name}\" team"
+ )
+ end
+ end
+
+ for role <- Teams.Invitation.roles() -- [:guest] do
+ test "creates an invitation with role #{role} for new user" do
+ inviter = new_user()
+ invitee = build(:user)
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+
+ assert {:ok, %Plausible.Teams.Invitation{} = team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, unquote(role))
+
+ assert team_invitation.team_id == team.id
+ assert team_invitation.role == unquote(role)
+ assert team_invitation.email == invitee.email
+ assert team_invitation.inviter_id == inviter.id
+ assert is_binary(team_invitation.invitation_id)
+ assert [] = Repo.preload(team_invitation, :guest_invitations).guest_invitations
+
+ assert_email_delivered_with(
+ to: [nil: invitee.email],
+ subject: @subject_prefix <> "You've been invited to \"#{team.name}\" team",
+ html_body: ~r/#{team_invitation.invitation_id}/
+ )
+ end
+ end
+
+ for role <- Enum.map(Teams.Invitation.roles() -- [:guest], &to_string/1) do
+ test "creates an invitation with role #{role} as a string" do
+ inviter = new_user()
+ invitee = new_user()
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+
+ assert {:ok, %Plausible.Teams.Invitation{} = team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, unquote(role))
+
+ assert team_invitation.team_id == team.id
+ assert team_invitation.role == unquote(String.to_existing_atom(role))
+ assert team_invitation.email == invitee.email
+ assert team_invitation.inviter_id == inviter.id
+ assert is_binary(team_invitation.invitation_id)
+ assert [] = Repo.preload(team_invitation, :guest_invitations).guest_invitations
+
+ assert_email_delivered_with(
+ to: [nil: invitee.email],
+ subject: @subject_prefix <> "You've been invited to \"#{team.name}\" team"
+ )
+ end
+ end
+
+ test "crashes on attempt to invite guest on a team level" do
+ inviter = new_user()
+ invitee = new_user()
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+
+ assert_raise RuntimeError, ~r/Invalid role passed/, fn ->
+ InviteToTeam.invite(team, inviter, invitee.email, :guest)
+ end
+
+ assert_raise RuntimeError, ~r/Invalid role passed/, fn ->
+ InviteToTeam.invite(team, inviter, invitee.email, "guest")
+ end
+ end
+
+ test "overwrites existing invitation" do
+ inviter = new_user()
+ invitee = new_user()
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+ existing_invitation = invite_member(team, invitee.email, role: :viewer, inviter: inviter)
+
+ assert {:ok, team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, :editor)
+
+ assert team_invitation.id == existing_invitation.id
+ assert team_invitation.team_id == existing_invitation.team_id
+ assert team_invitation.email == invitee.email
+ assert team_invitation.role == :editor
+ end
+
+ test "overwrites existing guest invitation and prunes guest invitation entries" do
+ inviter = new_user()
+ invitee = new_user()
+ site = new_site(owner: inviter)
+ team = team_of(inviter)
+ existing_invitation = invite_guest(site, invitee.email, role: :viewer, inviter: inviter)
+
+ assert {:ok, team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, :viewer)
+
+ assert team_invitation.id == existing_invitation.team_invitation.id
+ assert team_invitation.team_id == existing_invitation.team_invitation.team_id
+ assert team_invitation.email == invitee.email
+ assert team_invitation.role == :viewer
+ assert [] = Repo.preload(team_invitation, :guest_invitations).guest_invitations
+ end
+
+ test "returns validation errors" do
+ inviter = new_user()
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+
+ assert {:error, changeset} = InviteToTeam.invite(team, inviter, "", :viewer)
+ assert {"can't be blank", _} = changeset.errors[:email]
+ end
+
+ for role <- Teams.Invitation.roles() -- [:guest] do
+ test "returns error when existing user is already a member (role #{role})" do
+ inviter = new_user()
+ invitee = new_user()
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+ add_member(team, user: invitee, role: unquote(role))
+
+ assert {:error, :already_a_member} =
+ InviteToTeam.invite(team, inviter, invitee.email, :editor)
+ end
+ end
+
+ test "succeeds when existing user is only a guest member" do
+ inviter = new_user()
+ invitee = new_user()
+ site = new_site(owner: inviter)
+ team = team_of(inviter)
+ add_guest(site, user: invitee, role: :viewer)
+
+ assert {:ok, _team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, :viewer)
+ end
+
+ @tag :ee_only
+ test "returns error when owner is over their team member limit" do
+ [owner, inviter, invitee] = for _ <- 1..3, do: new_user()
+ _site = new_site(owner: owner)
+ team = team_of(owner)
+ inviter = add_member(team, user: inviter, role: :admin)
+ for _ <- 1..2, do: add_member(team, role: :viewer)
+
+ assert {:error, {:over_limit, 3}} =
+ InviteToTeam.invite(team, inviter, invitee.email, :viewer)
+ end
+
+ @tag :ee_only
+ test "allows creating an ownership transfer even when at team member limit" do
+ inviter = new_user()
+ invitee = build(:user)
+ _site = new_site(owner: inviter)
+ team = team_of(inviter)
+ for _ <- 1..3, do: add_member(team, role: :viewer)
+
+ assert {:ok, _team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, :owner)
+ end
+
+ for role <- Teams.Invitation.roles() -- [:guest, :owner] do
+ test "allows admins to invite new members except owners (invite role: #{role})" do
+ owner = new_user()
+ inviter = new_user()
+ invitee = build(:user)
+ _site = new_site(owner: owner)
+ team = team_of(owner)
+ add_member(team, user: inviter, role: :admin)
+
+ assert {:ok, _team_invitation} =
+ InviteToTeam.invite(team, inviter, invitee.email, unquote(role))
+ end
+ end
+
+ for role <- Teams.Invitation.roles() -- [:owner] do
+ test "only allows owners to invite new owners (inviter role: #{role})" do
+ owner = new_user()
+ inviter = new_user()
+ invitee = build(:user)
+ _site = new_site(owner: owner)
+ team = team_of(owner)
+ add_member(team, user: inviter, role: unquote(role))
+
+ assert {:error, :forbidden} =
+ InviteToTeam.invite(team, inviter, invitee.email, :owner)
+ end
+ end
+ end
+end
diff --git a/test/plausible_web/live/register_form_test.exs b/test/plausible_web/live/register_form_test.exs
index b4b05037ba79..d500e89305e4 100644
--- a/test/plausible_web/live/register_form_test.exs
+++ b/test/plausible_web/live/register_form_test.exs
@@ -143,16 +143,16 @@ defmodule PlausibleWeb.Live.RegisterFormTest do
inviter = new_user()
site = new_site(owner: inviter)
- invitation =
+ guest_invitation =
invite_guest(site, "user@email.co", role: :editor, inviter: inviter)
- {:ok, %{site: site, invitation: invitation, inviter: inviter}}
+ {:ok, %{site: site, guest_invitation: guest_invitation, inviter: inviter}}
end
- test "registers user from invitation", %{conn: conn, invitation: invitation} do
+ test "registers user from guest invitation", %{conn: conn, guest_invitation: guest_invitation} do
mock_captcha_success()
- lv = get_liveview(conn, "/register/invitation/#{invitation.invitation_id}")
+ lv = get_liveview(conn, "/register/invitation/#{guest_invitation.invitation_id}")
type_into_input(lv, "user[name]", "Mary Sue")
type_into_input(lv, "user[password]", "very-long-and-very-secret-123")
@@ -192,6 +192,43 @@ defmodule PlausibleWeb.Live.RegisterFormTest do
assert String.length(password_hash) > 0
end
+ test "registers user from team invitation", %{conn: conn, inviter: inviter} do
+ mock_captcha_success()
+
+ team = team_of(inviter)
+
+ team_invitation =
+ invite_member(team, "team-user@email.co", role: :editor, inviter: inviter)
+
+ lv = get_liveview(conn, "/register/invitation/#{team_invitation.invitation_id}")
+
+ type_into_input(lv, "user[name]", "Mary Sue")
+ type_into_input(lv, "user[password]", "very-long-and-very-secret-123")
+ type_into_input(lv, "user[password_confirmation]", "very-long-and-very-secret-123")
+
+ html = lv |> element("form") |> render_submit()
+
+ on_ee do
+ assert_push_event(lv, "send-metrics", %{event_name: "Signup via invitation"})
+ end
+
+ assert [
+ csrf_input,
+ action_input,
+ email_input,
+ name_input,
+ password_input,
+ password_confirmation_input | _
+ ] = find(html, "input")
+
+ assert String.length(text_of_attr(csrf_input, "value")) > 0
+ assert text_of_attr(action_input, "value") == "register_from_invitation_form"
+ assert text_of_attr(name_input, "value") == "Mary Sue"
+ assert text_of_attr(email_input, "value") == "team-user@email.co"
+ assert text_of_attr(password_input, "value") == "very-long-and-very-secret-123"
+ assert text_of_attr(password_confirmation_input, "value") == "very-long-and-very-secret-123"
+ end
+
test "preserves trial_expiry_date when invitation role is :owner", %{
conn: conn,
site: site,
@@ -199,9 +236,9 @@ defmodule PlausibleWeb.Live.RegisterFormTest do
} do
mock_captcha_success()
- invitation = invite_transfer(site, "owner_user@email.co", inviter: inviter)
+ site_transfer = invite_transfer(site, "owner_user@email.co", inviter: inviter)
- lv = get_liveview(conn, "/register/invitation/#{invitation.transfer_id}")
+ lv = get_liveview(conn, "/register/invitation/#{site_transfer.transfer_id}")
type_into_input(lv, "user[name]", "Mary Sue")
type_into_input(lv, "user[password]", "very-long-and-very-secret-123")
@@ -214,10 +251,13 @@ defmodule PlausibleWeb.Live.RegisterFormTest do
assert team_of(user).trial_expiry_date != nil
end
- test "always uses original email from the invitation", %{conn: conn, invitation: invitation} do
+ test "always uses original email from the invitation", %{
+ conn: conn,
+ guest_invitation: guest_invitation
+ } do
mock_captcha_success()
- lv = get_liveview(conn, "/register/invitation/#{invitation.invitation_id}")
+ lv = get_liveview(conn, "/register/invitation/#{guest_invitation.invitation_id}")
type_into_input(lv, "user[name]", "Mary Sue")
type_into_input(lv, "user[email]", "mary.sue@plausible.test")
@@ -247,10 +287,10 @@ defmodule PlausibleWeb.Live.RegisterFormTest do
assert html =~ "Your invitation has expired or been revoked"
end
- test "renders error on failed captcha", %{conn: conn, invitation: invitation} do
+ test "renders error on failed captcha", %{conn: conn, guest_invitation: guest_invitation} do
mock_captcha_failure()
- lv = get_liveview(conn, "/register/invitation/#{invitation.invitation_id}")
+ lv = get_liveview(conn, "/register/invitation/#{guest_invitation.invitation_id}")
type_into_input(lv, "user[name]", "Mary Sue")
type_into_input(lv, "user[password]", "very-long-and-very-secret-123")
@@ -265,9 +305,9 @@ defmodule PlausibleWeb.Live.RegisterFormTest do
test "pushing send-metrics-after event submits the form", %{
conn: conn,
- invitation: invitation
+ guest_invitation: guest_invitation
} do
- lv = get_liveview(conn, "/register/invitation/#{invitation.invitation_id}")
+ lv = get_liveview(conn, "/register/invitation/#{guest_invitation.invitation_id}")
refute render(lv) =~ ~s|phx-trigger-action="phx-trigger-action"|
diff --git a/test/support/teams/test.ex b/test/support/teams/test.ex
index bc435e223441..ef420db9ad1c 100644
--- a/test/support/teams/test.ex
+++ b/test/support/teams/test.ex
@@ -110,6 +110,15 @@ defmodule Plausible.Teams.Test do
user |> Repo.preload(:team_memberships)
end
+ def add_member(team, args \\ []) do
+ user = Keyword.get(args, :user, new_user())
+ role = Keyword.fetch!(args, :role)
+
+ insert(:team_membership, team: team, user: user, role: role)
+
+ user |> Repo.preload(:team_memberships)
+ end
+
def invite_guest(site, invitee_or_email, args \\ []) when not is_nil(invitee_or_email) do
{role, args} = Keyword.pop!(args, :role)
{inviter, args} = Keyword.pop!(args, :inviter)
@@ -149,6 +158,30 @@ defmodule Plausible.Teams.Test do
)
end
+ def invite_member(team, invitee_or_email, args \\ []) when not is_nil(invitee_or_email) do
+ {role, args} = Keyword.pop!(args, :role)
+ {inviter, args} = Keyword.pop!(args, :inviter)
+
+ email =
+ case invitee_or_email do
+ %{email: email} -> email
+ email when is_binary(email) -> email
+ end
+
+ insert(
+ :team_invitation,
+ Keyword.merge(
+ [
+ team: team,
+ email: email,
+ inviter: inviter,
+ role: role
+ ],
+ args
+ )
+ )
+ end
+
def invite_transfer(site, invitee_or_email, args \\ []) do
{inviter, args} = Keyword.pop!(args, :inviter)