defmodule PlausibleWeb.Site.MembershipControllerTest do use PlausibleWeb.ConnCase use Plausible.Repo use Bamboo.Test @subject_prefix if ee?(), do: "[Plausible Analytics] ", else: "[Plausible CE] " setup [:create_user, :log_in] describe "GET /sites/:domain/memberships/invite" do test "shows invite form", %{conn: conn, user: user} do site = new_site(owner: user) html = conn |> get("/sites/#{site.domain}/memberships/invite") |> html_response(200) assert html =~ "Invite guest to" assert element_exists?(html, ~s/button[type=submit]/) refute element_exists?(html, ~s/button[type=submit][disabled]/) end @tag :ee_only test "display a notice when is over limit", %{conn: conn, user: user} do site = new_site(owner: user) for _ <- 1..11 do add_guest(site, role: :viewer) end html = conn |> get("/sites/#{site.domain}/memberships/invite") |> html_response(200) assert text_of_element(html, ~s/[data-test="limit-exceeded-notice"]/) =~ "This account is limited to 10 members" end end describe "POST /sites/:domain/memberships/invite" do test "creates invitation", %{conn: conn, user: user} do site = new_site(owner: user) conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: "john.doe@example.com", role: "editor" }) assert_guest_invitation(site.team, site, "john.doe@example.com", :editor) assert redirected_to(conn) == "/#{URI.encode_www_form(site.domain)}/settings/people" end @tag :ee_only test "fails to create invitation when is over limit", %{conn: conn, user: user} do site = new_site(owner: user) for _ <- 1..11 do add_guest(site, role: :viewer) end conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: "john.doe@example.com", role: "editor" }) assert html_response(conn, 200) =~ "Your account is limited to 10 team members. You can upgrade your plan to increase this limit." end test "fails to create invitation with insufficient permissions", %{conn: conn, user: user} do site = new_site() add_guest(site, user: user, role: :viewer) conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: "john.doe@example.com", role: "editor" }) assert conn.status == 404 end test "fails to create invitation if site transfer already exists", %{conn: conn, user: user} do site = new_site(owner: user) new_owner = new_user() {:ok, _invitation} = Plausible.Teams.Invitations.InviteToSite.invite(site, user, new_owner.email, :owner) assert_site_transfer(site, new_owner.email) conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: new_owner.email, role: "editor" }) conn = get(recycle(conn), redirected_to(conn, 302)) html = html_response(conn, 200) assert html =~ "Error" assert html =~ "This invitation has been already sent" end test "fails to create invitation for a foreign site", %{conn: my_conn, user: me} do _my_site = new_site(owner: me) other_user = new_user() other_site = new_site(owner: other_user) my_conn = post(my_conn, "/sites/#{other_site.domain}/memberships/invite", %{ email: "john.doe@example.com", role: "editor" }) assert my_conn.status == 404 refute Repo.get_by(Plausible.Teams.Invitation, email: "john.doe@example.com") end test "sends invitation email for new user", %{conn: conn, user: user} do site = new_site(owner: user) post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: "john.doe@example.com", role: "editor" }) assert_email_delivered_with( to: [nil: "john.doe@example.com"], subject: @subject_prefix <> "You've been invited to #{site.domain}" ) end test "sends invitation email for existing user", %{conn: conn, user: user} do existing_user = new_user() site = new_site(owner: user) post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: existing_user.email, role: "editor" }) assert_email_delivered_with( to: [nil: existing_user.email], subject: @subject_prefix <> "You've been invited to #{site.domain}" ) end test "renders form with error if the invitee is already a member", %{conn: conn, user: user} do site = new_site(owner: user) second_member = new_user() add_guest(site, user: second_member, role: :viewer) conn = post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: second_member.email, role: "editor" }) assert html_response(conn, 200) =~ "#{second_member.email} is already a member of #{site.domain}" end test "handles repeat invitation gracefully", %{ conn: conn, user: user } do site = new_site(owner: user) _req1 = post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: "joe@example.com", role: "editor" }) assert_email_delivered_with( to: [nil: "joe@example.com"], subject: @subject_prefix <> "You've been invited to #{site.domain}" ) req2 = post(conn, "/sites/#{site.domain}/memberships/invite", %{ email: "joe@example.com", role: "editor" }) assert_email_delivered_with( to: [nil: "joe@example.com"], subject: @subject_prefix <> "You've been invited to #{site.domain}" ) assert Phoenix.Flash.get(req2.assigns.flash, :success) =~ "has been invited to" end end describe "PUT /sites/memberships/:id/role/:new_role" do test "updates a site member's role by user id", %{conn: conn, user: user} do site = new_site(owner: user) collaborator = add_guest(site, role: :editor) assert_guest_membership(site.team, site, collaborator, :editor) put(conn, "/sites/#{site.domain}/memberships/u/#{collaborator.id}/role/viewer") assert_guest_membership(site.team, site, collaborator, :viewer) end test "team admin can update a site member's role by user id (from editor to viewer)", %{ conn: conn, user: user } do site = new_site() team = Plausible.Teams.complete_setup(site.team) add_member(team, user: user, role: :admin) collaborator = add_guest(site, role: :editor) assert_guest_membership(team, site, collaborator, :editor) conn = set_current_team(conn, team) put(conn, "/sites/#{site.domain}/memberships/u/#{collaborator.id}/role/viewer") assert_guest_membership(team, site, collaborator, :viewer) end test "team admin can update a site member's role by user id (from viewer to editor)", %{ conn: conn, user: user } do site = new_site() team = Plausible.Teams.complete_setup(site.team) add_member(team, user: user, role: :admin) collaborator = add_guest(site, role: :viewer) assert_guest_membership(team, site, collaborator, :viewer) conn = set_current_team(conn, team) put(conn, "/sites/#{site.domain}/memberships/u/#{collaborator.id}/role/editor") assert_guest_membership(team, site, collaborator, :editor) end test "team editor can't update site member's role", %{conn: conn, user: user} do site = new_site() team = Plausible.Teams.complete_setup(site.team) add_member(team, user: user, role: :editor) collaborator = add_guest(site, role: :editor) assert_guest_membership(team, site, collaborator, :editor) conn = set_current_team(conn, team) conn = put(conn, "/sites/#{site.domain}/memberships/u/#{collaborator.id}/role/viewer") assert html_response(conn, 404) assert_guest_membership(team, site, collaborator, :editor) end test "can't update role when an editor", %{ conn: conn, user: user } do site = new_site() add_guest(site, user: user, role: :editor) conn = put(conn, "/sites/#{site.domain}/memberships/u/#{user.id}/role/viewer") assert_guest_membership(site.team, site, user, :editor) assert html_response(conn, 404) end test "can't update role when a viewer", %{ conn: conn, user: user } do site = new_site() add_guest(site, user: user, role: :viewer) another_guest = add_guest(site, role: :editor) conn = put(conn, "/sites/#{site.domain}/memberships/u/#{another_guest.id}/role/viewer") assert_guest_membership(site.team, site, another_guest, :editor) assert html_response(conn, 404) end test "owner cannot make anyone else owner", %{ conn: conn, user: user } do site = new_site(owner: user) editor = new_user() add_guest(site, user: editor, role: :editor) conn = put(conn, "/sites/#{site.domain}/memberships/u/#{editor.id}/role/owner") assert_guest_membership(site.team, site, editor, :editor) assert Phoenix.Flash.get(conn.assigns.flash, :error) == "You are not allowed to grant the owner role" end test "owner cannot downgrade themselves", %{ conn: conn, user: user } do site = new_site(owner: user) conn = put(conn, "/sites/#{site.domain}/memberships/u/#{user.id}/role/admin") membership = Repo.get_by(Plausible.Teams.Membership, user_id: user.id) assert membership.role == :owner assert Phoenix.Flash.get(conn.assigns.flash, :error) == "You are not allowed to grant the admin role" end end describe "DELETE /sites/:domain/memberships/:id" do test "removes a member from a site by user id", %{conn: conn, user: user} do site = new_site(owner: user) admin = add_guest(site, role: :editor) conn = delete(conn, "/sites/#{site.domain}/memberships/u/#{admin.id}") assert Phoenix.Flash.get(conn.assigns.flash, :success) =~ "has been removed" refute Repo.exists?(from tm in Plausible.Teams.Membership, where: tm.user_id == ^admin.id) end test "when members is removed, associated personal segment is deleted", %{ conn: conn, user: user } do site = new_site(owner: user) admin = add_guest(site, role: :editor) segment = insert(:segment, type: :personal, owner: admin, site: site, name: "personal segment" ) delete(conn, "/sites/#{site.domain}/memberships/u/#{admin.id}") refute Repo.reload(segment) end test "when members is removed, associated site segment will be owner-less", %{ conn: conn, user: user } do site = new_site(owner: user) admin = add_guest(site, role: :editor) segment = insert(:segment, type: :site, owner: admin, site: site, name: "site segment" ) delete(conn, "/sites/#{site.domain}/memberships/u/#{admin.id}") assert Repo.reload(segment).owner_id == nil end test "fails to remove a member from a foreign site (silently)", %{conn: conn, user: user} do foreign_member = new_user() foreign_site = new_site() add_guest(foreign_site, user: foreign_member, role: :editor) site = new_site(owner: user) conn = delete(conn, "/sites/#{site.domain}/memberships/u/#{foreign_member.id}") assert Phoenix.Flash.get(conn.assigns.flash, :success) =~ "has been removed" assert_guest_membership(foreign_site.team, foreign_site, foreign_member, :editor) end test "notifies the user who has been removed via email", %{conn: conn, user: user} do site = new_site(owner: user) editor = add_guest(site, role: :editor) delete(conn, "/sites/#{site.domain}/memberships/u/#{editor.id}") assert_email_delivered_with( to: [nil: editor.email], subject: @subject_prefix <> "Your access to #{site.domain} has been revoked" ) end end end