analytics/test/plausible_web/controllers/settings_controller_test.exs
hq1 2359cb920c
Account settings w sidebar (#4654)
* Outline /settings/v2 fundamentals

* Add setting tiles stubs

* Bootstrap name change

* Bootstrap theme change

* Bootstrap security settings

* Use table component for listing sessions

* Disable current e-mail field

* Implement Danger Zone

* Deal with compilation warnings

* Implement "Subscription" section

* Implement invoices list

* Fix invoices empty state & add API keys

* Fix headings in Subscription section

* Fix API keys mobile view

* Fix subscription boxes width

* Fix formatting

* Move tests for settings WIP

* Adjust remaining tests and router placement

Include docs links in tiles, where applicable.

* Fix remaining routes and remove dead code

* Fix route in a live view where no @conn is available

* Update mobile view settings picker

* Format

* Fix subscription section headings

* Fix account e-mail on dark mode

* Delete unused template

* Fix mobile setting section picker

* Optimize Login Management tile for mobile

* Update invoices section with docs link

* Update copy

* Remove trailing dots from (sub)titles

* Fix CSV export padding for "exporting" state

* Align subscription status to the right

* Fix failing test

* Fix subscription status alignment once again

* Improve subscription mobile view a little

* Fixup test compilation 🙈

* Add extra margin to subscription status box

* Make cancel button in 2FA modals expand in mobile view

* Stats API only

* Capitalize "Current session" indicator

* Show "Show More" invoices button only when there's >12

* tiny change

* Update changelog

---------

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
Co-authored-by: Marko Saric <34340819+metmarkosaric@users.noreply.github.com>
2024-10-08 08:30:01 +00:00

1136 lines
35 KiB
Elixir

defmodule PlausibleWeb.SettingsControllerTest do
use PlausibleWeb.ConnCase, async: true
use Bamboo.Test
use Plausible.Repo
import Mox
import Plausible.Test.Support.HTML
import Ecto.Query
require Plausible.Billing.Subscription.Status
alias Plausible.Auth
alias Plausible.Billing.Subscription
@v3_plan_id "749355"
@v4_plan_id "857097"
@configured_enterprise_plan_paddle_plan_id "123"
setup [:verify_on_exit!]
describe "GET /billing/subscription" do
setup [:create_user, :log_in]
@tag :ee_only
test "shows subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "558018", user: user)
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "10k pageviews"
assert html_response(conn, 200) =~ "monthly billing"
end
@tag :ee_only
test "shows yearly subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "590752", user: user)
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "100k pageviews"
assert html_response(conn, 200) =~ "yearly billing"
end
@tag :ee_only
test "shows free subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "free_10k", user: user)
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "10k pageviews"
assert html_response(conn, 200) =~ "N/A billing"
end
@tag :ee_only
test "shows enterprise plan subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "123", user: user)
configure_enterprise_plan(user)
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "20M pageviews"
assert html_response(conn, 200) =~ "yearly billing"
end
@tag :ee_only
test "shows current enterprise plan subscription when user has a new one to upgrade to", %{
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
user: user
)
insert(:enterprise_plan,
paddle_plan_id: "1234",
user: user,
monthly_pageview_limit: 10_000_000,
billing_interval: :yearly
)
configure_enterprise_plan(user)
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "20M pageviews"
assert html_response(conn, 200) =~ "yearly billing"
end
@tag :ee_only
test "renders two links to '/billing/choose-plan` with the text 'Upgrade'", %{conn: conn} do
doc =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
upgrade_link_1 = find(doc, "#monthly-quota-box a")
upgrade_link_2 = find(doc, "#upgrade-link-2")
assert text(upgrade_link_1) == "Upgrade"
assert text_of_attr(upgrade_link_1, "href") == Routes.billing_path(conn, :choose_plan)
assert text(upgrade_link_2) == "Upgrade"
assert text_of_attr(upgrade_link_2, "href") == Routes.billing_path(conn, :choose_plan)
end
@tag :ee_only
test "renders a link to '/billing/choose-plan' with the text 'Change plan' + cancel link", %{
conn: conn,
user: user
} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
doc =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
refute element_exists?(doc, "#upgrade-link-2")
assert doc =~ "Cancel my subscription"
change_plan_link = find(doc, "#monthly-quota-box a")
assert text(change_plan_link) == "Change plan"
assert text_of_attr(change_plan_link, "href") == Routes.billing_path(conn, :choose_plan)
end
test "/billing/choose-plan link does not show up when enterprise subscription is past_due", %{
conn: conn,
user: user
} do
configure_enterprise_plan(user)
insert(:subscription,
user: user,
status: Subscription.Status.past_due(),
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id
)
doc =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
refute element_exists?(doc, "#upgrade-or-change-plan-link")
end
test "/billing/choose-plan link does not show up when enterprise subscription is paused", %{
conn: conn,
user: user
} do
configure_enterprise_plan(user)
insert(:subscription,
user: user,
status: Subscription.Status.paused(),
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id
)
doc =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
refute element_exists?(doc, "#upgrade-or-change-plan-link")
end
@tag :ee_only
test "renders two links to '/billing/choose-plan' with the text 'Upgrade' for a configured enterprise plan",
%{conn: conn, user: user} do
configure_enterprise_plan(user)
doc =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
upgrade_link_1 = find(doc, "#monthly-quota-box a")
upgrade_link_2 = find(doc, "#upgrade-link-2")
assert text(upgrade_link_1) == "Upgrade"
assert text_of_attr(upgrade_link_1, "href") ==
Routes.billing_path(conn, :choose_plan)
assert text(upgrade_link_2) == "Upgrade"
assert text_of_attr(upgrade_link_2, "href") ==
Routes.billing_path(conn, :choose_plan)
end
@tag :ee_only
test "links to '/billing/choose-plan' with the text 'Change plan' for a configured enterprise plan with an existing subscription + renders cancel button",
%{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
configure_enterprise_plan(user)
doc =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
refute element_exists?(doc, "#upgrade-link-2")
assert doc =~ "Cancel my subscription"
change_plan_link = find(doc, "#monthly-quota-box a")
assert text(change_plan_link) == "Change plan"
assert text_of_attr(change_plan_link, "href") ==
Routes.billing_path(conn, :choose_plan)
end
@tag :ee_only
test "renders cancelled subscription notice", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
next_bill_date: ~D[2023-01-01]
)
notice_text =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> text_of_element("#global-subscription-cancelled-notice")
assert notice_text =~ "Subscription cancelled"
assert notice_text =~ "Upgrade your subscription to get access to your stats again"
end
@tag :ee_only
test "renders cancelled subscription notice with some subscription days still left", %{
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
next_bill_date: Timex.shift(Timex.today(), days: 10)
)
notice_text =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> text_of_element("#global-subscription-cancelled-notice")
assert notice_text =~ "Subscription cancelled"
assert notice_text =~ "You have access to your stats until"
assert notice_text =~ "Upgrade your subscription to make sure you don't lose access"
end
@tag :ee_only
test "renders cancelled subscription notice with a warning about losing grandfathering", %{
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @v3_plan_id,
user: user,
status: :deleted,
next_bill_date: Timex.shift(Timex.today(), days: 10)
)
notice_text =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> text_of_element("#global-subscription-cancelled-notice")
assert notice_text =~ "Subscription cancelled"
assert notice_text =~ "You have access to your stats until"
assert notice_text =~
"by letting your subscription expire, you lose access to our grandfathered terms"
end
test "does not show invoice section for a user with no subscription", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :invoices))
assert html_response(conn, 200) =~ "No invoices issued yet"
end
@tag :ee_only
test "renders pageview usage for current, last, and penultimate billing cycles", %{
conn: conn,
user: user
} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -20)),
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -50)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -50))
])
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
last_bill_date: last_bill_date
)
html =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
assert text_of_element(html, "#billing_cycle_tab_current_cycle") =~
Date.range(
last_bill_date,
Timex.shift(last_bill_date, months: 1, days: -1)
)
|> PlausibleWeb.TextHelpers.format_date_range()
assert text_of_element(html, "#billing_cycle_tab_last_cycle") =~
Date.range(
Timex.shift(last_bill_date, months: -1),
Timex.shift(last_bill_date, days: -1)
)
|> PlausibleWeb.TextHelpers.format_date_range()
assert text_of_element(html, "#billing_cycle_tab_penultimate_cycle") =~
Date.range(
Timex.shift(last_bill_date, months: -2),
Timex.shift(last_bill_date, months: -1, days: -1)
)
|> PlausibleWeb.TextHelpers.format_date_range()
assert text_of_element(html, "#total_pageviews_current_cycle") =~
"Total billable pageviews 1"
assert text_of_element(html, "#pageviews_current_cycle") =~ "Pageviews 1"
assert text_of_element(html, "#custom_events_current_cycle") =~ "Custom events 0"
assert text_of_element(html, "#total_pageviews_last_cycle") =~
"Total billable pageviews 1 / 10,000"
assert text_of_element(html, "#pageviews_last_cycle") =~ "Pageviews 0"
assert text_of_element(html, "#custom_events_last_cycle") =~ "Custom events 1"
assert text_of_element(html, "#total_pageviews_penultimate_cycle") =~
"Total billable pageviews 2 / 10,000"
assert text_of_element(html, "#pageviews_penultimate_cycle") =~ "Pageviews 1"
assert text_of_element(html, "#custom_events_penultimate_cycle") =~ "Custom events 1"
end
@tag :ee_only
test "renders pageview usage per billing cycle for active subscribers", %{
conn: conn,
user: user
} do
assert_cycles_rendered = fn doc ->
refute element_exists?(doc, "#total_pageviews_last_30_days")
assert element_exists?(doc, "#total_pageviews_current_cycle")
assert element_exists?(doc, "#total_pageviews_last_cycle")
assert element_exists?(doc, "#total_pageviews_penultimate_cycle")
end
# for an active subscription
subscription =
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :active,
last_bill_date: Timex.shift(Timex.now(), months: -6)
)
get(conn, Routes.settings_path(conn, :subscription))
|> html_response(200)
|> assert_cycles_rendered.()
# for a past_due subscription
subscription =
subscription
|> Plausible.Billing.Subscription.changeset(%{status: :past_due})
|> Repo.update!()
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> assert_cycles_rendered.()
# for a deleted (but not expired) subscription
subscription
|> Plausible.Billing.Subscription.changeset(%{
status: :deleted,
next_bill_date: Timex.shift(Timex.now(), months: 6)
})
|> Repo.update!()
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> assert_cycles_rendered.()
end
@tag :ee_only
test "penultimate cycle is disabled if there's no usage", %{conn: conn, user: user} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -20))
])
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
last_bill_date: last_bill_date
)
html =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
assert class_of_element(html, "#billing_cycle_tab_penultimate_cycle button") =~
"pointer-events-none"
assert text_of_element(html, "#billing_cycle_tab_penultimate_cycle") =~ "Not available"
end
@tag :ee_only
test "last cycle tab is selected by default", %{
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
html =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
assert text_of_attr(find(html, "#monthly_pageview_usage_container"), "x-data") ==
"{ tab: 'last_cycle' }"
end
@tag :ee_only
test "renders last 30 days pageview usage for trials and non-active/free_10k subscriptions",
%{
conn: conn,
user: user
} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -1)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -10)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -20))
])
assert_usage = fn doc ->
refute element_exists?(doc, "#total_pageviews_current_cycle")
assert text_of_element(doc, "#total_pageviews_last_30_days") =~
"Total billable pageviews (last 30 days) 3"
assert text_of_element(doc, "#pageviews_last_30_days") =~ "Pageviews 1"
assert text_of_element(doc, "#custom_events_last_30_days") =~ "Custom events 2"
end
# for a trial user
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> assert_usage.()
# for an expired subscription
subscription =
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
last_bill_date: ~D[2022-01-01],
next_bill_date: ~D[2022-02-01]
)
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> assert_usage.()
# for a paused subscription
subscription =
subscription
|> Plausible.Billing.Subscription.changeset(%{status: :paused})
|> Repo.update!()
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> assert_usage.()
# for a free_10k subscription (without a `last_bill_date`)
Repo.delete!(subscription)
Plausible.Billing.Subscription.free(%{user_id: user.id})
|> Repo.insert!()
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> assert_usage.()
end
@tag :ee_only
test "renders sites usage and limit", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
insert(:site, members: [user])
site_usage_row_text =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> text_of_element("#site-usage-row")
assert site_usage_row_text =~ "Owned sites 1 / 50"
end
@tag :ee_only
test "renders team members usage and limit", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v4_plan_id, user: user)
team_member_usage_row_text =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> text_of_element("#team-member-usage-row")
assert team_member_usage_row_text =~ "Team members 0 / 3"
end
@tag :ee_only
test "renders team member usage without limit if it's unlimited", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
team_member_usage_row_text =
conn
|> get(Routes.settings_path(conn, :subscription))
|> html_response(200)
|> text_of_element("#team-member-usage-row")
assert team_member_usage_row_text == "Team members 0"
end
end
describe "GET /billing/invoices" do
setup [:create_user, :log_in]
test "does not show invoice section for a free subscription", %{conn: conn, user: user} do
Plausible.Billing.Subscription.free(%{user_id: user.id, currency_code: "EUR"})
|> Repo.insert!()
html =
conn
|> get(Routes.settings_path(conn, :invoices))
|> html_response(200)
assert html =~ "No invoices issued yet"
end
@tag :ee_only
test "shows invoices for subscribed user", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: "558018",
paddle_subscription_id: "redundant",
user: user
)
html =
conn
|> get(Routes.settings_path(conn, :invoices))
|> html_response(200)
assert html =~ "Dec 24, 2020"
assert html =~ "€11.11"
assert html =~ "Nov 24, 2020"
assert html =~ "$22.00"
end
@tag :ee_only
test "shows message on failed invoice request'", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: "558018",
paddle_subscription_id: "invalid_subscription_id",
user: user
)
html =
conn
|> get(Routes.settings_path(conn, :invoices))
|> html_response(200)
assert html =~ "Invoices"
assert text(html) =~ "We couldn't retrieve your invoices"
end
end
describe "GET /security" do
setup [:create_user, :log_in]
test "renders 2FA section in disabled state", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :security))
assert html_response(conn, 200) =~ "Enable 2FA"
end
test "renders 2FA in enabled state", %{conn: conn, user: user} do
{:ok, user, _} = Auth.TOTP.initiate(user)
{:ok, _, _} = Auth.TOTP.enable(user, :skip_verify)
conn = get(conn, Routes.settings_path(conn, :security))
assert html_response(conn, 200) =~ "Disable 2FA"
end
test "renders active user sessions with an option to revoke them", %{conn: conn, user: user} do
now = NaiveDateTime.utc_now(:second)
seventy_minutes_ago = NaiveDateTime.shift(now, minute: -70)
another_session =
user
|> Auth.UserSession.new_session("Some Device", seventy_minutes_ago)
|> Repo.insert!()
conn = get(conn, Routes.settings_path(conn, :security))
assert html = html_response(conn, 200)
assert html =~ "Unknown"
assert html =~ "Current session"
assert html =~ "Just recently"
assert html =~ "Some Device"
assert html =~ "1 hour ago"
assert html =~ Routes.settings_path(conn, :delete_session, another_session.id)
end
end
describe "DELETE /security/user-sessions/:id" do
setup [:create_user, :log_in]
test "deletes session", %{conn: conn, user: user} do
another_session =
user
|> Auth.UserSession.new_session("Some Device")
|> Repo.insert!()
conn = delete(conn, Routes.settings_path(conn, :delete_session, another_session.id))
assert Phoenix.Flash.get(conn.assigns.flash, :success) == "Session logged out successfully"
assert redirected_to(conn, 302) ==
Routes.settings_path(conn, :security) <> "#user-sessions"
refute Repo.reload(another_session)
end
test "refuses deletion when not logged in" do
another_session =
insert(:user)
|> Auth.UserSession.new_session("Some Device")
|> Repo.insert!()
conn = build_conn()
conn = delete(conn, Routes.settings_path(conn, :delete_session, another_session.id))
assert redirected_to(conn, 302) == Routes.auth_path(conn, :login_form)
assert Repo.reload(another_session)
end
end
describe "POST /preferences/name" do
setup [:create_user, :log_in]
test "updates user's name", %{conn: conn, user: user} do
conn =
post(conn, Routes.settings_path(conn, :update_name), %{"user" => %{"name" => "New name"}})
assert redirected_to(conn, 302) ==
Routes.settings_path(conn, :preferences) <> "#update-name"
user = Plausible.Repo.get(Plausible.Auth.User, user.id)
assert user.name == "New name"
end
test "renders form with error if form validations fail", %{conn: conn} do
conn = post(conn, Routes.settings_path(conn, :update_name), %{"user" => %{"name" => ""}})
assert text(html_response(conn, 200)) =~ "can't be blank"
end
end
describe "POST /security/password" do
setup [:create_user, :log_in]
test "updates the password and kills other sessions", %{conn: conn, user: user} do
password = "very-long-very-secret-123"
new_password = "super-long-super-secret-999"
another_session =
user
|> Auth.UserSession.new_session("Some Device")
|> Repo.insert!()
original =
user
|> Auth.User.set_password(password)
|> Repo.update!()
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{
"password" => new_password,
"old_password" => password,
"password_confirmation" => new_password
}
})
assert redirected_to(conn, 302) ==
Routes.settings_path(conn, :security) <> "#update-password"
current_hash = Repo.reload!(user).password_hash
assert current_hash != original.password_hash
assert Plausible.Auth.Password.match?(new_password, current_hash)
assert [remaining_session] = Repo.preload(user, :sessions).sessions
assert remaining_session.id != another_session.id
end
test "fails to update weak password", %{conn: conn} do
password = "very-long-very-secret-123"
new_password = "weak"
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{
"password" => new_password,
"old_password" => password,
"password_confirmation" => new_password
}
})
assert html = html_response(conn, 200)
assert html =~ "is too weak"
end
test "fails to update confirmation mismatch", %{conn: conn} do
password = "very-long-very-secret-123"
new_password = "super-long-super-secret-999"
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{
"password" => new_password,
"old_password" => password,
"password_confirmation" => new_password <> "mismatch"
}
})
assert html = html_response(conn, 200)
assert html =~ "does not match confirmation"
end
test "updates the password when 2FA is enabled", %{conn: conn, user: user} do
password = "very-long-very-secret-123"
new_password = "super-long-super-secret-999"
original =
user
|> Auth.User.set_password(password)
|> Repo.update!()
{:ok, user, _} = Auth.TOTP.initiate(user)
{:ok, user, _} = Auth.TOTP.enable(user, :skip_verify)
code = NimbleTOTP.verification_code(user.totp_secret)
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{
"password" => new_password,
"old_password" => password,
"password_confirmation" => new_password,
"two_factor_code" => code
}
})
assert redirected_to(conn, 302) ==
Routes.settings_path(conn, :security) <> "#update-password"
current_hash = Repo.reload!(user).password_hash
assert current_hash != original.password_hash
assert Plausible.Auth.Password.match?(new_password, current_hash)
end
test "fails to update with wrong 2fa code", %{conn: conn, user: user} do
password = "very-long-very-secret-123"
user =
user
|> Auth.User.set_password(password)
|> Repo.update!()
new_password = "super-long-super-secret-999"
{:ok, user, _} = Auth.TOTP.initiate(user)
{:ok, _, _} = Auth.TOTP.enable(user, :skip_verify)
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{
"password" => new_password,
"old_password" => password,
"password_confirmation" => new_password,
"two_factor_code" => "111111"
}
})
assert html = html_response(conn, 200)
assert html =~ "invalid 2FA code"
end
test "fails to update with missing 2fa code", %{conn: conn, user: user} do
password = "very-long-very-secret-123"
user =
user
|> Auth.User.set_password(password)
|> Repo.update!()
new_password = "super-long-super-secret-999"
{:ok, user, _} = Auth.TOTP.initiate(user)
{:ok, _, _} = Auth.TOTP.enable(user, :skip_verify)
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{
"password" => new_password,
"old_password" => password,
"password_confirmation" => new_password
}
})
assert html = html_response(conn, 200)
assert html =~ "invalid 2FA code"
end
test "fails to update with no input", %{conn: conn} do
conn =
post(conn, Routes.settings_path(conn, :update_password), %{
"user" => %{}
})
assert html = html_response(conn, 200)
assert text(html) =~ "can't be blank"
end
end
describe "POST /security/email" do
setup [:create_user, :log_in]
test "updates email and forces reverification", %{conn: conn, user: user} do
password = "very-long-very-secret-123"
user
|> Auth.User.set_password(password)
|> Repo.update!()
assert user.email_verified
conn =
post(conn, Routes.settings_path(conn, :update_email), %{
"user" => %{"email" => "new" <> user.email, "password" => password}
})
assert redirected_to(conn, 302) == Routes.auth_path(conn, :activate)
updated_user = Repo.reload!(user)
assert updated_user.email == "new" <> user.email
assert updated_user.previous_email == user.email
refute updated_user.email_verified
assert_delivered_email_matches(%{to: [{_, user_email}], subject: subject})
assert user_email == updated_user.email
assert subject =~ "is your Plausible email verification code"
end
test "renders an error on third change attempt (allows 2 per hour)", %{conn: conn, user: user} do
payload = %{
"user" => %{"email" => "new" <> user.email, "password" => "badpass"}
}
resp1 =
conn |> post(Routes.settings_path(conn, :update_email), payload) |> html_response(200)
assert resp1 =~ "is invalid"
refute resp1 =~ "too many requests, try again in an hour"
resp2 =
conn |> post(Routes.settings_path(conn, :update_email), payload) |> html_response(200)
assert resp2 =~ "is invalid"
refute resp2 =~ "too many requests, try again in an hour"
resp3 =
conn |> post(Routes.settings_path(conn, :update_email), payload) |> html_response(200)
assert resp3 =~ "is invalid"
assert resp3 =~ "too many requests, try again in an hour"
end
test "renders form with error on no fields filled", %{conn: conn} do
conn = post(conn, Routes.settings_path(conn, :update_email), %{"user" => %{}})
assert text(html_response(conn, 200)) =~ "can't be blank"
end
test "renders form with error on invalid password", %{conn: conn, user: user} do
conn =
post(conn, Routes.settings_path(conn, :update_email), %{
"user" => %{"password" => "invalid", "email" => "new" <> user.email}
})
assert html_response(conn, 200) =~ "is invalid"
end
test "renders form with error on already taken email", %{conn: conn, user: user} do
other_user = insert(:user)
password = "very-long-very-secret-123"
user
|> Auth.User.set_password(password)
|> Repo.update!()
conn =
post(conn, Routes.settings_path(conn, :update_email), %{
"user" => %{"password" => password, "email" => other_user.email}
})
assert html_response(conn, 200) =~ "has already been taken"
end
test "renders form with error when email is identical with the current one", %{
conn: conn,
user: user
} do
password = "very-long-very-secret-123"
user
|> Auth.User.set_password(password)
|> Repo.update!()
conn =
post(conn, Routes.settings_path(conn, :update_email), %{
"user" => %{"password" => password, "email" => user.email}
})
assert html_response(conn, 200) =~ "can&#39;t be the same"
end
end
describe "POST /security/email/cancel" do
setup [:create_user, :log_in]
test "cancels email reverification in progress", %{conn: conn, user: user} do
user =
user
|> Ecto.Changeset.change(
email_verified: false,
email: "new" <> user.email,
previous_email: user.email
)
|> Repo.update!()
conn = post(conn, Routes.settings_path(conn, :cancel_update_email))
assert redirected_to(conn, 302) ==
Routes.settings_path(conn, :security) <> "#update-email"
updated_user = Repo.reload!(user)
assert updated_user.email_verified
assert updated_user.email == user.previous_email
refute updated_user.previous_email
end
test "fails to cancel reverification when previous email is already retaken", %{
conn: conn,
user: user
} do
user =
user
|> Ecto.Changeset.change(
email_verified: false,
email: "new" <> user.email,
previous_email: user.email
)
|> Repo.update!()
_other_user = insert(:user, email: user.previous_email)
conn =
post(conn, Routes.settings_path(conn, :cancel_update_email))
assert redirected_to(conn, 302) == Routes.auth_path(conn, :activate_form)
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
"Could not cancel email update"
end
test "crashes when previous email is empty on cancel (should not happen)", %{
conn: conn,
user: user
} do
user
|> Ecto.Changeset.change(
email_verified: false,
email: "new" <> user.email,
previous_email: nil
)
|> Repo.update!()
assert_raise RuntimeError, ~r/Previous email is empty for user/, fn ->
post(conn, Routes.settings_path(conn, :cancel_update_email))
end
end
end
describe "POST /settings/api-keys" do
setup [:create_user, :log_in]
test "can create an API key", %{conn: conn, user: user} do
insert(:site, memberships: [build(:site_membership, user: user, role: "owner")])
conn =
post(conn, Routes.settings_path(conn, :api_keys), %{
"api_key" => %{
"user_id" => user.id,
"name" => "all your code are belong to us",
"key" => "swordfish"
}
})
key = Plausible.Auth.ApiKey |> where(user_id: ^user.id) |> Repo.one()
assert conn.status == 302
assert key.name == "all your code are belong to us"
end
test "cannot create a duplicate API key", %{conn: conn, user: user} do
insert(:site, memberships: [build(:site_membership, user: user, role: "owner")])
conn =
post(conn, Routes.settings_path(conn, :api_keys), %{
"api_key" => %{
"user_id" => user.id,
"name" => "all your code are belong to us",
"key" => "swordfish"
}
})
conn2 =
post(conn, Routes.settings_path(conn, :api_keys), %{
"api_key" => %{
"user_id" => user.id,
"name" => "all your code are belong to us",
"key" => "swordfish"
}
})
assert html_response(conn2, 200) =~ "has already been taken"
end
test "can't create api key into another site", %{conn: conn, user: me} do
_my_site = insert(:site, memberships: [build(:site_membership, user: me, role: "owner")])
other_user = insert(:user)
_other_site =
insert(:site, memberships: [build(:site_membership, user: other_user, role: "owner")])
conn =
post(conn, Routes.settings_path(conn, :api_keys), %{
"api_key" => %{
"user_id" => other_user.id,
"name" => "all your code are belong to us",
"key" => "swordfish"
}
})
assert conn.status == 302
refute Plausible.Auth.ApiKey |> where(user_id: ^other_user.id) |> Repo.one()
end
end
describe "DELETE /settings/api-keys/:id" do
setup [:create_user, :log_in]
alias Plausible.Auth.ApiKey
test "can't delete api key that doesn't belong to me", %{conn: conn} do
other_user = insert(:user)
insert(:site, memberships: [build(:site_membership, user: other_user, role: "owner")])
assert {:ok, %ApiKey{} = api_key} =
%ApiKey{user_id: other_user.id}
|> ApiKey.changeset(%{"name" => "other user's key"})
|> Repo.insert()
conn = delete(conn, Routes.settings_path(conn, :delete_api_key, api_key.id))
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Could not find API Key to delete"
assert Repo.get(ApiKey, api_key.id)
end
end
defp configure_enterprise_plan(user) do
insert(:enterprise_plan,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
user: user,
monthly_pageview_limit: 20_000_000,
billing_interval: :yearly
)
end
end