Replace the create_fee_type/create_cycle helpers duplicated across 18/8 membership-fee test files with a single shared definition in Mv.Fixtures, reconciling the divergent local signatures (including the reversed argument order) into one superset so behavior is unchanged.
416 lines
12 KiB
Elixir
416 lines
12 KiB
Elixir
defmodule Mv.Fixtures do
|
|
@moduledoc """
|
|
Shared test fixtures for consistent test data creation.
|
|
"""
|
|
|
|
alias Mv.Accounts
|
|
alias Mv.Authorization
|
|
alias Mv.Helpers.SystemActor
|
|
alias Mv.Membership
|
|
|
|
@doc """
|
|
Creates a member with default or custom attributes.
|
|
|
|
Uses system_actor for authorization to bypass permission checks in tests.
|
|
|
|
## Parameters
|
|
- `attrs` - Map or keyword list of attributes to override defaults
|
|
|
|
## Returns
|
|
- Member struct
|
|
|
|
## Examples
|
|
|
|
iex> member_fixture()
|
|
%Mv.Membership.Member{first_name: "Test", ...}
|
|
|
|
iex> member_fixture(%{first_name: "Alice", email: "alice@example.com"})
|
|
%Mv.Membership.Member{first_name: "Alice", email: "alice@example.com"}
|
|
|
|
"""
|
|
def member_fixture(attrs \\ %{}) do
|
|
system_actor = SystemActor.get_system_actor()
|
|
|
|
attrs
|
|
|> Enum.into(%{
|
|
first_name: "Test",
|
|
last_name: "Member",
|
|
email: "test#{System.unique_integer([:positive])}@example.com"
|
|
})
|
|
|> Membership.create_member(actor: system_actor)
|
|
|> case do
|
|
{:ok, member} -> member
|
|
{:error, error} -> raise "Failed to create member: #{inspect(error)}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a user with default or custom attributes.
|
|
|
|
Uses system_actor for authorization to bypass permission checks in tests.
|
|
|
|
Note: create_user action should work via AshAuthentication bypass,
|
|
but we use system_actor for consistency and safety.
|
|
|
|
## Parameters
|
|
- `attrs` - Map or keyword list of attributes to override defaults
|
|
|
|
## Returns
|
|
- User struct
|
|
|
|
## Examples
|
|
|
|
iex> user_fixture()
|
|
%Mv.Accounts.User{email: "user123@example.com"}
|
|
|
|
iex> user_fixture(%{email: "custom@example.com"})
|
|
%Mv.Accounts.User{email: "custom@example.com"}
|
|
|
|
"""
|
|
def user_fixture(attrs \\ %{}) do
|
|
system_actor = SystemActor.get_system_actor()
|
|
|
|
attrs
|
|
|> Enum.into(%{
|
|
email: "user#{System.unique_integer([:positive])}@example.com"
|
|
})
|
|
|> Accounts.create_user(actor: system_actor)
|
|
|> case do
|
|
{:ok, user} -> user
|
|
{:error, error} -> raise "Failed to create user: #{inspect(error)}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a user linked to a member.
|
|
|
|
## Parameters
|
|
- `user_attrs` - Map or keyword list of user attributes
|
|
- `member_attrs` - Map or keyword list of member attributes
|
|
|
|
## Returns
|
|
- Tuple of {user, member}
|
|
|
|
## Examples
|
|
|
|
iex> {user, member} = linked_user_member_fixture()
|
|
iex> user.member_id == member.id
|
|
true
|
|
|
|
"""
|
|
def linked_user_member_fixture(user_attrs \\ %{}, member_attrs \\ %{}) do
|
|
member = member_fixture(member_attrs)
|
|
|
|
user_attrs = Map.put(user_attrs, :member, %{id: member.id})
|
|
user = user_fixture(user_attrs)
|
|
|
|
{user, member}
|
|
end
|
|
|
|
@doc """
|
|
Creates a role with a specific permission set.
|
|
|
|
Uses system_actor for authorization to bypass permission checks in tests.
|
|
|
|
## Parameters
|
|
- `permission_set_name` - The permission set name (e.g., "admin", "read_only", "normal_user", "own_data")
|
|
|
|
## Returns
|
|
- Role struct
|
|
|
|
## Examples
|
|
|
|
iex> role_fixture("admin")
|
|
%Mv.Authorization.Role{permission_set_name: "admin", ...}
|
|
|
|
"""
|
|
def role_fixture(permission_set_name) do
|
|
system_actor = SystemActor.get_system_actor()
|
|
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
|
|
|
case Authorization.create_role(
|
|
%{
|
|
name: role_name,
|
|
description: "Test role for #{permission_set_name}",
|
|
permission_set_name: permission_set_name
|
|
},
|
|
actor: system_actor
|
|
) do
|
|
{:ok, role} -> role
|
|
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a user with a specific permission set (role).
|
|
|
|
## Parameters
|
|
- `permission_set_name` - The permission set name (e.g., "admin", "read_only", "normal_user", "own_data")
|
|
- `user_attrs` - Optional user attributes
|
|
|
|
## Returns
|
|
- User struct with role preloaded
|
|
|
|
## Examples
|
|
|
|
iex> admin_user = user_with_role_fixture("admin")
|
|
iex> admin_user.role.permission_set_name
|
|
"admin"
|
|
|
|
"""
|
|
def user_with_role_fixture(permission_set_name \\ "admin", user_attrs \\ %{}) do
|
|
system_actor = SystemActor.get_system_actor()
|
|
|
|
# Create role with permission set
|
|
role = role_fixture(permission_set_name)
|
|
|
|
# Create user
|
|
{:ok, user} =
|
|
user_attrs
|
|
|> Enum.into(%{
|
|
email: "user#{System.unique_integer([:positive])}@example.com"
|
|
})
|
|
|> Accounts.create_user(actor: system_actor)
|
|
|
|
# Assign role to user
|
|
{:ok, user} =
|
|
user
|
|
|> Ash.Changeset.for_update(:update, %{})
|
|
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
|
|> Ash.update(actor: system_actor)
|
|
|
|
# Reload user with role preloaded (critical for authorization!)
|
|
{:ok, user_with_role} = Ash.load(user, :role, domain: Accounts, actor: system_actor)
|
|
user_with_role
|
|
end
|
|
|
|
@doc """
|
|
Creates a user with password authentication and a specific role.
|
|
|
|
This is useful for tests that need to use password-based registration
|
|
but also require the user to have a role for authorization.
|
|
|
|
## Parameters
|
|
- `email` - User email (defaults to unique generated email)
|
|
- `password` - User password (defaults to "testpassword123")
|
|
- `permission_set_name` - The permission set name (defaults to "own_data")
|
|
|
|
## Returns
|
|
- User struct with role preloaded
|
|
|
|
## Examples
|
|
|
|
iex> user = password_user_with_role_fixture()
|
|
iex> user.role.permission_set_name
|
|
"own_data"
|
|
|
|
"""
|
|
def password_user_with_role_fixture(opts \\ %{}) do
|
|
email = Map.get(opts, :email, "user#{System.unique_integer([:positive])}@example.com")
|
|
password = Map.get(opts, :password, "testpassword123")
|
|
permission_set_name = Map.get(opts, :permission_set_name, "own_data")
|
|
|
|
# Create role with permission set
|
|
role = role_fixture(permission_set_name)
|
|
|
|
# Create user with password (without password_confirmation as it's optional)
|
|
{:ok, user} =
|
|
Mv.Accounts.User
|
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
|
email: email,
|
|
password: password
|
|
})
|
|
|> Ash.create()
|
|
|
|
# Assign role to user
|
|
{:ok, user} =
|
|
user
|
|
|> Ash.Changeset.for_update(:update, %{})
|
|
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
|
|> Ash.update()
|
|
|
|
# Reload user with role preloaded
|
|
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts)
|
|
user_with_role
|
|
end
|
|
|
|
@doc """
|
|
Creates a member with an actor (for use in tests with policies).
|
|
|
|
## Parameters
|
|
- `attrs` - Map or keyword list of attributes to override defaults
|
|
- `actor` - The actor (user) to use for authorization
|
|
|
|
## Returns
|
|
- Member struct
|
|
|
|
## Examples
|
|
|
|
iex> admin = user_with_role_fixture("admin")
|
|
iex> member_fixture_with_actor(%{first_name: "Alice"}, admin)
|
|
%Mv.Membership.Member{first_name: "Alice", ...}
|
|
|
|
"""
|
|
def member_fixture_with_actor(attrs \\ %{}, actor) do
|
|
attrs
|
|
|> Enum.into(%{
|
|
first_name: "Test",
|
|
last_name: "Member",
|
|
email: "test#{System.unique_integer([:positive])}@example.com"
|
|
})
|
|
|> Mv.Membership.create_member(actor: actor)
|
|
|> case do
|
|
{:ok, member} -> member
|
|
{:error, error} -> raise "Failed to create member: #{inspect(error)}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a group with default or custom attributes.
|
|
|
|
Uses system_actor for authorization to bypass permission checks in tests.
|
|
|
|
## Parameters
|
|
- `attrs` - Map or keyword list of attributes to override defaults
|
|
|
|
## Returns
|
|
- Group struct
|
|
|
|
## Examples
|
|
|
|
iex> group_fixture()
|
|
%Mv.Membership.Group{name: "Test Group", slug: "test-group", ...}
|
|
|
|
iex> group_fixture(%{name: "Board Members", description: "Board members group"})
|
|
%Mv.Membership.Group{name: "Board Members", slug: "board-members", ...}
|
|
|
|
"""
|
|
def group_fixture(attrs \\ %{}) do
|
|
system_actor = SystemActor.get_system_actor()
|
|
|
|
attrs
|
|
|> Enum.into(%{
|
|
name: "Test Group #{System.unique_integer([:positive])}",
|
|
description: "Test description"
|
|
})
|
|
|> Membership.create_group(actor: system_actor)
|
|
|> case do
|
|
{:ok, group} -> group
|
|
{:error, error} -> raise "Failed to create group: #{inspect(error)}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a join request in status :submitted (for approval UI tests).
|
|
|
|
Uses the public flow: submit_join_request then confirm_join_request with a known token.
|
|
Returns the JoinRequest struct so tests can use its id for approve/reject.
|
|
|
|
## Parameters
|
|
- `attrs` - Optional map: :email, :first_name, :last_name, :form_data, :schema_version.
|
|
Defaults: unique email; confirmation_token is generated and used internally.
|
|
|
|
## Returns
|
|
- JoinRequest struct with status :submitted
|
|
|
|
## Examples
|
|
|
|
iex> request = submitted_join_request_fixture()
|
|
iex> request.status
|
|
:submitted
|
|
|
|
iex> request = submitted_join_request_fixture(%{first_name: "Jane", last_name: "Doe"})
|
|
"""
|
|
def submitted_join_request_fixture(attrs \\ %{}) do
|
|
token = "fixture-token-#{System.unique_integer([:positive])}"
|
|
|
|
base = %{
|
|
email: "join#{System.unique_integer([:positive])}@example.com",
|
|
confirmation_token: token
|
|
}
|
|
|
|
attrs = base |> Map.merge(attrs) |> Map.put(:confirmation_token, token)
|
|
|
|
{:ok, _} = Membership.submit_join_request(attrs, actor: nil)
|
|
{:ok, request} = Membership.confirm_join_request(token, actor: nil)
|
|
request
|
|
end
|
|
|
|
@doc """
|
|
Creates a membership fee type with default or custom attributes.
|
|
|
|
Defaults: a unique `name`, `amount` 50.00, `interval` :yearly.
|
|
|
|
## Parameters
|
|
- `attrs` - Map of attributes to override defaults (e.g. `%{interval: :monthly}`).
|
|
- `actor` - the authorization actor; defaults to the system actor when omitted/nil.
|
|
|
|
## Returns
|
|
- MembershipFeeType struct.
|
|
|
|
## Examples
|
|
|
|
iex> create_fee_type(%{interval: :monthly})
|
|
%Mv.MembershipFees.MembershipFeeType{interval: :monthly, ...}
|
|
|
|
iex> create_fee_type(%{amount: Decimal.new("10.00")}, admin)
|
|
%Mv.MembershipFees.MembershipFeeType{...}
|
|
"""
|
|
def create_fee_type(attrs \\ %{}, actor \\ nil) do
|
|
actor = actor || SystemActor.get_system_actor()
|
|
|
|
default_attrs = %{
|
|
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
|
amount: Decimal.new("50.00"),
|
|
interval: :yearly
|
|
}
|
|
|
|
Mv.MembershipFees.MembershipFeeType
|
|
|> Ash.Changeset.for_create(:create, Map.merge(default_attrs, attrs))
|
|
|> Ash.create!(actor: actor)
|
|
end
|
|
|
|
@doc """
|
|
Creates a membership fee cycle for the given member and fee type.
|
|
|
|
Defaults: `cycle_start` ~D[2024-01-01], `amount` 50.00, `status` :unpaid,
|
|
with `member_id`/`membership_fee_type_id` derived from the passed structs.
|
|
|
|
## Parameters
|
|
- `member` - the Member struct the cycle belongs to.
|
|
- `fee_type` - the MembershipFeeType struct the cycle references.
|
|
- `attrs` - Map overriding the cycle defaults (e.g. `%{cycle_start: ~D[2023-01-01], status: :paid}`).
|
|
A reserved `:replace_existing` key (truthy) deletes any pre-existing cycles for the member
|
|
before creating the new one (used where auto-generated cycles would otherwise conflict);
|
|
it is stripped from the attrs and never passed to the create action. Defaults to absent/false.
|
|
- `actor` - the authorization actor; defaults to the system actor when omitted/nil.
|
|
|
|
## Returns
|
|
- MembershipFeeCycle struct.
|
|
"""
|
|
def create_cycle(member, fee_type, attrs \\ %{}, actor \\ nil) do
|
|
actor = actor || SystemActor.get_system_actor()
|
|
{replace_existing, attrs} = Map.pop(attrs, :replace_existing, false)
|
|
|
|
if replace_existing do
|
|
require Ash.Query
|
|
|
|
Mv.MembershipFees.MembershipFeeCycle
|
|
|> Ash.Query.filter(member_id == ^member.id)
|
|
|> Ash.read!(actor: actor)
|
|
|> Enum.each(&Ash.destroy!(&1, actor: actor))
|
|
end
|
|
|
|
default_attrs = %{
|
|
cycle_start: ~D[2024-01-01],
|
|
amount: Decimal.new("50.00"),
|
|
member_id: member.id,
|
|
membership_fee_type_id: fee_type.id,
|
|
status: :unpaid
|
|
}
|
|
|
|
Mv.MembershipFees.MembershipFeeCycle
|
|
|> Ash.Changeset.for_create(:create, Map.merge(default_attrs, attrs))
|
|
|> Ash.create!(actor: actor)
|
|
end
|
|
end
|