Ensure team is present before use in sync logic (#4709)

* Ensure team is present before use in sync logic

* Ensure teams backfill works against partially assigned sites

* Associate site with team on creation

* Associate site with team on sync

* Reuse alias

* Add tests for invitation creation sync

* Move team assertions to a helper module

* Format

* Test team creation on site creation via Sites context module

* Add tests for teams sync on subscription changes

* Tag tests

* Test grace period start syncing up with teams

* Test grace period manual lock sycning w/ teams

* Test grace period end sycing up w/ teams

* Test clearing grace period sync with teams

* Update moduledoc

* Fix missing preloads and wrong result pattern matching in sync logic

* Test sync on accepting invites and site transfers

* Test sync on membership role update and member removal

* transfer async fix WIP

* Stop privisioning team in site factory

* Remove unused relationship from Site schema

* Ensure consistent parsing of `passthrough` from Paddle webhook

* Update team passthrough notification tests & logic

---------

Co-authored-by: Adam Rutkowski <hq@mtod.org>
This commit is contained in:
Adrian Gruntkowski 2024-10-23 13:29:15 +02:00 committed by GitHub
parent 7794ff1417
commit 7d6f10f0cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 712 additions and 115 deletions

View File

@ -74,18 +74,18 @@ defmodule Plausible.Auth.GracePeriod do
Ecto.Changeset.change(user, grace_period: nil)
end
@spec active?(User.t()) :: boolean()
@spec active?(Plausible.Teams.Team.t() | User.t()) :: boolean()
@doc """
Returns whether the grace period is still active for a User. Defaults to
false if the user is nil or there is no grace period.
"""
def active?(user)
def active?(user_or_team)
def active?(%User{grace_period: %__MODULE__{end_date: %Date{} = end_date}}) do
def active?(%{grace_period: %__MODULE__{end_date: %Date{} = end_date}}) do
Timex.diff(end_date, Date.utc_today(), :days) >= 0
end
def active?(%User{grace_period: %__MODULE__{manual_lock: true}}) do
def active?(%{grace_period: %__MODULE__{manual_lock: true}}) do
true
end

View File

@ -120,25 +120,7 @@ defmodule Plausible.Billing do
defp handle_subscription_created(params) do
params =
if present?(params["passthrough"]) do
case String.split(to_string(params["passthrough"]), ";") do
[user_id] ->
user = Repo.get!(User, user_id)
{:ok, team} = Plausible.Teams.get_or_create(user)
Map.put(params, "team_id", team.id)
[user_id, ""] ->
user = Repo.get!(User, user_id)
{:ok, team} = Plausible.Teams.get_or_create(user)
params
|> Map.put("passthrough", user_id)
|> Map.put("team_id", team.id)
[user_id, team_id] ->
params
|> Map.put("passthrough", user_id)
|> Map.put("team_id", team_id)
end
format_params(params)
else
user = Repo.get_by!(User, email: params["email"])
{:ok, team} = Plausible.Teams.get_or_create(user)
@ -148,7 +130,10 @@ defmodule Plausible.Billing do
|> Map.put("team_id", team.id)
end
subscription_params = format_subscription(params) |> add_last_bill_date(params)
subscription_params =
params
|> format_subscription()
|> add_last_bill_date(params)
%Subscription{}
|> Subscription.changeset(subscription_params)
@ -175,8 +160,13 @@ defmodule Plausible.Billing do
irrelevant? = params["old_status"] == "paused" && params["status"] == "past_due"
if subscription && not irrelevant? do
params =
params
|> format_params()
|> format_subscription()
subscription
|> Subscription.changeset(format_subscription(params))
|> Subscription.changeset(params)
|> Repo.update!()
|> after_subscription_update()
end
@ -230,8 +220,26 @@ defmodule Plausible.Billing do
end
end
defp format_params(%{"passthrough" => passthrough} = params) do
case String.split(to_string(passthrough), ";") do
[user_id] ->
user = Repo.get!(User, user_id)
{:ok, team} = Plausible.Teams.get_or_create(user)
Map.put(params, "team_id", team.id)
["user:" <> user_id, "team:" <> team_id] ->
params
|> Map.put("passthrough", user_id)
|> Map.put("team_id", team_id)
end
end
defp format_params(params) do
params
end
defp format_subscription(params) do
params = %{
subscription_params = %{
paddle_subscription_id: params["subscription_id"],
paddle_plan_id: params["subscription_plan_id"],
cancel_url: params["cancel_url"],
@ -244,9 +252,9 @@ defmodule Plausible.Billing do
}
if team_id = params["team_id"] do
Map.put(params, :team_id, team_id)
Map.put(subscription_params, :team_id, team_id)
else
params
subscription_params
end
end

View File

@ -402,9 +402,11 @@ defmodule Plausible.DataMigration.BackfillTeams do
fn {{owner, site_ids}, idx} ->
@repo.transaction(
fn ->
{:ok, team} = Teams.get_or_create(owner)
team =
"My Team"
|> Teams.Team.changeset()
team
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_change(:trial_expiry_date, owner.trial_expiry_date)
|> Ecto.Changeset.put_change(:accept_traffic_until, owner.accept_traffic_until)
|> Ecto.Changeset.put_change(
@ -412,15 +414,8 @@ defmodule Plausible.DataMigration.BackfillTeams do
owner.allow_next_upgrade_override
)
|> Ecto.Changeset.put_embed(:grace_period, owner.grace_period)
|> Ecto.Changeset.put_change(:inserted_at, owner.inserted_at)
|> Ecto.Changeset.put_change(:updated_at, owner.updated_at)
|> @repo.insert!()
team
|> Teams.Membership.changeset(owner, :owner)
|> Ecto.Changeset.put_change(:inserted_at, owner.inserted_at)
|> Ecto.Changeset.put_change(:updated_at, owner.updated_at)
|> @repo.insert!()
|> Ecto.Changeset.force_change(:updated_at, owner.updated_at)
|> @repo.update!()
@repo.update_all(from(s in Plausible.Site, where: s.id in ^site_ids),
set: [team_id: team.id]

View File

@ -42,12 +42,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do
case Repo.transaction(multi) do
{:ok, changes} ->
with_teams do
sync_job =
Task.async(fn ->
Plausible.Teams.Invitations.transfer_site_sync(site, user)
end)
Task.await(sync_job)
end
membership = Repo.preload(changes.membership, [:site, :user])

View File

@ -179,6 +179,9 @@ defmodule Plausible.Sites do
with :ok <- Quota.ensure_can_add_new_site(user) do
Ecto.Multi.new()
|> Ecto.Multi.put(:site_changeset, Site.new(params))
|> Ecto.Multi.run(:create_team, fn _repo, _context ->
Plausible.Teams.get_or_create(user)
end)
|> Ecto.Multi.run(:clear_changed_from, fn
_repo, %{site_changeset: %{changes: %{domain: domain}}} ->
case get_for_user(user.id, domain, [:owner]) do
@ -196,7 +199,9 @@ defmodule Plausible.Sites do
_repo, _context ->
{:ok, :ignore}
end)
|> Ecto.Multi.insert(:site, fn %{site_changeset: site} -> site end)
|> Ecto.Multi.insert(:site, fn %{site_changeset: site, create_team: team} ->
Ecto.Changeset.put_assoc(site, :team, team)
end)
|> Ecto.Multi.insert(:site_membership, fn %{site: site} ->
Site.Membership.new(site, user)
end)

View File

@ -16,6 +16,27 @@ defmodule Plausible.Teams do
Repo.preload(team, :sites).sites
end
@doc """
Create (when necessary) and load team relation for provided site.
Used for sync logic to work smoothly during transitional period.
"""
def load_for_site(site) do
site = Repo.preload(site, [:team, :owner])
if site.team do
site
else
{:ok, team} = get_or_create(site.owner)
site
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:team, team)
|> Ecto.Changeset.force_change(:updated_at, site.updated_at)
|> Repo.update!()
end
end
@doc """
Get or create user's team.
@ -31,24 +52,21 @@ defmodule Plausible.Teams do
def get_or_create(user) do
with {:error, :no_team} <- get_owned_by_user(user) do
case create_my_team(user) do
{:ok, team} -> {:ok, team}
{:error, :exists_already} -> get_owned_by_user(user)
{:ok, team} ->
{:ok, team}
{:error, :exists_already} ->
get_owned_by_user(user)
end
end
end
def sync_team(user) do
case get_owned_by_user(user) do
{:ok, team} ->
{:ok, team} = get_or_create(user)
team
|> Teams.Team.sync_changeset(user)
|> Repo.update!()
_ ->
:skip
end
:ok
end
defp create_my_team(user) do
@ -88,8 +106,11 @@ defmodule Plausible.Teams do
|> Repo.one()
case result do
nil -> {:error, :no_team}
team -> {:ok, team}
nil ->
{:error, :no_team}
team ->
{:ok, team}
end
end

View File

@ -39,7 +39,7 @@ defmodule Plausible.Teams.Invitations do
end
def invite_sync(site, site_invitation) do
site = Repo.preload(site, :team)
site = Teams.load_for_site(site)
site_invitation = Repo.preload(site_invitation, :inviter)
role = translate_role(site_invitation.role)
@ -57,14 +57,6 @@ defmodule Plausible.Teams.Invitations do
site_invitation.inviter
)
end
catch
_, thrown ->
Sentry.capture_message(
"Failed to sync invitation for site ##{site.id} and email ##{site_invitation.email}",
extra: %{
error: inspect(thrown)
}
)
end
def transfer_site(site, new_owner, now \\ NaiveDateTime.utc_now(:second)) do
@ -75,6 +67,7 @@ defmodule Plausible.Teams.Invitations do
:ok <- ensure_can_take_ownership(site, team) do
site =
Repo.preload(site, [
:team,
:owner,
guest_memberships: [team_membership: :user],
guest_invitations: [team_invitation: :user]
@ -92,10 +85,12 @@ defmodule Plausible.Teams.Invitations do
end
def transfer_site_sync(site, user) do
{:ok, team} = Plausible.Teams.get_or_create(user)
{:ok, team} = Teams.get_or_create(user)
site = Teams.load_for_site(site)
site =
Repo.preload(site, [
:team,
:owner,
guest_memberships: [team_membership: :user],
guest_invitations: [team_invitation: :user]
@ -105,14 +100,6 @@ defmodule Plausible.Teams.Invitations do
Repo.transaction(fn ->
:ok = transfer_site_ownership(site, team, NaiveDateTime.utc_now(:second))
end)
catch
_, thrown ->
Sentry.capture_message(
"Failed to sync transfer site for site ##{site.id} and user ##{user.id}",
extra: %{
error: inspect(thrown)
}
)
end
def accept(invitation_id, user, now \\ NaiveDateTime.utc_now(:second)) do
@ -135,55 +122,52 @@ defmodule Plausible.Teams.Invitations do
site: :team
)
site = Teams.load_for_site(site_invitation.site)
site_invitation = %{site_invitation | site: site}
role =
case site_invitation.role do
:viewer -> :viewer
:admin -> :editor
end
guest_invitation =
{:ok, guest_invitation} =
create_invitation(
site_invitation.site,
site_invitation.invitee_email,
site_invitation.email,
role,
site_invitation.inviter
)
team_invitation =
guest_invitation.team_invitation
|> Repo.preload([:team, :inviter, guest_invitations: :site])
{:ok, _} =
do_accept(guest_invitation.team_invitation, user, NaiveDateTime.utc_now(:second),
send_email?: false
)
catch
_, thrown ->
Sentry.capture_message(
"Failed to sync accept invitation for site ##{site_invitation.site_id} and user ##{user.id}",
extra: %{
error: inspect(thrown)
}
)
do_accept(team_invitation, user, NaiveDateTime.utc_now(:second), send_email?: false)
end
def accept_transfer_sync(site_invitation, user) do
{:ok, team} = Teams.get_or_create(user)
site =
Repo.preload(site_invitation.site, [:owner, guest_memberships: [team_membership: :user]])
site_invitation.site
|> Teams.load_for_site()
|> Repo.preload([
:team,
:owner,
guest_memberships: [team_membership: :user],
guest_invitations: [team_invitation: :user]
])
site_transfer = create_site_transfer(site, site_invitation.inviter, site_invitation.email)
{:ok, site_transfer} =
create_site_transfer(site, site_invitation.inviter, site_invitation.email)
{:ok, _} =
Repo.transaction(fn ->
:ok = transfer_site_ownership(site, team, NaiveDateTime.utc_now(:second))
Repo.delete!(site_transfer)
end)
catch
_, thrown ->
Sentry.capture_message(
"Failed to sync accept transfer for site ##{site_invitation.site_id} and user ##{user.id}",
extra: %{
error: inspect(thrown)
}
)
end
defp check_transfer_permissions(_team, _initiator, false = _check_permissions?) do
@ -536,7 +520,7 @@ defmodule Plausible.Teams.Invitations do
team_membership
|> Teams.GuestMembership.changeset(guest_invitation.site, guest_invitation.role)
|> Repo.insert(
on_conflict: [set: [updated_at: now, role: guest_invitation.role]],
on_conflict: [set: [updated_at: now]],
conflict_target: [:team_membership_id, :site_id]
)

View File

@ -1,5 +1,6 @@
defmodule Plausible.BillingTest do
use Plausible.DataCase
use Plausible.Teams.Test
use Bamboo.Test, shared: true
require Plausible.Billing.Subscription.Status
alias Plausible.Billing
@ -131,6 +132,42 @@ defmodule Plausible.BillingTest do
assert subscription.currency_code == "EUR"
end
@tag :teams
test "creates a subscription with teams passthrough" do
user = insert(:user)
{:ok, team} = Plausible.Teams.get_or_create(user)
assert_team_exists(user)
%{@subscription_created_params | "passthrough" => "user:#{user.id};team:#{team.id}"}
|> Billing.subscription_created()
assert Repo.get_by(Plausible.Billing.Subscription, user_id: user.id, team_id: team.id)
end
@tag :teams
test "creates a team on create subscription" do
user = insert(:user)
%{@subscription_created_params | "passthrough" => user.id}
|> Billing.subscription_created()
team = assert_team_exists(user)
assert Repo.get_by(Plausible.Billing.Subscription, user_id: user.id, team_id: team.id)
end
@tag :teams
test "doesn't create additional teams on create subscription" do
user = insert(:user)
{:ok, team} = Plausible.Teams.get_or_create(user)
%{@subscription_created_params | "passthrough" => user.id}
|> Billing.subscription_created()
team = assert_team_exists(user, team.id)
assert Repo.get_by(Plausible.Billing.Subscription, user_id: user.id, team_id: team.id)
end
test "create with email address" do
user = insert(:user)
@ -212,6 +249,63 @@ defmodule Plausible.BillingTest do
assert subscription.next_bill_amount == "12.00"
end
@tag :teams
test "creates a team on subscription update" do
user = insert(:user)
subscription = insert(:subscription, user: user)
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"passthrough" => user.id
})
|> Billing.subscription_updated()
team = assert_team_exists(user)
assert Repo.get_by(Plausible.Billing.Subscription, user_id: user.id, team_id: team.id)
end
@tag :teams
test "updates subscription with user/team passthrough" do
user = insert(:user)
subscription = insert(:subscription, user: user)
{:ok, team} = Plausible.Teams.get_or_create(user)
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"passthrough" => "user:#{user.id};team:#{team.id}"
})
|> Billing.subscription_updated()
team = assert_team_exists(user)
assert Repo.get_by(Plausible.Billing.Subscription, user_id: user.id, team_id: team.id)
end
@tag :teams
test "syncs team properties with user on subscription update" do
user =
insert(:user, accept_traffic_until: ~D[2001-01-01], allow_next_upgrade_override: true)
user = Plausible.Users.start_grace_period(user)
subscription = insert(:subscription, user: user)
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"passthrough" => user.id
})
|> Billing.subscription_updated()
team = assert_team_exists(user)
user = Repo.reload!(user)
assert team.grace_period == user.grace_period
assert team.trial_expiry_date == user.trial_expiry_date
assert team.accept_traffic_until == user.accept_traffic_until
assert team.allow_next_upgrade_override == user.allow_next_upgrade_override
end
test "status update from 'paused' to 'past_due' is ignored" do
user = insert(:user)
subscription = insert(:subscription, user: user, status: Subscription.Status.paused())

View File

@ -1,6 +1,7 @@
defmodule Plausible.Billing.SiteLockerTest do
use Plausible.DataCase
use Bamboo.Test, shared: true
use Plausible.Teams.Test
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.{SiteLocker, Subscription}
@ -127,6 +128,27 @@ defmodule Plausible.Billing.SiteLockerTest do
assert Repo.reload!(site).locked
end
@tag :teams
test "syncs grace period end with teams" do
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
user = insert(:user, grace_period: grace_period)
insert(:subscription, status: Subscription.Status.active(), user: user)
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
assert SiteLocker.update_sites_for(user) == {:locked, :grace_period_ended_now}
assert user = Repo.reload!(user)
team = assert_team_exists(user)
assert user.grace_period.is_over
assert team.grace_period.is_over
end
test "sends email if grace period has ended" do
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
user = insert(:user, grace_period: grace_period)

View File

@ -3,6 +3,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
require Plausible.Billing.Subscription.Status
use Plausible.DataCase, async: true
use Bamboo.Test
use Plausible.Teams.Test
alias Plausible.Site.Memberships.AcceptInvitation
@ -11,6 +12,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
describe "transfer_ownership/3" do
test "transfers ownership successfully" do
site = insert(:site, memberships: [])
existing_owner = insert(:user)
existing_membership =
@ -34,6 +36,49 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
assert_no_emails_delivered()
end
@tag :teams
test "syncs ownership transfers provided memberships exist already" do
site = insert(:site, memberships: [])
existing_owner = insert(:user)
_existing_membership =
insert(:site_membership, user: existing_owner, site: site, role: :owner)
{:ok, old_team} = Plausible.Teams.get_or_create(existing_owner)
another_user = insert(:user)
insert(:site_membership, user: another_user, site: site, role: :viewer)
another_team_membership =
insert(:team_membership, user: another_user, team: old_team, role: :guest)
_another_guest_membership =
insert(:guest_membership,
team_membership: another_team_membership,
site: site,
role: :viewer
)
new_owner = insert(:user)
insert(:growth_subscription, user: new_owner)
assert {:ok, new_membership} =
AcceptInvitation.transfer_ownership(site, new_owner)
assert new_membership.site_id == site.id
assert new_membership.user_id == new_owner.id
assert new_membership.role == :owner
team = assert_team_exists(new_owner)
assert team.id != old_team.id
assert_team_attached(site, team.id)
assert_guest_membership(team, site, another_user, :viewer)
assert_guest_membership(team, site, existing_owner, :editor)
end
@tag :teams
test "transfers ownership successfully (TEAM)" do
site = insert(:site, memberships: [])
existing_owner = insert(:user)
@ -41,11 +86,9 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
_existing_membership =
insert(:site_membership, user: existing_owner, site: site, role: :owner)
site = Plausible.Teams.load_for_site(site)
old_team = site.team
existing_team_membership =
insert(:team_membership, user: existing_owner, team: old_team, role: :owner)
another_user = insert(:user)
another_team_membership =
@ -70,10 +113,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
assert new_team_membership.user_id == new_owner.id
assert new_team_membership.role == :owner
existing_team_membership = Repo.reload!(existing_team_membership)
assert existing_team_membership.user_id == existing_owner.id
assert existing_team_membership.team_id == old_team.id
assert existing_team_membership.role == :owner
assert_team_membership(existing_owner, old_team)
refute Repo.reload(another_team_membership)
refute Repo.reload(another_guest_membership)
@ -278,6 +318,34 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
)
end
@tag :teams
test "sync newly converted membership with team" do
inviter = insert(:user)
invitee = insert(:user)
site = insert(:site, members: [inviter])
invitation =
insert(:invitation,
site_id: site.id,
inviter: inviter,
email: invitee.email,
role: :admin
)
assert {:ok, membership} =
AcceptInvitation.accept_invitation(invitation.invitation_id, invitee)
assert membership.site_id == site.id
assert membership.user_id == invitee.id
assert membership.role == :admin
team = assert_team_exists(inviter)
assert_team_attached(site, team.id)
assert_guest_membership(team, site, invitee, :editor)
end
@tag :teams
test "converts an invitation into a membership (TEAMS)" do
inviter = insert(:user)
invitee = insert(:user)
@ -424,6 +492,42 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
)
end
@tag :teams
test "syncs accepted ownership transfer to teams" do
site = insert(:site, memberships: [])
existing_owner = insert(:user)
_existing_membership =
insert(:site_membership, user: existing_owner, site: site, role: :owner)
site = Plausible.Teams.load_for_site(site)
old_team = site.team
# site = Repo.reload!(site)
new_owner = insert(:user)
insert(:growth_subscription, user: new_owner)
invitation =
insert(:invitation,
site_id: site.id,
inviter: existing_owner,
email: new_owner.email,
role: :owner
)
assert {:ok, _new_membership} =
AcceptInvitation.accept_invitation(
invitation.invitation_id,
new_owner
)
team = assert_team_exists(new_owner)
assert team.id != old_team.id
assert_team_attached(site, team.id)
assert_guest_membership(team, site, existing_owner, :editor)
end
@tag :ee_only
test "unlocks a previously locked site after transfer" do
site = insert(:site, locked: true, memberships: [])

View File

@ -3,6 +3,7 @@ defmodule Plausible.Site.Memberships.CreateInvitationTest do
use Plausible
use Plausible.DataCase
use Bamboo.Test
use Plausible.Teams.Test
@subject_prefix if ee?(), do: "[Plausible Analytics] ", else: "[Plausible CE] "
@ -27,6 +28,64 @@ defmodule Plausible.Site.Memberships.CreateInvitationTest do
Plausible.Teams.Invitations.invite(site, inviter, invitee.email, :viewer)
end
@tag :teams
test "[TEAMS] syncs a created invitation" do
inviter = insert(:user)
invitee = insert(:user)
site =
insert(:site,
team: nil,
memberships: [build(:site_membership, user: inviter, role: :owner)]
)
assert {:ok, %Plausible.Auth.Invitation{}} =
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
team = assert_team_attached(site)
assert_guest_invitation(team, site, invitee.email, :viewer)
end
@tag :teams
test "[TEAMS] sync a created invitation with team already setup but site not assigned yet" do
inviter = insert(:user)
invitee = insert(:user)
{:ok, %{id: team_id}} = Plausible.Teams.get_or_create(inviter)
site =
insert(:site,
team: nil,
memberships: [build(:site_membership, user: inviter, role: :owner)]
)
assert {:ok, %Plausible.Auth.Invitation{}} =
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
team = assert_team_attached(site, team_id)
assert_guest_invitation(team, site, invitee.email, :viewer)
end
@tag :teams
test "[TEAMS] sync a created invitation with team fully setup" do
inviter = insert(:user)
invitee = insert(:user)
{:ok, %{id: team_id} = team} = Plausible.Teams.get_or_create(inviter)
site =
insert(:site,
team: team,
memberships: [build(:site_membership, user: inviter, role: :owner)]
)
assert {:ok, %Plausible.Auth.Invitation{}} =
CreateInvitation.create_invitation(site, inviter, invitee.email, :viewer)
team = assert_team_attached(site, team_id)
assert_guest_invitation(team, site, invitee.email, :viewer)
end
test "returns validation errors" do
inviter = insert(:user)
team = insert(:team)
@ -211,8 +270,8 @@ defmodule Plausible.Site.Memberships.CreateInvitationTest do
build(:site_membership, user: invitee, role: :viewer)
]
)
|> Plausible.Teams.load_for_site()
insert(:team_membership, team: site.team, user: inviter, role: :owner)
insert(:team_membership, team: site.team, user: invitee, role: :viewer)
assert {:ok, %Plausible.Auth.Invitation{}} =
@ -232,7 +291,7 @@ defmodule Plausible.Site.Memberships.CreateInvitationTest do
]
)
insert(:team_membership, team: site.team, user: inviter, role: :owner)
site = Plausible.Teams.load_for_site(site)
assert {:error, :transfer_to_self} =
CreateInvitation.create_invitation(site, inviter, "vini@plausible.test", :owner)
@ -269,8 +328,8 @@ defmodule Plausible.Site.Memberships.CreateInvitationTest do
build(:site_membership, user: inviter, role: :viewer)
]
)
|> Plausible.Teams.load_for_site()
insert(:team_membership, team: site.team, user: owner, role: :owner)
insert(:team_membership, team: site.team, user: inviter, role: :viewer)
assert {:error, :forbidden} =

View File

@ -1,18 +1,38 @@
defmodule Plausible.SitesTest do
use Plausible.DataCase
use Plausible.Teams.Test
alias Plausible.Sites
describe "create a site" do
@tag :teams
test "creates a site" do
user = insert(:user)
params = %{"domain" => "example.com", "timezone" => "Europe/London"}
assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"}}} =
assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"} = site}} =
Sites.create(user, params)
assert_team_attached(site)
end
@tag :teams
test "creates a site and syncs the team properties" do
user = insert(:user, trial_expiry_date: nil)
params = %{"domain" => "example.com", "timezone" => "Europe/London"}
assert {:ok, %{site: %{domain: "example.com", timezone: "Europe/London"} = site}} =
Sites.create(user, params)
team = assert_team_attached(site)
user = Repo.reload!(user)
assert not is_nil(user.trial_expiry_date)
assert user.trial_expiry_date == team.trial_expiry_date
end
@tag :teams
test "creates a site (TEAM)" do
user = insert(:user)
{:ok, team} = Plausible.Teams.get_or_create(user)
@ -32,6 +52,7 @@ defmodule Plausible.SitesTest do
Sites.create(user, params)
end
@tag :teams
test "fails on invalid timezone (TEAM)" do
user = insert(:user)
{:ok, team} = Plausible.Teams.get_or_create(user)

View File

@ -308,6 +308,32 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
assert membership.role == :viewer
end
@tag :teams
test "syncs role update to team", %{conn: conn, user: user} do
admin = insert(:user)
site =
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner),
build(:site_membership, user: admin, role: :admin)
]
)
|> Plausible.Teams.load_for_site()
team_membership =
insert(:team_membership, user: admin, team: site.team, role: :guest)
guest_membership =
insert(:guest_membership, team_membership: team_membership, site: site, role: :editor)
membership = Repo.get_by(Plausible.Site.Membership, user_id: admin.id)
put(conn, "/sites/#{site.domain}/memberships/#{membership.id}/role/viewer")
assert Repo.reload!(guest_membership).role == :viewer
end
test "can downgrade yourself from admin to viewer, redirects to stats instead", %{
conn: conn,
user: user
@ -436,6 +462,83 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
refute Repo.exists?(from sm in Plausible.Site.Membership, where: sm.user_id == ^admin.id)
end
@tag :teams
test "syncs member removal to team", %{conn: conn, user: user} do
admin = insert(:user)
site =
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner),
build(:site_membership, user: admin, role: :admin)
]
)
|> Plausible.Teams.load_for_site()
team_membership =
insert(:team_membership, user: admin, team: site.team, role: :guest)
guest_membership =
insert(:guest_membership, team_membership: team_membership, site: site, role: :editor)
membership = Enum.find(site.memberships, &(&1.role == :admin))
conn = delete(conn, "/sites/#{site.domain}/memberships/#{membership.id}")
assert Phoenix.Flash.get(conn.assigns.flash, :success) =~ "has been removed"
refute Repo.reload(guest_membership)
refute Repo.reload(team_membership)
end
@tag :teams
test "sync retains team guest membership when there's another guest membership on it", %{
conn: conn,
user: user
} do
admin = insert(:user)
site =
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner),
build(:site_membership, user: admin, role: :admin)
]
)
|> Plausible.Teams.load_for_site()
another_site =
insert(:site,
team: site.team,
memberships: [
build(:site_membership, user: user, role: :owner),
build(:site_membership, user: admin, role: :admin)
]
)
|> Plausible.Teams.load_for_site()
team_membership =
insert(:team_membership, user: admin, team: site.team, role: :guest)
guest_membership =
insert(:guest_membership, team_membership: team_membership, site: site, role: :editor)
another_guest_membership =
insert(:guest_membership,
team_membership: team_membership,
site: another_site,
role: :editor
)
membership = Enum.find(site.memberships, &(&1.role == :admin))
conn = delete(conn, "/sites/#{site.domain}/memberships/#{membership.id}")
assert Phoenix.Flash.get(conn.assigns.flash, :success) =~ "has been removed"
refute Repo.reload(guest_membership)
assert Repo.reload(another_guest_membership)
assert Repo.reload(team_membership)
end
test "fails to remove a member from a foreign site", %{conn: conn, user: user} do
foreign_site =
insert(:site,

View File

@ -75,10 +75,12 @@ defmodule Plausible.Factory do
Map.has_key?(attrs, :members) ||
Map.has_key?(attrs, :owner)
attrs = if defined_memberships?, do: attrs, else: Map.put_new(attrs, :members, [build(:user)])
attrs =
if defined_memberships?,
do: attrs,
else: Map.put_new(attrs, :members, [build(:user)])
site = %Plausible.Site{
team: build(:team),
native_stats_start_at: ~N[2000-01-01 00:00:00],
domain: domain,
timezone: "UTC"

View File

@ -0,0 +1,88 @@
defmodule Plausible.Teams.Test do
@moduledoc """
Convenience assertions for teams schema transition
"""
alias Plausible.Repo
use ExUnit.CaseTemplate
defmacro __using__(_) do
quote do
import Plausible.Teams.Test
end
end
def assert_team_exists(user, team_id \\ nil) do
assert %{team_memberships: memberships} = Repo.preload(user, team_memberships: :team)
tm =
case memberships do
[tm] -> tm
_ -> raise "Team doesn't exist for user #{user.id}"
end
assert tm.role == :owner
assert tm.team.id
if team_id do
assert tm.team.id == team_id
end
tm.team
end
def assert_team_membership(user, team, role \\ :owner) do
assert membership =
Repo.get_by(Plausible.Teams.Membership,
team_id: team.id,
user_id: user.id,
role: role
)
membership
end
def assert_team_attached(site, team_id \\ nil) do
assert site = %{team: team} = site |> Repo.reload!() |> Repo.preload([:team, :owner])
assert membership = assert_team_membership(site.owner, team)
assert membership.team_id == team.id
if team_id do
assert team.id == team_id
end
team
end
def assert_guest_invitation(team, site, email, role) do
assert team_invitation =
Repo.get_by(Plausible.Teams.Invitation,
email: email,
team_id: team.id,
role: :guest
)
assert Repo.get_by(Plausible.Teams.GuestInvitation,
team_invitation_id: team_invitation.id,
site_id: site.id,
role: role
)
end
def assert_guest_membership(team, site, user, role) do
assert team_membership =
Repo.get_by(Plausible.Teams.Membership,
user_id: user.id,
team_id: team.id,
role: :guest
)
assert Repo.get_by(Plausible.Teams.GuestMembership,
team_membership_id: team_membership.id,
site_id: site.id,
role: role
)
end
end

View File

@ -1,6 +1,7 @@
defmodule Plausible.Workers.CheckUsageTest do
use Plausible.DataCase, async: true
use Bamboo.Test
use Plausible.Teams.Test
import Double
alias Plausible.Workers.CheckUsage
@ -204,6 +205,37 @@ defmodule Plausible.Workers.CheckUsageTest do
assert Repo.reload(user).grace_period.end_date == Timex.shift(Timex.today(), days: 7)
end
@tag :teams
test "syncs grace period start with teams",
%{
user: user
} do
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
user: user,
paddle_plan_id: @paddle_id_10k,
last_bill_date: Timex.shift(Timex.today(), days: -1),
status: unquote(status)
)
CheckUsage.perform(nil, usage_stub)
assert user = Repo.reload!(user)
assert user.grace_period.end_date == Timex.shift(Timex.today(), days: 7)
team = assert_team_exists(user)
assert team.grace_period.end_date == user.grace_period.end_date
assert team.grace_period.id == user.grace_period.id
end
test "sends an email suggesting enterprise plan when usage is greater than 10M ", %{
user: user
} do
@ -316,6 +348,42 @@ defmodule Plausible.Workers.CheckUsageTest do
CheckUsage.perform(nil, usage_stub)
refute user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
end
@tag :teams
test "syncs clearing grace period with teams", %{user: user} do
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
user: user,
paddle_plan_id: @paddle_id_10k,
last_bill_date: Timex.shift(Timex.today(), days: -1),
status: unquote(status)
)
CheckUsage.perform(nil, usage_stub)
assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
team = assert_team_exists(user)
assert Plausible.Auth.GracePeriod.active?(team)
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 9_000}
}
end)
CheckUsage.perform(nil, usage_stub)
refute team |> Repo.reload!() |> Plausible.Auth.GracePeriod.active?()
end
end
end
@ -435,6 +503,34 @@ defmodule Plausible.Workers.CheckUsageTest do
CheckUsage.perform(nil, usage_stub)
assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
end
@tag :teams
test "manual lock grace period is synced with teams", %{user: user} do
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
last_cycle: %{date_range: @date_range, total: 1_100_000}
}
end)
enterprise_plan = insert(:enterprise_plan, user: user, monthly_pageview_limit: 1_000_000)
insert(:subscription,
user: user,
paddle_plan_id: enterprise_plan.paddle_plan_id,
last_bill_date: Timex.shift(Timex.today(), days: -1),
status: unquote(status)
)
CheckUsage.perform(nil, usage_stub)
assert user = Repo.reload!(user)
team = assert_team_exists(user)
assert team.grace_period.manual_lock == user.grace_period.manual_lock
assert team.grace_period.manual_lock == true
end
end
end