| 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) |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|