defmodule PlausibleWeb.Site.InvitationControllerTest do use PlausibleWeb.ConnCase, async: true use Plausible.Repo use Bamboo.Test alias Plausible.Teams setup [:create_user, :log_in] describe "POST /sites/invitations/:invitation_id/accept" do test "converts the invitation into a membership", %{conn: conn, user: user} do owner = new_user() site = new_site(owner: owner) invitation = invite_guest(site, user.email, inviter: owner, role: :editor) conn = post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") assert Phoenix.Flash.get(conn.assigns.flash, :success) == "You now have access to #{site.domain}" assert redirected_to(conn) == Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain) refute Repo.exists?(from(i in Plausible.Teams.Invitation, where: i.email == ^user.email)) assert_guest_membership(site.team, site, user, :editor) end test "converts the team invitation into a team membership", %{conn: conn, user: user} do owner = new_user() _site = new_site(owner: owner) team = team_of(owner) invitation = invite_member(team, user.email, inviter: owner, role: :editor) conn = post(conn, "/settings/team/invitations/#{invitation.invitation_id}/accept") assert Phoenix.Flash.get(conn.assigns.flash, :success) == "You now have access to \"#{team.name}\" team" assert redirected_to(conn) == "/sites?__team=#{team.identifier}" refute Repo.get_by(Teams.Invitation, email: user.email) assert_team_membership(user, team, :editor) end test "does not crash if clicked for the 2nd time in another tab", %{conn: conn, user: user} do owner = new_user() site = new_site(owner: owner) invitation = invite_guest(site, user.email, role: :editor, inviter: owner) c1 = post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") assert redirected_to(c1) == Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain) assert Phoenix.Flash.get(c1.assigns.flash, :success) == "You now have access to #{site.domain}" c2 = post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") assert redirected_to(c2) == "/sites" assert Phoenix.Flash.get(c2.assigns.flash, :error) == "Invitation missing or already accepted" end end describe "POST /sites/invitations/:invitation_id/accept - ownership transfer" do test "downgrades previous owner to admin", %{conn: conn, user: user} do old_owner = new_user() site = new_site(owner: old_owner) subscribe_to_growth_plan(user) new_team = team_of(user) transfer = invite_transfer(site, user, inviter: old_owner) conn = post(conn, "/sites/invitations/#{transfer.transfer_id}/accept") assert redirected_to(conn, 302) == Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain) assert Phoenix.Flash.get(conn.assigns.flash, :success) =~ "You now have access to" refute Repo.reload(transfer) assert_guest_membership(new_team, site, old_owner, :editor) assert_team_attached(site, new_team.id) end test "fails when new owner has no permissions for current team", %{conn: conn, user: user} do old_owner = new_user() site = new_site(owner: old_owner) other_owner = new_user() |> subscribe_to_growth_plan() new_team = team_of(other_owner) add_member(new_team, user: user, role: :viewer) conn = set_current_team(conn, new_team) transfer = invite_transfer(site, user, inviter: old_owner) conn = post(conn, "/sites/invitations/#{transfer.transfer_id}/accept") assert redirected_to(conn, 302) == "/sites" assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "You can't add sites in the current team" end test "fails when transferring to the same team", %{conn: conn, user: user} do current_owner = user |> subscribe_to_growth_plan() site = new_site(owner: current_owner) transfer = invite_transfer(site, current_owner, inviter: current_owner) conn = post(conn, "/sites/invitations/#{transfer.transfer_id}/accept") assert redirected_to(conn, 302) == "/sites" assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "The site is already in the current team" end test "allows transferring between different teams of the same owner", %{ conn: conn, user: user } do current_owner = user |> subscribe_to_growth_plan() site = new_site(owner: current_owner) another_owner = new_user() |> subscribe_to_growth_plan() new_team = team_of(another_owner) add_member(new_team, user: current_owner, role: :owner) transfer = invite_transfer(site, current_owner, inviter: current_owner) conn = set_current_team(conn, new_team) conn = post(conn, "/sites/invitations/#{transfer.transfer_id}/accept") assert redirected_to(conn, 302) == Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain) assert Phoenix.Flash.get(conn.assigns.flash, :success) =~ "You now have access to" refute Repo.reload(transfer) assert_team_membership(current_owner, new_team, :owner) assert_team_attached(site, new_team.id) end @tag :ee_only test "fails when new owner has no plan", %{conn: conn, user: user} do old_owner = new_user() site = new_site(owner: old_owner) transfer = invite_transfer(site, user, inviter: old_owner) conn = post(conn, "/sites/invitations/#{transfer.transfer_id}/accept") assert redirected_to(conn, 302) == Routes.site_path(conn, :index) assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "No existing subscription" end @tag :ee_only test "fails when new owner's plan is unsuitable", %{conn: conn, user: user} do old_owner = new_user() site = new_site(owner: old_owner) subscribe_to_growth_plan(user) # fill site limit quota for _ <- 1..10, do: new_site(owner: user) transfer = invite_transfer(site, user, inviter: old_owner) conn = post(conn, "/sites/invitations/#{transfer.transfer_id}/accept") assert redirected_to(conn, 302) == Routes.site_path(conn, :index) assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "Plan limits exceeded: site limit." end test "succeeds despite exceeding members limit if parameter provided", %{ conn: conn, user: user } do current_owner = new_user() site = new_site(owner: current_owner) subscribe_to_growth_plan(user) new_team = team_of(user) # fill site limit quota for _ <- 1..3, do: add_guest(site, role: :editor) transfer = invite_transfer(site, user, inviter: current_owner) conn = post( conn, "/sites/invitations/#{transfer.transfer_id}/accept?skip_site_members_transfer=true" ) assert redirected_to(conn, 302) == Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain) assert Phoenix.Flash.get(conn.assigns.flash, :success) =~ "You now have access to" refute Repo.reload(transfer) assert_team_attached(site, new_team.id) end end describe "POST /sites/invitations/:invitation_id/reject" do test "rejects the invitation", %{conn: conn, user: user} do owner = new_user() site = new_site(owner: owner) invitation = invite_guest(site, user.email, inviter: owner, role: :editor) conn = post(conn, "/sites/invitations/#{invitation.invitation_id}/reject") assert redirected_to(conn, 302) == "/sites" refute Repo.reload(invitation) end test "rejects the team invitation", %{conn: conn, user: user} do owner = new_user() _site = new_site(owner: owner) team = team_of(owner) invitation = invite_member(team, user.email, inviter: owner, role: :editor) conn = post(conn, "/settings/team/invitations/#{invitation.invitation_id}/reject") assert redirected_to(conn, 302) == "/sites" refute Repo.reload(invitation) end test "renders error for non-existent invitation", %{conn: conn} do conn = post(conn, "/sites/invitations/does-not-exist/reject") assert redirected_to(conn, 302) == "/sites" assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invitation missing or already accepted" end end describe "DELETE /sites/:domain/invitations/:invitation_id" do test "removes the invitation", %{conn: conn, user: user} do owner = new_user() site = new_site(owner: owner) team = Plausible.Teams.complete_setup(site.team) add_member(team, user: user, role: :admin) invitation = invite_guest(site, "jane@example.com", inviter: owner, role: :editor) conn = delete( conn, Routes.invitation_path(conn, :remove_invitation, site.domain, invitation.invitation_id) ) assert redirected_to(conn, 302) == "/#{URI.encode_www_form(site.domain)}/settings/people" refute Repo.reload(invitation) end test "removes the invitation for ownership transfer", %{conn: conn, user: user} do owner = new_user() site = new_site(owner: owner) team = Plausible.Teams.complete_setup(site.team) add_member(team, user: user, role: :admin) transfer = invite_transfer(site, "jane@example.com", inviter: owner) conn = delete( conn, Routes.invitation_path(conn, :remove_invitation, site.domain, transfer.transfer_id) ) assert redirected_to(conn, 302) == "/#{URI.encode_www_form(site.domain)}/settings/people" refute Repo.reload(transfer) end test "fails to remove an invitation with insufficient permission", %{conn: conn, user: user} do owner = new_user() site = new_site(owner: owner) add_guest(site, user: user, role: :editor) invitation = invite_guest(site, "jane@example.com", inviter: owner, role: :editor) delete( conn, Routes.invitation_path(conn, :remove_invitation, site.domain, invitation.invitation_id) ) assert Repo.reload(invitation) end test "fails to remove an invitation from the outside", %{conn: my_conn, user: me} do new_site(owner: me) other_user = new_user() other_site = new_site(owner: other_user) invitation = invite_guest(other_site, "jane@example.com", role: :editor, inviter: other_user) remove_invitation_path = Routes.invitation_path( my_conn, :remove_invitation, other_site.domain, invitation.invitation_id ) delete(my_conn, remove_invitation_path) assert Repo.reload(invitation) end test "renders error for non-existent invitation", %{conn: conn, user: user} do site = new_site() team = Plausible.Teams.complete_setup(site.team) add_member(team, user: user, role: :admin) remove_invitation_path = Routes.invitation_path( conn, :remove_invitation, site.domain, "does_not_exist" ) conn = delete(conn, remove_invitation_path) assert redirected_to(conn, 302) == "/#{URI.encode_www_form(site.domain)}/settings/people" assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invitation missing or already removed" end end describe "DELETE /team/invitations/:invitation_id" do for role <- [:owner, :admin], invitee_role <- Teams.Membership.roles() do test "#{role} removes the #{invitee_role} team invitation", %{conn: conn, user: user} do owner = new_user() _site = new_site(owner: owner) team = team_of(owner) add_member(team, user: user, role: unquote(role)) conn = set_current_team(conn, team) invitation = invite_member(team, "jane@example.com", inviter: owner, role: unquote(invitee_role)) conn = delete( conn, Routes.invitation_path(conn, :remove_team_invitation, invitation.invitation_id) ) assert redirected_to(conn, 302) == "/settings/team/general" refute Repo.reload(invitation) end end for role <- Teams.Membership.roles() -- [:owner, :admin] do test "#{role} can't remove a team invitation", %{conn: conn, user: user} do owner = new_user() _site = new_site(owner: owner) team = team_of(owner) add_member(team, user: user, role: unquote(role)) invitation = invite_member(team, "jane@example.com", inviter: owner, role: :viewer) conn = delete( conn, Routes.invitation_path(conn, :remove_team_invitation, invitation.invitation_id) ) assert redirected_to(conn, 302) == "/settings/team/general" assert Phoenix.Flash.get(conn.assigns.flash, :error) == "You are not allowed to remove invitations" assert Repo.reload(invitation) end end test "fails to remove a team invitation from the outside", %{conn: my_conn, user: me} do new_site(owner: me) other_user = new_user() _other_site = new_site(owner: other_user) other_team = team_of(other_user) invitation = invite_member(other_team, "jane@example.com", role: :editor, inviter: other_user) remove_invitation_path = Routes.invitation_path( my_conn, :remove_team_invitation, invitation.invitation_id ) my_conn = delete(my_conn, remove_invitation_path) assert Phoenix.Flash.get(my_conn.assigns.flash, :error) == "Invitation missing or already removed" assert Repo.reload(invitation) end test "renders error for non-existent team invitation", %{conn: conn, user: user} do owner = new_user() _site = new_site(owner: owner) team = team_of(owner) add_member(team, user: user, role: :editor) conn = set_current_team(conn, team) remove_invitation_path = Routes.invitation_path( conn, :remove_team_invitation, "does_not_exist" ) conn = delete(conn, remove_invitation_path) assert redirected_to(conn, 302) == "/settings/team/general" assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invitation missing or already removed" end end end