analytics/test/plausible_web/live/team_management_test.exs

456 lines
13 KiB
Elixir

defmodule PlausibleWeb.Live.TeamMangementTest do
use PlausibleWeb.ConnCase, async: false
use Bamboo.Test, shared: true
use Plausible.Teams.Test
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
def team_general_path(), do: Routes.settings_path(PlausibleWeb.Endpoint, :team_general)
@subject_prefix if ee?(), do: "[Plausible Analytics] ", else: "[Plausible CE] "
describe "/settings/team/general" do
setup [:create_user, :log_in, :create_team, :setup_team]
test "renders team management section", %{conn: conn} do
resp =
conn
|> get(team_general_path())
|> html_response(200)
|> text()
assert resp =~ "Add or remove team members and adjust their roles"
refute element_exists?(resp, ~s|button[phx-click="save-team-layout"]|)
end
test "renders existing guests under Guest divider", %{conn: conn, team: team, user: user} do
site = new_site(team: team)
add_guest(site, role: :viewer, user: new_user(name: "Mr Guest", email: "guest@example.com"))
resp =
conn
|> get(team_general_path())
|> html_response(200)
assert element_exists?(resp, "#guests-hr")
assert find(resp, "#{member_el()}:first-of-type") |> text() =~ "#{user.email}"
assert find(resp, "#{guest_el()}:first-of-type") |> text() =~ "guest@example.com"
end
test "does not render Guest divider when no guests found", %{conn: conn} do
resp =
conn
|> get(team_general_path())
|> html_response(200)
refute element_exists?(resp, "#guests-hr")
refute element_exists?(resp, "#guest-list")
end
end
on_ee do
describe "live - SSO user" do
setup [
:create_user,
:create_team,
:setup_sso,
:provision_sso_user,
:log_in,
:setup_team
]
test "fails to save layout with SSO user updated to owner with Force SSO but without MFA",
%{
conn: conn,
team: team,
user: user
} do
{:ok, user, _} = Plausible.Auth.TOTP.initiate(user)
{:ok, _user, _} = Plausible.Auth.TOTP.enable(user, :skip_verify)
{:ok, team} = Plausible.Auth.SSO.set_force_sso(team, :all_but_owners)
member = add_member(team, role: :viewer)
{:ok, _, _, _member} =
new_identity(member.name, member.email)
|> Plausible.Auth.SSO.provision_user()
lv = get_liveview(conn)
html = render(lv)
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") == "Viewer"
change_role(lv, 2, "owner")
html = render(lv)
assert html =~ "User must have 2FA enabled to become an owner"
end
end
end
describe "live" do
setup [:create_user, :log_in, :create_team, :setup_team]
test "renders member, immediately delivers invitation", %{conn: conn, user: user, team: team} do
{lv, html} = get_liveview(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 Sent"
member_row2 = find(html, "#{member_el()}:nth-of-type(2)") |> text()
assert member_row2 =~ "#{user.name}"
assert member_row2 =~ "#{user.email}"
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 = get_liveview(conn)
html = render(lv)
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"
assert_no_emails_delivered()
assert_team_membership(member2, team, :viewer)
end
test "allows updating guest membership so it moves sections", %{conn: conn, team: team} do
site = new_site(team: team)
add_guest(site, role: :viewer, user: new_user(name: "Mr Guest", email: "guest@example.com"))
lv = get_liveview(conn)
html = render(lv)
assert length(find(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 length(find(html, member_el())) == 2
refute element_exists?(html, "#guest-list")
end
@tag :ee_only
test "fails to save layout with limits breached", %{conn: conn, team: team} do
lv = get_liveview(conn)
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")
assert lv |> render() |> text() =~ "Your account is limited to 3 team members"
assert Enum.count(Plausible.Teams.Invitations.all(team)) == 3
end
test "fails to accept invitation to already existing e-mail", %{
conn: conn,
user: user
} do
lv = get_liveview(conn)
add_invite(lv, user.email, "admin")
assert lv |> render() |> text() =~
"Error! Make sure the e-mail is valid and is not taken already"
end
@tag :ee_only
test "allows removing any type of entry (EE)", %{
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(team: team)
guest =
add_guest(site,
role: :viewer,
user: new_user(name: "Mr Guest", email: "guest@example.com")
)
lv = get_liveview(conn)
add_invite(lv, "pending@example.com", "admin")
html = render(lv)
assert html |> find(member_el()) |> Enum.count() == 4
assert html |> find(guest_el()) |> Enum.count() == 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 =~ "Team Member"
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"
html = render(lv)
assert html |> find(member_el()) |> Enum.count() == 1
refute element_exists?(html, "#guest-list")
assert_email_delivered_with(
to: [nil: member2.email],
subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked"
)
assert_email_delivered_with(
to: [nil: guest.email],
subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked"
)
assert_no_emails_delivered()
end
@tag :ce_only
test "allows removing any type of entry (CE)", %{
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(team: team)
guest =
add_guest(site,
role: :viewer,
user: new_user(name: "Mr Guest", email: "guest@example.com")
)
lv = get_liveview(conn)
html = render(lv)
assert html |> find(member_el()) |> Enum.count() == 3
assert html |> find(guest_el()) |> Enum.count() == 1
sent = find(html, "#{member_el()}:nth-of-type(1)") |> text()
owner = find(html, "#{member_el()}:nth-of-type(2)") |> text()
admin = find(html, "#{member_el()}:nth-of-type(3)") |> text()
guest_member = find(html, "#{guest_el()}:first-of-type") |> text()
assert sent =~ "Invitation Sent"
assert owner =~ "You"
assert admin =~ "Team Member"
assert guest_member =~ "Guest"
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 Sent"
refute html =~ "Team Member"
refute html =~ "Guest"
html = render(lv)
assert html |> find(member_el()) |> Enum.count() == 1
refute element_exists?(html, "#guest-list")
assert_email_delivered_with(
to: [nil: member2.email],
subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked"
)
assert_email_delivered_with(
to: [nil: guest.email],
subject: @subject_prefix <> "Your access to \"#{team.name}\" team has been revoked"
)
assert_no_emails_delivered()
end
test "guest->owner promotion",
%{
conn: conn,
user: user,
team: team
} do
site = new_site(team: team)
member2 =
add_guest(site,
role: :viewer,
user: new_user(name: "Mr Guest", email: "guest@example.com")
)
lv = get_liveview(conn)
change_role(lv, 1, "owner", guest_el())
html = render(lv)
refute html =~ "Error!"
assert_team_membership(user, team, :owner)
assert_team_membership(member2, team, :owner)
assert_email_delivered_with(
to: [nil: member2.email],
subject: @subject_prefix <> "Welcome to \"#{team.name}\" team"
)
end
test "multiple-owners",
%{
conn: conn,
team: team,
user: user
} do
member2 = add_member(team, role: :admin)
lv = get_liveview(conn)
change_role(lv, 2, "owner")
html = render(lv)
refute html =~ "Error!"
assert_team_membership(user, team, :owner)
assert_team_membership(member2, team, :owner)
end
test "billing role is supported",
%{
conn: conn,
team: team
} do
member2 = add_member(team, role: :admin)
lv = get_liveview(conn)
change_role(lv, 2, "billing")
assert_team_membership(member2, team, :billing)
end
test "degrading owner to non-admin makes everything read-only", %{conn: conn, team: team} do
_owner2 = add_member(team, role: :owner)
lv = get_liveview(conn)
change_role(lv, 1, "billing")
html = render(lv)
options =
lv
|> render()
|> find("#{member_el()} a")
assert Enum.empty?(options)
assert attr_defined?(html, ~s|#team-layout-form input[name="input-email"]|, "readonly")
assert attr_defined?(html, ~s|#invite-member|, "disabled")
end
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 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 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 get_liveview(conn, opts \\ []) do
conn = assign(conn, :live_module, PlausibleWeb.Live.TeamManagement)
{:ok, lv, html} = live(conn, team_general_path())
if Keyword.get(opts, :with_html?) do
{lv, html}
else
lv
end
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