defmodule PlausibleWeb.Live.TeamSetupTest do use PlausibleWeb.ConnCase, async: false use Bamboo.Test, shared: true import Phoenix.LiveViewTest alias Plausible.Teams alias Plausible.Repo @url "/team/setup" @subject_prefix if ee?(), do: "[Plausible Analytics] ", else: "[Plausible CE] " describe "/team/setup - edge cases" do setup [:create_user, :log_in] test "redirects if there's no implicit team created", %{conn: conn} do assert {:error, {:redirect, %{to: "/sites"}}} = live(conn, @url) end test "redirects to /team/general if team is already set up", %{conn: conn, user: user} do {:ok, team} = Teams.get_or_create(user) Teams.complete_setup(team) conn = set_current_team(conn, team) assert {:error, {:redirect, %{to: "/settings/team/general"}}} = live(conn, @url) end end describe "/team/setup - main differences from team management" do setup [:create_user, :log_in, :create_team] test "renames the team on first render", %{conn: conn, team: team} do assert team.name == "My personal sites" {:ok, _lv, html} = live(conn, @url) assert text_of_attr(html, ~s|input#update-team-form_name[name="team[name]"]|, "value") == "Jane Smith's team" assert Repo.reload!(team).name == "Jane Smith's team" end test "renames even if team already has non-default name", %{conn: conn, team: team} do assert team.name == "My personal sites" Repo.update!(Teams.Team.name_changeset(team, %{name: "Foo"})) {:ok, _lv, html} = live(conn, @url) assert text_of_attr(html, ~s|input#update-team-form_name[name="team[name]"]|, "value") == "Jane Smith's team" assert Repo.reload!(team).name == "Jane Smith's team" end test "renders form", %{conn: conn} do {:ok, lv, html} = live(conn, @url) assert element_exists?(html, ~s|input#update-team-form_name[name="team[name]"]|) assert element_exists?(html, ~s|button[phx-click="save-team-layout"]|) _ = render(lv) end test "changing team name, updates team name in db", %{conn: conn, team: team} do {:ok, lv, _html} = live(conn, @url) type_into_input(lv, "team[name]", "New Team Name") assert Repo.reload!(team).name == "New Team Name" _ = render(lv) end test "setting team name to 'My personal sites' is reserved", %{ conn: conn, team: team, user: user } do {:ok, lv, html} = live(conn, @url) assert text_of_attr(html, ~s|input#update-team-form_name[name="team[name]"]|, "value") == "#{user.name}'s team" type_into_input(lv, "team[name]", "Team Name 1") _ = render(lv) type_into_input(lv, "team[name]", "My personal sites") _ = render(lv) assert Repo.reload!(team).name == "Team Name 1" end @tag :ee_only test "blurs UI with an upgrade CTA if the subscription team member limit is 0", %{ conn: conn, user: user } do subscribe_to_starter_plan(user) {:ok, _lv, html} = live(conn, @url) assert element_exists?(html, "#feature-gate-inner-block-container") assert element_exists?(html, "#feature-gate-overlay") assert text_of_element(html, "#feature-gate-overlay") =~ "Upgrade to unlock" end end describe "/team/setup - full integration" do setup [:create_user, :log_in, :create_team] test "renders member, enqueues invitation, delivers it", %{conn: conn, user: user, team: team} do {lv, html} = get_child_lv(conn, with_html?: true) member_row1 = find(html, "#{member_el()}:nth-of-type(1)") |> text() assert member_row1 =~ "#{user.name}" assert member_row1 =~ "#{user.email}" assert member_row1 =~ "You" add_invite(lv, "new@example.com", "admin") html = render(lv) member_row1 = find(html, "#{member_el()}:nth-of-type(1)") |> text() assert member_row1 =~ "new@example.com" assert member_row1 =~ "Invited User" assert member_row1 =~ "Invitation pending" member_row2 = find(html, "#{member_el()}:nth-of-type(2)") |> text() assert member_row2 =~ "#{user.name}" assert member_row2 =~ "#{user.email}" save_layout(lv) assert_redirect(lv, "/settings/team/general?__team=" <> team.identifier) team = Repo.reload!(team) assert_email_delivered_with( to: [nil: "new@example.com"], subject: @subject_prefix <> "You've been invited to \"#{team.name}\" team" ) end test "allows updating pending invitation role in place", %{conn: conn, team: team} do lv = get_child_lv(conn) add_invite(lv, "new@example.com", "admin") html = render(lv) assert text_of_element(html, "#{member_el()}:nth-of-type(1) button") == "Admin" assert text_of_element(html, "#{member_el()}:nth-of-type(2) button") == "Owner" change_role(lv, 1, "viewer") html = render(lv) assert text_of_element(html, "#{member_el()}:nth-of-type(1) button") == "Viewer" save_layout(lv) team = Repo.reload!(team) assert_email_delivered_with( to: [nil: "new@example.com"], subject: @subject_prefix <> "You've been invited to \"#{team.name}\" team" ) end test "allows updating membership role in place", %{conn: conn, team: team} do member2 = add_member(team, role: :admin) {lv, html} = get_child_lv(conn, with_html?: true) assert text_of_element(html, "#{member_el()}:nth-of-type(1) button") == "Owner" assert text_of_element(html, "#{member_el()}:nth-of-type(2) button") == "Admin" change_role(lv, 2, "viewer") html = render(lv) assert text_of_element(html, "#{member_el()}:nth-of-type(2) button") == "Viewer" save_layout(lv) assert_no_emails_delivered() assert_team_membership(member2, team, :viewer) end test "allows updating guest membership so it moves sections and sends out promotion e-mail", %{ conn: conn, user: user, team: team } do site = new_site(owner: user) add_guest(site, role: :viewer, user: new_user(name: "Mr Guest", email: "guest@example.com")) {:ok, main_lv, _html} = live(conn, @url) lv = get_child_lv(conn) type_into_input(main_lv, "team[name]", "A-Team!") assert Repo.reload!(team).name == "A-Team!" html = render(lv) assert elem_count(html, member_el()) == 1 assert text_of_element(html, "#{guest_el()}:first-of-type button") == "Guest" change_role(lv, 1, "viewer", guest_el()) html = render(lv) assert elem_count(html, member_el()) == 2 refute element_exists?(html, "#guest-list") save_layout(lv) assert_email_delivered_with( to: [nil: "guest@example.com"], subject: @subject_prefix <> "Welcome to \"A-Team!\" team" ) end @tag :ee_only test "fails to save layout with limits breached", %{conn: conn, team: team} do insert(:growth_subscription, team: team) lv = get_child_lv(conn) html = render(lv) refute attr_defined?(html, ~s|#team-layout-form input[name="input-email"]|, "readonly") refute attr_defined?(html, ~s|#invite-member|, "disabled") add_invite(lv, "new1@example.com", "admin") add_invite(lv, "new2@example.com", "admin") add_invite(lv, "new3@example.com", "admin") add_invite(lv, "new4@example.com", "admin") html = render(lv) assert attr_defined?(html, ~s|#team-layout-form input[name="input-email"]|, "readonly") assert attr_defined?(html, ~s|#invite-member|, "disabled") assert text_of_element(html, ~s/[data-test="limit-exceeded-notice"]/) =~ "This account is limited to 3 members" end test "all options are disabled for the sole owner", %{conn: conn} do lv = get_child_lv(conn) options = lv |> render() |> find("#{member_el()} a") assert Enum.empty?(options) end test "in case of >1 owner, the one owner limit is still enforced", %{conn: conn, team: team} do _other_owner = add_member(team, role: :owner) lv = get_child_lv(conn) options = lv |> render() |> find("#{member_el()} a") refute Enum.empty?(options) change_role(lv, 1, "viewer") html = lv |> render() assert element_exists?(html, "#{member_el()}:nth-of-type(1) a") refute element_exists?(html, "#{member_el()}:nth-of-type(2) a") end test "allows removing any type of entry", %{ conn: conn, user: user, team: team } do member2 = add_member(team, role: :admin) _invitation = invite_member(team, "sent@example.com", inviter: user, role: :viewer) site = new_site(owner: user) guest = add_guest(site, role: :viewer, user: new_user(name: "Mr Guest", email: "guest@example.com") ) lv = get_child_lv(conn) add_invite(lv, "pending@example.com", "admin") html = render(lv) assert elem_count(html, member_el()) == 4 assert elem_count(html, guest_el()) == 1 pending = find(html, "#{member_el()}:nth-of-type(1)") |> text() sent = find(html, "#{member_el()}:nth-of-type(2)") |> text() owner = find(html, "#{member_el()}:nth-of-type(3)") |> text() admin = find(html, "#{member_el()}:nth-of-type(4)") |> text() guest_member = find(html, "#{guest_el()}:first-of-type") |> text() assert pending =~ "Invitation pending" assert sent =~ "Invitation sent" assert owner =~ "You" assert admin != "" assert guest_member =~ "Guest" remove_member(lv, 1) # next becomes first remove_member(lv, 1) # last becomes second remove_member(lv, 2) # remove guest remove_member(lv, 1, guest_el()) html = render(lv) |> text() refute html =~ "Invitation pending" refute html =~ "Invitation sent" refute html =~ "Team member" refute html =~ "Guest" save_layout(lv) team = Repo.reload!(team) assert_email_delivered_with( to: [nil: guest.email], subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked" ) assert_email_delivered_with( to: [nil: member2.email], subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked" ) assert_no_emails_delivered() end test "respawns membersip enqueued for deletion", %{ conn: conn, team: team } do member2 = add_member(team, role: :editor, user: new_user(email: "another@example.com")) lv = get_child_lv(conn) remove_member(lv, 1) add_invite(lv, "another@example.com", "viewer") html = render(lv) assert find(html, "#{member_el()}:nth-of-type(2)") |> text() =~ "You" save_layout(lv) assert_no_emails_delivered() assert_team_membership(member2, team, :viewer) end end defp type_into_input(lv, id, text) do lv |> element("form#update-team-form") |> render_change(%{id => text}) end defp add_invite(lv, email, role) do lv |> element(~s|#input-role-picker a[phx-value-role="#{role}"]|) |> render_click() lv |> element("#team-layout-form") |> render_submit(%{ "input-email" => email }) end defp save_layout(lv) do lv |> element("button#save-layout") |> render_click() end defp change_role(lv, index, role, main_selector \\ member_el()) do lv |> element(~s|#{main_selector}:nth-of-type(#{index}) a[phx-value-role="#{role}"]|) |> render_click() end defp get_child_lv(conn, opts \\ []) do {:ok, lv, _} = live(conn, @url) assert lv = find_live_child(lv, "team-management-setup") if Keyword.get(opts, :with_html?) do {lv, render(lv)} else lv end end defp remove_member(lv, index, main_selector \\ member_el()) do lv |> element(~s|#{main_selector}:nth-of-type(#{index}) a[phx-click="remove-member"]|) |> render_click() end defp member_el() do ~s|#member-list div[data-test-kind="member"]| end defp guest_el() do ~s|#guest-list div[data-test-kind="guest"]| end end