diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index 17b03d0..5bee497 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -690,9 +690,16 @@ end **Authorization Bootstrap Patterns:** -Two mechanisms exist for bypassing standard authorization: +Three mechanisms exist for bypassing standard authorization: -1. **system_actor** (systemic operations) - Admin user for operations that must always succeed +1. **NoActor** (test only) - Allows operations without actor in test environment + ```elixir + # Automatically enabled in tests via config/test.exs + # Policies use: bypass action_type(...) do authorize_if NoActor end + member = create_member(%{name: "Test"}) # Works in tests + ``` + +2. **system_actor** (systemic operations) - Admin user for operations that must always succeed ```elixir # Good: Systemic operation system_actor = SystemActor.get_system_actor() @@ -702,7 +709,7 @@ Two mechanisms exist for bypassing standard authorization: # Never use system_actor for user-initiated actions! ``` -2. **authorize?: false** (bootstrap only) - Skips policies for circular dependencies +3. **authorize?: false** (bootstrap only) - Skips policies for circular dependencies ```elixir # Good: Bootstrap (seeds, SystemActor loading) Accounts.create_user!(%{email: admin_email}, authorize?: false) @@ -712,10 +719,10 @@ Two mechanisms exist for bypassing standard authorization: ``` **Decision Guide:** -- Use **system_actor** for email sync, cycle generation, validations, and test fixtures +- Use **NoActor** for test fixtures (automatic via config) +- Use **system_actor** for email sync, cycle generation, validations - Use **authorize?: false** only for bootstrap (seeds, circular dependencies) - Always document why `authorize?: false` is necessary -- **Note:** NoActor bypass was removed to prevent masking authorization bugs in tests **See also:** `docs/roles-and-permissions-architecture.md` (Authorization Bootstrap Patterns section) @@ -1695,72 +1702,65 @@ case Ash.read(Mv.Membership.Member, actor: actor) do end ``` -### 5.1a Authorization in Tests +### 5.1a NoActor Pattern - Test Environment Only -**IMPORTANT:** All tests must explicitly provide an actor for Ash operations. The NoActor bypass has been removed to prevent masking authorization bugs. +**IMPORTANT:** The `Mv.Authorization.Checks.NoActor` check is **ONLY for test environment**. It must NEVER be used in production. -**Exception: AshAuthentication Bypass Tests** +**What NoActor Does:** -Tests that verify the AshAuthentication bypass mechanism are a **conscious exception**. These tests must verify that registration/login works **without an actor** via the `AshAuthenticationInteraction` check. To enable this bypass in tests, set the context explicitly: +- Allows CRUD operations without an actor in **test environment only** +- Denies all operations without an actor in **production/dev** (fail-closed) +- Uses compile-time config check to prevent accidental production use (release-safe) + +**Security Guards:** ```elixir -# ✅ GOOD - Testing AshAuthentication bypass (conscious exception) -changeset = - Accounts.User - |> Ash.Changeset.for_create(:register_with_password, %{...}) - |> Ash.Changeset.set_context(%{private: %{ash_authentication?: true}}) +# config/test.exs +config :mv, :allow_no_actor_bypass, true -{:ok, user} = Ash.create(changeset) # No actor - tests bypass mechanism +# lib/mv/authorization/checks/no_actor.ex +# Compile-time check from config (release-safe, no Mix.env) +@allow_no_actor_bypass Application.compile_env(:mv, :allow_no_actor_bypass, false) -# ❌ BAD - Using system_actor masks the bypass test -system_actor = Mv.Helpers.SystemActor.get_system_actor() -Ash.create(changeset, actor: system_actor) # Tests admin permissions, not bypass! -``` - -**Test Fixtures:** - -All test fixtures use `system_actor` for authorization: - -```elixir -# test/support/fixtures.ex -def member_fixture(attrs \\ %{}) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - attrs - |> Enum.into(%{...}) - |> Mv.Membership.create_member(actor: system_actor) +# Uses compile-time flag only (no runtime Mix.env needed) +def match?(nil, _context, _opts) do + @allow_no_actor_bypass # true in test, false in prod/dev end ``` -**Why Explicit Actors in Tests:** +**Why This Pattern Exists:** -- Prevents masking authorization bugs -- Makes authorization requirements explicit -- Tests fail if authorization is broken (fail-fast) -- Consistent with production code patterns +- Test fixtures often need to create resources without an actor +- Production operations MUST always have an actor for security +- Config-based guard (not Mix.env) ensures release-safety +- Defaults to `false` (fail-closed) if config not set -**Using system_actor in Tests:** +**NEVER Use NoActor in Production:** ```elixir -# ✅ GOOD - Explicit actor in tests -system_actor = Mv.Helpers.SystemActor.get_system_actor() +# ❌ BAD - Don't do this in production code +Ash.create!(Member, attrs) # No actor - will fail in prod + +# ✅ GOOD - Use admin actor for system operations +admin_user = get_admin_user() +Ash.create!(Member, attrs, actor: admin_user) +``` + +**Alternative: System Actor Pattern** + +For production system operations, use the System Actor Pattern (see Section 3.3) instead of NoActor: + +```elixir +# System operations in production +system_actor = get_system_actor() Ash.create!(Member, attrs, actor: system_actor) - -# ❌ BAD - Missing actor (will fail) -Ash.create!(Member, attrs) # Forbidden error! ``` -**For Bootstrap Operations:** +**Testing:** -Use `authorize?: false` only for bootstrap scenarios (seeds, SystemActor initialization): - -```elixir -# ✅ GOOD - Bootstrap only -Accounts.create_user!(%{email: admin_email}, authorize?: false) - -# ❌ BAD - Never use in tests for normal operations -Ash.create!(Member, attrs, authorize?: false) # Never do this! -``` +- NoActor tests verify the compile-time config guard +- Production safety is guaranteed by config (only set in test.exs, defaults to false) +- See `test/mv/authorization/checks/no_actor_test.exs` ### 5.2 Password Security diff --git a/config/test.exs b/config/test.exs index fe2b855..b48c408 100644 --- a/config/test.exs +++ b/config/test.exs @@ -12,10 +12,7 @@ config :mv, Mv.Repo, port: System.get_env("TEST_POSTGRES_PORT", "5000"), database: "mv_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Ecto.Adapters.SQL.Sandbox, - pool_size: System.schedulers_online() * 8, - queue_target: 5000, - queue_interval: 1000, - timeout: 60_000 + pool_size: System.schedulers_online() * 4 # We don't run a server during test. If one is required, # you can enable the server option below. @@ -52,3 +49,7 @@ config :mv, :require_token_presence_for_authentication, false # Enable SQL Sandbox for async LiveView tests # This flag controls sync vs async behavior in CycleGenerator after_action hooks config :mv, :sql_sandbox, true + +# Allow operations without actor in test environment (NoActor check) +# SECURITY: This must ONLY be true in test.exs, never in prod/dev +config :mv, :allow_no_actor_bypass, true diff --git a/docs/policy-bypass-vs-haspermission.md b/docs/policy-bypass-vs-haspermission.md index 31bb737..8a65c6f 100644 --- a/docs/policy-bypass-vs-haspermission.md +++ b/docs/policy-bypass-vs-haspermission.md @@ -262,7 +262,7 @@ The bypass is not a design choice but a **technical necessity** due to Ash's pol - ✅ UPDATE operations via HasPermission with `scope :own` - ✅ Admin operations via HasPermission with `scope :all` - ✅ AshAuthentication bypass (registration/login) -- ✅ Tests use system_actor for authorization +- ✅ NoActor bypass (test environment) **Key Tests Proving Pattern:** diff --git a/docs/roles-and-permissions-architecture.md b/docs/roles-and-permissions-architecture.md index 8934688..bc1b75c 100644 --- a/docs/roles-and-permissions-architecture.md +++ b/docs/roles-and-permissions-architecture.md @@ -946,7 +946,12 @@ defmodule Mv.Accounts.User do authorize_if always() end - # 2. SPECIAL CASE: Users can always READ their own account + # 2. NoActor Bypass (test environment only, for test fixtures) + bypass action_type([:create, :read, :update, :destroy]) do + authorize_if Mv.Authorization.Checks.NoActor + end + + # 3. SPECIAL CASE: Users can always READ their own account # Bypass needed for list queries (expr() triggers auto_filter in Ash) # UPDATE is handled by HasPermission below (scope :own works with changesets) bypass action_type(:read) do @@ -954,7 +959,7 @@ defmodule Mv.Accounts.User do authorize_if expr(id == ^actor(:id)) end - # 3. GENERAL: Check permissions from user's role + # 4. GENERAL: Check permissions from user's role # - :own_data → can UPDATE own user (scope :own via HasPermission) # - :read_only → can UPDATE own user (scope :own via HasPermission) # - :normal_user → can UPDATE own user (scope :own via HasPermission) @@ -964,7 +969,7 @@ defmodule Mv.Accounts.User do authorize_if Mv.Authorization.Checks.HasPermission end - # 4. DEFAULT: Ash implicitly forbids if no policy authorizes (fail-closed) + # 5. DEFAULT: Ash implicitly forbids if no policy authorizes (fail-closed) end # ... @@ -1002,7 +1007,12 @@ defmodule Mv.Membership.Member do use Ash.Resource, ... policies do - # 1. SPECIAL CASE: Users can always READ their linked member + # 1. NoActor Bypass (test environment only, for test fixtures) + bypass action_type([:create, :read, :update, :destroy]) do + authorize_if Mv.Authorization.Checks.NoActor + end + + # 2. SPECIAL CASE: Users can always READ their linked member # Bypass needed for list queries (expr() triggers auto_filter in Ash) # UPDATE is handled by HasPermission below (scope :linked works with changesets) bypass action_type(:read) do @@ -1010,7 +1020,7 @@ defmodule Mv.Membership.Member do authorize_if expr(id == ^actor(:member_id)) end - # 2. GENERAL: Check permissions from role + # 3. GENERAL: Check permissions from role # - :own_data → can UPDATE linked member (scope :linked via HasPermission) # - :read_only → can READ all members (scope :all), no update permission # - :normal_user → can CRUD all members (scope :all) @@ -2619,16 +2629,45 @@ This section clarifies three different mechanisms for bypassing standard authori ### Overview -The codebase uses two authorization bypass mechanisms: +The codebase uses three authorization bypass mechanisms: -1. **system_actor** - Admin user for systemic operations -2. **authorize?: false** - Bootstrap bypass for circular dependencies +1. **NoActor** - Test-only bypass (compile-time secured) +2. **system_actor** - Admin user for systemic operations +3. **authorize?: false** - Bootstrap bypass for circular dependencies -**Both are necessary and serve different purposes.** +**All three are necessary and serve different purposes.** -**Note:** The NoActor bypass has been removed to prevent masking authorization bugs in tests. All tests now explicitly use `system_actor` for authorization. +### 1. NoActor Check -### 1. System Actor +**Purpose:** Allows CRUD operations without actor in test environment only. + +**Implementation:** +```elixir +# lib/mv/authorization/checks/no_actor.ex +@allow_no_actor_bypass Application.compile_env(:mv, :allow_no_actor_bypass, false) + +def match?(nil, _context, _opts) do + @allow_no_actor_bypass # true in test.exs, false elsewhere +end +``` + +**Security:** +- Compile-time flag (not runtime `Mix.env()` check) +- Default: false (fail-closed) +- Only enabled in `config/test.exs` + +**Use Case:** Test fixtures without verbose actor setup: +```elixir +# With NoActor (test environment only) +member = create_member(%{name: "Test"}) + +# Production behavior (NoActor returns false) +member = create_member(%{name: "Test"}, actor: user) +``` + +**Trade-off:** May mask tests that should fail without actor. Mitigated by explicit policy tests (e.g., `test/mv/accounts/user_policies_test.exs`). + +### 2. System Actor **Purpose:** Admin user for systemic operations that must always succeed regardless of user permissions. @@ -2669,7 +2708,7 @@ end - Consistent authorization flow - Testable -### 2. authorize?: false +### 3. authorize?: false **Purpose:** Skip policies for bootstrap scenarios with circular dependencies. @@ -2720,17 +2759,21 @@ Mv.Authorization.Role ### Comparison -| Aspect | system_actor | authorize?: false | -|--------|--------------|-------------------| -| **Environment** | All | All | -| **Actor** | Admin user | nil | -| **Policies** | Evaluated | Skipped | -| **Audit Trail** | Yes (system@mila.local) | No | -| **Use Case** | Systemic operations, test fixtures | Bootstrap | -| **Explicit?** | Function call | Query option | +| Aspect | NoActor | system_actor | authorize?: false | +|--------|---------|--------------|-------------------| +| **Environment** | Test only | All | All | +| **Actor** | nil | Admin user | nil | +| **Policies** | Bypassed | Evaluated | Skipped | +| **Audit Trail** | No | Yes (system@mila.local) | No | +| **Use Case** | Test fixtures | Systemic operations | Bootstrap | +| **Explicit?** | Policy bypass | Function call | Query option | ### Decision Guide +**Use NoActor when:** +- ✅ Writing test fixtures +- ✅ Compile-time guard ensures test-only + **Use system_actor when:** - ✅ Systemic operation must always succeed - ✅ Email synchronization @@ -2746,7 +2789,7 @@ Mv.Authorization.Role **DON'T:** - ❌ Use `authorize?: false` for user-initiated actions - ❌ Use `authorize?: false` when `system_actor` would work -- ❌ Skip actor in tests (always use system_actor) +- ❌ Enable NoActor outside test environment ### The Circular Dependency Problem @@ -2830,8 +2873,7 @@ end - Enhanced edge case documentation **Changes from V2.0:** -- Added "Authorization Bootstrap Patterns" section explaining system_actor and authorize?: false -- Removed NoActor bypass (all tests now use system_actor for explicit authorization) +- Added "Authorization Bootstrap Patterns" section explaining NoActor, system_actor, and authorize?: false --- diff --git a/docs/roles-and-permissions-implementation-plan.md b/docs/roles-and-permissions-implementation-plan.md index 23b045c..33b1702 100644 --- a/docs/roles-and-permissions-implementation-plan.md +++ b/docs/roles-and-permissions-implementation-plan.md @@ -542,7 +542,7 @@ Following the same pattern as Member resource: 1. ✅ Open `lib/accounts/user.ex` 2. ✅ Add `policies` block 3. ✅ Add AshAuthentication bypass (registration/login without actor) -4. ✅ ~~Add NoActor bypass (test environment only)~~ **REMOVED** - NoActor bypass was removed to prevent masking authorization bugs. All tests now use `system_actor`. +4. ✅ Add NoActor bypass (test environment only) 5. ✅ Add bypass for READ: Allow user to always read their own account ```elixir bypass action_type(:read) do @@ -556,11 +556,10 @@ Following the same pattern as Member resource: **Policy Order:** 1. ✅ AshAuthentication bypass (registration/login) -2. ✅ Bypass: User can READ own account (id == actor.id) -3. ✅ HasPermission: General permission check (UPDATE uses scope :own, admin uses scope :all) -4. ✅ Default: Ash implicitly forbids (fail-closed) - -**Note:** NoActor bypass was removed. All tests now use `system_actor` for authorization. +2. ✅ NoActor bypass (test environment) +3. ✅ Bypass: User can READ own account (id == actor.id) +4. ✅ HasPermission: General permission check (UPDATE uses scope :own, admin uses scope :all) +5. ✅ Default: Ash implicitly forbids (fail-closed) **Why Bypass for READ but not UPDATE?** @@ -575,7 +574,7 @@ This ensures `scope :own` in PermissionSets is actually used (not redundant). - ✅ User can always update own credentials (via HasPermission with scope :own) - ✅ Only admin can read/update other users (scope :all) - ✅ Only admin can destroy users (scope :all) -- ✅ Policy order is correct (AshAuth → Bypass READ → HasPermission) +- ✅ Policy order is correct (AshAuth → NoActor → Bypass READ → HasPermission) - ✅ Actor preloads :role relationship - ✅ All tests pass (30/31 pass, 1 skipped) @@ -585,7 +584,7 @@ This ensures `scope :own` in PermissionSets is actually used (not redundant). - ✅ 31 tests total: 30 passing, 1 skipped (AshAuthentication edge case) - ✅ Tests for all 4 permission sets: own_data, read_only, normal_user, admin - ✅ Tests for AshAuthentication bypass (registration/login) -- ✅ Tests use system_actor for authorization (NoActor bypass removed) +- ✅ Tests for NoActor bypass (test environment) - ✅ Tests verify scope :own is used for UPDATE (not redundant) --- diff --git a/docs/user-resource-policies-implementation-summary.md b/docs/user-resource-policies-implementation-summary.md index c939c6b..c85d3d7 100644 --- a/docs/user-resource-policies-implementation-summary.md +++ b/docs/user-resource-policies-implementation-summary.md @@ -22,13 +22,18 @@ policies do authorize_if always() end - # 2. Bypass for READ (list queries via auto_filter) + # 2. NoActor Bypass (test environment only) + bypass action_type([:create, :read, :update, :destroy]) do + authorize_if Mv.Authorization.Checks.NoActor + end + + # 3. Bypass for READ (list queries via auto_filter) bypass action_type(:read) do description "Users can always read their own account" authorize_if expr(id == ^actor(:id)) end - # 3. HasPermission for all operations (uses scope from PermissionSets) + # 4. HasPermission for all operations (uses scope from PermissionSets) policy action_type([:read, :create, :update, :destroy]) do description "Check permissions from user's role and permission set" authorize_if Mv.Authorization.Checks.HasPermission @@ -46,7 +51,7 @@ end - ✅ CREATE operations (admin only) - ✅ DESTROY operations (admin only) - ✅ AshAuthentication bypass (registration/login) -- ✅ Tests use system_actor for authorization +- ✅ NoActor bypass (test environment) --- @@ -185,7 +190,7 @@ mix test test/mv/accounts/user_policies_test.exs \ **Test Environment:** - ✅ Operations without actor work in test environment -- ✅ All tests explicitly use system_actor for authorization +- ✅ NoActor bypass correctly detects compile-time environment --- diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index bcaf506..08d1130 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -67,10 +67,6 @@ defmodule Mv.Accounts.User do identity_field :email hash_provider AshAuthentication.BcryptProvider confirmation_required? false - - resettable do - sender Mv.Accounts.User.Senders.SendPasswordResetEmail - end end end end @@ -119,8 +115,6 @@ defmodule Mv.Accounts.User do argument :member, :map, allow_nil?: true upsert? true - # Note: Default role is automatically assigned via attribute default (see attributes block) - # Manage the member relationship during user creation change manage_relationship(:member, :member, # Look up existing member and relate to it @@ -245,8 +239,6 @@ defmodule Mv.Accounts.User do upsert? true # Upsert based on oidc_id (primary match for existing OIDC users) upsert_identity :unique_oidc_id - # On upsert, only update email - preserve existing role_id - upsert_fields [:email] validate &__MODULE__.validate_oidc_id_present/2 @@ -269,9 +261,6 @@ defmodule Mv.Accounts.User do # - The LinkOidcAccountLive will auto-link passwordless users without password prompt validate Mv.Accounts.User.Validations.OidcEmailCollision - # Note: Default role is automatically assigned via attribute default (see attributes block) - # upsert_fields [:email] ensures existing users' roles are preserved during upserts - # Sync user email to member when linking (User → Member) change Mv.EmailSync.Changes.SyncUserEmailToMember end @@ -286,6 +275,12 @@ defmodule Mv.Accounts.User do authorize_if always() end + # NoActor bypass (test fixtures only, see no_actor.ex) + bypass action_type([:create, :read, :update, :destroy]) do + description "Allow system operations without actor (test environment only)" + authorize_if Mv.Authorization.Checks.NoActor + end + # READ bypass for list queries (scope :own via expr) bypass action_type(:read) do description "Users can always read their own account" @@ -390,15 +385,6 @@ defmodule Mv.Accounts.User do attribute :hashed_password, :string, sensitive?: true, allow_nil?: true attribute :oidc_id, :string, allow_nil?: true - - # Role assignment: Explicitly defined to enforce default value - # This ensures every user has a role, regardless of creation path - # (register_with_password, create_user, seeds, etc.) - attribute :role_id, :uuid do - allow_nil? false - default &__MODULE__.default_role_id/0 - public? false - end end relationships do @@ -408,13 +394,10 @@ defmodule Mv.Accounts.User do belongs_to :member, Mv.Membership.Member # 1:1 relationship - User belongs to a Role - # We define role_id ourselves (above in attributes) to control default value + # This automatically creates a `role_id` attribute in the User table + # The relationship is optional (allow_nil? true by default) # Foreign key constraint: on_delete: :restrict (prevents deleting roles assigned to users) - belongs_to :role, Mv.Authorization.Role do - define_attribute? false - source_attribute :role_id - allow_nil? false - end + belongs_to :role, Mv.Authorization.Role end identities do @@ -434,60 +417,4 @@ defmodule Mv.Accounts.User do # forbid_if(always()) # end # end - - @doc """ - Returns the default role ID for new users. - - This function is called automatically when creating a user without an explicit role_id. - It fetches the "Mitglied" role from the database without authorization checks - (safe during user creation bootstrap phase). - - The result is cached in the process dictionary to avoid repeated database queries - during high-volume user creation. The cache is invalidated on application restart. - - ## Bootstrap Safety - - Only non-nil values are cached. If the role doesn't exist yet (e.g., before seeds run), - `nil` is not cached, allowing subsequent calls to retry after the role is created. - This prevents bootstrap issues where a process would be permanently stuck with `nil` - if the first call happens before the role exists. - - ## Performance Note - - This function makes one database query per process (cached in process dictionary). - For very high-volume scenarios, consider using a fixed UUID from Application config - instead of querying the database. - - ## Returns - - - UUID of the "Mitglied" role if it exists - - `nil` if the role doesn't exist (will cause validation error due to `allow_nil? false`) - - ## Examples - - iex> Mv.Accounts.User.default_role_id() - "019bf2e2-873a-7712-a7ce-a5a1f90c5f4f" - """ - @spec default_role_id() :: Ecto.UUID.t() | nil - def default_role_id do - # Cache in process dictionary to avoid repeated queries - # IMPORTANT: Only cache non-nil values to avoid bootstrap issues. - # If the role doesn't exist yet (e.g., before seeds run), we don't cache nil - # so that subsequent calls can retry after the role is created. - case Process.get({__MODULE__, :default_role_id}) do - nil -> - role_id = - case Mv.Authorization.Role.get_mitglied_role() do - {:ok, %Mv.Authorization.Role{id: id}} -> id - _ -> nil - end - - # Only cache non-nil values to allow retry if role is created later - if role_id, do: Process.put({__MODULE__, :default_role_id}, role_id) - role_id - - cached_role_id -> - cached_role_id - end - end end diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 1a5d805..650cf43 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -303,6 +303,15 @@ defmodule Mv.Membership.Member do # Authorization Policies # Order matters: Most specific policies first, then general permission check policies do + # SYSTEM OPERATIONS: Allow CRUD operations without actor (TEST ENVIRONMENT ONLY) + # In test: All operations allowed (for test fixtures) + # In production/dev: ALL operations denied without actor (fail-closed for security) + # NoActor.check uses compile-time environment detection to prevent security issues + bypass action_type([:create, :read, :update, :destroy]) do + description "Allow system operations without actor (test environment only)" + authorize_if Mv.Authorization.Checks.NoActor + end + # SPECIAL CASE: Users can always READ their linked member # This allows users with ANY permission set to read their own linked member # Check using the inverse relationship: User.member_id → Member.id @@ -393,9 +402,11 @@ defmodule Mv.Membership.Member do user_id = user_arg[:id] current_member_id = changeset.data.id - # Get actor from changeset context (may be nil) + # Get actor from changeset context for authorization + # If no actor is present, this will fail in production (fail-closed) actor = Map.get(changeset.context || %{}, :actor) + # Check the current state of the user in the database # Check if authorization is disabled in the parent operation's context # Access private context where authorize? flag is stored authorize? = @@ -404,17 +415,8 @@ defmodule Mv.Membership.Member do _ -> true end - # Use actor for authorization when available and authorize? is true - # Fall back to authorize?: false only for bootstrap/system operations - # This ensures normal operations respect authorization while system operations work - query_opts = - if actor && authorize? do - [actor: actor] - else - [authorize?: false] - end - - case Ash.get(Mv.Accounts.User, user_id, query_opts) do + # Pass actor and authorize? to ensure proper authorization (User might have policies in future) + case Ash.get(Mv.Accounts.User, user_id, actor: actor, authorize?: authorize?) do # User is free to be linked {:ok, %{member_id: nil}} -> :ok @@ -427,9 +429,6 @@ defmodule Mv.Membership.Member do # User is linked to a different member - prevent "stealing" {:error, field: :user, message: "User is already linked to another member"} - {:error, %Ash.Error.Query.NotFound{}} -> - {:error, field: :user, message: "User not found"} - {:error, _} -> {:error, field: :user, message: "User not found"} end diff --git a/lib/membership_fees/membership_fee_type.ex b/lib/membership_fees/membership_fee_type.ex index 498ff75..01ae625 100644 --- a/lib/membership_fees/membership_fee_type.ex +++ b/lib/membership_fees/membership_fee_type.ex @@ -85,11 +85,10 @@ defmodule Mv.MembershipFees.MembershipFeeType do if changeset.action_type == :destroy do require Ash.Query - # Integrity check: count members without authorization (systemic operation) member_count = Mv.Membership.Member |> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id) - |> Ash.count!(authorize?: false) + |> Ash.count!() if member_count > 0 do {:error, @@ -109,11 +108,10 @@ defmodule Mv.MembershipFees.MembershipFeeType do if changeset.action_type == :destroy do require Ash.Query - # Integrity check: count cycles without authorization (systemic operation) cycle_count = Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(membership_fee_type_id == ^changeset.data.id) - |> Ash.count!(authorize?: false) + |> Ash.count!() if cycle_count > 0 do {:error, @@ -133,11 +131,10 @@ defmodule Mv.MembershipFees.MembershipFeeType do if changeset.action_type == :destroy do require Ash.Query - # Integrity check: count settings without authorization (systemic operation) setting_count = Mv.Membership.Setting |> Ash.Query.filter(default_membership_fee_type_id == ^changeset.data.id) - |> Ash.count!(authorize?: false) + |> Ash.count!() if setting_count > 0 do {:error, diff --git a/lib/mv/authorization/checks/has_permission.ex b/lib/mv/authorization/checks/has_permission.ex index 1a478b8..97b74c0 100644 --- a/lib/mv/authorization/checks/has_permission.ex +++ b/lib/mv/authorization/checks/has_permission.ex @@ -348,22 +348,12 @@ defmodule Mv.Authorization.Checks.HasPermission do "Member" -> # User.member_id → Member.id (inverse relationship) # Filter: member.id == actor.member_id - # If actor has no member_id, return no results (use false or impossible condition) - if is_nil(actor.member_id) do - {:filter, expr(false)} - else - {:filter, expr(id == ^actor.member_id)} - end + {:filter, expr(id == ^actor.member_id)} "CustomFieldValue" -> # CustomFieldValue.member_id → Member.id → User.member_id # Filter: custom_field_value.member_id == actor.member_id - # If actor has no member_id, return no results - if is_nil(actor.member_id) do - {:filter, expr(false)} - else - {:filter, expr(member_id == ^actor.member_id)} - end + {:filter, expr(member_id == ^actor.member_id)} _ -> # Fallback for other resources diff --git a/lib/mv/authorization/checks/no_actor.ex b/lib/mv/authorization/checks/no_actor.ex new file mode 100644 index 0000000..1c4946f --- /dev/null +++ b/lib/mv/authorization/checks/no_actor.ex @@ -0,0 +1,70 @@ +defmodule Mv.Authorization.Checks.NoActor do + @moduledoc """ + Custom Ash Policy Check that allows actions when no actor is present. + + **IMPORTANT:** This check ONLY works in test environment for security reasons. + In production/dev, ALL operations without an actor are denied. + + ## Security Note + + This check uses compile-time environment detection to prevent accidental + security issues in production. In production, ALL operations (including :create + and :read) will be denied if no actor is present. + + For seeds and system operations in production, use an admin actor instead: + + admin_user = get_admin_user() + Ash.create!(resource, attrs, actor: admin_user) + + ## Usage in Policies + + policies do + # Allow system operations without actor (TEST ENVIRONMENT ONLY) + # In test: All operations allowed + # In production: ALL operations denied (fail-closed) + bypass action_type([:create, :read, :update, :destroy]) do + authorize_if NoActor + end + + # Check permissions when actor is present + policy action_type([:read, :create, :update, :destroy]) do + authorize_if HasPermission + end + end + + ## Behavior + + - In test environment: Returns `true` when actor is nil (allows all operations) + - In production/dev: Returns `false` when actor is nil (denies all operations - fail-closed) + - Returns `false` when actor is present (delegates to other policies) + """ + + use Ash.Policy.SimpleCheck + + # Compile-time check: Only allow no-actor bypass in test environment + # SECURITY: This must ONLY be true in test.exs, never in prod/dev + # Using compile_env instead of Mix.env() for release-safety + @allow_no_actor_bypass Application.compile_env(:mv, :allow_no_actor_bypass, false) + + @impl true + def describe(_opts) do + if @allow_no_actor_bypass do + "allows actions when no actor is present (test environment only)" + else + "denies all actions when no actor is present (production/dev - fail-closed)" + end + end + + @impl true + def match?(nil, _context, _opts) do + # Actor is nil + # SECURITY: Only allow if compile_env flag is set (test.exs only) + # No runtime Mix.env() check - fail-closed by default (false) + @allow_no_actor_bypass + end + + def match?(_actor, _context, _opts) do + # Actor is present - don't match (let other policies decide) + false + end +end diff --git a/lib/mv/authorization/role.ex b/lib/mv/authorization/role.ex index 9c33e2d..da43510 100644 --- a/lib/mv/authorization/role.ex +++ b/lib/mv/authorization/role.ex @@ -67,11 +67,6 @@ defmodule Mv.Authorization.Role do # Custom validations will still work end - create :create_role_with_system_flag do - description "Internal action to create roles, allowing `is_system_role` to be set. Used by seeds and migrations." - accept [:name, :description, :permission_set_name, :is_system_role] - end - update :update_role do primary? true # is_system_role is intentionally excluded - should only be set via seeds/internal actions @@ -144,33 +139,4 @@ defmodule Mv.Authorization.Role do identities do identity :unique_name, [:name] end - - @doc """ - Loads the "Mitglied" role without authorization (for bootstrap operations). - - This is a helper function to avoid code duplication when loading the default - role in changes, migrations, and test setup. - - ## Returns - - - `{:ok, %Mv.Authorization.Role{}}` - The "Mitglied" role - - `{:ok, nil}` - Role doesn't exist - - `{:error, term()}` - Error during lookup - - ## Examples - - {:ok, mitglied_role} = Mv.Authorization.Role.get_mitglied_role() - # => {:ok, %Mv.Authorization.Role{name: "Mitglied", ...}} - - {:ok, nil} = Mv.Authorization.Role.get_mitglied_role() - # => Role doesn't exist (e.g., in test environment before seeds run) - """ - @spec get_mitglied_role() :: {:ok, t() | nil} | {:error, term()} - def get_mitglied_role do - require Ash.Query - - __MODULE__ - |> Ash.Query.filter(name == "Mitglied") - |> Ash.read_one(authorize?: false, domain: Mv.Authorization) - end end diff --git a/lib/mv/helpers/system_actor.ex b/lib/mv/helpers/system_actor.ex index 565c2ef..7a8ab8b 100644 --- a/lib/mv/helpers/system_actor.ex +++ b/lib/mv/helpers/system_actor.ex @@ -271,12 +271,11 @@ defmodule Mv.Helpers.SystemActor do end # Finds admin role in existing roles - # SECURITY: Uses authorize?: false for bootstrap role lookup. @spec find_admin_role() :: {:ok, Mv.Authorization.Role.t()} | {:error, :not_found} defp find_admin_role do alias Mv.Authorization - case Authorization.list_roles(authorize?: false) do + case Authorization.list_roles() do {:ok, roles} -> case Enum.find(roles, &(&1.permission_set_name == "admin")) do nil -> {:error, :not_found} @@ -306,20 +305,16 @@ defmodule Mv.Helpers.SystemActor do end # Attempts to create admin role - # SECURITY: Uses authorize?: false for bootstrap role creation. @spec create_admin_role() :: {:ok, Mv.Authorization.Role.t()} | {:error, :already_exists | term()} defp create_admin_role do alias Mv.Authorization - case Authorization.create_role( - %{ - name: "Admin", - description: "Administrator with full access", - permission_set_name: "admin" - }, - authorize?: false - ) do + case Authorization.create_role(%{ + name: "Admin", + description: "Administrator with full access", + permission_set_name: "admin" + }) do {:ok, role} -> {:ok, role} @@ -332,12 +327,11 @@ defmodule Mv.Helpers.SystemActor do end # Finds existing admin role after creation attempt failed due to race condition - # SECURITY: Uses authorize?: false for bootstrap role lookup. @spec find_existing_admin_role() :: Mv.Authorization.Role.t() | no_return() defp find_existing_admin_role do alias Mv.Authorization - case Authorization.list_roles(authorize?: false) do + case Authorization.list_roles() do {:ok, roles} -> Enum.find(roles, &(&1.permission_set_name == "admin")) || raise "Admin role should exist but was not found" @@ -356,22 +350,14 @@ defmodule Mv.Helpers.SystemActor do defp create_system_user_with_role(admin_role) do alias Mv.Accounts - # SECURITY: Uses authorize?: false for bootstrap user creation. - # This is necessary because we're creating the system actor itself, - # which would otherwise be needed for authorization (chicken-and-egg). - # This is safe because: - # 1. Only creates system user with known email - # 2. Only called during system actor initialization (bootstrap) - # 3. Once created, all subsequent operations use proper authorization Accounts.create_user!(%{email: system_user_email_config()}, upsert?: true, - upsert_identity: :unique_email, - authorize?: false + upsert_identity: :unique_email ) |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!(authorize?: false) - |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) + |> Ash.update!() + |> Ash.load!(:role, domain: Mv.Accounts) end # Finds a user by email address @@ -390,12 +376,9 @@ defmodule Mv.Helpers.SystemActor do end # Loads a user with their role preloaded (required for authorization) - # SECURITY: Uses authorize?: false for bootstrap role loading. - # This is necessary because loading the role is part of system actor initialization, - # which would otherwise require an actor (chicken-and-egg). @spec load_user_with_role(Mv.Accounts.User.t()) :: Mv.Accounts.User.t() | no_return() defp load_user_with_role(user) do - case Ash.load(user, :role, domain: Mv.Accounts, authorize?: false) do + case Ash.load(user, :role, domain: Mv.Accounts) do {:ok, user_with_role} -> validate_admin_role(user_with_role) diff --git a/lib/mv/membership/import/member_csv.ex b/lib/mv/membership/import/member_csv.ex index e351d68..4222fc3 100644 --- a/lib/mv/membership/import/member_csv.ex +++ b/lib/mv/membership/import/member_csv.ex @@ -299,7 +299,7 @@ defmodule Mv.Membership.Import.MemberCSV do custom_field_lookup = Keyword.get(opts, :custom_field_lookup, %{}) existing_error_count = Keyword.get(opts, :existing_error_count, 0) max_errors = Keyword.get(opts, :max_errors, @default_max_errors) - actor = Keyword.fetch!(opts, :actor) + actor = Keyword.get(opts, :actor) {inserted, failed, errors, _collected_error_count, truncated?} = Enum.reduce(chunk_rows_with_lines, {0, 0, [], 0, false}, fn {line_number, row_map}, @@ -528,9 +528,7 @@ defmodule Mv.Membership.Import.MemberCSV do {:ok, member} {:error, %Ash.Error.Invalid{} = error} -> - # Extract email from final_attrs for better error messages - email = Map.get(final_attrs, :email) || Map.get(trimmed_member_attrs, :email) - {:error, format_ash_error(error, line_number, email)} + {:error, format_ash_error(error, line_number)} {:error, error} -> {:error, %Error{csv_line_number: line_number, field: nil, message: inspect(error)}} @@ -623,7 +621,7 @@ defmodule Mv.Membership.Import.MemberCSV do end # Formats Ash errors into MemberCSV.Error structs - defp format_ash_error(%Ash.Error.Invalid{errors: errors}, line_number, email) do + defp format_ash_error(%Ash.Error.Invalid{errors: errors}, line_number) do # Try to find email-related errors first (for better error messages) email_error = Enum.find(errors, fn error -> @@ -638,37 +636,35 @@ defmodule Mv.Membership.Import.MemberCSV do %Error{ csv_line_number: line_number, field: field, - message: format_error_message(message, field, email) + message: format_error_message(message, field) } %{message: message} -> %Error{ csv_line_number: line_number, field: nil, - message: format_error_message(message, nil, email) + message: format_error_message(message, nil) } _ -> %Error{ csv_line_number: line_number, field: nil, - message: gettext("Validation failed") + message: "Validation failed" } end end # Formats error messages, handling common cases like email uniqueness - defp format_error_message(message, field, email) when is_binary(message) do + defp format_error_message(message, field) when is_binary(message) do if email_uniqueness_error?(message, field) do - # Include email in error message for better user feedback - email_str = if email, do: to_string(email), else: gettext("email") - gettext("email %{email} has already been taken", email: email_str) + "email has already been taken" else message end end - defp format_error_message(message, _field, _email), do: to_string(message) + defp format_error_message(message, _field), do: to_string(message) # Checks if error message indicates email uniqueness constraint violation defp email_uniqueness_error?(message, :email) do diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index 0fbcbbe..d133c5a 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -59,13 +59,9 @@ defmodule MvWeb.GlobalSettingsLive do @max_errors 50 @impl true - def mount(_params, session, socket) do + def mount(_params, _session, socket) do {:ok, settings} = Membership.get_settings() - # Get locale from session for translations - locale = session["locale"] || "de" - Gettext.put_locale(MvWeb.Gettext, locale) - socket = socket |> assign(:page_title, gettext("Settings")) @@ -74,8 +70,6 @@ defmodule MvWeb.GlobalSettingsLive do |> assign(:import_state, nil) |> assign(:import_progress, nil) |> assign(:import_status, :idle) - |> assign(:locale, locale) - |> assign(:max_errors, @max_errors) |> assign_form() # Configure file upload with auto-upload enabled # Files are uploaded automatically when selected, no need for manual trigger @@ -592,10 +586,6 @@ defmodule MvWeb.GlobalSettingsLive do actor: actor ] - # Get locale from socket for translations in background tasks - locale = socket.assigns[:locale] || "de" - Gettext.put_locale(MvWeb.Gettext, locale) - if Config.sql_sandbox?() do # Run synchronously in tests to avoid Ecto Sandbox issues with async tasks {:ok, chunk_result} = @@ -614,9 +604,6 @@ defmodule MvWeb.GlobalSettingsLive do # Use start_child for fire-and-forget: no monitor, no Task messages # We only use our own send/2 messages for communication Task.Supervisor.start_child(Mv.TaskSupervisor, fn -> - # Set locale in task process for translations - Gettext.put_locale(MvWeb.Gettext, locale) - {:ok, chunk_result} = MemberCSV.process_chunk( chunk, @@ -687,19 +674,13 @@ defmodule MvWeb.GlobalSettingsLive do end defp consume_and_read_csv(socket) do - result = - consume_uploaded_entries(socket, :csv_file, fn %{path: path}, _entry -> - case File.read(path) do - {:ok, content} -> {:ok, content} - {:error, reason} -> {:error, Exception.message(reason)} - end - end) - - result + consume_uploaded_entries(socket, :csv_file, fn %{path: path}, _entry -> + case File.read(path) do + {:ok, content} -> {:ok, content} + {:error, reason} -> {:error, Exception.message(reason)} + end + end) |> case do - [content] when is_binary(content) -> - {:ok, content} - [{:ok, content}] when is_binary(content) -> {:ok, content} diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index 6cf3f0f..3e773cb 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -33,8 +33,6 @@ defmodule MvWeb.UserLive.Form do """ use MvWeb, :live_view - require Jason - import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3] @impl true @@ -327,7 +325,6 @@ defmodule MvWeb.UserLive.Form do @impl true def handle_event("save", %{"user" => user_params}, socket) do actor = current_actor(socket) - # First save the user without member changes case submit_form(socket.assigns.form, user_params, actor) do {:ok, user} -> diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 5496213..8ad73fa 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -2110,18 +2110,3 @@ msgstr "Verwenden Sie den Namen des benutzerdefinierten Feldes als CSV-Spaltenü #, elixir-autogen, elixir-format, fuzzy msgid "Warnings" msgstr "Warnungen" - -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Validation failed" -msgstr "Validierung fehlgeschlagen: %{message}" - -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "email" -msgstr "E-Mail" - -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format -msgid "email %{email} has already been taken" -msgstr "E-Mail %{email} wurde bereits verwendet" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index fc3a78c..17e2039 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -2111,18 +2111,3 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Warnings" msgstr "" - -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format -msgid "Validation failed" -msgstr "" - -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format -msgid "email" -msgstr "" - -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format -msgid "email %{email} has already been taken" -msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 9432a47..596625e 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2112,17 +2112,304 @@ msgstr "" msgid "Warnings" msgstr "" -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "Validation failed" -msgstr "" +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Use this form to manage Custom Field Value records in your database." +#~ msgstr "" -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format, fuzzy -msgid "email" -msgstr "" +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Choose a custom field" +#~ msgstr "" -#: lib/mv/membership/import/member_csv.ex -#, elixir-autogen, elixir-format -msgid "email %{email} has already been taken" -msgstr "" +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Joining year - reduced to 0" +#~ msgstr "" + +#~ #: lib/mv_web/components/layouts/sidebar.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Admin" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Regular" +#~ msgstr "" + +#~ #: lib/mv_web/live/components/member_filter_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Payment" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Current" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Paid via bank transfer" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Mark as Unpaid" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Half-yearly contribution for supporting members" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Reduced fee for unemployed, pensioners, or low income" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/index.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Custom field value not found" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Supporting Member" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Monthly fee for students and trainees" +#~ msgstr "" + +#~ #: lib/mv_web/live/components/payment_filter_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Filter by payment status" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Custom field value %{action} successfully" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Total Contributions" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Manage contribution types for membership fees." +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Change Contribution Type" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "New Contribution Type" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Time Period" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/index.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Custom field value deleted successfully" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/index.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "You do not have permission to access this custom field value" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Cannot delete - members assigned" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Preview Mockup" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Contribution Types" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "This page is not functional and only displays the planned features." +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Member since" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Unsupported value type: %{type}" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Custom field" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Mark as Paid" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Contribution type" +#~ msgstr "" + +#~ #: lib/mv_web/components/layouts/sidebar.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Contributions" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Reduced" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "No fee for honorary members" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/index.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "You do not have permission to delete this custom field value" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "%{count} period selected" +#~ msgid_plural "%{count} periods selected" +#~ msgstr[0] "" +#~ msgstr[1] "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Mark as Suspended" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Choose a member" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Suspend" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Reopen" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Value" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Why are not all contribution types shown?" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Contribution Start" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Standard membership fee for regular members" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/form.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Save Custom Field Value" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Honorary" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Contributions for %{name}" +#~ msgstr "" + +#~ #: lib/mv_web/live/components/member_filter_component.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "Payment status filter" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Family" +#~ msgstr "" + +#~ #: lib/mv_web/live/custom_field_value_live/index.ex +#~ #, elixir-autogen, elixir-format, fuzzy +#~ msgid "You do not have permission to view custom field values" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Student" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Quarterly fee for family memberships" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps." +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Open Contributions" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_period_live/show.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Member Contributions" +#~ msgstr "" + +#~ #: lib/mv_web/live/contribution_type_live/index.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "About Contribution Types" +#~ msgstr "" + +#~ #: lib/mv_web/live/components/member_filter_component.ex +#~ #, elixir-autogen, elixir-format +#~ msgid "Filter by %{name}" +#~ msgstr "" diff --git a/priv/repo/migrations/20260122231235_assign_mitglied_role_to_existing_users.exs b/priv/repo/migrations/20260122231235_assign_mitglied_role_to_existing_users.exs deleted file mode 100644 index 548de8b..0000000 --- a/priv/repo/migrations/20260122231235_assign_mitglied_role_to_existing_users.exs +++ /dev/null @@ -1,60 +0,0 @@ -defmodule Mv.Repo.Migrations.AssignMitgliedRoleToExistingUsers do - @moduledoc """ - Assigns the "Mitglied" role to all existing users without a role. - - This migration runs once during deployment to ensure all users have a role assigned. - New users will automatically get the "Mitglied" role via the role_id attribute's default function. - """ - use Ecto.Migration - import Ecto.Query - - def up do - # Find or create the "Mitglied" role - # This ensures the migration works even if seeds haven't run yet - mitglied_role_id = - case repo().one( - from(r in "roles", - where: r.name == "Mitglied", - select: r.id - ) - ) do - nil -> - # Role doesn't exist - create it - # This is idempotent and safe because the role name is unique - # Use execute with SQL string to properly use uuid_generate_v7() function - execute(""" - INSERT INTO roles (id, name, description, permission_set_name, is_system_role, inserted_at, updated_at) - VALUES (uuid_generate_v7(), 'Mitglied', 'Default member role with access to own data only', 'own_data', true, (now() AT TIME ZONE 'utc'), (now() AT TIME ZONE 'utc')) - """) - - # Get the created role ID - role_id = - repo().one( - from(r in "roles", - where: r.name == "Mitglied", - select: r.id - ) - ) - - IO.puts("✅ Created 'Mitglied' role (was missing)") - role_id - - role_id -> - role_id - end - - # Assign Mitglied role to all users without a role - {count, _} = - repo().update_all( - from(u in "users", where: is_nil(u.role_id)), - set: [role_id: mitglied_role_id] - ) - - IO.puts("✅ Assigned 'Mitglied' role to #{count} existing user(s)") - end - - def down do - # Not reversible - we can't know which users had no role before - :ok - end -end diff --git a/priv/repo/migrations/20260125155125_add_not_null_constraint_to_users_role_id.exs b/priv/repo/migrations/20260125155125_add_not_null_constraint_to_users_role_id.exs deleted file mode 100644 index 0de605d..0000000 --- a/priv/repo/migrations/20260125155125_add_not_null_constraint_to_users_role_id.exs +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Mv.Repo.Migrations.AddNotNullConstraintToUsersRoleId do - @moduledoc """ - Adds NOT NULL constraint to users.role_id column. - - This ensures that role_id can never be NULL at the database level, - providing an additional safety layer beyond Ash's allow_nil? false. - - Before running this migration, ensure all existing users have a role_id - (the previous migration AssignMitgliedRoleToExistingUsers handles this). - """ - use Ecto.Migration - - def up do - # First ensure all users have a role_id (safety check) - # This should already be done by the previous migration, but we check anyway - execute(""" - UPDATE users - SET role_id = ( - SELECT id FROM roles WHERE name = 'Mitglied' LIMIT 1 - ) - WHERE role_id IS NULL - """) - - # Now add NOT NULL constraint - alter table(:users) do - modify :role_id, :uuid, null: false - end - end - - def down do - # Remove NOT NULL constraint (allow NULL again) - alter table(:users) do - modify :role_id, :uuid, null: true - end - end -end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 1a1f80f..91b6fa3 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -5,6 +5,7 @@ alias Mv.Membership alias Mv.Accounts +alias Mv.Authorization alias Mv.MembershipFees.MembershipFeeType alias Mv.MembershipFees.CycleGenerator @@ -128,79 +129,28 @@ end # Get admin email from environment variable or use default admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" -# Create all authorization roles (idempotent - creates only if they don't exist) -# Roles are created using create_role_with_system_flag to allow setting is_system_role -role_configs = [ - %{ - name: "Mitglied", - description: "Default member role with access to own data only", - permission_set_name: "own_data", - is_system_role: true - }, - %{ - name: "Vorstand", - description: "Board member with read access to all member data", - permission_set_name: "read_only", - is_system_role: false - }, - %{ - name: "Kassenwart", - description: "Treasurer with full member and payment management", - permission_set_name: "normal_user", - is_system_role: false - }, - %{ - name: "Buchhaltung", - description: "Accounting with read-only access for auditing", - permission_set_name: "read_only", - is_system_role: false - }, - %{ - name: "Admin", - description: "Administrator with unrestricted access", - permission_set_name: "admin", - is_system_role: false - } -] +# Create admin role (used for assigning to admin users) +admin_role = + case Authorization.list_roles() do + {:ok, roles} -> + case Enum.find(roles, &(&1.name == "Admin" && &1.permission_set_name == "admin")) do + nil -> + # Create admin role if it doesn't exist + case Authorization.create_role(%{ + name: "Admin", + description: "Administrator with full access", + permission_set_name: "admin" + }) do + {:ok, role} -> role + {:error, _error} -> nil + end -# Create or update each role -Enum.each(role_configs, fn role_data -> - # Bind role name to variable to avoid issues with ^ pinning in macros - role_name = role_data.name - - case Mv.Authorization.Role - |> Ash.Query.filter(name == ^role_name) - |> Ash.read_one(authorize?: false, domain: Mv.Authorization) do - {:ok, existing_role} when not is_nil(existing_role) -> - # Role exists - update if needed (preserve is_system_role) - if existing_role.permission_set_name != role_data.permission_set_name or - existing_role.description != role_data.description do - existing_role - |> Ash.Changeset.for_update(:update_role, %{ - description: role_data.description, - permission_set_name: role_data.permission_set_name - }) - |> Ash.update!(authorize?: false, domain: Mv.Authorization) + role -> + role end - {:ok, nil} -> - # Role doesn't exist - create it - Mv.Authorization.Role - |> Ash.Changeset.for_create(:create_role_with_system_flag, role_data) - |> Ash.create!(authorize?: false, domain: Mv.Authorization) - - {:error, error} -> - IO.puts("Warning: Failed to check for role #{role_data.name}: #{inspect(error)}") - end -end) - -# Get admin role for assignment to admin user -admin_role = - case Mv.Authorization.Role - |> Ash.Query.filter(name == "Admin") - |> Ash.read_one(authorize?: false, domain: Mv.Authorization) do - {:ok, role} when not is_nil(role) -> role - _ -> nil + {:error, _error} -> + nil end if is_nil(admin_role) do diff --git a/priv/resource_snapshots/repo/members/20260125155125.json b/priv/resource_snapshots/repo/members/20260125155125.json deleted file mode 100644 index 3af9f69..0000000 --- a/priv/resource_snapshots/repo/members/20260125155125.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "attributes": [ - { - "allow_nil?": false, - "default": "fragment(\"uuid_generate_v7()\")", - "generated?": false, - "precision": null, - "primary_key?": true, - "references": null, - "scale": null, - "size": null, - "source": "id", - "type": "uuid" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "first_name", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "last_name", - "type": "text" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "email", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "join_date", - "type": "date" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "exit_date", - "type": "date" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "notes", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "city", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "street", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "house_number", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "postal_code", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "search_vector", - "type": "tsvector" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "membership_fee_start_date", - "type": "date" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": { - "deferrable": false, - "destination_attribute": "id", - "destination_attribute_default": null, - "destination_attribute_generated": null, - "index?": false, - "match_type": null, - "match_with": null, - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "name": "members_membership_fee_type_id_fkey", - "on_delete": null, - "on_update": null, - "primary_key?": true, - "schema": "public", - "table": "membership_fee_types" - }, - "scale": null, - "size": null, - "source": "membership_fee_type_id", - "type": "uuid" - } - ], - "base_filter": null, - "check_constraints": [], - "custom_indexes": [], - "custom_statements": [], - "has_create_action": true, - "hash": "107B69E0A6FDBE7FAE4B1EABBF3E8C3B1F004B8D96B3759C95071169288968CC", - "identities": [ - { - "all_tenants?": false, - "base_filter": null, - "index_name": "members_unique_email_index", - "keys": [ - { - "type": "atom", - "value": "email" - } - ], - "name": "unique_email", - "nils_distinct?": true, - "where": null - } - ], - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "repo": "Elixir.Mv.Repo", - "schema": null, - "table": "members" -} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/users/20260125155125.json b/priv/resource_snapshots/repo/users/20260125155125.json deleted file mode 100644 index c214ad5..0000000 --- a/priv/resource_snapshots/repo/users/20260125155125.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "attributes": [ - { - "allow_nil?": false, - "default": "fragment(\"gen_random_uuid()\")", - "generated?": false, - "precision": null, - "primary_key?": true, - "references": null, - "scale": null, - "size": null, - "source": "id", - "type": "uuid" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "email", - "type": "citext" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "hashed_password", - "type": "text" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": null, - "scale": null, - "size": null, - "source": "oidc_id", - "type": "text" - }, - { - "allow_nil?": false, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": { - "deferrable": false, - "destination_attribute": "id", - "destination_attribute_default": null, - "destination_attribute_generated": null, - "index?": false, - "match_type": null, - "match_with": null, - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "name": "users_role_id_fkey", - "on_delete": "restrict", - "on_update": null, - "primary_key?": true, - "schema": "public", - "table": "roles" - }, - "scale": null, - "size": null, - "source": "role_id", - "type": "uuid" - }, - { - "allow_nil?": true, - "default": "nil", - "generated?": false, - "precision": null, - "primary_key?": false, - "references": { - "deferrable": false, - "destination_attribute": "id", - "destination_attribute_default": null, - "destination_attribute_generated": null, - "index?": false, - "match_type": null, - "match_with": null, - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "name": "users_member_id_fkey", - "on_delete": "nilify", - "on_update": null, - "primary_key?": true, - "schema": "public", - "table": "members" - }, - "scale": null, - "size": null, - "source": "member_id", - "type": "uuid" - } - ], - "base_filter": null, - "check_constraints": [], - "custom_indexes": [], - "custom_statements": [], - "has_create_action": true, - "hash": "3E8D3C1A8834053B947F08369B81216A0B13019E5FD6FBFB706968FABA49EC06", - "identities": [ - { - "all_tenants?": false, - "base_filter": null, - "index_name": "users_unique_email_index", - "keys": [ - { - "type": "atom", - "value": "email" - } - ], - "name": "unique_email", - "nils_distinct?": true, - "where": null - }, - { - "all_tenants?": false, - "base_filter": null, - "index_name": "users_unique_member_index", - "keys": [ - { - "type": "atom", - "value": "member_id" - } - ], - "name": "unique_member", - "nils_distinct?": true, - "where": null - }, - { - "all_tenants?": false, - "base_filter": null, - "index_name": "users_unique_oidc_id_index", - "keys": [ - { - "type": "atom", - "value": "oidc_id" - } - ], - "name": "unique_oidc_id", - "nils_distinct?": true, - "where": null - } - ], - "multitenancy": { - "attribute": null, - "global": null, - "strategy": null - }, - "repo": "Elixir.Mv.Repo", - "schema": null, - "table": "users" -} \ No newline at end of file diff --git a/test/accounts/email_sync_edge_cases_test.exs b/test/accounts/email_sync_edge_cases_test.exs index 00ae5f9..b872235 100644 --- a/test/accounts/email_sync_edge_cases_test.exs +++ b/test/accounts/email_sync_edge_cases_test.exs @@ -7,11 +7,6 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "Email sync edge cases" do @valid_user_attrs %{ email: "user@example.com" @@ -23,15 +18,15 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do email: "member@example.com" } - test "simultaneous email updates use user email as source of truth", %{actor: actor} do + test "simultaneous email updates use user email as source of truth" do # Create linked user and member - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) {:ok, user} = - Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor) + Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id})) # Verify link and initial sync - {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id) assert synced_member.email == "user@example.com" # Scenario: Both emails are updated "simultaneously" @@ -40,60 +35,58 @@ defmodule Mv.Accounts.EmailSyncEdgeCasesTest do # Update member email first {:ok, _updated_member} = - Membership.update_member(member, %{email: "member-new@example.com"}, actor: actor) + Membership.update_member(member, %{email: "member-new@example.com"}) # Verify it synced to user - {:ok, user_after_member_update} = Ash.get(Mv.Accounts.User, user.id, actor: actor) + {:ok, user_after_member_update} = Ash.get(Mv.Accounts.User, user.id) assert to_string(user_after_member_update.email) == "member-new@example.com" # Now update user email - this should override {:ok, _updated_user} = - Accounts.update_user(user_after_member_update, %{email: "user-final@example.com"}, - actor: actor - ) + Accounts.update_user(user_after_member_update, %{email: "user-final@example.com"}) # Reload both - {:ok, final_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor) - {:ok, final_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, final_user} = Ash.get(Mv.Accounts.User, user.id) + {:ok, final_member} = Ash.get(Mv.Membership.Member, member.id) # User email should be the final truth assert to_string(final_user.email) == "user-final@example.com" assert final_member.email == "user-final@example.com" end - test "email validation works for both user and member", %{actor: actor} do + test "email validation works for both user and member" do # Test that invalid emails are rejected for both resources # Invalid email for user - invalid_user_result = Accounts.create_user(%{email: "not-an-email"}, actor: actor) + invalid_user_result = Accounts.create_user(%{email: "not-an-email"}) assert {:error, %Ash.Error.Invalid{}} = invalid_user_result # Invalid email for member invalid_member_attrs = Map.put(@valid_member_attrs, :email, "also-not-an-email") - invalid_member_result = Membership.create_member(invalid_member_attrs, actor: actor) + invalid_member_result = Membership.create_member(invalid_member_attrs) assert {:error, %Ash.Error.Invalid{}} = invalid_member_result # Valid emails should work - {:ok, _user} = Accounts.create_user(@valid_user_attrs, actor: actor) - {:ok, _member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, _user} = Accounts.create_user(@valid_user_attrs) + {:ok, _member} = Membership.create_member(@valid_member_attrs) end - test "identity constraints prevent duplicate emails", %{actor: actor} do + test "identity constraints prevent duplicate emails" do # Create first user with an email - {:ok, user1} = Accounts.create_user(%{email: "duplicate@example.com"}, actor: actor) + {:ok, user1} = Accounts.create_user(%{email: "duplicate@example.com"}) assert to_string(user1.email) == "duplicate@example.com" # Try to create second user with same email - should fail due to unique constraint - result = Accounts.create_user(%{email: "duplicate@example.com"}, actor: actor) + result = Accounts.create_user(%{email: "duplicate@example.com"}) assert {:error, %Ash.Error.Invalid{}} = result # Same for members member_attrs = Map.put(@valid_member_attrs, :email, "member-dup@example.com") - {:ok, member1} = Membership.create_member(member_attrs, actor: actor) + {:ok, member1} = Membership.create_member(member_attrs) assert member1.email == "member-dup@example.com" # Try to create second member with same email - should fail - result2 = Membership.create_member(member_attrs, actor: actor) + result2 = Membership.create_member(member_attrs) assert {:error, %Ash.Error.Invalid{}} = result2 end end diff --git a/test/accounts/email_uniqueness_test.exs b/test/accounts/email_uniqueness_test.exs index 4a21b39..a16ebdd 100644 --- a/test/accounts/email_uniqueness_test.exs +++ b/test/accounts/email_uniqueness_test.exs @@ -4,177 +4,121 @@ defmodule Mv.Accounts.EmailUniquenessTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "Email uniqueness validation - Creation" do - test "CAN create member with existing unlinked user email", %{actor: actor} do + test "CAN create member with existing unlinked user email" do # Create a user with email {:ok, _user} = - Accounts.create_user( - %{ - email: "existing@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "existing@example.com" + }) # Create member with same email - should succeed {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "existing@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "existing@example.com" + }) assert to_string(member.email) == "existing@example.com" end - test "CAN create user with existing unlinked member email", %{actor: actor} do + test "CAN create user with existing unlinked member email" do # Create a member with email {:ok, _member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "existing@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "existing@example.com" + }) # Create user with same email - should succeed {:ok, user} = - Accounts.create_user( - %{ - email: "existing@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "existing@example.com" + }) assert to_string(user.email) == "existing@example.com" end end describe "Email uniqueness validation - Updating unlinked entities" do - test "unlinked member email CAN be changed to an existing unlinked user email", %{ - actor: actor - } do + test "unlinked member email CAN be changed to an existing unlinked user email" do # Create a user with email {:ok, _user} = - Accounts.create_user( - %{ - email: "existing_user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "existing_user@example.com" + }) # Create an unlinked member with different email {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "member@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "member@example.com" + }) # Change member email to existing user email - should succeed (member is unlinked) {:ok, updated_member} = - Membership.update_member( - member, - %{ - email: "existing_user@example.com" - }, - actor: actor - ) + Membership.update_member(member, %{ + email: "existing_user@example.com" + }) assert to_string(updated_member.email) == "existing_user@example.com" end - test "unlinked user email CAN be changed to an existing unlinked member email", %{ - actor: actor - } do + test "unlinked user email CAN be changed to an existing unlinked member email" do # Create a member with email {:ok, _member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "existing_member@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "existing_member@example.com" + }) # Create an unlinked user with different email {:ok, user} = - Accounts.create_user( - %{ - email: "user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com" + }) # Change user email to existing member email - should succeed (user is unlinked) {:ok, updated_user} = - Accounts.update_user( - user, - %{ - email: "existing_member@example.com" - }, - actor: actor - ) + Accounts.update_user(user, %{ + email: "existing_member@example.com" + }) assert to_string(updated_user.email) == "existing_member@example.com" end - test "unlinked member email CANNOT be changed to an existing linked user email", %{ - actor: actor - } do + test "unlinked member email CANNOT be changed to an existing linked user email" do # Create a user and link it to a member - this makes the user "linked" {:ok, user} = - Accounts.create_user( - %{ - email: "linked_user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "linked_user@example.com" + }) {:ok, _member_a} = - Membership.create_member( - %{ - first_name: "Member", - last_name: "A", - email: "temp@example.com", - user: %{id: user.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Member", + last_name: "A", + email: "temp@example.com", + user: %{id: user.id} + }) # Create an unlinked member with different email {:ok, member_b} = - Membership.create_member( - %{ - first_name: "Member", - last_name: "B", - email: "member_b@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Member", + last_name: "B", + email: "member_b@example.com" + }) # Try to change unlinked member's email to linked user's email - should fail result = - Membership.update_member( - member_b, - %{ - email: "linked_user@example.com" - }, - actor: actor - ) + Membership.update_member(member_b, %{ + email: "linked_user@example.com" + }) assert {:error, %Ash.Error.Invalid{} = error} = result @@ -185,52 +129,37 @@ defmodule Mv.Accounts.EmailUniquenessTest do end) end - test "unlinked user email CANNOT be changed to an existing linked member email", %{ - actor: actor - } do + test "unlinked user email CANNOT be changed to an existing linked member email" do # Create a user and link it to a member - this makes the member "linked" {:ok, user_a} = - Accounts.create_user( - %{ - email: "user_a@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user_a@example.com" + }) {:ok, _member_a} = - Membership.create_member( - %{ - first_name: "Member", - last_name: "A", - email: "temp@example.com", - user: %{id: user_a.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Member", + last_name: "A", + email: "temp@example.com", + user: %{id: user_a.id} + }) # Reload user to get updated member_id and linked member email - {:ok, user_a_reloaded} = Ash.get(Mv.Accounts.User, user_a.id, actor: actor) - {:ok, user_a_with_member} = Ash.load(user_a_reloaded, :member, actor: actor) + {:ok, user_a_reloaded} = Ash.get(Mv.Accounts.User, user_a.id) + {:ok, user_a_with_member} = Ash.load(user_a_reloaded, :member) linked_member_email = to_string(user_a_with_member.member.email) # Create an unlinked user with different email {:ok, user_b} = - Accounts.create_user( - %{ - email: "user_b@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user_b@example.com" + }) # Try to change unlinked user's email to linked member's email - should fail result = - Accounts.update_user( - user_b, - %{ - email: linked_member_email - }, - actor: actor - ) + Accounts.update_user(user_b, %{ + email: linked_member_email + }) assert {:error, %Ash.Error.Invalid{} = error} = result @@ -243,37 +172,28 @@ defmodule Mv.Accounts.EmailUniquenessTest do end describe "Email uniqueness validation - Creating with linked emails" do - test "CANNOT create member with existing linked user email", %{actor: actor} do + test "CANNOT create member with existing linked user email" do # Create a user and link it to a member {:ok, user} = - Accounts.create_user( - %{ - email: "linked@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "linked@example.com" + }) {:ok, _member} = - Membership.create_member( - %{ - first_name: "First", - last_name: "Member", - email: "temp@example.com", - user: %{id: user.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "First", + last_name: "Member", + email: "temp@example.com", + user: %{id: user.id} + }) # Try to create a new member with the linked user's email - should fail result = - Membership.create_member( - %{ - first_name: "Second", - last_name: "Member", - email: "linked@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Second", + last_name: "Member", + email: "linked@example.com" + }) assert {:error, %Ash.Error.Invalid{} = error} = result @@ -284,40 +204,31 @@ defmodule Mv.Accounts.EmailUniquenessTest do end) end - test "CANNOT create user with existing linked member email", %{actor: actor} do + test "CANNOT create user with existing linked member email" do # Create a user and link it to a member {:ok, user} = - Accounts.create_user( - %{ - email: "user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com" + }) {:ok, _member} = - Membership.create_member( - %{ - first_name: "Member", - last_name: "One", - email: "temp@example.com", - user: %{id: user.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Member", + last_name: "One", + email: "temp@example.com", + user: %{id: user.id} + }) # Reload user to get the linked member's email - {:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id, actor: actor) - {:ok, user_with_member} = Ash.load(user_reloaded, :member, actor: actor) + {:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id) + {:ok, user_with_member} = Ash.load(user_reloaded, :member) linked_member_email = to_string(user_with_member.member.email) # Try to create a new user with the linked member's email - should fail result = - Accounts.create_user( - %{ - email: linked_member_email - }, - actor: actor - ) + Accounts.create_user(%{ + email: linked_member_email + }) assert {:error, %Ash.Error.Invalid{} = error} = result @@ -330,45 +241,32 @@ defmodule Mv.Accounts.EmailUniquenessTest do end describe "Email uniqueness validation - Updating linked entities" do - test "linked member email CANNOT be changed to an existing user email", %{actor: actor} do + test "linked member email CANNOT be changed to an existing user email" do # Create a user with email {:ok, _other_user} = - Accounts.create_user( - %{ - email: "other_user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "other_user@example.com" + }) # Create a user and link it to a member {:ok, user} = - Accounts.create_user( - %{ - email: "user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com" + }) {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "temp@example.com", - user: %{id: user.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "temp@example.com", + user: %{id: user.id} + }) # Try to change linked member's email to other user's email - should fail result = - Membership.update_member( - member, - %{ - email: "other_user@example.com" - }, - actor: actor - ) + Membership.update_member(member, %{ + email: "other_user@example.com" + }) assert {:error, %Ash.Error.Invalid{} = error} = result @@ -379,50 +277,37 @@ defmodule Mv.Accounts.EmailUniquenessTest do end) end - test "linked user email CANNOT be changed to an existing member email", %{actor: actor} do + test "linked user email CANNOT be changed to an existing member email" do # Create a member with email {:ok, _other_member} = - Membership.create_member( - %{ - first_name: "Jane", - last_name: "Doe", - email: "other_member@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Jane", + last_name: "Doe", + email: "other_member@example.com" + }) # Create a user and link it to a member {:ok, user} = - Accounts.create_user( - %{ - email: "user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com" + }) {:ok, _member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "temp@example.com", - user: %{id: user.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "temp@example.com", + user: %{id: user.id} + }) # Reload user to get updated member_id - {:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id, actor: actor) + {:ok, user_reloaded} = Ash.get(Mv.Accounts.User, user.id) # Try to change linked user's email to other member's email - should fail result = - Accounts.update_user( - user_reloaded, - %{ - email: "other_member@example.com" - }, - actor: actor - ) + Accounts.update_user(user_reloaded, %{ + email: "other_member@example.com" + }) assert {:error, %Ash.Error.Invalid{} = error} = result @@ -435,49 +320,34 @@ defmodule Mv.Accounts.EmailUniquenessTest do end describe "Email uniqueness validation - Linking" do - test "CANNOT link user to member if user email is already used by another unlinked member", %{ - actor: actor - } do + test "CANNOT link user to member if user email is already used by another unlinked member" do # Create a member with email {:ok, _other_member} = - Membership.create_member( - %{ - first_name: "Jane", - last_name: "Doe", - email: "duplicate@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Jane", + last_name: "Doe", + email: "duplicate@example.com" + }) # Create a user with same email {:ok, user} = - Accounts.create_user( - %{ - email: "duplicate@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "duplicate@example.com" + }) # Create a member to link with the user {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Smith", - email: "john@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Smith", + email: "john@example.com" + }) # Try to link user to member - should fail because user.email is already used by other_member result = - Accounts.update_user( - user, - %{ - member: %{id: member.id} - }, - actor: actor - ) + Accounts.update_user(user, %{ + member: %{id: member.id} + }) assert {:error, %Ash.Error.Invalid{} = error} = result @@ -488,160 +358,120 @@ defmodule Mv.Accounts.EmailUniquenessTest do end) end - test "CAN link member to user even if member email is used by another user (member email gets overridden)", - %{actor: actor} do + test "CAN link member to user even if member email is used by another user (member email gets overridden)" do # Create a user with email {:ok, _other_user} = - Accounts.create_user( - %{ - email: "duplicate@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "duplicate@example.com" + }) # Create a member with same email {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "duplicate@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "duplicate@example.com" + }) # Create a user to link with the member {:ok, user} = - Accounts.create_user( - %{ - email: "user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com" + }) # Link member to user - should succeed because member.email will be overridden {:ok, updated_member} = - Membership.update_member( - member, - %{ - user: %{id: user.id} - }, - actor: actor - ) + Membership.update_member(member, %{ + user: %{id: user.id} + }) # Member email should now be the same as user email - {:ok, member_reloaded} = Ash.get(Mv.Membership.Member, updated_member.id, actor: actor) + {:ok, member_reloaded} = Ash.get(Mv.Membership.Member, updated_member.id) assert to_string(member_reloaded.email) == "user@example.com" end end describe "Email syncing" do - test "member email syncs to linked user email without validation error", %{actor: actor} do + test "member email syncs to linked user email without validation error" do # Create a user {:ok, user} = - Accounts.create_user( - %{ - email: "user@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com" + }) # Create a member linked to this user # The override change will set member.email = user.email automatically {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "member@example.com", - user: %{id: user.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "member@example.com", + user: %{id: user.id} + }) # Member email should have been overridden to user email # This happens through our sync mechanism, which should NOT trigger # the "email already used" validation because it's the same user - {:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id) assert member_after_link.email == "user@example.com" end - test "user email syncs to linked member without validation error", %{actor: actor} do + test "user email syncs to linked member without validation error" do # Create a member {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "member@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "member@example.com" + }) # Create a user linked to this member # The override change will set member.email = user.email automatically {:ok, _user} = - Accounts.create_user( - %{ - email: "user@example.com", - member: %{id: member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com", + member: %{id: member.id} + }) # Member email should have been overridden to user email # This happens through our sync mechanism, which should NOT trigger # the "email already used" validation because it's the same member - {:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id) assert member_after_link.email == "user@example.com" end - test "two unlinked users cannot have the same email", %{actor: actor} do + test "two unlinked users cannot have the same email" do # Create first user {:ok, _user1} = - Accounts.create_user( - %{ - email: "duplicate@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "duplicate@example.com" + }) # Try to create second user with same email result = - Accounts.create_user( - %{ - email: "duplicate@example.com" - }, - actor: actor - ) + Accounts.create_user(%{ + email: "duplicate@example.com" + }) assert {:error, %Ash.Error.Invalid{}} = result end - test "two unlinked members cannot have the same email (members have unique constraint)", %{ - actor: actor - } do + test "two unlinked members cannot have the same email (members have unique constraint)" do # Create first member {:ok, _member1} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "duplicate@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "duplicate@example.com" + }) # Try to create second member with same email - should fail result = - Membership.create_member( - %{ - first_name: "Jane", - last_name: "Smith", - email: "duplicate@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Jane", + last_name: "Smith", + email: "duplicate@example.com" + }) assert {:error, %Ash.Error.Invalid{}} = result # Members DO have a unique email constraint at database level diff --git a/test/accounts/user_authentication_test.exs b/test/accounts/user_authentication_test.exs index da84e81..caa3359 100644 --- a/test/accounts/user_authentication_test.exs +++ b/test/accounts/user_authentication_test.exs @@ -10,11 +10,6 @@ defmodule Mv.Accounts.UserAuthenticationTest do use MvWeb.ConnCase, async: true require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "Password authentication user identification" do @tag :test_proposal test "password login uses email as identifier" do @@ -32,7 +27,7 @@ defmodule Mv.Accounts.UserAuthenticationTest do {:ok, users} = Mv.Accounts.User |> Ash.Query.filter(email == ^email_to_find) - |> Ash.read(actor: user) + |> Ash.read() assert length(users) == 1 found_user = List.first(users) @@ -118,16 +113,11 @@ defmodule Mv.Accounts.UserAuthenticationTest do # Use sign_in_with_rauthy to find user by oidc_id # Note: This test will FAIL until we implement the security fix # that changes the filter from email to oidc_id - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) case result do {:ok, [found_user]} -> @@ -151,16 +141,11 @@ defmodule Mv.Accounts.UserAuthenticationTest do } # Should create via register_with_rauthy - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, new_user} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert to_string(new_user.email) == "newuser@example.com" assert new_user.oidc_id == "brand_new_oidc_789" @@ -185,12 +170,12 @@ defmodule Mv.Accounts.UserAuthenticationTest do {:ok, users1} = Mv.Accounts.User |> Ash.Query.filter(oidc_id == "oidc_unique_1") - |> Ash.read(actor: user1) + |> Ash.read() {:ok, users2} = Mv.Accounts.User |> Ash.Query.filter(oidc_id == "oidc_unique_2") - |> Ash.read(actor: user2) + |> Ash.read() assert length(users1) == 1 assert length(users2) == 1 @@ -220,16 +205,11 @@ defmodule Mv.Accounts.UserAuthenticationTest do } # Should NOT find the user (security requirement) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Either returns empty list OR authentication error - both mean "user not found" case result do @@ -261,16 +241,11 @@ defmodule Mv.Accounts.UserAuthenticationTest do } # Should NOT find the user because oidc_id is nil - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Either returns empty list OR authentication error - both mean "user not found" case result do diff --git a/test/accounts/user_email_sync_test.exs b/test/accounts/user_email_sync_test.exs index d324783..6d08d61 100644 --- a/test/accounts/user_email_sync_test.exs +++ b/test/accounts/user_email_sync_test.exs @@ -8,11 +8,6 @@ defmodule Mv.Accounts.UserEmailSyncTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "User email synchronization to linked Member" do @valid_user_attrs %{ email: "user@example.com" @@ -24,100 +19,96 @@ defmodule Mv.Accounts.UserEmailSyncTest do email: "member@example.com" } - test "updating user email syncs to linked member", %{actor: actor} do + test "updating user email syncs to linked member" do # Create a member - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) assert member.email == "member@example.com" # Create a user linked to the member {:ok, user} = - Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor) + Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id})) # Verify initial state - member email should be overridden by user email - {:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, member_after_link} = Ash.get(Mv.Membership.Member, member.id) assert member_after_link.email == "user@example.com" # Update user email - {:ok, updated_user} = - Accounts.update_user(user, %{email: "newemail@example.com"}, actor: actor) - + {:ok, updated_user} = Accounts.update_user(user, %{email: "newemail@example.com"}) assert to_string(updated_user.email) == "newemail@example.com" # Verify member email was also updated - {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id) assert synced_member.email == "newemail@example.com" end - test "creating user linked to member overrides member email", %{actor: actor} do + test "creating user linked to member overrides member email" do # Create a member with their own email - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) assert member.email == "member@example.com" # Create a user linked to this member {:ok, user} = - Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor) + Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id})) assert to_string(user.email) == "user@example.com" assert user.member_id == member.id # Verify member email was overridden with user email - {:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id) assert updated_member.email == "user@example.com" end - test "linking user to existing member syncs user email to member", %{actor: actor} do + test "linking user to existing member syncs user email to member" do # Create a standalone member - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) assert member.email == "member@example.com" # Create a standalone user - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(@valid_user_attrs) assert to_string(user.email) == "user@example.com" assert user.member_id == nil # Link the user to the member - {:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor) + {:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}) assert linked_user.member_id == member.id # Verify member email was overridden with user email - {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id) assert synced_member.email == "user@example.com" end - test "updating user email when no member linked does not error", %{actor: actor} do + test "updating user email when no member linked does not error" do # Create a standalone user without member link - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(@valid_user_attrs) assert to_string(user.email) == "user@example.com" assert user.member_id == nil # Update user email - should work fine without error - {:ok, updated_user} = - Accounts.update_user(user, %{email: "newemail@example.com"}, actor: actor) - + {:ok, updated_user} = Accounts.update_user(user, %{email: "newemail@example.com"}) assert to_string(updated_user.email) == "newemail@example.com" assert updated_user.member_id == nil end - test "unlinking user from member does not sync email", %{actor: actor} do + test "unlinking user from member does not sync email" do # Create member - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) # Create user linked to member {:ok, user} = - Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor) + Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id})) assert user.member_id == member.id # Verify member email was synced - {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id) assert synced_member.email == "user@example.com" # Unlink user from member - {:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}, actor: actor) + {:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}) assert unlinked_user.member_id == nil # Member email should remain unchanged after unlinking - {:ok, member_after_unlink} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, member_after_unlink} = Ash.get(Mv.Membership.Member, member.id) assert member_after_unlink.email == "user@example.com" end end @@ -128,8 +119,6 @@ defmodule Mv.Accounts.UserEmailSyncTest do email = "test@example.com" password = "securepassword123" - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create user with password strategy (simulating registration) {:ok, user} = Mv.Accounts.User @@ -137,7 +126,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do email: email, password: password }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert to_string(user.email) == email assert user.hashed_password != nil @@ -149,7 +138,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do email: email, password: password }) - |> Ash.read_one(actor: system_actor) + |> Ash.read_one() assert signed_in_user.id == user.id assert to_string(signed_in_user.email) == email @@ -164,8 +153,6 @@ defmodule Mv.Accounts.UserEmailSyncTest do oauth_tokens = %{"access_token" => "mock_token"} - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Simulate OIDC registration {:ok, user} = Mv.Accounts.User @@ -173,7 +160,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do user_info: user_info, oauth_tokens: oauth_tokens }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert to_string(user.email) == "oidc@example.com" assert user.oidc_id == "oidc-user-123" diff --git a/test/accounts/user_member_deletion_test.exs b/test/accounts/user_member_deletion_test.exs index feb7180..52a3865 100644 --- a/test/accounts/user_member_deletion_test.exs +++ b/test/accounts/user_member_deletion_test.exs @@ -18,86 +18,71 @@ defmodule Mv.Accounts.UserMemberDeletionTest do email: "john@example.com" } - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - - test "deleting a member sets the user's member_id to NULL", %{actor: actor} do + test "deleting a member sets the user's member_id to NULL" do # Create a member - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) # Create a user linked to the member {:ok, user} = - Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id}), actor: actor) + Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member.id})) # Verify the relationship is established - {:ok, user_before_delete} = - Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member]) - + {:ok, user_before_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member]) assert user_before_delete.member_id == member.id assert user_before_delete.member.id == member.id # Delete the member - :ok = Membership.destroy_member(member, actor: actor) + :ok = Membership.destroy_member(member) # Verify the user still exists but member_id is NULL - {:ok, user_after_delete} = - Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member]) - + {:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id, load: [:member]) assert user_after_delete.id == user.id assert user_after_delete.member_id == nil assert user_after_delete.member == nil end - test "user can be linked to a new member after old member is deleted", %{actor: actor} do + test "user can be linked to a new member after old member is deleted" do # Create first member - {:ok, member1} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member1} = Membership.create_member(@valid_member_attrs) # Create user linked to first member {:ok, user} = - Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member1.id}), actor: actor) + Accounts.create_user(Map.put(@valid_user_attrs, :member, %{id: member1.id})) assert user.member_id == member1.id # Delete first member - :ok = Membership.destroy_member(member1, actor: actor) + :ok = Membership.destroy_member(member1) # Reload user from database to get updated member_id (should be NULL) - {:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id, actor: actor) + {:ok, user_after_delete} = Ash.get(Mv.Accounts.User, user.id) assert user_after_delete.member_id == nil # Create second member {:ok, member2} = - Membership.create_member( - %{ - first_name: "Jane", - last_name: "Smith", - email: "jane@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Jane", + last_name: "Smith", + email: "jane@example.com" + }) # Link user to second member (use reloaded user) - {:ok, updated_user} = - Accounts.update_user(user_after_delete, %{member: %{id: member2.id}}, actor: actor) + {:ok, updated_user} = Accounts.update_user(user_after_delete, %{member: %{id: member2.id}}) # Verify new relationship - {:ok, final_user} = - Ash.get(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member]) - + {:ok, final_user} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member]) assert final_user.member_id == member2.id assert final_user.member.id == member2.id end - test "member without linked user can be deleted normally", %{actor: actor} do - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "member without linked user can be deleted normally" do + {:ok, member} = Membership.create_member(@valid_member_attrs) # Delete member (no users linked) - assert :ok = Membership.destroy_member(member, actor: actor) + assert :ok = Membership.destroy_member(member) # Verify member is deleted - assert {:error, _} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + assert {:error, _} = Ash.get(Mv.Membership.Member, member.id) end end end diff --git a/test/accounts/user_member_linking_email_test.exs b/test/accounts/user_member_linking_email_test.exs index 62886ca..d7c2817 100644 --- a/test/accounts/user_member_linking_email_test.exs +++ b/test/accounts/user_member_linking_email_test.exs @@ -10,70 +10,51 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "link with same email" do - test "succeeds when user.email == member.email", %{actor: actor} do + test "succeeds when user.email == member.email" do # Create member with specific email {:ok, member} = - Membership.create_member( - %{ - first_name: "Alice", - last_name: "Johnson", - email: "alice@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Alice", + last_name: "Johnson", + email: "alice@example.com" + }) # Create user with same email and link to member result = - Accounts.create_user( - %{ - email: "alice@example.com", - member: %{id: member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "alice@example.com", + member: %{id: member.id} + }) # Should succeed without errors assert {:ok, user} = result assert to_string(user.email) == "alice@example.com" # Reload to verify link - user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor) + user = Ash.load!(user, [:member], domain: Mv.Accounts) assert user.member.id == member.id assert user.member.email == "alice@example.com" end - test "no validation error triggered when updating linked pair with same email", %{ - actor: actor - } do + test "no validation error triggered when updating linked pair with same email" do # Create member {:ok, member} = - Membership.create_member( - %{ - first_name: "Bob", - last_name: "Smith", - email: "bob@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Bob", + last_name: "Smith", + email: "bob@example.com" + }) # Create user and link {:ok, user} = - Accounts.create_user( - %{ - email: "bob@example.com", - member: %{id: member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "bob@example.com", + member: %{id: member.id} + }) # Update user (should not trigger email validation error) - result = Accounts.update_user(user, %{email: "bob@example.com"}, actor: actor) + result = Accounts.update_user(user, %{email: "bob@example.com"}) assert {:ok, updated_user} = result assert to_string(updated_user.email) == "bob@example.com" @@ -81,88 +62,70 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do end describe "link with different emails" do - test "fails if member.email is used by a DIFFERENT linked user", %{actor: actor} do + test "fails if member.email is used by a DIFFERENT linked user" do # Create first user and link to a different member {:ok, other_member} = - Membership.create_member( - %{ - first_name: "Other", - last_name: "Member", - email: "other@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Other", + last_name: "Member", + email: "other@example.com" + }) {:ok, _user1} = - Accounts.create_user( - %{ - email: "user1@example.com", - member: %{id: other_member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user1@example.com", + member: %{id: other_member.id} + }) # Reload to ensure email sync happened - _other_member = Ash.reload!(other_member, actor: actor) + _other_member = Ash.reload!(other_member) # Create a NEW member with different email {:ok, member} = - Membership.create_member( - %{ - first_name: "Charlie", - last_name: "Brown", - email: "charlie@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Charlie", + last_name: "Brown", + email: "charlie@example.com" + }) # Try to create user2 with email that matches the linked other_member result = - Accounts.create_user( - %{ - email: "user1@example.com", - member: %{id: member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user1@example.com", + member: %{id: member.id} + }) # Should fail because user1@example.com is already used by other_member (which is linked to user1) assert {:error, _error} = result end - test "succeeds for unique emails", %{actor: actor} do + test "succeeds for unique emails" do # Create member {:ok, member} = - Membership.create_member( - %{ - first_name: "David", - last_name: "Wilson", - email: "david@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "David", + last_name: "Wilson", + email: "david@example.com" + }) # Create user with different but unique email result = - Accounts.create_user( - %{ - email: "user@example.com", - member: %{id: member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com", + member: %{id: member.id} + }) # Should succeed assert {:ok, user} = result # Email sync should update member's email to match user's - user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor) + user = Ash.load!(user, [:member], domain: Mv.Accounts) assert user.member.email == "user@example.com" end end describe "edge cases" do - test "unlinking and relinking with same email works (Problem #4)", %{actor: actor} do + test "unlinking and relinking with same email works (Problem #4)" do # This is the exact scenario from Problem #4: # 1. Link user and member (both have same email) # 2. Unlink them (member keeps the email) @@ -170,40 +133,34 @@ defmodule Mv.Accounts.UserMemberLinkingEmailTest do # Create member {:ok, member} = - Membership.create_member( - %{ - first_name: "Emma", - last_name: "Davis", - email: "emma@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Emma", + last_name: "Davis", + email: "emma@example.com" + }) # Create user and link {:ok, user} = - Accounts.create_user( - %{ - email: "emma@example.com", - member: %{id: member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "emma@example.com", + member: %{id: member.id} + }) # Verify they are linked - user = Ash.load!(user, [:member], domain: Mv.Accounts, actor: actor) + user = Ash.load!(user, [:member], domain: Mv.Accounts) assert user.member.id == member.id assert user.member.email == "emma@example.com" # Unlink - {:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}, actor: actor) + {:ok, unlinked_user} = Accounts.update_user(user, %{member: nil}) assert is_nil(unlinked_user.member_id) # Member still has the email after unlink - member = Ash.reload!(member, actor: actor) + member = Ash.reload!(member) assert member.email == "emma@example.com" # Relink (should work - this is Problem #4) - result = Accounts.update_user(unlinked_user, %{member: %{id: member.id}}, actor: actor) + result = Accounts.update_user(unlinked_user, %{member: %{id: member.id}}) assert {:ok, relinked_user} = result assert relinked_user.member_id == member.id diff --git a/test/accounts/user_member_linking_test.exs b/test/accounts/user_member_linking_test.exs index 54c7aa5..1111436 100644 --- a/test/accounts/user_member_linking_test.exs +++ b/test/accounts/user_member_linking_test.exs @@ -9,150 +9,121 @@ defmodule Mv.Accounts.UserMemberLinkingTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "User-Member Linking with Email Sync" do - test "link user to member with different email syncs member email", %{actor: actor} do + test "link user to member with different email syncs member email" do # Create user with one email - {:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor) + {:ok, user} = Accounts.create_user(%{email: "user@example.com"}) # Create member with different email {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "member@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "member@example.com" + }) # Link user to member - {:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor) + {:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}) # Verify link exists - user_with_member = - Ash.get!(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member]) - + user_with_member = Ash.get!(Mv.Accounts.User, updated_user.id, load: [:member]) assert user_with_member.member.id == member.id # Verify member email was synced to match user email - synced_member = Ash.get!(Mv.Membership.Member, member.id, actor: actor) + synced_member = Ash.get!(Mv.Membership.Member, member.id) assert synced_member.email == "user@example.com" end - test "unlink member from user sets member to nil", %{actor: actor} do + test "unlink member from user sets member to nil" do # Create and link user and member - {:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor) + {:ok, user} = Accounts.create_user(%{email: "user@example.com"}) {:ok, member} = - Membership.create_member( - %{ - first_name: "Jane", - last_name: "Smith", - email: "jane@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Jane", + last_name: "Smith", + email: "jane@example.com" + }) - {:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor) + {:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member.id}}) # Verify link exists - user_with_member = Ash.get!(Mv.Accounts.User, linked_user.id, actor: actor, load: [:member]) + user_with_member = Ash.get!(Mv.Accounts.User, linked_user.id, load: [:member]) assert user_with_member.member.id == member.id # Unlink by setting member to nil - {:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}, actor: actor) + {:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}) # Verify link is removed - user_without_member = - Ash.get!(Mv.Accounts.User, unlinked_user.id, actor: actor, load: [:member]) - + user_without_member = Ash.get!(Mv.Accounts.User, unlinked_user.id, load: [:member]) assert is_nil(user_without_member.member) # Verify member still exists independently - member_still_exists = Ash.get!(Mv.Membership.Member, member.id, actor: actor) + member_still_exists = Ash.get!(Mv.Membership.Member, member.id) assert member_still_exists.id == member.id end - test "cannot link member already linked to another user", %{actor: actor} do + test "cannot link member already linked to another user" do # Create first user and link to member - {:ok, user1} = Accounts.create_user(%{email: "user1@example.com"}, actor: actor) + {:ok, user1} = Accounts.create_user(%{email: "user1@example.com"}) {:ok, member} = - Membership.create_member( - %{ - first_name: "Bob", - last_name: "Wilson", - email: "bob@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Bob", + last_name: "Wilson", + email: "bob@example.com" + }) - {:ok, _linked_user1} = - Accounts.update_user(user1, %{member: %{id: member.id}}, actor: actor) + {:ok, _linked_user1} = Accounts.update_user(user1, %{member: %{id: member.id}}) # Create second user and try to link to same member - {:ok, user2} = Accounts.create_user(%{email: "user2@example.com"}, actor: actor) + {:ok, user2} = Accounts.create_user(%{email: "user2@example.com"}) # Should fail because member is already linked assert {:error, %Ash.Error.Invalid{}} = - Accounts.update_user(user2, %{member: %{id: member.id}}, actor: actor) + Accounts.update_user(user2, %{member: %{id: member.id}}) end - test "cannot change member link directly, must unlink first", %{actor: actor} do + test "cannot change member link directly, must unlink first" do # Create user and link to first member - {:ok, user} = Accounts.create_user(%{email: "user@example.com"}, actor: actor) + {:ok, user} = Accounts.create_user(%{email: "user@example.com"}) {:ok, member1} = - Membership.create_member( - %{ - first_name: "Alice", - last_name: "Johnson", - email: "alice@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Alice", + last_name: "Johnson", + email: "alice@example.com" + }) - {:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member1.id}}, actor: actor) + {:ok, linked_user} = Accounts.update_user(user, %{member: %{id: member1.id}}) # Create second member {:ok, member2} = - Membership.create_member( - %{ - first_name: "Charlie", - last_name: "Brown", - email: "charlie@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Charlie", + last_name: "Brown", + email: "charlie@example.com" + }) # Try to directly change member link (should fail) assert {:error, %Ash.Error.Invalid{errors: errors}} = - Accounts.update_user(linked_user, %{member: %{id: member2.id}}, actor: actor) + Accounts.update_user(linked_user, %{member: %{id: member2.id}}) # Verify error message mentions "Remove existing member first" error_messages = Enum.map(errors, & &1.message) assert Enum.any?(error_messages, &String.contains?(&1, "Remove existing member first")) # Two-step process: first unlink, then link new member - {:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}, actor: actor) + {:ok, unlinked_user} = Accounts.update_user(linked_user, %{member: nil}) # After unlinking, member1 still has the user's email # Change member1's email to avoid conflict when relinking to member2 - {:ok, _} = - Membership.update_member(member1, %{email: "alice_changed@example.com"}, actor: actor) + {:ok, _} = Membership.update_member(member1, %{email: "alice_changed@example.com"}) - {:ok, relinked_user} = - Accounts.update_user(unlinked_user, %{member: %{id: member2.id}}, actor: actor) + {:ok, relinked_user} = Accounts.update_user(unlinked_user, %{member: %{id: member2.id}}) # Verify new link is established - user_with_new_member = - Ash.get!(Mv.Accounts.User, relinked_user.id, actor: actor, load: [:member]) - + user_with_new_member = Ash.get!(Mv.Accounts.User, relinked_user.id, load: [:member]) assert user_with_new_member.member.id == member2.id end end diff --git a/test/accounts/user_member_relationship_test.exs b/test/accounts/user_member_relationship_test.exs index daafa1b..b64f5ec 100644 --- a/test/accounts/user_member_relationship_test.exs +++ b/test/accounts/user_member_relationship_test.exs @@ -5,11 +5,6 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "User-Member Relationship - Basic Tests" do @valid_user_attrs %{ email: "test@example.com" @@ -21,26 +16,22 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do email: "john@example.com" } - test "user can exist without member", %{actor: actor} do - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + test "user can exist without member" do + {:ok, user} = Accounts.create_user(@valid_user_attrs) assert user.member_id == nil # Load the relationship to test it - {:ok, user_with_member} = - Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member]) - + {:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member]) assert user_with_member.member == nil end - test "member can exist without user", %{actor: actor} do - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "member can exist without user" do + {:ok, member} = Membership.create_member(@valid_member_attrs) assert member.id != nil assert member.first_name == "John" # Load the relationship to test it - {:ok, member_with_user} = - Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user]) - + {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert member_with_user.user == nil end end @@ -56,58 +47,47 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do email: "alice@example.com" } - test "user can be linked to member during user creation", %{actor: actor} do - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "user can be linked to member during user creation" do + {:ok, member} = Membership.create_member(@valid_member_attrs) user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id}) - {:ok, user} = Accounts.create_user(user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(user_attrs) # Load the relationship to test it - {:ok, user_with_member} = - Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member]) - + {:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member]) assert user_with_member.member.id == member.id end - test "member can be linked to user during member creation using manage_relationship", %{ - actor: actor - } do - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + test "member can be linked to user during member creation using manage_relationship" do + {:ok, user} = Accounts.create_user(@valid_user_attrs) member_attrs = Map.put(@valid_member_attrs, :user, %{id: user.id}) - {:ok, member} = Membership.create_member(member_attrs, actor: actor) + {:ok, member} = Membership.create_member(member_attrs) # Load the relationship to test it - {:ok, member_with_user} = - Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user]) - + {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert member_with_user.user.id == user.id end - test "user can be linked to member during update", %{actor: actor} do - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "user can be linked to member during update" do + {:ok, user} = Accounts.create_user(@valid_user_attrs) + {:ok, member} = Membership.create_member(@valid_member_attrs) - {:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}, actor: actor) + {:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}}) # Load the relationship to test it - {:ok, user_with_member} = - Ash.get(Mv.Accounts.User, updated_user.id, actor: actor, load: [:member]) - + {:ok, user_with_member} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member]) assert user_with_member.member.id == member.id end - test "member can be linked to user during update using manage_relationship", %{actor: actor} do - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "member can be linked to user during update using manage_relationship" do + {:ok, user} = Accounts.create_user(@valid_user_attrs) + {:ok, member} = Membership.create_member(@valid_member_attrs) - {:ok, _updated_member} = - Membership.update_member(member, %{user: %{id: user.id}}, actor: actor) + {:ok, _updated_member} = Membership.update_member(member, %{user: %{id: user.id}}) # Load the relationship to test it - {:ok, member_with_user} = - Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user]) - + {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert member_with_user.user.id == user.id end end @@ -123,39 +103,25 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do email: "bob@example.com" } - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - - test "ash resolves inverse relationship automatically", %{actor: actor} do - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "ash resolves inverse relationship automatically" do + {:ok, member} = Membership.create_member(@valid_member_attrs) user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id}) - {:ok, user} = Accounts.create_user(user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(user_attrs) # Load relationships - {:ok, user_with_member} = - Ash.get(Mv.Accounts.User, user.id, actor: actor, load: [:member]) - - {:ok, member_with_user} = - Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user]) + {:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member]) + {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert user_with_member.member.id == member.id assert member_with_user.user.id == user.id end - test "member can find associated user", %{actor: actor} do - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) - - {:ok, user} = - Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}}, - actor: actor - ) - - {:ok, member_with_user} = - Ash.get(Mv.Membership.Member, member.id, actor: actor, load: [:user]) + test "member can find associated user" do + {:ok, member} = Membership.create_member(@valid_member_attrs) + {:ok, user} = Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}}) + {:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert member_with_user.user.id == user.id end end @@ -171,77 +137,61 @@ defmodule Mv.Accounts.UserMemberRelationshipTest do email: "charlie@example.com" } - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - - test "prevents overwriting a member of already linked user on update", %{actor: actor} do - {:ok, existing_member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "prevents overwriting a member of already linked user on update" do + {:ok, existing_member} = Membership.create_member(@valid_member_attrs) user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id}) - {:ok, user} = Accounts.create_user(user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(user_attrs) {:ok, member2} = - Membership.create_member( - %{ - first_name: "Dave", - last_name: "Wilson", - email: "dave@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Dave", + last_name: "Wilson", + email: "dave@example.com" + }) assert {:error, %Ash.Error.Invalid{}} = - Accounts.update_user(user, %{member: %{id: member2.id}}, actor: actor) + Accounts.update_user(user, %{member: %{id: member2.id}}) end - test "prevents linking user to already linked member on update", %{actor: actor} do - {:ok, existing_user} = Accounts.create_user(@valid_user_attrs, actor: actor) - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "prevents linking user to already linked member on update" do + {:ok, existing_user} = Accounts.create_user(@valid_user_attrs) + {:ok, member} = Membership.create_member(@valid_member_attrs) - {:ok, _updated_user} = - Accounts.update_user(existing_user, %{member: %{id: member.id}}, actor: actor) + {:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}}) - {:ok, user2} = Accounts.create_user(%{email: "test5@example.com"}, actor: actor) + {:ok, user2} = Accounts.create_user(%{email: "test5@example.com"}) assert {:error, %Ash.Error.Invalid{}} = - Accounts.update_user(user2, %{member: %{id: member.id}}, actor: actor) + Accounts.update_user(user2, %{member: %{id: member.id}}) end - test "prevents linking member to already linked user on creation", %{actor: actor} do - {:ok, existing_member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "prevents linking member to already linked user on creation" do + {:ok, existing_member} = Membership.create_member(@valid_member_attrs) user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id}) - {:ok, user} = Accounts.create_user(user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(user_attrs) assert {:error, %Ash.Error.Invalid{}} = - Membership.create_member( - %{ - first_name: "Dave", - last_name: "Wilson", - email: "dave@example.com", - user: %{id: user.id} - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Dave", + last_name: "Wilson", + email: "dave@example.com", + user: %{id: user.id} + }) end - test "prevents linking user to already linked member on creation", %{actor: actor} do - {:ok, existing_user} = Accounts.create_user(@valid_user_attrs, actor: actor) - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + test "prevents linking user to already linked member on creation" do + {:ok, existing_user} = Accounts.create_user(@valid_user_attrs) + {:ok, member} = Membership.create_member(@valid_member_attrs) - {:ok, _updated_user} = - Accounts.update_user(existing_user, %{member: %{id: member.id}}, actor: actor) + {:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}}) assert {:error, %Ash.Error.Invalid{}} = - Accounts.create_user( - %{ - email: "test5@example.com", - member: %{id: member.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "test5@example.com", + member: %{id: member.id} + }) end end end diff --git a/test/membership/custom_field_deletion_test.exs b/test/membership/custom_field_deletion_test.exs index ffc7294..50623b6 100644 --- a/test/membership/custom_field_deletion_test.exs +++ b/test/membership/custom_field_deletion_test.exs @@ -13,28 +13,23 @@ defmodule Mv.Membership.CustomFieldDeletionTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "assigned_members_count calculation" do - test "returns 0 for custom field without any values", %{actor: actor} do + test "returns 0 for custom field without any values" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() - custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor) + custom_field_with_count = Ash.load!(custom_field, :assigned_members_count) assert custom_field_with_count.assigned_members_count == 0 end - test "returns correct count for custom field with one member", %{actor: actor} do - {:ok, member} = create_member(actor) - {:ok, custom_field} = create_custom_field("test_field", :string, actor) + test "returns correct count for custom field with one member" do + {:ok, member} = create_member() + {:ok, custom_field} = create_custom_field("test_field", :string) {:ok, _custom_field_value} = CustomFieldValue @@ -43,17 +38,17 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => "test"} }) - |> Ash.create(actor: actor) + |> Ash.create() - custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor) + custom_field_with_count = Ash.load!(custom_field, :assigned_members_count) assert custom_field_with_count.assigned_members_count == 1 end - test "returns correct count for custom field with multiple members", %{actor: actor} do - {:ok, member1} = create_member(actor) - {:ok, member2} = create_member(actor) - {:ok, member3} = create_member(actor) - {:ok, custom_field} = create_custom_field("test_field", :string, actor) + test "returns correct count for custom field with multiple members" do + {:ok, member1} = create_member() + {:ok, member2} = create_member() + {:ok, member3} = create_member() + {:ok, custom_field} = create_custom_field("test_field", :string) # Create custom field value for each member for member <- [member1, member2, member3] do @@ -64,16 +59,16 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => "test"} }) - |> Ash.create(actor: actor) + |> Ash.create() end - custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor) + custom_field_with_count = Ash.load!(custom_field, :assigned_members_count) assert custom_field_with_count.assigned_members_count == 3 end - test "counts distinct members (not multiple values per member)", %{actor: actor} do - {:ok, member} = create_member(actor) - {:ok, custom_field} = create_custom_field("test_field", :string, actor) + test "counts distinct members (not multiple values per member)" do + {:ok, member} = create_member() + {:ok, custom_field} = create_custom_field("test_field", :string) # Create custom field value for member {:ok, _} = @@ -83,9 +78,9 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => "test"} }) - |> Ash.create(actor: actor) + |> Ash.create() - custom_field_with_count = Ash.load!(custom_field, :assigned_members_count, actor: actor) + custom_field_with_count = Ash.load!(custom_field, :assigned_members_count) # Should still be 1, not 2, even if we tried to create multiple (which would fail due to uniqueness) assert custom_field_with_count.assigned_members_count == 1 @@ -93,9 +88,9 @@ defmodule Mv.Membership.CustomFieldDeletionTest do end describe "prepare_deletion action" do - test "loads assigned_members_count for deletion preparation", %{actor: actor} do - {:ok, member} = create_member(actor) - {:ok, custom_field} = create_custom_field("test_field", :string, actor) + test "loads assigned_members_count for deletion preparation" do + {:ok, member} = create_member() + {:ok, custom_field} = create_custom_field("test_field", :string) {:ok, _} = CustomFieldValue @@ -104,43 +99,43 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => "test"} }) - |> Ash.create(actor: actor) + |> Ash.create() # Use prepare_deletion action [prepared_custom_field] = CustomField |> Ash.Query.for_read(:prepare_deletion, %{id: custom_field.id}) - |> Ash.read!(actor: actor) + |> Ash.read!() assert prepared_custom_field.assigned_members_count == 1 assert prepared_custom_field.id == custom_field.id end - test "returns empty list for non-existent custom field", %{actor: actor} do + test "returns empty list for non-existent custom field" do non_existent_id = Ash.UUID.generate() result = CustomField |> Ash.Query.for_read(:prepare_deletion, %{id: non_existent_id}) - |> Ash.read!(actor: actor) + |> Ash.read!() assert result == [] end end describe "destroy_with_values action" do - test "deletes custom field without any values", %{actor: actor} do - {:ok, custom_field} = create_custom_field("test_field", :string, actor) + test "deletes custom field without any values" do + {:ok, custom_field} = create_custom_field("test_field", :string) - assert :ok = Ash.destroy(custom_field, actor: actor) + assert :ok = Ash.destroy(custom_field) # Verify custom field is deleted - assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: actor) + assert {:error, _} = Ash.get(CustomField, custom_field.id) end - test "deletes custom field and cascades to all its values", %{actor: actor} do - {:ok, member} = create_member(actor) - {:ok, custom_field} = create_custom_field("test_field", :string, actor) + test "deletes custom field and cascades to all its values" do + {:ok, member} = create_member() + {:ok, custom_field} = create_custom_field("test_field", :string) {:ok, custom_field_value} = CustomFieldValue @@ -149,25 +144,25 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => "test"} }) - |> Ash.create(actor: actor) + |> Ash.create() # Delete custom field - assert :ok = Ash.destroy(custom_field, actor: actor) + assert :ok = Ash.destroy(custom_field) # Verify custom field is deleted - assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: actor) + assert {:error, _} = Ash.get(CustomField, custom_field.id) # Verify custom field value is also deleted (CASCADE) - assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id, actor: actor) + assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id) # Verify member still exists - assert {:ok, _} = Ash.get(Member, member.id, actor: actor) + assert {:ok, _} = Ash.get(Member, member.id) end - test "deletes only values of the specific custom field", %{actor: actor} do - {:ok, member} = create_member(actor) - {:ok, custom_field1} = create_custom_field("field1", :string, actor) - {:ok, custom_field2} = create_custom_field("field2", :string, actor) + test "deletes only values of the specific custom field" do + {:ok, member} = create_member() + {:ok, custom_field1} = create_custom_field("field1", :string) + {:ok, custom_field2} = create_custom_field("field2", :string) # Create value for custom_field1 {:ok, value1} = @@ -177,7 +172,7 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field1.id, value: %{"_union_type" => "string", "_union_value" => "value1"} }) - |> Ash.create(actor: actor) + |> Ash.create() # Create value for custom_field2 {:ok, value2} = @@ -187,25 +182,25 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field2.id, value: %{"_union_type" => "string", "_union_value" => "value2"} }) - |> Ash.create(actor: actor) + |> Ash.create() # Delete custom_field1 - assert :ok = Ash.destroy(custom_field1, actor: actor) + assert :ok = Ash.destroy(custom_field1) # Verify custom_field1 and value1 are deleted - assert {:error, _} = Ash.get(CustomField, custom_field1.id, actor: actor) - assert {:error, _} = Ash.get(CustomFieldValue, value1.id, actor: actor) + assert {:error, _} = Ash.get(CustomField, custom_field1.id) + assert {:error, _} = Ash.get(CustomFieldValue, value1.id) # Verify custom_field2 and value2 still exist - assert {:ok, _} = Ash.get(CustomField, custom_field2.id, actor: actor) - assert {:ok, _} = Ash.get(CustomFieldValue, value2.id, actor: actor) + assert {:ok, _} = Ash.get(CustomField, custom_field2.id) + assert {:ok, _} = Ash.get(CustomFieldValue, value2.id) end - test "deletes custom field with values from multiple members", %{actor: actor} do - {:ok, member1} = create_member(actor) - {:ok, member2} = create_member(actor) - {:ok, member3} = create_member(actor) - {:ok, custom_field} = create_custom_field("test_field", :string, actor) + test "deletes custom field with values from multiple members" do + {:ok, member1} = create_member() + {:ok, member2} = create_member() + {:ok, member3} = create_member() + {:ok, custom_field} = create_custom_field("test_field", :string) # Create value for each member values = @@ -217,43 +212,43 @@ defmodule Mv.Membership.CustomFieldDeletionTest do custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => "test"} }) - |> Ash.create(actor: actor) + |> Ash.create() value end # Delete custom field - assert :ok = Ash.destroy(custom_field, actor: actor) + assert :ok = Ash.destroy(custom_field) # Verify all values are deleted for value <- values do - assert {:error, _} = Ash.get(CustomFieldValue, value.id, actor: actor) + assert {:error, _} = Ash.get(CustomFieldValue, value.id) end # Verify all members still exist for member <- [member1, member2, member3] do - assert {:ok, _} = Ash.get(Member, member.id, actor: actor) + assert {:ok, _} = Ash.get(Member, member.id) end end end # Helper functions - defp create_member(actor) do + defp create_member do Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Test", last_name: "User#{System.unique_integer([:positive])}", email: "test#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: actor) + |> Ash.create() end - defp create_custom_field(name, value_type, actor) do + defp create_custom_field(name, value_type) do CustomField |> Ash.Changeset.for_create(:create, %{ name: "#{name}_#{System.unique_integer([:positive])}", value_type: value_type }) - |> Ash.create(actor: actor) + |> Ash.create() end end diff --git a/test/membership/custom_field_show_in_overview_test.exs b/test/membership/custom_field_show_in_overview_test.exs index a9e0345..adac600 100644 --- a/test/membership/custom_field_show_in_overview_test.exs +++ b/test/membership/custom_field_show_in_overview_test.exs @@ -12,13 +12,8 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do alias Mv.Membership.CustomField - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "show_in_overview attribute" do - test "creates custom field with show_in_overview: true", %{actor: actor} do + test "creates custom field with show_in_overview: true" do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ @@ -26,24 +21,24 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.show_in_overview == true end - test "creates custom field with show_in_overview: true (default)", %{actor: actor} do + test "creates custom field with show_in_overview: true (default)" do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field_hide", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.show_in_overview == true end - test "updates show_in_overview to true", %{actor: actor} do + test "updates show_in_overview to true" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ @@ -51,17 +46,17 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do value_type: :string, show_in_overview: false }) - |> Ash.create(actor: actor) + |> Ash.create() assert {:ok, updated_field} = custom_field |> Ash.Changeset.for_update(:update, %{show_in_overview: true}) - |> Ash.update(actor: actor) + |> Ash.update() assert updated_field.show_in_overview == true end - test "updates show_in_overview to false", %{actor: actor} do + test "updates show_in_overview to false" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ @@ -69,12 +64,12 @@ defmodule Mv.Membership.CustomFieldShowInOverviewTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: actor) + |> Ash.create() assert {:ok, updated_field} = custom_field |> Ash.Changeset.for_update(:update, %{show_in_overview: false}) - |> Ash.update(actor: actor) + |> Ash.update() assert updated_field.show_in_overview == false end diff --git a/test/membership/custom_field_slug_test.exs b/test/membership/custom_field_slug_test.exs index 76ab5c7..ae6c42e 100644 --- a/test/membership/custom_field_slug_test.exs +++ b/test/membership/custom_field_slug_test.exs @@ -13,99 +13,94 @@ defmodule Mv.Membership.CustomFieldSlugTest do alias Mv.Membership.CustomField - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "automatic slug generation on create" do - test "generates slug from name with simple ASCII text", %{actor: actor} do + test "generates slug from name with simple ASCII text" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Mobile Phone", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "mobile-phone" end - test "generates slug from name with German umlauts", %{actor: actor} do + test "generates slug from name with German umlauts" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Café Müller", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "cafe-muller" end - test "generates slug with lowercase conversion", %{actor: actor} do + test "generates slug with lowercase conversion" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "TEST NAME", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "test-name" end - test "generates slug by removing special characters", %{actor: actor} do + test "generates slug by removing special characters" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "E-Mail & Address!", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "e-mail-address" end - test "generates slug by replacing multiple spaces with single hyphen", %{actor: actor} do + test "generates slug by replacing multiple spaces with single hyphen" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Multiple Spaces", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "multiple-spaces" end - test "trims leading and trailing hyphens", %{actor: actor} do + test "trims leading and trailing hyphens" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "-Test-", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "test" end - test "handles unicode characters properly (ß becomes ss)", %{actor: actor} do + test "handles unicode characters properly (ß becomes ss)" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Straße", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "strasse" end end describe "slug uniqueness" do - test "prevents creating custom field with duplicate slug", %{actor: actor} do + test "prevents creating custom field with duplicate slug" do # Create first custom field {:ok, _custom_field} = CustomField @@ -113,7 +108,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do name: "Test", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Attempt to create second custom field with same slug (different case in name) assert {:error, %Ash.Error.Invalid{} = error} = @@ -122,19 +117,19 @@ defmodule Mv.Membership.CustomFieldSlugTest do name: "test", value_type: :integer }) - |> Ash.create(actor: actor) + |> Ash.create() assert Exception.message(error) =~ "has already been taken" end - test "allows custom fields with different slugs", %{actor: actor} do + test "allows custom fields with different slugs" do {:ok, custom_field1} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test One", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() {:ok, custom_field2} = CustomField @@ -142,21 +137,21 @@ defmodule Mv.Membership.CustomFieldSlugTest do name: "Test Two", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field1.slug == "test-one" assert custom_field2.slug == "test-two" assert custom_field1.slug != custom_field2.slug end - test "prevents duplicate slugs when names differ only in special characters", %{actor: actor} do + test "prevents duplicate slugs when names differ only in special characters" do {:ok, custom_field1} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test!!!", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field1.slug == "test" @@ -167,7 +162,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do name: "Test???", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Should fail with uniqueness constraint error assert Exception.message(error) =~ "has already been taken" @@ -175,7 +170,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do end describe "slug immutability" do - test "slug cannot be manually set on create", %{actor: actor} do + test "slug cannot be manually set on create" do # Attempting to set slug manually should fail because slug is not writable result = CustomField @@ -184,14 +179,14 @@ defmodule Mv.Membership.CustomFieldSlugTest do value_type: :string, slug: "custom-slug" }) - |> Ash.create(actor: actor) + |> Ash.create() # Should fail because slug is not an accepted input assert {:error, %Ash.Error.Invalid{}} = result assert Exception.message(elem(result, 1)) =~ "No such input" end - test "slug does not change when name is updated", %{actor: actor} do + test "slug does not change when name is updated" do # Create custom field {:ok, custom_field} = CustomField @@ -199,7 +194,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do name: "Original Name", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() original_slug = custom_field.slug assert original_slug == "original-name" @@ -210,7 +205,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do |> Ash.Changeset.for_update(:update, %{ name: "New Different Name" }) - |> Ash.update(actor: actor) + |> Ash.update() # Slug should remain unchanged assert updated_custom_field.slug == original_slug @@ -218,14 +213,14 @@ defmodule Mv.Membership.CustomFieldSlugTest do assert updated_custom_field.name == "New Different Name" end - test "slug cannot be manually updated", %{actor: actor} do + test "slug cannot be manually updated" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() original_slug = custom_field.slug assert original_slug == "test" @@ -236,20 +231,20 @@ defmodule Mv.Membership.CustomFieldSlugTest do |> Ash.Changeset.for_update(:update, %{ slug: "new-slug" }) - |> Ash.update(actor: actor) + |> Ash.update() # Should fail because slug is not an accepted input assert {:error, %Ash.Error.Invalid{}} = result assert Exception.message(elem(result, 1)) =~ "No such input" # Reload to verify slug hasn't changed - reloaded = Ash.get!(CustomField, custom_field.id, actor: actor) + reloaded = Ash.get!(CustomField, custom_field.id) assert reloaded.slug == "test" end end describe "slug edge cases" do - test "handles very long names by truncating slug", %{actor: actor} do + test "handles very long names by truncating slug" do # Create a name at the maximum length (100 chars) long_name = String.duplicate("abcdefghij", 10) # 100 characters exactly @@ -260,7 +255,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do name: long_name, value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Slug should be truncated to maximum 100 characters assert String.length(custom_field.slug) <= 100 @@ -268,7 +263,7 @@ defmodule Mv.Membership.CustomFieldSlugTest do assert custom_field.slug == long_name end - test "rejects name with only special characters", %{actor: actor} do + test "rejects name with only special characters" do # When name contains only special characters, slug would be empty # This should fail validation assert {:error, %Ash.Error.Invalid{} = error} = @@ -277,59 +272,59 @@ defmodule Mv.Membership.CustomFieldSlugTest do name: "!!!", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Should fail because slug would be empty error_message = Exception.message(error) assert error_message =~ "Slug cannot be empty" or error_message =~ "is required" end - test "handles mixed special characters and text", %{actor: actor} do + test "handles mixed special characters and text" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test@#$%Name", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # slugify keeps the hyphen between words assert custom_field.slug == "test-name" end - test "handles numbers in name", %{actor: actor} do + test "handles numbers in name" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Field 123 Test", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.slug == "field-123-test" end - test "handles consecutive hyphens in name", %{actor: actor} do + test "handles consecutive hyphens in name" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test---Name", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Should reduce multiple hyphens to single hyphen assert custom_field.slug == "test-name" end - test "handles name with dots and underscores", %{actor: actor} do + test "handles name with dots and underscores" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test.field_name", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Dots and underscores should be handled (either kept or converted) assert custom_field.slug =~ ~r/^[a-z0-9-]+$/ @@ -337,45 +332,45 @@ defmodule Mv.Membership.CustomFieldSlugTest do end describe "slug in queries and responses" do - test "slug is included in struct after create", %{actor: actor} do + test "slug is included in struct after create" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Slug should be present in the struct assert Map.has_key?(custom_field, :slug) assert custom_field.slug != nil end - test "can load custom field and slug is present", %{actor: actor} do + test "can load custom field and slug is present" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # Load it back - loaded_custom_field = Ash.get!(CustomField, custom_field.id, actor: actor) + loaded_custom_field = Ash.get!(CustomField, custom_field.id) assert loaded_custom_field.slug == "test" end - test "slug is returned in list queries", %{actor: actor} do + test "slug is returned in list queries" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() - custom_fields = Ash.read!(CustomField, actor: actor) + custom_fields = Ash.read!(CustomField) found = Enum.find(custom_fields, &(&1.id == custom_field.id)) assert found.slug == "test" @@ -384,18 +379,18 @@ defmodule Mv.Membership.CustomFieldSlugTest do describe "slug-based lookup (future feature)" do @tag :skip - test "can find custom field by slug", %{actor: actor} do + test "can find custom field by slug" do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Test Field", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() # This test is for future implementation # We might add a custom action like :by_slug - found = Ash.get!(CustomField, custom_field.slug, load: [:slug], actor: actor) + found = Ash.get!(CustomField, custom_field.slug, load: [:slug]) assert found.id == custom_field.id end end diff --git a/test/membership/custom_field_validation_test.exs b/test/membership/custom_field_validation_test.exs index d0711ad..a5c1f2d 100644 --- a/test/membership/custom_field_validation_test.exs +++ b/test/membership/custom_field_validation_test.exs @@ -13,13 +13,8 @@ defmodule Mv.Membership.CustomFieldValidationTest do alias Mv.Membership.CustomField - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "name validation" do - test "accepts name with exactly 100 characters", %{actor: actor} do + test "accepts name with exactly 100 characters" do name = String.duplicate("a", 100) assert {:ok, custom_field} = @@ -28,13 +23,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do name: name, value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.name == name assert String.length(custom_field.name) == 100 end - test "rejects name with 101 characters", %{actor: actor} do + test "rejects name with 101 characters" do name = String.duplicate("a", 101) assert {:error, changeset} = @@ -43,50 +38,50 @@ defmodule Mv.Membership.CustomFieldValidationTest do name: name, value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert [%{field: :name, message: message}] = changeset.errors assert message =~ "max" or message =~ "length" or message =~ "100" end - test "trims whitespace from name", %{actor: actor} do + test "trims whitespace from name" do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: " test_field ", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.name == "test_field" end - test "rejects empty name", %{actor: actor} do + test "rejects empty name" do assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert Enum.any?(changeset.errors, fn error -> error.field == :name end) end - test "rejects nil name", %{actor: actor} do + test "rejects nil name" do assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert Enum.any?(changeset.errors, fn error -> error.field == :name end) end end describe "description validation" do - test "accepts description with exactly 500 characters", %{actor: actor} do + test "accepts description with exactly 500 characters" do description = String.duplicate("a", 500) assert {:ok, custom_field} = @@ -96,13 +91,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do value_type: :string, description: description }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.description == description assert String.length(custom_field.description) == 500 end - test "rejects description with 501 characters", %{actor: actor} do + test "rejects description with 501 characters" do description = String.duplicate("a", 501) assert {:error, changeset} = @@ -112,13 +107,13 @@ defmodule Mv.Membership.CustomFieldValidationTest do value_type: :string, description: description }) - |> Ash.create(actor: actor) + |> Ash.create() assert [%{field: :description, message: message}] = changeset.errors assert message =~ "max" or message =~ "length" or message =~ "500" end - test "trims whitespace from description", %{actor: actor} do + test "trims whitespace from description" do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ @@ -126,24 +121,24 @@ defmodule Mv.Membership.CustomFieldValidationTest do value_type: :string, description: " A nice description " }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.description == "A nice description" end - test "accepts nil description (optional field)", %{actor: actor} do + test "accepts nil description (optional field)" do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "test_field", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.description == nil end - test "accepts empty description after trimming", %{actor: actor} do + test "accepts empty description after trimming" do assert {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ @@ -151,7 +146,7 @@ defmodule Mv.Membership.CustomFieldValidationTest do value_type: :string, description: " " }) - |> Ash.create(actor: actor) + |> Ash.create() # After trimming whitespace, becomes nil (empty strings are converted to nil) assert custom_field.description == nil @@ -159,14 +154,14 @@ defmodule Mv.Membership.CustomFieldValidationTest do end describe "name uniqueness" do - test "rejects duplicate names", %{actor: actor} do + test "rejects duplicate names" do assert {:ok, _} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "unique_field", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() assert {:error, changeset} = CustomField @@ -174,14 +169,14 @@ defmodule Mv.Membership.CustomFieldValidationTest do name: "unique_field", value_type: :integer }) - |> Ash.create(actor: actor) + |> Ash.create() assert Enum.any?(changeset.errors, fn error -> error.field == :name end) end end describe "value_type validation" do - test "accepts all valid value types", %{actor: actor} do + test "accepts all valid value types" do for value_type <- [:string, :integer, :boolean, :date, :email] do assert {:ok, custom_field} = CustomField @@ -189,20 +184,20 @@ defmodule Mv.Membership.CustomFieldValidationTest do name: "field_#{value_type}", value_type: value_type }) - |> Ash.create(actor: actor) + |> Ash.create() assert custom_field.value_type == value_type end end - test "rejects invalid value type", %{actor: actor} do + test "rejects invalid value type" do assert {:error, changeset} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "invalid_field", value_type: :invalid_type }) - |> Ash.create(actor: actor) + |> Ash.create() assert [%{field: :value_type}] = changeset.errors end diff --git a/test/membership/custom_field_value_validation_test.exs b/test/membership/custom_field_value_validation_test.exs index d39e85c..dd3438a 100644 --- a/test/membership/custom_field_value_validation_test.exs +++ b/test/membership/custom_field_value_validation_test.exs @@ -13,8 +13,6 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create a test member {:ok, member} = Member @@ -23,7 +21,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do last_name: "User", email: "test.validation@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom fields for different types {:ok, string_field} = @@ -32,7 +30,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do name: "string_field", value_type: :string }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, integer_field} = CustomField @@ -40,7 +38,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do name: "integer_field", value_type: :integer }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, email_field} = CustomField @@ -48,10 +46,9 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do name: "email_field", value_type: :email }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{ - actor: system_actor, member: member, string_field: string_field, integer_field: integer_field, @@ -61,7 +58,6 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do describe "string value length validation" do test "accepts string value with exactly 10,000 characters", %{ - actor: system_actor, member: member, string_field: string_field } do @@ -77,14 +73,13 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do "_union_value" => value_string } }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == value_string assert String.length(custom_field_value.value.value) == 10_000 end test "rejects string value with 10,001 characters", %{ - actor: system_actor, member: member, string_field: string_field } do @@ -97,18 +92,14 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => value_string} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert Enum.any?(changeset.errors, fn error -> error.field == :value and (error.message =~ "max" or error.message =~ "length") end) end - test "trims whitespace from string value", %{ - actor: system_actor, - member: member, - string_field: string_field - } do + test "trims whitespace from string value", %{member: member, string_field: string_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -116,16 +107,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => " test value "} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == "test value" end - test "accepts empty string value", %{ - actor: system_actor, - member: member, - string_field: string_field - } do + test "accepts empty string value", %{member: member, string_field: string_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -133,17 +120,13 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => ""} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Empty strings after trimming become nil assert custom_field_value.value.value == nil end - test "accepts string with special characters", %{ - actor: system_actor, - member: member, - string_field: string_field - } do + test "accepts string with special characters", %{member: member, string_field: string_field} do special_string = "Hello 世界! 🎉 @#$%^&*()" assert {:ok, custom_field_value} = @@ -153,18 +136,14 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => special_string} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == special_string end end describe "integer value validation" do - test "accepts valid integer value", %{ - actor: system_actor, - member: member, - integer_field: integer_field - } do + test "accepts valid integer value", %{member: member, integer_field: integer_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -172,16 +151,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: integer_field.id, value: %{"_union_type" => "integer", "_union_value" => 42} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == 42 end - test "accepts negative integer", %{ - actor: system_actor, - member: member, - integer_field: integer_field - } do + test "accepts negative integer", %{member: member, integer_field: integer_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -189,12 +164,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: integer_field.id, value: %{"_union_type" => "integer", "_union_value" => -100} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == -100 end - test "accepts zero", %{actor: system_actor, member: member, integer_field: integer_field} do + test "accepts zero", %{member: member, integer_field: integer_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -202,18 +177,14 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: integer_field.id, value: %{"_union_type" => "integer", "_union_value" => 0} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == 0 end end describe "email value validation" do - test "accepts nil value (optional field)", %{ - actor: system_actor, - member: member, - email_field: email_field - } do + test "accepts nil value (optional field)", %{member: member, email_field: email_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -221,13 +192,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => nil} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == nil end test "accepts empty string (becomes nil after trim)", %{ - actor: system_actor, member: member, email_field: email_field } do @@ -238,13 +208,13 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => ""} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Empty string after trim should become nil assert custom_field_value.value.value == nil end - test "accepts valid email", %{actor: system_actor, member: member, email_field: email_field} do + test "accepts valid email", %{member: member, email_field: email_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -252,16 +222,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => "test@example.com"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == "test@example.com" end - test "rejects invalid email format", %{ - actor: system_actor, - member: member, - email_field: email_field - } do + test "rejects invalid email format", %{member: member, email_field: email_field} do assert {:error, changeset} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -269,16 +235,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => "not-an-email"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert Enum.any?(changeset.errors, fn error -> error.field == :value end) end - test "rejects email longer than 254 characters", %{ - actor: system_actor, - member: member, - email_field: email_field - } do + test "rejects email longer than 254 characters", %{member: member, email_field: email_field} do # Create an email with >254 chars (243 + 12 = 255) long_email = String.duplicate("a", 243) <> "@example.com" @@ -289,16 +251,12 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => long_email} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert Enum.any?(changeset.errors, fn error -> error.field == :value end) end - test "trims whitespace from email", %{ - actor: system_actor, - member: member, - email_field: email_field - } do + test "trims whitespace from email", %{member: member, email_field: email_field} do assert {:ok, custom_field_value} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ @@ -306,7 +264,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => " test@example.com "} }) - |> Ash.create(actor: system_actor) + |> Ash.create() assert custom_field_value.value.value == "test@example.com" end @@ -314,7 +272,6 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do describe "uniqueness constraint" do test "rejects duplicate custom_field_id per member", %{ - actor: system_actor, member: member, string_field: string_field } do @@ -326,7 +283,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "first value"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Try to create second custom field value with same custom_field_id for same member assert {:error, changeset} = @@ -336,7 +293,7 @@ defmodule Mv.Membership.CustomFieldValueValidationTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "second value"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Should have uniqueness error assert Enum.any?(changeset.errors, fn error -> diff --git a/test/membership/fuzzy_search_test.exs b/test/membership/fuzzy_search_test.exs index 257d097..19286df 100644 --- a/test/membership/fuzzy_search_test.exs +++ b/test/membership/fuzzy_search_test.exs @@ -1,93 +1,70 @@ defmodule Mv.Membership.FuzzySearchTest do use Mv.DataCase, async: false - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - test "fuzzy_search/2 function exists" do assert function_exported?(Mv.Membership.Member, :fuzzy_search, 2) end - test "fuzzy_search returns only John Doe by fuzzy query 'john'", %{actor: actor} do + test "fuzzy_search returns only John Doe by fuzzy query 'john'" do {:ok, john} = - Mv.Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "john.doe@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john.doe@example.com" + }) {:ok, _jane} = - Mv.Membership.create_member( - %{ - first_name: "Adriana", - last_name: "Smith", - email: "adriana.smith@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Adriana", + last_name: "Smith", + email: "adriana.smith@example.com" + }) {:ok, alice} = - Mv.Membership.create_member( - %{ - first_name: "Alice", - last_name: "Johnson", - email: "alice.johnson@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Alice", + last_name: "Johnson", + email: "alice.johnson@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{ query: "john" }) - |> Ash.read!(actor: actor) + |> Ash.read!() assert Enum.map(result, & &1.id) == [john.id, alice.id] end - test "fuzzy_search finds 'Thomas' when searching misspelled 'tomas'", %{actor: actor} do + test "fuzzy_search finds 'Thomas' when searching misspelled 'tomas'" do {:ok, thomas} = - Mv.Membership.create_member( - %{ - first_name: "Thomas", - last_name: "Doe", - email: "john.doe@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Thomas", + last_name: "Doe", + email: "john.doe@example.com" + }) {:ok, jane} = - Mv.Membership.create_member( - %{ - first_name: "Jane", - last_name: "Smith", - email: "jane.smith@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Jane", + last_name: "Smith", + email: "jane.smith@example.com" + }) {:ok, _alice} = - Mv.Membership.create_member( - %{ - first_name: "Alice", - last_name: "Johnson", - email: "alice.johnson@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Alice", + last_name: "Johnson", + email: "alice.johnson@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{ query: "tomas" }) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert thomas.id in ids @@ -95,21 +72,17 @@ defmodule Mv.Membership.FuzzySearchTest do assert not Enum.empty?(ids) end - test "empty query returns all members", %{actor: actor} do + test "empty query returns all members" do {:ok, a} = - Mv.Membership.create_member(%{first_name: "A", last_name: "One", email: "a1@example.com"}, - actor: actor - ) + Mv.Membership.create_member(%{first_name: "A", last_name: "One", email: "a1@example.com"}) {:ok, b} = - Mv.Membership.create_member(%{first_name: "B", last_name: "Two", email: "b2@example.com"}, - actor: actor - ) + Mv.Membership.create_member(%{first_name: "B", last_name: "Two", email: "b2@example.com"}) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: ""}) - |> Ash.read!(actor: actor) + |> Ash.read!() assert Enum.sort(Enum.map(result, & &1.id)) |> Enum.uniq() @@ -117,435 +90,352 @@ defmodule Mv.Membership.FuzzySearchTest do |> Enum.all?(fn id -> id in [a.id, b.id] end) end - test "substring numeric search matches postal_code mid-string", %{actor: actor} do + test "substring numeric search matches postal_code mid-string" do {:ok, m1} = - Mv.Membership.create_member( - %{ - first_name: "Num", - last_name: "One", - email: "n1@example.com", - postal_code: "12345" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Num", + last_name: "One", + email: "n1@example.com", + postal_code: "12345" + }) {:ok, _m2} = - Mv.Membership.create_member( - %{ - first_name: "Num", - last_name: "Two", - email: "n2@example.com", - postal_code: "67890" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Num", + last_name: "Two", + email: "n2@example.com", + postal_code: "67890" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "345"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert m1.id in ids end - test "substring numeric search matches house_number mid-string", %{actor: actor} do + test "substring numeric search matches house_number mid-string" do {:ok, m1} = - Mv.Membership.create_member( - %{ - first_name: "Home", - last_name: "One", - email: "h1@example.com", - house_number: "A345B" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Home", + last_name: "One", + email: "h1@example.com", + house_number: "A345B" + }) {:ok, _m2} = - Mv.Membership.create_member( - %{ - first_name: "Home", - last_name: "Two", - email: "h2@example.com", - house_number: "77" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Home", + last_name: "Two", + email: "h2@example.com", + house_number: "77" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "345"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert m1.id in ids end - test "fuzzy matches street misspelling", %{actor: actor} do + test "fuzzy matches street misspelling" do {:ok, s1} = - Mv.Membership.create_member( - %{ - first_name: "Road", - last_name: "Test", - email: "s1@example.com", - street: "Main Street" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Road", + last_name: "Test", + email: "s1@example.com", + street: "Main Street" + }) {:ok, _s2} = - Mv.Membership.create_member( - %{ - first_name: "Road", - last_name: "Other", - email: "s2@example.com", - street: "Second Avenue" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Road", + last_name: "Other", + email: "s2@example.com", + street: "Second Avenue" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "mainn"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert s1.id in ids end - test "substring in city matches mid-string", %{actor: actor} do + test "substring in city matches mid-string" do {:ok, b} = - Mv.Membership.create_member( - %{ - first_name: "City", - last_name: "One", - email: "city1@example.com", - city: "Berlin" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "City", + last_name: "One", + email: "city1@example.com", + city: "Berlin" + }) {:ok, _m} = - Mv.Membership.create_member( - %{ - first_name: "City", - last_name: "Two", - email: "city2@example.com", - city: "München" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "City", + last_name: "Two", + email: "city2@example.com", + city: "München" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "erl"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert b.id in ids end - test "blank character handling: query with spaces matches full name", %{actor: actor} do + test "blank character handling: query with spaces matches full name" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "john.doe@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john.doe@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "Jane", - last_name: "Smith", - email: "jane.smith@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Jane", + last_name: "Smith", + email: "jane.smith@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "john doe"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "blank character handling: query with multiple spaces is handled", %{actor: actor} do + test "blank character handling: query with multiple spaces is handled" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Mary", - last_name: "Jane", - email: "mary.jane@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Mary", + last_name: "Jane", + email: "mary.jane@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "mary jane"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "special character handling: @ symbol in query matches email", %{actor: actor} do + test "special character handling: @ symbol in query matches email" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Test", - last_name: "User", - email: "test.user@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Test", + last_name: "User", + email: "test.user@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "Other", - last_name: "Person", - email: "other.person@different.org" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Other", + last_name: "Person", + email: "other.person@different.org" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "example"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "special character handling: dot in query matches email", %{actor: actor} do + test "special character handling: dot in query matches email" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Dot", - last_name: "Test", - email: "dot.test@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Dot", + last_name: "Test", + email: "dot.test@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "No", - last_name: "Dot", - email: "nodot@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "No", + last_name: "Dot", + email: "nodot@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "dot.test"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "special character handling: hyphen in query matches data", %{actor: actor} do + test "special character handling: hyphen in query matches data" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Mary-Jane", - last_name: "Watson", - email: "mary.jane@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Mary-Jane", + last_name: "Watson", + email: "mary.jane@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "Mary", - last_name: "Smith", - email: "mary.smith@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Mary", + last_name: "Smith", + email: "mary.smith@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "mary-jane"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "unicode character handling: umlaut ö in query matches data", %{actor: actor} do + test "unicode character handling: umlaut ö in query matches data" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Jörg", - last_name: "Schmidt", - email: "joerg.schmidt@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Jörg", + last_name: "Schmidt", + email: "joerg.schmidt@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "John", - last_name: "Smith", - email: "john.smith@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "John", + last_name: "Smith", + email: "john.smith@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "jörg"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "unicode character handling: umlaut ä in query matches data", %{actor: actor} do + test "unicode character handling: umlaut ä in query matches data" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Märta", - last_name: "Andersson", - email: "maerta.andersson@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Märta", + last_name: "Andersson", + email: "maerta.andersson@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "Marta", - last_name: "Johnson", - email: "marta.johnson@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Marta", + last_name: "Johnson", + email: "marta.johnson@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "märta"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "unicode character handling: umlaut ü in query matches data", %{actor: actor} do + test "unicode character handling: umlaut ü in query matches data" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Günther", - last_name: "Müller", - email: "guenther.mueller@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Günther", + last_name: "Müller", + email: "guenther.mueller@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "Gunter", - last_name: "Miller", - email: "gunter.miller@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Gunter", + last_name: "Miller", + email: "gunter.miller@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "müller"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "unicode character handling: query without umlaut matches data with umlaut", %{ - actor: actor - } do + test "unicode character handling: query without umlaut matches data with umlaut" do {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Müller", - last_name: "Schmidt", - email: "mueller.schmidt@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Müller", + last_name: "Schmidt", + email: "mueller.schmidt@example.com" + }) {:ok, _other} = - Mv.Membership.create_member( - %{ - first_name: "Miller", - last_name: "Smith", - email: "miller.smith@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Miller", + last_name: "Smith", + email: "miller.smith@example.com" + }) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: "muller"}) - |> Ash.read!(actor: actor) + |> Ash.read!() ids = Enum.map(result, & &1.id) assert member.id in ids end - test "very long search strings: handles long query without error", %{actor: actor} do + test "very long search strings: handles long query without error" do {:ok, _member} = - Mv.Membership.create_member( - %{ - first_name: "Test", - last_name: "User", - email: "test@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Test", + last_name: "User", + email: "test@example.com" + }) long_query = String.duplicate("a", 1000) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: long_query}) - |> Ash.read!(actor: actor) + |> Ash.read!() # Should not crash, may return empty or some results assert is_list(result) end - test "very long search strings: handles extremely long query", %{actor: actor} do + test "very long search strings: handles extremely long query" do {:ok, _member} = - Mv.Membership.create_member( - %{ - first_name: "Test", - last_name: "User", - email: "test@example.com" - }, - actor: actor - ) + Mv.Membership.create_member(%{ + first_name: "Test", + last_name: "User", + email: "test@example.com" + }) very_long_query = String.duplicate("test query ", 1000) result = Mv.Membership.Member |> Mv.Membership.Member.fuzzy_search(%{query: very_long_query}) - |> Ash.read!(actor: actor) + |> Ash.read!() # Should not crash, may return empty or some results assert is_list(result) diff --git a/test/membership/member_available_for_linking_test.exs b/test/membership/member_available_for_linking_test.exs index 5cf9c5b..2f3e018 100644 --- a/test/membership/member_available_for_linking_test.exs +++ b/test/membership/member_available_for_linking_test.exs @@ -13,87 +13,64 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do describe "available_for_linking/2" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create 5 unlinked members with distinct names {:ok, member1} = - Membership.create_member( - %{ - first_name: "Alice", - last_name: "Anderson", - email: "alice@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Alice", + last_name: "Anderson", + email: "alice@example.com" + }) {:ok, member2} = - Membership.create_member( - %{ - first_name: "Bob", - last_name: "Williams", - email: "bob@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Bob", + last_name: "Williams", + email: "bob@example.com" + }) {:ok, member3} = - Membership.create_member( - %{ - first_name: "Charlie", - last_name: "Davis", - email: "charlie@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Charlie", + last_name: "Davis", + email: "charlie@example.com" + }) {:ok, member4} = - Membership.create_member( - %{ - first_name: "Diana", - last_name: "Martinez", - email: "diana@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Diana", + last_name: "Martinez", + email: "diana@example.com" + }) {:ok, member5} = - Membership.create_member( - %{ - first_name: "Emma", - last_name: "Taylor", - email: "emma@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Emma", + last_name: "Taylor", + email: "emma@example.com" + }) unlinked_members = [member1, member2, member3, member4, member5] # Create 2 linked members (with users) - {:ok, user1} = Mv.Accounts.create_user(%{email: "user1@example.com"}, actor: system_actor) + {:ok, user1} = Mv.Accounts.create_user(%{email: "user1@example.com"}) {:ok, linked_member1} = - Membership.create_member( - %{ - first_name: "Linked", - last_name: "Member1", - email: "linked1@example.com", - user: %{id: user1.id} - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Linked", + last_name: "Member1", + email: "linked1@example.com", + user: %{id: user1.id} + }) - {:ok, user2} = Mv.Accounts.create_user(%{email: "user2@example.com"}, actor: system_actor) + {:ok, user2} = Mv.Accounts.create_user(%{email: "user2@example.com"}) {:ok, linked_member2} = - Membership.create_member( - %{ - first_name: "Linked", - last_name: "Member2", - email: "linked2@example.com", - user: %{id: user2.id} - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Linked", + last_name: "Member2", + email: "linked2@example.com", + user: %{id: user2.id} + }) %{ unlinked_members: unlinked_members, @@ -105,13 +82,11 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do unlinked_members: unlinked_members, linked_members: _linked_members } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Call the action without any arguments members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() # Should return only the 5 unlinked members, not the 2 linked ones assert length(members) == 5 @@ -123,32 +98,25 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do # Verify none of the returned members have a user_id Enum.each(members, fn member -> - member_with_user = - Ash.get!(Mv.Membership.Member, member.id, actor: system_actor, load: [:user]) - + member_with_user = Ash.get!(Mv.Membership.Member, member.id, load: [:user]) assert is_nil(member_with_user.user) end) end test "limits results to 10 members even when more exist" do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create 15 additional unlinked members (total 20 unlinked) for i <- 6..20 do - Membership.create_member( - %{ - first_name: "Extra#{i}", - last_name: "Member#{i}", - email: "extra#{i}@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Extra#{i}", + last_name: "Member#{i}", + email: "extra#{i}@example.com" + }) end members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() # Should be limited to 10 assert length(members) == 10 @@ -157,8 +125,6 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do test "email match: returns only member with matching email when exists", %{ unlinked_members: unlinked_members } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Get one of the unlinked members' email target_member = List.first(unlinked_members) user_email = target_member.email @@ -166,7 +132,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do raw_members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{user_email: user_email}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() # Apply email match filtering (sorted results come from query) # When user_email matches, only that member should be returned @@ -179,15 +145,13 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do end test "email match: returns all unlinked members when no email match" do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Use an email that doesn't match any member non_matching_email = "nonexistent@example.com" raw_members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{user_email: non_matching_email}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() # Apply email match filtering members = Mv.Membership.Member.filter_by_email_match(raw_members, non_matching_email) @@ -199,13 +163,11 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do test "search query: filters by first_name, last_name, and email", %{ unlinked_members: _unlinked_members } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Search by first name members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{search_query: "Alice"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(members) == 1 assert List.first(members).first_name == "Alice" @@ -214,7 +176,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{search_query: "Williams"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(members) == 1 assert List.first(members).last_name == "Williams" @@ -223,7 +185,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{search_query: "charlie@"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(members) == 1 assert List.first(members).email == "charlie@example.com" @@ -232,13 +194,12 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do members = Mv.Membership.Member |> Ash.Query.for_read(:available_for_linking, %{search_query: "NonExistent"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.empty?(members) end test "user_email takes precedence over search_query", %{unlinked_members: unlinked_members} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() target_member = List.first(unlinked_members) # Pass both email match and search query that would match different members @@ -248,7 +209,7 @@ defmodule Mv.Membership.MemberAvailableForLinkingTest do user_email: target_member.email, search_query: "Bob" }) - |> Ash.read!(actor: system_actor) + |> Ash.read!() # Apply email-match filter (as LiveView does) members = Mv.Membership.Member.filter_by_email_match(raw_members, target_member.email) diff --git a/test/membership/member_cycle_calculations_test.exs b/test/membership/member_cycle_calculations_test.exs index ea7f378..5a9e501 100644 --- a/test/membership/member_cycle_calculations_test.exs +++ b/test/membership/member_cycle_calculations_test.exs @@ -9,13 +9,8 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do alias Mv.MembershipFees.MembershipFeeCycle alias Mv.MembershipFees.CalendarCycles - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -26,11 +21,11 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a member - defp create_member(attrs, actor) do + defp create_member(attrs) do default_attrs = %{ first_name: "Test", last_name: "Member", @@ -41,11 +36,11 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a cycle - defp create_cycle(member, fee_type, attrs, actor) do + defp create_cycle(member, fee_type, attrs) do default_attrs = %{ cycle_start: ~D[2024-01-01], amount: Decimal.new("50.00"), @@ -58,198 +53,153 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end describe "current_cycle_status" do - test "returns status of current cycle for member with active cycle", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns status of current cycle for member with active cycle" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # Create a cycle that is active today (2024-01-01 to 2024-12-31) # Assuming today is in 2024 today = Date.utc_today() cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly) - create_cycle( - member, - fee_type, - %{ - cycle_start: cycle_start, - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: cycle_start, + status: :paid + }) - member = Ash.load!(member, :current_cycle_status, actor: actor) + member = Ash.load!(member, :current_cycle_status) assert member.current_cycle_status == :paid end - test "returns nil for member without current cycle", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns nil for member without current cycle" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # Create a cycle in the past (not current) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2020-01-01], - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2020-01-01], + status: :paid + }) - member = Ash.load!(member, :current_cycle_status, actor: actor) + member = Ash.load!(member, :current_cycle_status) assert member.current_cycle_status == nil end - test "returns nil for member without cycles", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns nil for member without cycles" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) - member = Ash.load!(member, :current_cycle_status, actor: actor) + member = Ash.load!(member, :current_cycle_status) assert member.current_cycle_status == nil end - test "returns status of current cycle for monthly interval", %{actor: actor} do - fee_type = create_fee_type(%{interval: :monthly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns status of current cycle for monthly interval" do + fee_type = create_fee_type(%{interval: :monthly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # Create a cycle that is active today (current month) today = Date.utc_today() cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly) - create_cycle( - member, - fee_type, - %{ - cycle_start: cycle_start, - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: cycle_start, + status: :unpaid + }) - member = Ash.load!(member, :current_cycle_status, actor: actor) + member = Ash.load!(member, :current_cycle_status) assert member.current_cycle_status == :unpaid end end describe "last_cycle_status" do - test "returns status of last completed cycle", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns status of last completed cycle" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # Create cycles: 2022 (completed), 2023 (completed), 2024 (current) today = Date.utc_today() - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2022-01-01], - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2022-01-01], + status: :paid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2023-01-01], - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2023-01-01], + status: :unpaid + }) # Current cycle cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly) - create_cycle( - member, - fee_type, - %{ - cycle_start: cycle_start, - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: cycle_start, + status: :paid + }) - member = Ash.load!(member, :last_cycle_status, actor: actor) + member = Ash.load!(member, :last_cycle_status) # Should return status of 2023 (last completed) assert member.last_cycle_status == :unpaid end - test "returns nil for member without completed cycles", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns nil for member without completed cycles" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # Only create current cycle (not completed yet) today = Date.utc_today() cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly) - create_cycle( - member, - fee_type, - %{ - cycle_start: cycle_start, - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: cycle_start, + status: :paid + }) - member = Ash.load!(member, :last_cycle_status, actor: actor) + member = Ash.load!(member, :last_cycle_status) assert member.last_cycle_status == nil end - test "returns nil for member without cycles", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns nil for member without cycles" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) - member = Ash.load!(member, :last_cycle_status, actor: actor) + member = Ash.load!(member, :last_cycle_status) assert member.last_cycle_status == nil end - test "returns status of last completed cycle for monthly interval", %{actor: actor} do - fee_type = create_fee_type(%{interval: :monthly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns status of last completed cycle for monthly interval" do + fee_type = create_fee_type(%{interval: :monthly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) today = Date.utc_today() # Create cycles: last month (completed), current month (not completed) last_month_start = Date.add(today, -32) |> CalendarCycles.calculate_cycle_start(:monthly) current_month_start = CalendarCycles.calculate_cycle_start(today, :monthly) - create_cycle( - member, - fee_type, - %{ - cycle_start: last_month_start, - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: last_month_start, + status: :paid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: current_month_start, - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: current_month_start, + status: :unpaid + }) - member = Ash.load!(member, :last_cycle_status, actor: actor) + member = Ash.load!(member, :last_cycle_status) # Should return status of last month (last completed) assert member.last_cycle_status == :paid end end describe "overdue_count" do - test "counts only unpaid cycles that have ended", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "counts only unpaid cycles that have ended" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) today = Date.utc_today() @@ -259,38 +209,23 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do # 2024: unpaid, current (not overdue) # 2025: unpaid, future (not overdue) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2022-01-01], - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2022-01-01], + status: :unpaid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2023-01-01], - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2023-01-01], + status: :paid + }) # Current cycle cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly) - create_cycle( - member, - fee_type, - %{ - cycle_start: cycle_start, - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: cycle_start, + status: :unpaid + }) # Future cycle (if we're not at the end of the year) next_year = today.year + 1 @@ -298,52 +233,42 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do if today.month < 12 or today.day < 31 do next_year_start = Date.new!(next_year, 1, 1) - create_cycle( - member, - fee_type, - %{ - cycle_start: next_year_start, - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: next_year_start, + status: :unpaid + }) end - member = Ash.load!(member, :overdue_count, actor: actor) + member = Ash.load!(member, :overdue_count) # Should only count 2022 (unpaid and ended) assert member.overdue_count == 1 end - test "returns 0 when no overdue cycles", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns 0 when no overdue cycles" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # Create only paid cycles - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2022-01-01], - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2022-01-01], + status: :paid + }) - member = Ash.load!(member, :overdue_count, actor: actor) + member = Ash.load!(member, :overdue_count) assert member.overdue_count == 0 end - test "returns 0 for member without cycles", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "returns 0 for member without cycles" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) - member = Ash.load!(member, :overdue_count, actor: actor) + member = Ash.load!(member, :overdue_count) assert member.overdue_count == 0 end - test "counts overdue cycles for monthly interval", %{actor: actor} do - fee_type = create_fee_type(%{interval: :monthly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "counts overdue cycles for monthly interval" do + fee_type = create_fee_type(%{interval: :monthly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) today = Date.utc_today() @@ -354,125 +279,78 @@ defmodule Mv.Membership.MemberCycleCalculationsTest do last_month_start = Date.add(today, -32) |> CalendarCycles.calculate_cycle_start(:monthly) current_month_start = CalendarCycles.calculate_cycle_start(today, :monthly) - create_cycle( - member, - fee_type, - %{ - cycle_start: two_months_ago_start, - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: two_months_ago_start, + status: :unpaid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: last_month_start, - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: last_month_start, + status: :paid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: current_month_start, - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: current_month_start, + status: :unpaid + }) - member = Ash.load!(member, :overdue_count, actor: actor) + member = Ash.load!(member, :overdue_count) # Should only count two_months_ago (unpaid and ended) assert member.overdue_count == 1 end - test "counts multiple overdue cycles", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "counts multiple overdue cycles" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # Create multiple unpaid, ended cycles - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2020-01-01], - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2020-01-01], + status: :unpaid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2021-01-01], - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2021-01-01], + status: :unpaid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2022-01-01], - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2022-01-01], + status: :unpaid + }) - member = Ash.load!(member, :overdue_count, actor: actor) + member = Ash.load!(member, :overdue_count) assert member.overdue_count == 3 end end describe "calculations with multiple cycles" do - test "all calculations work correctly with multiple cycles", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "all calculations work correctly with multiple cycles" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) today = Date.utc_today() # Create cycles: 2022 (unpaid, ended), 2023 (paid, ended), 2024 (unpaid, current) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2022-01-01], - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2022-01-01], + status: :unpaid + }) - create_cycle( - member, - fee_type, - %{ - cycle_start: ~D[2023-01-01], - status: :paid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: ~D[2023-01-01], + status: :paid + }) cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly) - create_cycle( - member, - fee_type, - %{ - cycle_start: cycle_start, - status: :unpaid - }, - actor - ) + create_cycle(member, fee_type, %{ + cycle_start: cycle_start, + status: :unpaid + }) member = - Ash.load!(member, [:current_cycle_status, :last_cycle_status, :overdue_count], - actor: actor - ) + Ash.load!(member, [:current_cycle_status, :last_cycle_status, :overdue_count]) assert member.current_cycle_status == :unpaid assert member.last_cycle_status == :paid diff --git a/test/membership/member_email_sync_test.exs b/test/membership/member_email_sync_test.exs index 784ebcc..eeef210 100644 --- a/test/membership/member_email_sync_test.exs +++ b/test/membership/member_email_sync_test.exs @@ -8,11 +8,6 @@ defmodule Mv.Membership.MemberEmailSyncTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "Member email synchronization to linked User" do @valid_user_attrs %{ email: "user@example.com" @@ -24,119 +19,108 @@ defmodule Mv.Membership.MemberEmailSyncTest do email: "member@example.com" } - test "updating member email syncs to linked user", %{actor: actor} do + test "updating member email syncs to linked user" do # Create a user - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(@valid_user_attrs) assert to_string(user.email) == "user@example.com" # Create a member linked to the user {:ok, member} = - Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}), - actor: actor - ) + Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id})) # Verify initial state - member email should be overridden by user email - {:ok, member_after_create} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, member_after_create} = Ash.get(Mv.Membership.Member, member.id) assert member_after_create.email == "user@example.com" # Update member email {:ok, updated_member} = - Membership.update_member(member, %{email: "newmember@example.com"}, actor: actor) + Membership.update_member(member, %{email: "newmember@example.com"}) assert updated_member.email == "newmember@example.com" # Verify user email was also updated - {:ok, synced_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor) + {:ok, synced_user} = Ash.get(Mv.Accounts.User, user.id) assert to_string(synced_user.email) == "newmember@example.com" end - test "creating member linked to user syncs user email to member", %{actor: actor} do + test "creating member linked to user syncs user email to member" do # Create a user with their own email - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(@valid_user_attrs) assert to_string(user.email) == "user@example.com" # Create a member linked to this user {:ok, member} = - Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}), - actor: actor - ) + Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id})) # Member should have been created with user's email (user is source of truth) assert member.email == "user@example.com" # Verify the link - {:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor) + {:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert loaded_member.user.id == user.id end - test "linking member to existing user syncs user email to member", %{actor: actor} do + test "linking member to existing user syncs user email to member" do # Create a standalone user - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(@valid_user_attrs) assert to_string(user.email) == "user@example.com" # Create a standalone member - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) assert member.email == "member@example.com" # Link the member to the user - {:ok, linked_member} = - Membership.update_member(member, %{user: %{id: user.id}}, actor: actor) + {:ok, linked_member} = Membership.update_member(member, %{user: %{id: user.id}}) # Verify the link - {:ok, loaded_member} = - Ash.get(Mv.Membership.Member, linked_member.id, load: [:user], actor: actor) - + {:ok, loaded_member} = Ash.get(Mv.Membership.Member, linked_member.id, load: [:user]) assert loaded_member.user.id == user.id # Verify member email was overridden with user email assert loaded_member.email == "user@example.com" end - test "updating member email when no user linked does not error", %{actor: actor} do + test "updating member email when no user linked does not error" do # Create a standalone member without user link - {:ok, member} = Membership.create_member(@valid_member_attrs, actor: actor) + {:ok, member} = Membership.create_member(@valid_member_attrs) assert member.email == "member@example.com" # Load to verify no user link - {:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor) + {:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert loaded_member.user == nil # Update member email - should work fine without error {:ok, updated_member} = - Membership.update_member(member, %{email: "newemail@example.com"}, actor: actor) + Membership.update_member(member, %{email: "newemail@example.com"}) assert updated_member.email == "newemail@example.com" end - test "unlinking member from user does not sync email", %{actor: actor} do + test "unlinking member from user does not sync email" do # Create user - {:ok, user} = Accounts.create_user(@valid_user_attrs, actor: actor) + {:ok, user} = Accounts.create_user(@valid_user_attrs) # Create member linked to user {:ok, member} = - Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id}), - actor: actor - ) + Membership.create_member(Map.put(@valid_member_attrs, :user, %{id: user.id})) # Verify member email was synced to user email - {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, synced_member} = Ash.get(Mv.Membership.Member, member.id) assert synced_member.email == "user@example.com" # Verify link exists - {:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user], actor: actor) + {:ok, loaded_member} = Ash.get(Mv.Membership.Member, member.id, load: [:user]) assert loaded_member.user != nil # Unlink member from user - {:ok, unlinked_member} = Membership.update_member(member, %{user: nil}, actor: actor) + {:ok, unlinked_member} = Membership.update_member(member, %{user: nil}) # Verify unlink - {:ok, loaded_unlinked} = - Ash.get(Mv.Membership.Member, unlinked_member.id, load: [:user], actor: actor) - + {:ok, loaded_unlinked} = Ash.get(Mv.Membership.Member, unlinked_member.id, load: [:user]) assert loaded_unlinked.user == nil # User email should remain unchanged after unlinking - {:ok, user_after_unlink} = Ash.get(Mv.Accounts.User, user.id, actor: actor) + {:ok, user_after_unlink} = Ash.get(Mv.Accounts.User, user.id) assert to_string(user_after_unlink.email) == "user@example.com" end end diff --git a/test/membership/member_fuzzy_search_linking_test.exs b/test/membership/member_fuzzy_search_linking_test.exs index f730eec..4cbd8d9 100644 --- a/test/membership/member_fuzzy_search_linking_test.exs +++ b/test/membership/member_fuzzy_search_linking_test.exs @@ -9,23 +9,15 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do alias Mv.Accounts alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "available_for_linking with fuzzy search" do - test "finds member despite typo", %{actor: actor} do + test "finds member despite typo" do # Create member with specific name {:ok, member} = - Membership.create_member( - %{ - first_name: "Jonathan", - last_name: "Smith", - email: "jonathan@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Jonathan", + last_name: "Smith", + email: "jonathan@example.com" + }) # Search with typo query = @@ -35,24 +27,21 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do search_query: "Jonatan" }) - {:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor) + {:ok, members} = Ash.read(query, domain: Mv.Membership) # Should find Jonathan despite typo assert length(members) == 1 assert hd(members).id == member.id end - test "finds member with partial match", %{actor: actor} do + test "finds member with partial match" do # Create member {:ok, member} = - Membership.create_member( - %{ - first_name: "Alexander", - last_name: "Williams", - email: "alex@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Alexander", + last_name: "Williams", + email: "alex@example.com" + }) # Search with partial query = @@ -62,34 +51,28 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do search_query: "Alex" }) - {:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor) + {:ok, members} = Ash.read(query, domain: Mv.Membership) # Should find Alexander assert length(members) == 1 assert hd(members).id == member.id end - test "email match overrides fuzzy search", %{actor: actor} do + test "email match overrides fuzzy search" do # Create two members {:ok, member1} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "john@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john@example.com" + }) {:ok, _member2} = - Membership.create_member( - %{ - first_name: "Jane", - last_name: "Smith", - email: "jane@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Jane", + last_name: "Smith", + email: "jane@example.com" + }) # Search with user_email that matches member1, but search_query that would match member2 query = @@ -99,7 +82,7 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do search_query: "Jane" }) - {:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor) + {:ok, members} = Ash.read(query, domain: Mv.Membership) # Apply email filter filtered_members = Mv.Membership.Member.filter_by_email_match(members, "john@example.com") @@ -109,17 +92,14 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do assert hd(filtered_members).id == member1.id end - test "limits to 10 results", %{actor: actor} do + test "limits to 10 results" do # Create 15 members with similar names for i <- 1..15 do - Membership.create_member( - %{ - first_name: "Test#{i}", - last_name: "Member", - email: "test#{i}@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Test#{i}", + last_name: "Member", + email: "test#{i}@example.com" + }) end # Search for "Test" @@ -130,43 +110,34 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do search_query: "Test" }) - {:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor) + {:ok, members} = Ash.read(query, domain: Mv.Membership) # Should return max 10 members assert length(members) == 10 end - test "excludes linked members", %{actor: actor} do + test "excludes linked members" do # Create member and link to user {:ok, member1} = - Membership.create_member( - %{ - first_name: "Linked", - last_name: "Member", - email: "linked@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Linked", + last_name: "Member", + email: "linked@example.com" + }) {:ok, _user} = - Accounts.create_user( - %{ - email: "user@example.com", - member: %{id: member1.id} - }, - actor: actor - ) + Accounts.create_user(%{ + email: "user@example.com", + member: %{id: member1.id} + }) # Create unlinked member {:ok, member2} = - Membership.create_member( - %{ - first_name: "Unlinked", - last_name: "Member", - email: "unlinked@example.com" - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "Unlinked", + last_name: "Member", + email: "unlinked@example.com" + }) # Search for "Member" query = @@ -176,7 +147,7 @@ defmodule Mv.Membership.MemberFuzzySearchLinkingTest do search_query: "Member" }) - {:ok, members} = Ash.read(query, domain: Mv.Membership, actor: actor) + {:ok, members} = Ash.read(query, domain: Mv.Membership) # Should only return unlinked member member_ids = Enum.map(members, & &1.id) diff --git a/test/membership/member_required_custom_fields_test.exs b/test/membership/member_required_custom_fields_test.exs index c3ede0f..ec8ebe3 100644 --- a/test/membership/member_required_custom_fields_test.exs +++ b/test/membership/member_required_custom_fields_test.exs @@ -14,8 +14,6 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do alias Mv.Membership setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create required custom fields for different types {:ok, required_string_field} = Membership.CustomField @@ -24,7 +22,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do value_type: :string, required: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, required_integer_field} = Membership.CustomField @@ -33,7 +31,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do value_type: :integer, required: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, required_boolean_field} = Membership.CustomField @@ -42,7 +40,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do value_type: :boolean, required: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, required_date_field} = Membership.CustomField @@ -51,7 +49,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do value_type: :date, required: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, required_email_field} = Membership.CustomField @@ -60,7 +58,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do value_type: :email, required: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, optional_field} = Membership.CustomField @@ -69,7 +67,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do value_type: :string, required: false }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{ required_string_field: required_string_field, @@ -77,8 +75,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do required_boolean_field: required_boolean_field, required_date_field: required_date_field, required_email_field: required_email_field, - optional_field: optional_field, - actor: system_actor + optional_field: optional_field } end @@ -121,23 +118,17 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do email: "john@example.com" } - test "fails when required custom field is missing", %{ - required_string_field: field, - actor: actor - } do + test "fails when required custom field is missing", %{required_string_field: field} do attrs = Map.put(@valid_attrs, :custom_field_values, []) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required string custom field has nil value", %{ - required_string_field: field, - actor: actor + required_string_field: field } = context do # Start with all required fields having valid values custom_field_values = @@ -152,17 +143,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required string custom field has empty string value", %{ - required_string_field: field, - actor: actor + required_string_field: field } = context do # Start with all required fields having valid values custom_field_values = @@ -177,17 +165,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required string custom field has whitespace-only value", %{ - required_string_field: field, - actor: actor + required_string_field: field } = context do # Start with all required fields having valid values custom_field_values = @@ -202,17 +187,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required string custom field has valid value", %{ - required_string_field: field, - actor: actor + required_string_field: field } = context do # Start with all required fields having valid values, then update the string field custom_field_values = @@ -227,13 +209,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required integer custom field has nil value", %{ - required_integer_field: field, - actor: actor + required_integer_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -247,17 +228,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required integer custom field has empty string value", %{ - required_integer_field: field, - actor: actor + required_integer_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -271,29 +249,25 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required integer custom field has zero value", %{ - required_integer_field: _field, - actor: actor + required_integer_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when required integer custom field has positive value", %{ - required_integer_field: field, - actor: actor + required_integer_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -307,13 +281,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required boolean custom field has nil value", %{ - required_boolean_field: field, - actor: actor + required_boolean_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -327,29 +300,25 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required boolean custom field has false value", %{ - required_boolean_field: _field, - actor: actor + required_boolean_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when required boolean custom field has true value", %{ - required_boolean_field: field, - actor: actor + required_boolean_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -363,13 +332,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required date custom field has nil value", %{ - required_date_field: field, - actor: actor + required_date_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -383,17 +351,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required date custom field has empty string value", %{ - required_date_field: field, - actor: actor + required_date_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -407,29 +372,25 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required date custom field has valid date value", %{ - required_date_field: _field, - actor: actor + required_date_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "fails when required email custom field has nil value", %{ - required_email_field: field, - actor: actor + required_email_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -443,17 +404,14 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "fails when required email custom field has empty string value", %{ - required_email_field: field, - actor: actor + required_email_field: field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -467,31 +425,27 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name end test "succeeds when required email custom field has valid email value", %{ - required_email_field: _field, - actor: actor + required_email_field: _field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when multiple required custom fields are provided", %{ required_string_field: string_field, required_integer_field: integer_field, - required_boolean_field: boolean_field, - actor: actor + required_boolean_field: boolean_field } = context do custom_field_values = all_required_custom_fields_with_defaults(context) @@ -513,14 +467,13 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "fails when one of multiple required custom fields is missing", %{ required_string_field: string_field, - required_integer_field: integer_field, - actor: actor + required_integer_field: integer_field } = context do # Provide only string field, missing integer, boolean, and date custom_field_values = @@ -534,24 +487,22 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ integer_field.name end - test "succeeds when optional custom field is missing", %{actor: actor} = context do + test "succeeds when optional custom field is missing", %{} = context do # Provide all required fields, but no optional field custom_field_values = all_required_custom_fields_with_defaults(context) attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end test "succeeds when optional custom field has nil value", - %{optional_field: field, actor: actor} = context do + %{optional_field: field} = context do # Provide all required fields plus optional field with nil custom_field_values = all_required_custom_fields_with_defaults(context) ++ @@ -564,33 +515,29 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end end describe "update_member with required custom fields" do test "fails when removing a required custom field value", %{ - required_string_field: field, - actor: actor + required_string_field: field } = context do # Create member with all required custom fields custom_field_values = all_required_custom_fields_with_defaults(context) {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "john@example.com", - custom_field_values: custom_field_values - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john@example.com", + custom_field_values: custom_field_values + }) # Try to update without the required custom field assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.update_member(member, %{custom_field_values: []}, actor: actor) + Membership.update_member(member, %{custom_field_values: []}) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name @@ -598,22 +545,18 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do test "fails when setting required custom field value to empty", %{ - required_string_field: field, - actor: actor + required_string_field: field } = context do # Create member with all required custom fields custom_field_values = all_required_custom_fields_with_defaults(context) {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "john@example.com", - custom_field_values: custom_field_values - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john@example.com", + custom_field_values: custom_field_values + }) # Try to update with empty value for the string field updated_custom_field_values = @@ -627,13 +570,9 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do end) assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.update_member( - member, - %{ - custom_field_values: updated_custom_field_values - }, - actor: actor - ) + Membership.update_member(member, %{ + custom_field_values: updated_custom_field_values + }) assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" assert error_message(errors, :custom_field_values) =~ field.name @@ -641,25 +580,21 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do test "succeeds when updating required custom field to valid value", %{ - required_string_field: field, - actor: actor + required_string_field: field } = context do # Create member with all required custom fields custom_field_values = all_required_custom_fields_with_defaults(context) {:ok, member} = - Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "john@example.com", - custom_field_values: custom_field_values - }, - actor: actor - ) + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john@example.com", + custom_field_values: custom_field_values + }) # Load existing custom field values to get their IDs - {:ok, member_with_cfvs} = Ash.load(member, :custom_field_values, actor: actor) + {:ok, member_with_cfvs} = Ash.load(member, :custom_field_values) # Update with new valid value for the string field, using existing IDs updated_custom_field_values = @@ -685,13 +620,9 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do end) assert {:ok, _updated_member} = - Membership.update_member( - member, - %{ - custom_field_values: updated_custom_field_values - }, - actor: actor - ) + Membership.update_member(member, %{ + custom_field_values: updated_custom_field_values + }) end end diff --git a/test/membership/member_search_with_custom_fields_test.exs b/test/membership/member_search_with_custom_fields_test.exs index bd28ce5..6711df8 100644 --- a/test/membership/member_search_with_custom_fields_test.exs +++ b/test/membership/member_search_with_custom_fields_test.exs @@ -10,8 +10,6 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test members {:ok, member1} = Member @@ -20,7 +18,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member2} = Member @@ -29,7 +27,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do last_name: "Brown", email: "bob@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member3} = Member @@ -38,7 +36,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do last_name: "Clark", email: "charlie@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom fields for different types {:ok, string_field} = @@ -47,7 +45,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do name: "membership_number", value_type: :string }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, integer_field} = CustomField @@ -55,7 +53,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do name: "member_id_number", value_type: :integer }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, email_field} = CustomField @@ -63,7 +61,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do name: "secondary_email", value_type: :email }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, date_field} = CustomField @@ -71,7 +69,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do name: "birthday", value_type: :date }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, boolean_field} = CustomField @@ -79,7 +77,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do name: "newsletter", value_type: :boolean }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{ member1: member1, @@ -89,14 +87,12 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do integer_field: integer_field, email_field: email_field, date_field: date_field, - boolean_field: boolean_field, - system_actor: system_actor + boolean_field: boolean_field } end describe "search with custom field values" do test "finds member by string custom field value", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -108,26 +104,25 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "MEMBER12345"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update by reloading member {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for the custom field value results = Member |> Member.fuzzy_search(%{query: "MEMBER12345"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "finds member by integer custom field value", %{ - system_actor: system_actor, member1: member1, integer_field: integer_field } do @@ -139,26 +134,25 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: integer_field.id, value: %{"_union_type" => "integer", "_union_value" => 42_424} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for the custom field value results = Member |> Member.fuzzy_search(%{query: "42424"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "finds member by email custom field value", %{ - system_actor: system_actor, member1: member1, email_field: email_field } do @@ -170,19 +164,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => "alice.secondary@example.com"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for partial custom field value (should work via FTS or custom field filter) results = Member |> Member.fuzzy_search(%{query: "alice.secondary"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id @@ -191,7 +185,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do results_full = Member |> Member.fuzzy_search(%{query: "alice.secondary@example.com"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results_full) == 1 assert List.first(results_full).id == member1.id @@ -201,7 +195,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do results_domain = Member |> Member.fuzzy_search(%{query: "example.com"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() # Verify that member1 is in the results (may have other members too) ids = Enum.map(results_domain, & &1.id) @@ -209,7 +203,6 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do end test "finds member by date custom field value", %{ - system_actor: system_actor, member1: member1, date_field: date_field } do @@ -221,26 +214,25 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: date_field.id, value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for the custom field value (date is stored as text in search_vector) results = Member |> Member.fuzzy_search(%{query: "1990-05-15"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "finds member by boolean custom field value", %{ - system_actor: system_actor, member1: member1, boolean_field: boolean_field } do @@ -252,26 +244,25 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: boolean_field.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for the custom field value (boolean is stored as "true" or "false" text) results = Member |> Member.fuzzy_search(%{query: "true"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() # Note: "true" might match other things, so we check that member1 is in results assert Enum.any?(results, fn m -> m.id == member1.id end) end test "custom field value update triggers search_vector update", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -283,13 +274,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "OLDVALUE"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Update custom field value {:ok, _updated_cfv} = @@ -297,13 +288,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do |> Ash.Changeset.for_update(:update, %{ value: %{"_union_type" => "string", "_union_value" => "NEWVALUE123"} }) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for the new value results = Member |> Member.fuzzy_search(%{query: "NEWVALUE123"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id @@ -312,13 +303,12 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do old_results = Member |> Member.fuzzy_search(%{query: "OLDVALUE"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() refute Enum.any?(old_results, fn m -> m.id == member1.id end) end test "custom field value delete triggers search_vector update", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -330,19 +320,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "TOBEDELETED"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Verify it's searchable results = Member |> Member.fuzzy_search(%{query: "TOBEDELETED"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id @@ -354,13 +344,12 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do deleted_results = Member |> Member.fuzzy_search(%{query: "TOBEDELETED"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() refute Enum.any?(deleted_results, fn m -> m.id == member1.id end) end test "custom field value create triggers search_vector update", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -372,20 +361,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "AUTOUPDATE"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Search should find it immediately (trigger should have updated search_vector) results = Member |> Member.fuzzy_search(%{query: "AUTOUPDATE"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "member update includes custom field values in search_vector", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -397,26 +385,25 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "MEMBERUPDATE"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Update member (should trigger search_vector update including custom fields) {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{notes: "Updated notes"}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search should find the custom field value results = Member |> Member.fuzzy_search(%{query: "MEMBERUPDATE"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "multiple custom field values are all searchable", %{ - system_actor: system_actor, member1: member1, string_field: string_field, integer_field: integer_field, @@ -430,7 +417,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "MULTI1"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = CustomFieldValue @@ -439,7 +426,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: integer_field.id, value: %{"_union_type" => "integer", "_union_value" => 99_999} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv3} = CustomFieldValue @@ -448,39 +435,38 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => "multi@test.com"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # All values should be searchable results1 = Member |> Member.fuzzy_search(%{query: "MULTI1"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results1, fn m -> m.id == member1.id end) results2 = Member |> Member.fuzzy_search(%{query: "99999"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results2, fn m -> m.id == member1.id end) results3 = Member |> Member.fuzzy_search(%{query: "multi@test.com"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results3, fn m -> m.id == member1.id end) end test "finds member by custom field value with numbers in text field (e.g. phone number)", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -492,19 +478,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "M-123-456"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for full value (should work via search_vector) results_full = Member |> Member.fuzzy_search(%{query: "M-123-456"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results_full, fn m -> m.id == member1.id end), "Full value search should find member via search_vector" @@ -515,7 +501,6 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do end test "finds member by phone number in Emergency Contact custom field", %{ - system_actor: system_actor, member1: member1 } do # Create Emergency Contact custom field @@ -525,7 +510,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do name: "Emergency Contact", value_type: :string }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field value with phone number phone_number = "+49 123 456789" @@ -537,19 +522,19 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: emergency_contact_field.id, value: %{"_union_type" => "string", "_union_value" => phone_number} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) - |> Ash.update(actor: system_actor) + |> Ash.update() # Search for full phone number (should work via search_vector) results_full = Member |> Member.fuzzy_search(%{query: phone_number}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results_full, fn m -> m.id == member1.id end), "Full phone number search should find member via search_vector" @@ -562,7 +547,6 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do describe "custom field substring search (ILIKE)" do test "finds member by prefix of custom field value", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -574,14 +558,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "Premium"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Test prefix searches - should all find the member for prefix <- ["Premium", "Premiu", "Premi", "Prem", "Pre"] do results = Member |> Member.fuzzy_search(%{query: prefix}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results, fn m -> m.id == member1.id end), "Prefix '#{prefix}' should find member with custom field 'Premium'" @@ -589,7 +573,6 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do end test "custom field search is case-insensitive", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -601,7 +584,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "GoldMember"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Test case variations - should all find the member for variant <- [ @@ -616,7 +599,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do results = Member |> Member.fuzzy_search(%{query: variant}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results, fn m -> m.id == member1.id end), "Case variant '#{variant}' should find member with custom field 'GoldMember'" @@ -624,7 +607,6 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do end test "finds member by suffix/middle of custom field value", %{ - system_actor: system_actor, member1: member1, string_field: string_field } do @@ -636,14 +618,14 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "ActiveMember"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Test suffix and middle substring searches for substring <- ["Member", "ember", "tiveMem", "ctive"] do results = Member |> Member.fuzzy_search(%{query: substring}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert Enum.any?(results, fn m -> m.id == member1.id end), "Substring '#{substring}' should find member with custom field 'ActiveMember'" @@ -651,7 +633,6 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do end test "finds correct member among multiple with different custom field values", %{ - system_actor: system_actor, member1: member1, member2: member2, member3: member3, @@ -665,7 +646,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "Beginner"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = CustomFieldValue @@ -674,7 +655,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "Advanced"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv3} = CustomFieldValue @@ -683,13 +664,13 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "Expert"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Search for "Begin" - should only find member1 results_begin = Member |> Member.fuzzy_search(%{query: "Begin"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results_begin) == 1 assert List.first(results_begin).id == member1.id @@ -698,7 +679,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do results_advan = Member |> Member.fuzzy_search(%{query: "Advan"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results_advan) == 1 assert List.first(results_advan).id == member2.id @@ -707,7 +688,7 @@ defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do results_exper = Member |> Member.fuzzy_search(%{query: "Exper"}) - |> Ash.read!(actor: system_actor) + |> Ash.read!() assert length(results_exper) == 1 assert List.first(results_exper).id == member3.id diff --git a/test/membership/member_test.exs b/test/membership/member_test.exs index 705ab61..6919ec1 100644 --- a/test/membership/member_test.exs +++ b/test/membership/member_test.exs @@ -2,11 +2,6 @@ defmodule Mv.Membership.MemberTest do use Mv.DataCase, async: false alias Mv.Membership - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "Fields and Validations" do @valid_attrs %{ first_name: "John", @@ -21,74 +16,60 @@ defmodule Mv.Membership.MemberTest do postal_code: "12345" } - test "First name is optional", %{actor: actor} do + test "First name is optional" do attrs = Map.delete(@valid_attrs, :first_name) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end - test "Last name is optional", %{actor: actor} do + test "Last name is optional" do attrs = Map.delete(@valid_attrs, :last_name) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end - test "Email is required", %{actor: actor} do + test "Email is required" do attrs = Map.put(@valid_attrs, :email, "") - - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :email) =~ "must be present" end - test "Email must be valid", %{actor: actor} do + test "Email must be valid" do attrs = Map.put(@valid_attrs, :email, "test@") - - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :email) =~ "is not a valid email" end - test "Join date cannot be in the future", %{actor: actor} do + test "Join date cannot be in the future" do attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1)) assert {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidAttribute{field: :join_date}]}} = - Membership.create_member(attrs, actor: actor) + Membership.create_member(attrs) end - test "Exit date is optional but must not be before join date if both are specified", %{ - actor: actor - } do + test "Exit date is optional but must not be before join date if both are specified" do attrs = Map.put(@valid_attrs, :exit_date, ~D[2010-01-01]) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :exit_date) =~ "cannot be before join date" attrs2 = Map.delete(@valid_attrs, :exit_date) - assert {:ok, _member} = Membership.create_member(attrs2, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs2) end - test "Notes is optional", %{actor: actor} do + test "Notes is optional" do attrs = Map.delete(@valid_attrs, :notes) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end - test "City, street, house number are optional", %{actor: actor} do + test "City, street, house number are optional" do attrs = @valid_attrs |> Map.drop([:city, :street, :house_number]) - assert {:ok, _member} = Membership.create_member(attrs, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs) end - test "Postal code is optional but must have 5 digits if specified", %{actor: actor} do + test "Postal code is optional but must have 5 digits if specified" do attrs = Map.put(@valid_attrs, :postal_code, "1234") - - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.create_member(attrs, actor: actor) - + assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) assert error_message(errors, :postal_code) =~ "must consist of 5 digits" attrs2 = Map.delete(@valid_attrs, :postal_code) - assert {:ok, _member} = Membership.create_member(attrs2, actor: actor) + assert {:ok, _member} = Membership.create_member(attrs2) end end diff --git a/test/membership/member_type_change_integration_test.exs b/test/membership/member_type_change_integration_test.exs index cb289be..f2dd0e0 100644 --- a/test/membership/member_type_change_integration_test.exs +++ b/test/membership/member_type_change_integration_test.exs @@ -11,13 +11,8 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -28,11 +23,11 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a member - defp create_member(attrs, actor) do + defp create_member(attrs) do default_attrs = %{ first_name: "Test", last_name: "Member", @@ -44,11 +39,11 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a cycle - defp create_cycle(member, fee_type, attrs, actor) do + defp create_cycle(member, fee_type, attrs) do default_attrs = %{ cycle_start: ~D[2024-01-01], amount: Decimal.new("50.00"), @@ -61,17 +56,17 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end describe "type change cycle regeneration" do - test "future unpaid cycles are regenerated with new amount", %{actor: actor} do + test "future unpaid cycles are regenerated with new amount" do today = Date.utc_today() - yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor) - yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor) + yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}) + yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}) # Create member without fee type first to avoid auto-generation - member = create_member(%{}, actor) + member = create_member(%{}) # Manually assign fee type (this will trigger cycle generation) member = @@ -79,7 +74,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type1.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Cycle generation runs synchronously in the same transaction # No need to wait for async completion @@ -94,49 +89,39 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do # Check if it already exists (from auto-generation), if not create it case MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start) - |> Ash.read_one(actor: actor) do + |> Ash.read_one() do {:ok, existing_cycle} when not is_nil(existing_cycle) -> # Update to paid existing_cycle |> Ash.Changeset.for_update(:update, %{status: :paid}) - |> Ash.update!(actor: actor) + |> Ash.update!() _ -> - create_cycle( - member, - yearly_type1, - %{ - cycle_start: past_cycle_start, - status: :paid, - amount: Decimal.new("100.00") - }, - actor - ) + create_cycle(member, yearly_type1, %{ + cycle_start: past_cycle_start, + status: :paid, + amount: Decimal.new("100.00") + }) end # Current cycle (unpaid) - should be regenerated # Delete if exists (from auto-generation), then create with old amount case MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start) - |> Ash.read_one(actor: actor) do + |> Ash.read_one() do {:ok, existing_cycle} when not is_nil(existing_cycle) -> - Ash.destroy!(existing_cycle, actor: actor) + Ash.destroy!(existing_cycle) _ -> :ok end _current_cycle = - create_cycle( - member, - yearly_type1, - %{ - cycle_start: current_cycle_start, - status: :unpaid, - amount: Decimal.new("100.00") - }, - actor - ) + create_cycle(member, yearly_type1, %{ + cycle_start: current_cycle_start, + status: :unpaid, + amount: Decimal.new("100.00") + }) # Change membership fee type (same interval, different amount) assert {:ok, _updated_member} = @@ -144,7 +129,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type2.id }) - |> Ash.update(actor: actor) + |> Ash.update() # Cycle regeneration runs synchronously in the same transaction # No need to wait for async completion @@ -153,7 +138,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do past_cycle_after = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() assert past_cycle_after.status == :paid assert Decimal.equal?(past_cycle_after.amount, Decimal.new("100.00")) @@ -164,7 +149,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do new_current_cycle = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() # Verify it has the new type and amount assert new_current_cycle.membership_fee_type_id == yearly_type2.id @@ -178,18 +163,18 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do member_id == ^member.id and cycle_start == ^current_cycle_start and membership_fee_type_id == ^yearly_type1.id ) - |> Ash.read!(actor: actor) + |> Ash.read!() assert Enum.empty?(old_current_cycles) end - test "paid cycles remain unchanged", %{actor: actor} do + test "paid cycles remain unchanged" do today = Date.utc_today() - yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor) - yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor) + yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}) + yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}) # Create member without fee type first to avoid auto-generation - member = create_member(%{}, actor) + member = create_member(%{}) # Manually assign fee type (this will trigger cycle generation) member = @@ -197,7 +182,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type1.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Cycle generation runs synchronously in the same transaction # No need to wait for async completion @@ -209,9 +194,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do paid_cycle = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:mark_as_paid) - |> Ash.update!(actor: actor) + |> Ash.update!() # Change membership fee type assert {:ok, _updated_member} = @@ -219,25 +204,25 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type2.id }) - |> Ash.update(actor: actor) + |> Ash.update() # Cycle regeneration runs synchronously in the same transaction # No need to wait for async completion # Verify paid cycle is unchanged (not deleted and regenerated) - {:ok, cycle_after} = Ash.get(MembershipFeeCycle, paid_cycle.id, actor: actor) + {:ok, cycle_after} = Ash.get(MembershipFeeCycle, paid_cycle.id) assert cycle_after.status == :paid assert Decimal.equal?(cycle_after.amount, Decimal.new("100.00")) assert cycle_after.membership_fee_type_id == yearly_type1.id end - test "suspended cycles remain unchanged", %{actor: actor} do + test "suspended cycles remain unchanged" do today = Date.utc_today() - yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor) - yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor) + yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}) + yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}) # Create member without fee type first to avoid auto-generation - member = create_member(%{}, actor) + member = create_member(%{}) # Manually assign fee type (this will trigger cycle generation) member = @@ -245,7 +230,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type1.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Cycle generation runs synchronously in the same transaction # No need to wait for async completion @@ -257,9 +242,9 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do suspended_cycle = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:mark_as_suspended) - |> Ash.update!(actor: actor) + |> Ash.update!() # Change membership fee type assert {:ok, _updated_member} = @@ -267,25 +252,25 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type2.id }) - |> Ash.update(actor: actor) + |> Ash.update() # Cycle regeneration runs synchronously in the same transaction # No need to wait for async completion # Verify suspended cycle is unchanged (not deleted and regenerated) - {:ok, cycle_after} = Ash.get(MembershipFeeCycle, suspended_cycle.id, actor: actor) + {:ok, cycle_after} = Ash.get(MembershipFeeCycle, suspended_cycle.id) assert cycle_after.status == :suspended assert Decimal.equal?(cycle_after.amount, Decimal.new("100.00")) assert cycle_after.membership_fee_type_id == yearly_type1.id end - test "only cycles that haven't ended yet are deleted", %{actor: actor} do + test "only cycles that haven't ended yet are deleted" do today = Date.utc_today() - yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor) - yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor) + yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}) + yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}) # Create member without fee type first to avoid auto-generation - member = create_member(%{}, actor) + member = create_member(%{}) # Manually assign fee type (this will trigger cycle generation) member = @@ -293,7 +278,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type1.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Cycle generation runs synchronously in the same transaction # No need to wait for async completion @@ -311,49 +296,39 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do # Delete existing cycle if it exists (from auto-generation) case MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start) - |> Ash.read_one(actor: actor) do + |> Ash.read_one() do {:ok, existing_cycle} when not is_nil(existing_cycle) -> - Ash.destroy!(existing_cycle, actor: actor) + Ash.destroy!(existing_cycle) _ -> :ok end past_cycle = - create_cycle( - member, - yearly_type1, - %{ - cycle_start: past_cycle_start, - status: :unpaid, - amount: Decimal.new("100.00") - }, - actor - ) + create_cycle(member, yearly_type1, %{ + cycle_start: past_cycle_start, + status: :unpaid, + amount: Decimal.new("100.00") + }) # Current cycle (unpaid) - should be regenerated (cycle_start >= today) # Delete existing cycle if it exists (from auto-generation) case MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start) - |> Ash.read_one(actor: actor) do + |> Ash.read_one() do {:ok, existing_cycle} when not is_nil(existing_cycle) -> - Ash.destroy!(existing_cycle, actor: actor) + Ash.destroy!(existing_cycle) _ -> :ok end _current_cycle = - create_cycle( - member, - yearly_type1, - %{ - cycle_start: current_cycle_start, - status: :unpaid, - amount: Decimal.new("100.00") - }, - actor - ) + create_cycle(member, yearly_type1, %{ + cycle_start: current_cycle_start, + status: :unpaid, + amount: Decimal.new("100.00") + }) # Change membership fee type assert {:ok, _updated_member} = @@ -361,13 +336,13 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type2.id }) - |> Ash.update(actor: actor) + |> Ash.update() # Cycle regeneration runs synchronously in the same transaction # No need to wait for async completion # Verify past cycle is unchanged - {:ok, past_cycle_after} = Ash.get(MembershipFeeCycle, past_cycle.id, actor: actor) + {:ok, past_cycle_after} = Ash.get(MembershipFeeCycle, past_cycle.id) assert past_cycle_after.status == :unpaid assert Decimal.equal?(past_cycle_after.amount, Decimal.new("100.00")) assert past_cycle_after.membership_fee_type_id == yearly_type1.id @@ -377,7 +352,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do new_current_cycle = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() assert new_current_cycle.membership_fee_type_id == yearly_type2.id assert Decimal.equal?(new_current_cycle.amount, Decimal.new("150.00")) @@ -389,19 +364,19 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do member_id == ^member.id and cycle_start == ^current_cycle_start and membership_fee_type_id == ^yearly_type1.id ) - |> Ash.read!(actor: actor) + |> Ash.read!() assert Enum.empty?(old_current_cycles) end - test "member calculations update after type change", %{actor: actor} do + test "member calculations update after type change" do today = Date.utc_today() - yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor) - yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor) + yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}) + yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}) # Create member with join_date = today to avoid past cycles # This ensures no overdue cycles exist - member = create_member(%{join_date: today}, actor) + member = create_member(%{join_date: today}) # Manually assign fee type (this will trigger cycle generation) member = @@ -409,7 +384,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type1.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Cycle generation runs synchronously in the same transaction # No need to wait for async completion @@ -422,38 +397,33 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do existing_cycles = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: actor) + |> Ash.read!() Enum.each(existing_cycles, fn cycle -> if cycle.cycle_start != current_cycle_start do - Ash.destroy!(cycle, actor: actor) + Ash.destroy!(cycle) end end) # Ensure current cycle exists and is unpaid case MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start) - |> Ash.read_one(actor: actor) do + |> Ash.read_one() do {:ok, existing_cycle} when not is_nil(existing_cycle) -> # Update to unpaid if it's not if existing_cycle.status != :unpaid do existing_cycle |> Ash.Changeset.for_update(:mark_as_unpaid) - |> Ash.update!(actor: actor) + |> Ash.update!() end _ -> # Create if it doesn't exist - create_cycle( - member, - yearly_type1, - %{ - cycle_start: current_cycle_start, - status: :unpaid, - amount: Decimal.new("100.00") - }, - actor - ) + create_cycle(member, yearly_type1, %{ + cycle_start: current_cycle_start, + status: :unpaid, + amount: Decimal.new("100.00") + }) end # Load calculations before change @@ -467,7 +437,7 @@ defmodule Mv.Membership.MemberTypeChangeIntegrationTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type2.id }) - |> Ash.update(actor: actor) + |> Ash.update() # Cycle regeneration runs synchronously in the same transaction # No need to wait for async completion diff --git a/test/membership/membership_fee_settings_test.exs b/test/membership/membership_fee_settings_test.exs index 744b6bd..05a0d04 100644 --- a/test/membership/membership_fee_settings_test.exs +++ b/test/membership/membership_fee_settings_test.exs @@ -7,11 +7,6 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do alias Mv.Membership.Setting alias Mv.MembershipFees.MembershipFeeType - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "membership fee settings" do test "default values are correct" do {:ok, settings} = Mv.Membership.get_settings() @@ -23,7 +18,7 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do assert %Setting{} = settings end - test "settings can be written via update_membership_fee_settings", %{actor: actor} do + test "settings can be written via update_membership_fee_settings" do {:ok, settings} = Mv.Membership.get_settings() {:ok, updated} = @@ -31,12 +26,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ include_joining_cycle: false }) - |> Ash.update(actor: actor) + |> Ash.update() assert updated.include_joining_cycle == false end - test "default_membership_fee_type_id can be nil (optional)", %{actor: actor} do + test "default_membership_fee_type_id can be nil (optional)" do {:ok, settings} = Mv.Membership.get_settings() {:ok, updated} = @@ -44,12 +39,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: nil }) - |> Ash.update(actor: actor) + |> Ash.update() assert updated.default_membership_fee_type_id == nil end - test "default_membership_fee_type_id validation: must exist if set", %{actor: actor} do + test "default_membership_fee_type_id validation: must exist if set" do {:ok, settings} = Mv.Membership.get_settings() # Create a valid fee type @@ -66,12 +61,12 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: fee_type.id }) - |> Ash.update(actor: actor) + |> Ash.update() assert updated.default_membership_fee_type_id == fee_type.id end - test "default_membership_fee_type_id validation: fails if not found", %{actor: actor} do + test "default_membership_fee_type_id validation: fails if not found" do {:ok, settings} = Mv.Membership.get_settings() # Use a non-existent UUID @@ -82,7 +77,7 @@ defmodule Mv.Membership.MembershipFeeSettingsTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: fake_uuid }) - |> Ash.update(actor: actor) + |> Ash.update() assert error_on_field?(error, :default_membership_fee_type_id) end diff --git a/test/membership_fees/changes/set_membership_fee_start_date_test.exs b/test/membership_fees/changes/set_membership_fee_start_date_test.exs index 0f8bae9..4af59db 100644 --- a/test/membership_fees/changes/set_membership_fee_start_date_test.exs +++ b/test/membership_fees/changes/set_membership_fee_start_date_test.exs @@ -6,18 +6,13 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do alias Mv.MembershipFees.Changes.SetMembershipFeeStartDate - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to set up settings with specific include_joining_cycle value - defp setup_settings(include_joining_cycle, actor) do + defp setup_settings(include_joining_cycle) do {:ok, settings} = Mv.Membership.get_settings() settings |> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle}) - |> Ash.update!(actor: actor) + |> Ash.update!() end describe "calculate_start_date/3" do @@ -132,8 +127,8 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do end describe "change/3 integration" do - test "sets membership_fee_start_date automatically on member creation", %{actor: actor} do - setup_settings(true, actor) + test "sets membership_fee_start_date automatically on member creation" do + setup_settings(true) # Create a fee type fee_type = @@ -143,7 +138,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do amount: Decimal.new("50.00"), interval: :yearly }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Create member with join_date and fee type but no explicit start date member = @@ -155,14 +150,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do join_date: ~D[2024-03-15], membership_fee_type_id: fee_type.id }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Should have auto-calculated start date (2024-01-01 for yearly with include_joining_cycle=true) assert member.membership_fee_start_date == ~D[2024-01-01] end - test "does not override manually set membership_fee_start_date", %{actor: actor} do - setup_settings(true, actor) + test "does not override manually set membership_fee_start_date" do + setup_settings(true) # Create a fee type fee_type = @@ -172,7 +167,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do amount: Decimal.new("50.00"), interval: :yearly }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Create member with explicit start date manual_start_date = ~D[2024-07-01] @@ -187,14 +182,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: manual_start_date }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Should keep the manually set date assert member.membership_fee_start_date == manual_start_date end - test "respects include_joining_cycle = false setting", %{actor: actor} do - setup_settings(false, actor) + test "respects include_joining_cycle = false setting" do + setup_settings(false) # Create a fee type fee_type = @@ -204,7 +199,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do amount: Decimal.new("50.00"), interval: :yearly }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Create member member = @@ -216,14 +211,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do join_date: ~D[2024-03-15], membership_fee_type_id: fee_type.id }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Should have next cycle start date (2025-01-01 for yearly with include_joining_cycle=false) assert member.membership_fee_start_date == ~D[2025-01-01] end - test "does not set start date without join_date", %{actor: actor} do - setup_settings(true, actor) + test "does not set start date without join_date" do + setup_settings(true) # Create a fee type fee_type = @@ -233,7 +228,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do amount: Decimal.new("50.00"), interval: :yearly }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Create member without join_date member = @@ -245,14 +240,14 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do membership_fee_type_id: fee_type.id # No join_date }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Should not have auto-calculated start date assert is_nil(member.membership_fee_start_date) end - test "does not set start date without membership_fee_type_id", %{actor: actor} do - setup_settings(true, actor) + test "does not set start date without membership_fee_type_id" do + setup_settings(true) # Create member without fee type member = @@ -264,7 +259,7 @@ defmodule Mv.MembershipFees.Changes.SetMembershipFeeStartDateTest do join_date: ~D[2024-03-15] # No membership_fee_type_id }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Should not have auto-calculated start date assert is_nil(member.membership_fee_start_date) diff --git a/test/membership_fees/changes/validate_same_interval_test.exs b/test/membership_fees/changes/validate_same_interval_test.exs index 82fbd6b..0f4501c 100644 --- a/test/membership_fees/changes/validate_same_interval_test.exs +++ b/test/membership_fees/changes/validate_same_interval_test.exs @@ -8,13 +8,8 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do alias Mv.MembershipFees.MembershipFeeType alias Mv.MembershipFees.Changes.ValidateSameInterval - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -25,11 +20,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a member - defp create_member(attrs, actor) do + defp create_member(attrs) do default_attrs = %{ first_name: "Test", last_name: "Member", @@ -40,15 +35,15 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end describe "validate_interval_match/1" do - test "allows change to type with same interval", %{actor: actor} do - yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}, actor) - yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}, actor) + test "allows change to type with same interval" do + yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}) + yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}) - member = create_member(%{membership_fee_type_id: yearly_type1.id}, actor) + member = create_member(%{membership_fee_type_id: yearly_type1.id}) changeset = member @@ -60,11 +55,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do assert changeset.valid? end - test "prevents change to type with different interval", %{actor: actor} do - yearly_type = create_fee_type(%{interval: :yearly}, actor) - monthly_type = create_fee_type(%{interval: :monthly}, actor) + test "prevents change to type with different interval" do + yearly_type = create_fee_type(%{interval: :yearly}) + monthly_type = create_fee_type(%{interval: :monthly}) - member = create_member(%{membership_fee_type_id: yearly_type.id}, actor) + member = create_member(%{membership_fee_type_id: yearly_type.id}) changeset = member @@ -83,10 +78,10 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do end) end - test "allows first assignment of membership fee type", %{actor: actor} do - yearly_type = create_fee_type(%{interval: :yearly}, actor) + test "allows first assignment of membership fee type" do + yearly_type = create_fee_type(%{interval: :yearly}) # No fee type assigned - member = create_member(%{}, actor) + member = create_member(%{}) changeset = member @@ -98,9 +93,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do assert changeset.valid? end - test "prevents removal of membership fee type", %{actor: actor} do - yearly_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: yearly_type.id}, actor) + test "prevents removal of membership fee type" do + yearly_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: yearly_type.id}) changeset = member @@ -118,9 +113,9 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do end) end - test "does nothing when membership_fee_type_id is not changed", %{actor: actor} do - yearly_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: yearly_type.id}, actor) + test "does nothing when membership_fee_type_id is not changed" do + yearly_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: yearly_type.id}) changeset = member @@ -132,11 +127,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do assert changeset.valid? end - test "error message is clear and helpful", %{actor: actor} do - yearly_type = create_fee_type(%{interval: :yearly}, actor) - quarterly_type = create_fee_type(%{interval: :quarterly}, actor) + test "error message is clear and helpful" do + yearly_type = create_fee_type(%{interval: :yearly}) + quarterly_type = create_fee_type(%{interval: :quarterly}) - member = create_member(%{membership_fee_type_id: yearly_type.id}, actor) + member = create_member(%{membership_fee_type_id: yearly_type.id}) changeset = member @@ -151,31 +146,25 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do assert error.message =~ "same-interval" end - test "handles all interval types correctly", %{actor: actor} do + test "handles all interval types correctly" do intervals = [:monthly, :quarterly, :half_yearly, :yearly] for interval1 <- intervals, interval2 <- intervals, interval1 != interval2 do type1 = - create_fee_type( - %{ - interval: interval1, - name: "Type #{interval1} #{System.unique_integer([:positive])}" - }, - actor - ) + create_fee_type(%{ + interval: interval1, + name: "Type #{interval1} #{System.unique_integer([:positive])}" + }) type2 = - create_fee_type( - %{ - interval: interval2, - name: "Type #{interval2} #{System.unique_integer([:positive])}" - }, - actor - ) + create_fee_type(%{ + interval: interval2, + name: "Type #{interval2} #{System.unique_integer([:positive])}" + }) - member = create_member(%{membership_fee_type_id: type1.id}, actor) + member = create_member(%{membership_fee_type_id: type1.id}) changeset = member @@ -191,11 +180,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do end describe "integration with update_member action" do - test "validation works when updating member via update_member action", %{actor: actor} do - yearly_type = create_fee_type(%{interval: :yearly}, actor) - monthly_type = create_fee_type(%{interval: :monthly}, actor) + test "validation works when updating member via update_member action" do + yearly_type = create_fee_type(%{interval: :yearly}) + monthly_type = create_fee_type(%{interval: :monthly}) - member = create_member(%{membership_fee_type_id: yearly_type.id}, actor) + member = create_member(%{membership_fee_type_id: yearly_type.id}) # Try to update member with different interval type assert {:error, %Ash.Error.Invalid{} = error} = @@ -203,7 +192,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: monthly_type.id }) - |> Ash.update(actor: actor) + |> Ash.update() # Check that error is about interval mismatch error_message = extract_error_message(error) @@ -212,11 +201,11 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do assert error_message =~ "same-interval" end - test "allows update when interval matches", %{actor: actor} do - yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}, actor) - yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}, actor) + test "allows update when interval matches" do + yearly_type1 = create_fee_type(%{interval: :yearly, name: "Yearly Type 1"}) + yearly_type2 = create_fee_type(%{interval: :yearly, name: "Yearly Type 2"}) - member = create_member(%{membership_fee_type_id: yearly_type1.id}, actor) + member = create_member(%{membership_fee_type_id: yearly_type1.id}) # Update member with same-interval type assert {:ok, updated_member} = @@ -224,7 +213,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do |> Ash.Changeset.for_update(:update_member, %{ membership_fee_type_id: yearly_type2.id }) - |> Ash.update(actor: actor) + |> Ash.update() assert updated_member.membership_fee_type_id == yearly_type2.id end diff --git a/test/membership_fees/foreign_key_test.exs b/test/membership_fees/foreign_key_test.exs index 54a7cc5..dd164a7 100644 --- a/test/membership_fees/foreign_key_test.exs +++ b/test/membership_fees/foreign_key_test.exs @@ -8,287 +8,211 @@ defmodule Mv.MembershipFees.ForeignKeyTest do alias Mv.MembershipFees.MembershipFeeType alias Mv.Membership.Member - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "CASCADE behavior" do - test "deleting member deletes associated membership_fee_cycles", %{actor: actor} do + test "deleting member deletes associated membership_fee_cycles" do # Create member {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Cascade", - last_name: "Test", - email: "cascade.test.#{System.unique_integer([:positive])}@example.com" - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Cascade", + last_name: "Test", + email: "cascade.test.#{System.unique_integer([:positive])}@example.com" + }) # Create fee type {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Cascade Test Fee #{System.unique_integer([:positive])}", - amount: Decimal.new("100.00"), - interval: :monthly - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Cascade Test Fee #{System.unique_integer([:positive])}", + amount: Decimal.new("100.00"), + interval: :monthly + }) # Create multiple cycles for this member {:ok, cycle1} = - Ash.create( - MembershipFeeCycle, - %{ - cycle_start: ~D[2025-01-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(MembershipFeeCycle, %{ + cycle_start: ~D[2025-01-01], + amount: Decimal.new("100.00"), + member_id: member.id, + membership_fee_type_id: fee_type.id + }) {:ok, cycle2} = - Ash.create( - MembershipFeeCycle, - %{ - cycle_start: ~D[2025-02-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(MembershipFeeCycle, %{ + cycle_start: ~D[2025-02-01], + amount: Decimal.new("100.00"), + member_id: member.id, + membership_fee_type_id: fee_type.id + }) # Verify cycles exist - assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle1.id, actor: actor) - assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle2.id, actor: actor) + assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle1.id) + assert {:ok, _} = Ash.get(MembershipFeeCycle, cycle2.id) # Delete member - assert :ok = Ash.destroy(member, actor: actor) + assert :ok = Ash.destroy(member) # Verify cycles are also deleted (CASCADE) # NotFound is wrapped in Ash.Error.Invalid - assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle1.id, actor: actor) - assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle2.id, actor: actor) + assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle1.id) + assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeCycle, cycle2.id) end end describe "RESTRICT behavior" do - test "cannot delete membership_fee_type if cycles reference it", %{actor: actor} do + test "cannot delete membership_fee_type if cycles reference it" do # Create member {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Restrict", - last_name: "Test", - email: "restrict.test.#{System.unique_integer([:positive])}@example.com" - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Restrict", + last_name: "Test", + email: "restrict.test.#{System.unique_integer([:positive])}@example.com" + }) # Create fee type {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Restrict Test Fee #{System.unique_integer([:positive])}", - amount: Decimal.new("100.00"), - interval: :monthly - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Restrict Test Fee #{System.unique_integer([:positive])}", + amount: Decimal.new("100.00"), + interval: :monthly + }) # Create a cycle referencing this fee type {:ok, _cycle} = - Ash.create( - MembershipFeeCycle, - %{ - cycle_start: ~D[2025-01-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(MembershipFeeCycle, %{ + cycle_start: ~D[2025-01-01], + amount: Decimal.new("100.00"), + member_id: member.id, + membership_fee_type_id: fee_type.id + }) # Try to delete fee type - should fail due to RESTRICT - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) # Check that it's a foreign key violation error assert is_struct(error, Ash.Error.Invalid) or is_struct(error, Ash.Error.Unknown) end - test "can delete membership_fee_type if no cycles reference it", %{actor: actor} do + test "can delete membership_fee_type if no cycles reference it" do # Create fee type without any cycles {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Deletable Fee #{System.unique_integer([:positive])}", - amount: Decimal.new("100.00"), - interval: :monthly - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Deletable Fee #{System.unique_integer([:positive])}", + amount: Decimal.new("100.00"), + interval: :monthly + }) # Should be able to delete - assert :ok = Ash.destroy(fee_type, actor: actor) + assert :ok = Ash.destroy(fee_type) # Verify it's gone (NotFound is wrapped in Ash.Error.Invalid) - assert {:error, %Ash.Error.Invalid{}} = - Ash.get(MembershipFeeType, fee_type.id, actor: actor) + assert {:error, %Ash.Error.Invalid{}} = Ash.get(MembershipFeeType, fee_type.id) end - test "cannot delete membership_fee_type if members reference it", %{actor: actor} do + test "cannot delete membership_fee_type if members reference it" do # Create fee type {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Member Ref Fee #{System.unique_integer([:positive])}", - amount: Decimal.new("100.00"), - interval: :monthly - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Member Ref Fee #{System.unique_integer([:positive])}", + amount: Decimal.new("100.00"), + interval: :monthly + }) # Create member with this fee type {:ok, _member} = - Ash.create( - Member, - %{ - first_name: "FeeType", - last_name: "Reference", - email: "feetype.ref.#{System.unique_integer([:positive])}@example.com", - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "FeeType", + last_name: "Reference", + email: "feetype.ref.#{System.unique_integer([:positive])}@example.com", + membership_fee_type_id: fee_type.id + }) # Try to delete fee type - should fail due to RESTRICT - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) assert is_struct(error, Ash.Error.Invalid) or is_struct(error, Ash.Error.Unknown) end end describe "member extensions" do - test "member can be created with membership_fee_type_id", %{actor: actor} do + test "member can be created with membership_fee_type_id" do # Create fee type first {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Create Test Fee #{System.unique_integer([:positive])}", - amount: Decimal.new("100.00"), - interval: :yearly - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Create Test Fee #{System.unique_integer([:positive])}", + amount: Decimal.new("100.00"), + interval: :yearly + }) # Create member with fee type {:ok, member} = - Ash.create( - Member, - %{ - first_name: "With", - last_name: "FeeType", - email: "with.feetype.#{System.unique_integer([:positive])}@example.com", - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "With", + last_name: "FeeType", + email: "with.feetype.#{System.unique_integer([:positive])}@example.com", + membership_fee_type_id: fee_type.id + }) assert member.membership_fee_type_id == fee_type.id end - test "member can be created with membership_fee_start_date", %{actor: actor} do + test "member can be created with membership_fee_start_date" do {:ok, member} = - Ash.create( - Member, - %{ - first_name: "With", - last_name: "StartDate", - email: "with.startdate.#{System.unique_integer([:positive])}@example.com", - membership_fee_start_date: ~D[2025-01-01] - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "With", + last_name: "StartDate", + email: "with.startdate.#{System.unique_integer([:positive])}@example.com", + membership_fee_start_date: ~D[2025-01-01] + }) assert member.membership_fee_start_date == ~D[2025-01-01] end - test "member can be created without membership fee fields", %{actor: actor} do + test "member can be created without membership fee fields" do {:ok, member} = - Ash.create( - Member, - %{ - first_name: "No", - last_name: "FeeFields", - email: "no.feefields.#{System.unique_integer([:positive])}@example.com" - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "No", + last_name: "FeeFields", + email: "no.feefields.#{System.unique_integer([:positive])}@example.com" + }) assert member.membership_fee_type_id == nil assert member.membership_fee_start_date == nil end - test "member can be updated with membership_fee_type_id", %{actor: actor} do + test "member can be updated with membership_fee_type_id" do # Create fee type {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Update Test Fee #{System.unique_integer([:positive])}", - amount: Decimal.new("100.00"), - interval: :yearly - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Update Test Fee #{System.unique_integer([:positive])}", + amount: Decimal.new("100.00"), + interval: :yearly + }) # Create member without fee type {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Update", - last_name: "Test", - email: "update.test.#{System.unique_integer([:positive])}@example.com" - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Update", + last_name: "Test", + email: "update.test.#{System.unique_integer([:positive])}@example.com" + }) assert member.membership_fee_type_id == nil # Update member with fee type - {:ok, updated_member} = - Ash.update(member, %{membership_fee_type_id: fee_type.id}, actor: actor) + {:ok, updated_member} = Ash.update(member, %{membership_fee_type_id: fee_type.id}) assert updated_member.membership_fee_type_id == fee_type.id end - test "member can be updated with membership_fee_start_date", %{actor: actor} do + test "member can be updated with membership_fee_start_date" do {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Start", - last_name: "Date", - email: "start.date.#{System.unique_integer([:positive])}@example.com" - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Start", + last_name: "Date", + email: "start.date.#{System.unique_integer([:positive])}@example.com" + }) assert member.membership_fee_start_date == nil - {:ok, updated_member} = - Ash.update(member, %{membership_fee_start_date: ~D[2025-06-01]}, actor: actor) + {:ok, updated_member} = Ash.update(member, %{membership_fee_start_date: ~D[2025-06-01]}) assert updated_member.membership_fee_start_date == ~D[2025-06-01] end diff --git a/test/membership_fees/member_cycle_integration_test.exs b/test/membership_fees/member_cycle_integration_test.exs index 6d5bc2e..5d1cf28 100644 --- a/test/membership_fees/member_cycle_integration_test.exs +++ b/test/membership_fees/member_cycle_integration_test.exs @@ -10,13 +10,8 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -27,30 +22,30 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to set up settings - defp setup_settings(include_joining_cycle, actor) do + defp setup_settings(include_joining_cycle) do {:ok, settings} = Mv.Membership.get_settings() settings |> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle}) - |> Ash.update!(actor: actor) + |> Ash.update!() end # Helper to get cycles for a member - defp get_member_cycles(member_id, actor) do + defp get_member_cycles(member_id) do MembershipFeeCycle |> Ash.Query.filter(member_id == ^member_id) |> Ash.Query.sort(cycle_start: :asc) - |> Ash.read!(actor: actor) + |> Ash.read!() end describe "member creation triggers cycle generation" do - test "creates cycles when member is created with fee type and join_date", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "creates cycles when member is created with fee type and join_date" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) member = Member @@ -61,9 +56,9 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do join_date: ~D[2023-03-15], membership_fee_type_id: fee_type.id }) - |> Ash.create!(actor: actor) + |> Ash.create!() - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have cycles for 2023 and 2024 (and possibly current year) assert length(cycles) >= 2 @@ -77,8 +72,8 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do end) end - test "does not create cycles when member has no fee type", %{actor: actor} do - setup_settings(true, actor) + test "does not create cycles when member has no fee type" do + setup_settings(true) member = Member @@ -89,16 +84,16 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do join_date: ~D[2023-03-15] # No membership_fee_type_id }) - |> Ash.create!(actor: actor) + |> Ash.create!() - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) assert cycles == [] end - test "does not create cycles when member has no join_date", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "does not create cycles when member has no join_date" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) member = Member @@ -109,18 +104,18 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do membership_fee_type_id: fee_type.id # No join_date }) - |> Ash.create!(actor: actor) + |> Ash.create!() - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) assert cycles == [] end end describe "member update triggers cycle generation" do - test "generates cycles when fee type is assigned to existing member", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "generates cycles when fee type is assigned to existing member" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create member without fee type member = @@ -131,17 +126,17 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-03-15] }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Verify no cycles yet - assert get_member_cycles(member.id, actor) == [] + assert get_member_cycles(member.id) == [] # Update to assign fee type member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have generated cycles assert length(cycles) >= 2 @@ -149,9 +144,9 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do end describe "concurrent cycle generation" do - test "handles multiple members being created concurrently", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "handles multiple members being created concurrently" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create multiple members concurrently tasks = @@ -165,7 +160,7 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do join_date: ~D[2023-03-15], membership_fee_type_id: fee_type.id }) - |> Ash.create!(actor: actor) + |> Ash.create!() end) end) @@ -173,16 +168,16 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do # Each member should have cycles Enum.each(members, fn member -> - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) assert length(cycles) >= 2, "Member #{member.id} should have at least 2 cycles" end) end end describe "idempotent cycle generation" do - test "running generation multiple times does not create duplicate cycles", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "running generation multiple times does not create duplicate cycles" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) member = Member @@ -193,9 +188,9 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do join_date: ~D[2023-03-15], membership_fee_type_id: fee_type.id }) - |> Ash.create!(actor: actor) + |> Ash.create!() - initial_cycles = get_member_cycles(member.id, actor) + initial_cycles = get_member_cycles(member.id) initial_count = length(initial_cycles) # Use a fixed "today" date to avoid date dependency @@ -206,7 +201,7 @@ defmodule Mv.MembershipFees.MemberCycleIntegrationTest do {:ok, _, _} = Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member.id, today: today) - final_cycles = get_member_cycles(member.id, actor) + final_cycles = get_member_cycles(member.id) final_count = length(final_cycles) # Should have same number of cycles (idempotent) diff --git a/test/membership_fees/membership_fee_cycle_test.exs b/test/membership_fees/membership_fee_cycle_test.exs index 46d6216..14bdf4b 100644 --- a/test/membership_fees/membership_fee_cycle_test.exs +++ b/test/membership_fees/membership_fee_cycle_test.exs @@ -8,13 +8,8 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do alias Mv.MembershipFees.MembershipFeeType alias Mv.Membership.Member - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -25,11 +20,11 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a member - defp create_member(attrs, actor) do + defp create_member(attrs) do default_attrs = %{ first_name: "Test", last_name: "Member", @@ -40,11 +35,11 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a cycle - defp create_cycle(member, fee_type, attrs, actor) do + defp create_cycle(member, fee_type, attrs) do default_attrs = %{ cycle_start: ~D[2024-01-01], amount: Decimal.new("50.00"), @@ -56,13 +51,13 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end describe "status defaults" do - test "status defaults to :unpaid when creating a cycle", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "status defaults to :unpaid when creating a cycle" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) cycle = MembershipFeeCycle @@ -72,30 +67,29 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do member_id: member.id, membership_fee_type_id: fee_type.id }) - |> Ash.create!(actor: actor) + |> Ash.create!() assert cycle.status == :unpaid end end describe "mark_as_paid" do - test "sets status to :paid", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) + test "sets status to :paid" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :unpaid}) - assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_paid) + assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_paid) assert updated.status == :paid end - test "can set notes when marking as paid", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) + test "can set notes when marking as paid" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :unpaid}) assert {:ok, updated} = Ash.update(cycle, %{notes: "Payment received via bank transfer"}, - actor: actor, action: :mark_as_paid ) @@ -103,34 +97,33 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do assert updated.notes == "Payment received via bank transfer" end - test "can change from suspended to paid", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :suspended}, actor) + test "can change from suspended to paid" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :suspended}) - assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_paid) + assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_paid) assert updated.status == :paid end end describe "mark_as_suspended" do - test "sets status to :suspended", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) + test "sets status to :suspended" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :unpaid}) - assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_suspended) + assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_suspended) assert updated.status == :suspended end - test "can set notes when marking as suspended", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :unpaid}, actor) + test "can set notes when marking as suspended" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :unpaid}) assert {:ok, updated} = Ash.update(cycle, %{notes: "Waived due to special circumstances"}, - actor: actor, action: :mark_as_suspended ) @@ -138,45 +131,42 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do assert updated.notes == "Waived due to special circumstances" end - test "can change from paid to suspended", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :paid}, actor) + test "can change from paid to suspended" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :paid}) - assert {:ok, updated} = Ash.update(cycle, %{}, actor: actor, action: :mark_as_suspended) + assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_suspended) assert updated.status == :suspended end end describe "mark_as_unpaid" do - test "sets status to :unpaid", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :paid}, actor) + test "sets status to :unpaid" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :paid}) assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid) assert updated.status == :unpaid end - test "can set notes when marking as unpaid", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :paid}, actor) + test "can set notes when marking as unpaid" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :paid}) assert {:ok, updated} = - Ash.update(cycle, %{notes: "Payment was reversed"}, - actor: actor, - action: :mark_as_unpaid - ) + Ash.update(cycle, %{notes: "Payment was reversed"}, action: :mark_as_unpaid) assert updated.status == :unpaid assert updated.notes == "Payment was reversed" end - test "can change from suspended to unpaid", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) - cycle = create_cycle(member, fee_type, %{status: :suspended}, actor) + test "can change from suspended to unpaid" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) + cycle = create_cycle(member, fee_type, %{status: :suspended}) assert {:ok, updated} = Ash.update(cycle, %{}, action: :mark_as_unpaid) assert updated.status == :unpaid @@ -184,33 +174,33 @@ defmodule Mv.MembershipFees.MembershipFeeCycleTest do end describe "status transitions" do - test "all status transitions are allowed", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) - member = create_member(%{membership_fee_type_id: fee_type.id}, actor) + test "all status transitions are allowed" do + fee_type = create_fee_type(%{interval: :yearly}) + member = create_member(%{membership_fee_type_id: fee_type.id}) # unpaid -> paid - cycle1 = create_cycle(member, fee_type, %{status: :unpaid}, actor) - assert {:ok, c1} = Ash.update(cycle1, %{}, actor: actor, action: :mark_as_paid) + cycle1 = create_cycle(member, fee_type, %{status: :unpaid}) + assert {:ok, c1} = Ash.update(cycle1, %{}, action: :mark_as_paid) assert c1.status == :paid # paid -> suspended - assert {:ok, c2} = Ash.update(c1, %{}, actor: actor, action: :mark_as_suspended) + assert {:ok, c2} = Ash.update(c1, %{}, action: :mark_as_suspended) assert c2.status == :suspended # suspended -> unpaid - assert {:ok, c3} = Ash.update(c2, %{}, actor: actor, action: :mark_as_unpaid) + assert {:ok, c3} = Ash.update(c2, %{}, action: :mark_as_unpaid) assert c3.status == :unpaid # unpaid -> suspended - assert {:ok, c4} = Ash.update(c3, %{}, actor: actor, action: :mark_as_suspended) + assert {:ok, c4} = Ash.update(c3, %{}, action: :mark_as_suspended) assert c4.status == :suspended # suspended -> paid - assert {:ok, c5} = Ash.update(c4, %{}, actor: actor, action: :mark_as_paid) + assert {:ok, c5} = Ash.update(c4, %{}, action: :mark_as_paid) assert c5.status == :paid # paid -> unpaid - assert {:ok, c6} = Ash.update(c5, %{}, actor: actor, action: :mark_as_unpaid) + assert {:ok, c6} = Ash.update(c5, %{}, action: :mark_as_unpaid) assert c6.status == :unpaid end end diff --git a/test/membership_fees/membership_fee_type_integration_test.exs b/test/membership_fees/membership_fee_type_integration_test.exs index e716b42..681bd02 100644 --- a/test/membership_fees/membership_fee_type_integration_test.exs +++ b/test/membership_fees/membership_fee_type_integration_test.exs @@ -10,13 +10,8 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -27,11 +22,11 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end describe "admin can create membership fee type" do - test "creates type with all fields", %{actor: actor} do + test "creates type with all fields" do attrs = %{ name: "Standard Membership", amount: Decimal.new("120.00"), @@ -39,8 +34,7 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do description: "Standard yearly membership fee" } - assert {:ok, %MembershipFeeType{} = fee_type} = - Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, %MembershipFeeType{} = fee_type} = Ash.create(MembershipFeeType, attrs) assert fee_type.name == "Standard Membership" assert Decimal.equal?(fee_type.amount, Decimal.new("120.00")) @@ -50,106 +44,88 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do end describe "admin can update membership fee type" do - setup %{actor: actor} do + setup do {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Original Name", - amount: Decimal.new("100.00"), - interval: :yearly, - description: "Original description" - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Original Name", + amount: Decimal.new("100.00"), + interval: :yearly, + description: "Original description" + }) %{fee_type: fee_type} end - test "can update name", %{actor: actor, fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}, actor: actor) + test "can update name", %{fee_type: fee_type} do + assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}) assert updated.name == "Updated Name" end - test "can update amount", %{actor: actor, fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}, actor: actor) + test "can update amount", %{fee_type: fee_type} do + assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}) assert Decimal.equal?(updated.amount, Decimal.new("150.00")) end - test "can update description", %{actor: actor, fee_type: fee_type} do - assert {:ok, updated} = - Ash.update(fee_type, %{description: "Updated description"}, actor: actor) - + test "can update description", %{fee_type: fee_type} do + assert {:ok, updated} = Ash.update(fee_type, %{description: "Updated description"}) assert updated.description == "Updated description" end - test "cannot update interval", %{actor: actor, fee_type: fee_type} do + test "cannot update interval", %{fee_type: fee_type} do # Currently, interval is not in the accept list, so it's rejected as "NoSuchInput" # After implementing validation, it should return a validation error - assert {:error, error} = Ash.update(fee_type, %{interval: :monthly}, actor: actor) + assert {:error, error} = Ash.update(fee_type, %{interval: :monthly}) # For now, check that it's an error (either NoSuchInput or validation error) assert %Ash.Error.Invalid{} = error end end describe "admin cannot delete membership fee type when in use" do - test "cannot delete when members are assigned", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "cannot delete when members are assigned" do + fee_type = create_fee_type(%{interval: :yearly}) # Create a member with this fee type {:ok, _member} = - Ash.create( - Member, - %{ - first_name: "Test", - last_name: "Member", - email: "test.member.#{System.unique_integer([:positive])}@example.com", - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Test", + last_name: "Member", + email: "test.member.#{System.unique_integer([:positive])}@example.com", + membership_fee_type_id: fee_type.id + }) - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) error_message = extract_error_message(error) assert error_message =~ "member(s) are assigned" end - test "cannot delete when cycles exist", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "cannot delete when cycles exist" do + fee_type = create_fee_type(%{interval: :yearly}) # Create a member with this fee type {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Test", - last_name: "Member", - email: "test.member.#{System.unique_integer([:positive])}@example.com", - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Test", + last_name: "Member", + email: "test.member.#{System.unique_integer([:positive])}@example.com", + membership_fee_type_id: fee_type.id + }) # Create a cycle for this fee type {:ok, _cycle} = - Ash.create( - MembershipFeeCycle, - %{ - cycle_start: ~D[2025-01-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(MembershipFeeCycle, %{ + cycle_start: ~D[2025-01-01], + amount: Decimal.new("100.00"), + member_id: member.id, + membership_fee_type_id: fee_type.id + }) - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) error_message = extract_error_message(error) assert error_message =~ "cycle(s) reference" end - test "cannot delete when used as default in settings", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "cannot delete when used as default in settings" do + fee_type = create_fee_type(%{interval: :yearly}) # Set as default in settings {:ok, settings} = Mv.Membership.get_settings() @@ -158,19 +134,19 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: fee_type.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Try to delete - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) error_message = extract_error_message(error) assert error_message =~ "used as default in settings" end end describe "settings integration" do - test "default_membership_fee_type_id is used during member creation", %{actor: actor} do + test "default_membership_fee_type_id is used during member creation" do # Create a fee type - fee_type = create_fee_type(%{interval: :yearly}, actor) + fee_type = create_fee_type(%{interval: :yearly}) # Set it as default in settings {:ok, settings} = Mv.Membership.get_settings() @@ -179,33 +155,29 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: fee_type.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Create a member without explicitly setting membership_fee_type_id # The Member resource automatically assigns the default_membership_fee_type_id # during creation via SetDefaultMembershipFeeType change. {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Test", - last_name: "Member", - email: "test.member.#{System.unique_integer([:positive])}@example.com" - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Test", + last_name: "Member", + email: "test.member.#{System.unique_integer([:positive])}@example.com" + }) # Verify that the default membership fee type was automatically assigned assert member.membership_fee_type_id == fee_type.id end - test "include_joining_cycle is used during cycle generation", %{actor: actor} do + test "include_joining_cycle is used during cycle generation" do # This test verifies that the include_joining_cycle setting affects # cycle generation. The actual cycle generation logic is tested in # CycleGeneratorTest, but this integration test ensures the setting # is properly used. - fee_type = create_fee_type(%{interval: :yearly}, actor) + fee_type = create_fee_type(%{interval: :yearly}) # Set include_joining_cycle to false {:ok, settings} = Mv.Membership.get_settings() @@ -214,21 +186,17 @@ defmodule Mv.MembershipFees.MembershipFeeTypeIntegrationTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ include_joining_cycle: false }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Create a member with join_date in the middle of a year {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Test", - last_name: "Member", - email: "test.member.#{System.unique_integer([:positive])}@example.com", - join_date: ~D[2023-03-15], - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Test", + last_name: "Member", + email: "test.member.#{System.unique_integer([:positive])}@example.com", + join_date: ~D[2023-03-15], + membership_fee_type_id: fee_type.id + }) # Verify that membership_fee_start_date was calculated correctly # (should be 2024-01-01, not 2023-01-01, because include_joining_cycle = false) diff --git a/test/membership_fees/membership_fee_type_test.exs b/test/membership_fees/membership_fee_type_test.exs index 80b7839..626e096 100644 --- a/test/membership_fees/membership_fee_type_test.exs +++ b/test/membership_fees/membership_fee_type_test.exs @@ -6,13 +6,8 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do alias Mv.MembershipFees.MembershipFeeType - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "create MembershipFeeType" do - test "can create membership fee type with valid attributes", %{actor: actor} do + test "can create membership fee type with valid attributes" do attrs = %{ name: "Standard Membership", amount: Decimal.new("120.00"), @@ -21,7 +16,7 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do } assert {:ok, %MembershipFeeType{} = fee_type} = - Ash.create(MembershipFeeType, attrs, actor: actor) + Ash.create(MembershipFeeType, attrs) assert fee_type.name == "Standard Membership" assert Decimal.equal?(fee_type.amount, Decimal.new("120.00")) @@ -29,237 +24,212 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do assert fee_type.description == "Standard yearly membership fee" end - test "can create membership fee type without description", %{actor: actor} do + test "can create membership fee type without description" do attrs = %{ name: "Basic", amount: Decimal.new("60.00"), interval: :monthly } - assert {:ok, %MembershipFeeType{}} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, %MembershipFeeType{}} = Ash.create(MembershipFeeType, attrs) end - test "requires name", %{actor: actor} do + test "requires name" do attrs = %{ amount: Decimal.new("100.00"), interval: :yearly } - assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:error, error} = Ash.create(MembershipFeeType, attrs) assert error_on_field?(error, :name) end - test "requires amount", %{actor: actor} do + test "requires amount" do attrs = %{ name: "Test Fee", interval: :yearly } - assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:error, error} = Ash.create(MembershipFeeType, attrs) assert error_on_field?(error, :amount) end - test "requires interval", %{actor: actor} do + test "requires interval" do attrs = %{ name: "Test Fee", amount: Decimal.new("100.00") } - assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:error, error} = Ash.create(MembershipFeeType, attrs) assert error_on_field?(error, :interval) end - test "validates interval enum values - monthly", %{actor: actor} do + test "validates interval enum values - monthly" do attrs = %{name: "Monthly", amount: Decimal.new("10.00"), interval: :monthly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) assert fee_type.interval == :monthly end - test "validates interval enum values - quarterly", %{actor: actor} do + test "validates interval enum values - quarterly" do attrs = %{name: "Quarterly", amount: Decimal.new("30.00"), interval: :quarterly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) assert fee_type.interval == :quarterly end - test "validates interval enum values - half_yearly", %{actor: actor} do + test "validates interval enum values - half_yearly" do attrs = %{name: "Half Yearly", amount: Decimal.new("60.00"), interval: :half_yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) assert fee_type.interval == :half_yearly end - test "validates interval enum values - yearly", %{actor: actor} do + test "validates interval enum values - yearly" do attrs = %{name: "Yearly", amount: Decimal.new("120.00"), interval: :yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) assert fee_type.interval == :yearly end - test "rejects invalid interval values", %{actor: actor} do + test "rejects invalid interval values" do attrs = %{name: "Invalid", amount: Decimal.new("100.00"), interval: :weekly} - assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:error, error} = Ash.create(MembershipFeeType, attrs) assert error_on_field?(error, :interval) end - test "name must be unique", %{actor: actor} do + test "name must be unique" do attrs = %{name: "Unique Name", amount: Decimal.new("100.00"), interval: :yearly} - assert {:ok, _} = Ash.create(MembershipFeeType, attrs, actor: actor) - assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, _} = Ash.create(MembershipFeeType, attrs) + assert {:error, error} = Ash.create(MembershipFeeType, attrs) # Check for uniqueness error assert error_on_field?(error, :name) end - test "rejects negative amount", %{actor: actor} do + test "rejects negative amount" do attrs = %{name: "Negative Test", amount: Decimal.new("-10.00"), interval: :yearly} - assert {:error, error} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:error, error} = Ash.create(MembershipFeeType, attrs) assert error_on_field?(error, :amount) end - test "accepts zero amount", %{actor: actor} do + test "accepts zero amount" do attrs = %{name: "Zero Amount", amount: Decimal.new("0.00"), interval: :yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) assert Decimal.equal?(fee_type.amount, Decimal.new("0.00")) end - test "amount respects scale of 2 decimal places", %{actor: actor} do + test "amount respects scale of 2 decimal places" do attrs = %{name: "Scale Test", amount: Decimal.new("100.50"), interval: :yearly} - assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor) + assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs) assert Decimal.equal?(fee_type.amount, Decimal.new("100.50")) end end describe "update MembershipFeeType" do - setup %{actor: actor} do + setup do {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Original Name", - amount: Decimal.new("100.00"), - interval: :yearly, - description: "Original description" - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Original Name", + amount: Decimal.new("100.00"), + interval: :yearly, + description: "Original description" + }) %{fee_type: fee_type} end - test "can update name", %{actor: actor, fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}, actor: actor) + test "can update name", %{fee_type: fee_type} do + assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}) assert updated.name == "Updated Name" end - test "can update amount", %{actor: actor, fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}, actor: actor) + test "can update amount", %{fee_type: fee_type} do + assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}) assert Decimal.equal?(updated.amount, Decimal.new("150.00")) end - test "can update description", %{actor: actor, fee_type: fee_type} do - assert {:ok, updated} = - Ash.update(fee_type, %{description: "Updated description"}, actor: actor) - + test "can update description", %{fee_type: fee_type} do + assert {:ok, updated} = Ash.update(fee_type, %{description: "Updated description"}) assert updated.description == "Updated description" end - test "can clear description", %{actor: actor, fee_type: fee_type} do - assert {:ok, updated} = Ash.update(fee_type, %{description: nil}, actor: actor) + test "can clear description", %{fee_type: fee_type} do + assert {:ok, updated} = Ash.update(fee_type, %{description: nil}) assert updated.description == nil end - test "interval immutability: update fails when interval is changed", %{ - actor: actor, - fee_type: fee_type - } do + test "interval immutability: update fails when interval is changed", %{fee_type: fee_type} do # Currently, interval is not in the accept list, so it's rejected as "NoSuchInput" # After implementing validation, it should return a validation error - assert {:error, error} = Ash.update(fee_type, %{interval: :monthly}, actor: actor) + assert {:error, error} = Ash.update(fee_type, %{interval: :monthly}) # For now, check that it's an error (either NoSuchInput or validation error) assert %Ash.Error.Invalid{} = error end end describe "delete MembershipFeeType" do - setup %{actor: actor} do + setup do {:ok, fee_type} = - Ash.create( - MembershipFeeType, - %{ - name: "Test Fee Type #{System.unique_integer([:positive])}", - amount: Decimal.new("100.00"), - interval: :yearly - }, - actor: actor - ) + Ash.create(MembershipFeeType, %{ + name: "Test Fee Type #{System.unique_integer([:positive])}", + amount: Decimal.new("100.00"), + interval: :yearly + }) %{fee_type: fee_type} end - test "can delete when not in use", %{actor: actor, fee_type: fee_type} do - result = Ash.destroy(fee_type, actor: actor) + test "can delete when not in use", %{fee_type: fee_type} do + result = Ash.destroy(fee_type) # Ash.destroy returns :ok or {:ok, _} depending on version assert result == :ok or match?({:ok, _}, result) end - test "cannot delete when members are assigned", %{actor: actor, fee_type: fee_type} do + test "cannot delete when members are assigned", %{fee_type: fee_type} do alias Mv.Membership.Member # Create a member with this fee type {:ok, _member} = - Ash.create( - Member, - %{ - first_name: "Test", - last_name: "Member", - email: "test.member.#{System.unique_integer([:positive])}@example.com", - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Test", + last_name: "Member", + email: "test.member.#{System.unique_integer([:positive])}@example.com", + membership_fee_type_id: fee_type.id + }) - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) # Check for either validation error message or DB constraint error error_message = extract_error_message(error) assert error_message =~ "member" or error_message =~ "referenced" end - test "cannot delete when cycles exist", %{actor: actor, fee_type: fee_type} do + test "cannot delete when cycles exist", %{fee_type: fee_type} do alias Mv.MembershipFees.MembershipFeeCycle alias Mv.Membership.Member # Create a member with this fee type {:ok, member} = - Ash.create( - Member, - %{ - first_name: "Test", - last_name: "Member", - email: "test.member.#{System.unique_integer([:positive])}@example.com", - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(Member, %{ + first_name: "Test", + last_name: "Member", + email: "test.member.#{System.unique_integer([:positive])}@example.com", + membership_fee_type_id: fee_type.id + }) # Create a cycle for this fee type {:ok, _cycle} = - Ash.create( - MembershipFeeCycle, - %{ - cycle_start: ~D[2025-01-01], - amount: Decimal.new("100.00"), - member_id: member.id, - membership_fee_type_id: fee_type.id - }, - actor: actor - ) + Ash.create(MembershipFeeCycle, %{ + cycle_start: ~D[2025-01-01], + amount: Decimal.new("100.00"), + member_id: member.id, + membership_fee_type_id: fee_type.id + }) - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) # Check for either validation error message or DB constraint error error_message = extract_error_message(error) assert error_message =~ "cycle" or error_message =~ "referenced" end - test "cannot delete when used as default in settings", %{actor: actor, fee_type: fee_type} do + test "cannot delete when used as default in settings", %{fee_type: fee_type} do # Set as default in settings {:ok, settings} = Mv.Membership.get_settings() @@ -267,10 +237,10 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: fee_type.id }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Try to delete - assert {:error, error} = Ash.destroy(fee_type, actor: actor) + assert {:error, error} = Ash.destroy(fee_type) error_message = extract_error_message(error) assert error_message =~ "used as default in settings" end diff --git a/test/mv/accounts/user_policies_test.exs b/test/mv/accounts/user_policies_test.exs index 7676403..bacb19d 100644 --- a/test/mv/accounts/user_policies_test.exs +++ b/test/mv/accounts/user_policies_test.exs @@ -14,23 +14,15 @@ defmodule Mv.Accounts.UserPoliciesTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a role with a specific permission set - defp create_role_with_permission_set(permission_set_name, actor) do + defp create_role_with_permission_set(permission_set_name) do 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: actor - ) do + case Authorization.create_role(%{ + name: role_name, + description: "Test role for #{permission_set_name}", + permission_set_name: permission_set_name + }) do {:ok, role} -> role {:error, error} -> raise "Failed to create role: #{inspect(error)}" end @@ -38,9 +30,9 @@ defmodule Mv.Accounts.UserPoliciesTest do # Helper to create a user with a specific permission set # Returns user with role preloaded (required for authorization) - defp create_user_with_permission_set(permission_set_name, actor) do + defp create_user_with_permission_set(permission_set_name) do # Create role with permission set - role = create_role_with_permission_set(permission_set_name, actor) + role = create_role_with_permission_set(permission_set_name) # Create user {:ok, user} = @@ -49,40 +41,39 @@ defmodule Mv.Accounts.UserPoliciesTest do email: "user#{System.unique_integer([:positive])}@example.com", password: "testpassword123" }) - |> Ash.create(actor: actor) + |> 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(actor: actor) + |> Ash.update() # Reload user with role preloaded (critical for authorization!) - {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor) + {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts) user_with_role end # Helper to create another user (for testing access to other users) - defp create_other_user(actor) do - create_user_with_permission_set("own_data", actor) + defp create_other_user do + create_user_with_permission_set("own_data") end # Shared test setup for permission sets with scope :own access - defp setup_user_with_own_access(permission_set, actor) do - user = create_user_with_permission_set(permission_set, actor) - other_user = create_other_user(actor) + defp setup_user_with_own_access(permission_set) do + user = create_user_with_permission_set(permission_set) + other_user = create_other_user() # Reload user to ensure role is preloaded - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) %{user: user, other_user: other_user} end describe "own_data permission set (Mitglied)" do - setup %{actor: actor} do - setup_user_with_own_access("own_data", actor) + setup do + setup_user_with_own_access("own_data") end test "can read own user record", %{user: user} do @@ -149,8 +140,8 @@ defmodule Mv.Accounts.UserPoliciesTest do end describe "read_only permission set (Vorstand/Buchhaltung)" do - setup %{actor: actor} do - setup_user_with_own_access("read_only", actor) + setup do + setup_user_with_own_access("read_only") end test "can read own user record", %{user: user} do @@ -217,8 +208,8 @@ defmodule Mv.Accounts.UserPoliciesTest do end describe "normal_user permission set (Kassenwart)" do - setup %{actor: actor} do - setup_user_with_own_access("normal_user", actor) + setup do + setup_user_with_own_access("normal_user") end test "can read own user record", %{user: user} do @@ -285,13 +276,12 @@ defmodule Mv.Accounts.UserPoliciesTest do end describe "admin permission set" do - setup %{actor: actor} do - user = create_user_with_permission_set("admin", actor) - other_user = create_other_user(actor) + setup do + user = create_user_with_permission_set("admin") + other_user = create_other_user() # Reload user to ensure role is preloaded - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) %{user: user, other_user: other_user} end @@ -343,29 +333,21 @@ defmodule Mv.Accounts.UserPoliciesTest do end describe "AshAuthentication bypass" do - test "register_with_password works without actor via AshAuthentication bypass" do - # Test that AshAuthentication bypass allows registration without actor - # This tests the actual bypass mechanism, not admin permissions - changeset = + test "register_with_password works without actor" do + # Registration should work without actor (AshAuthentication bypass) + {:ok, user} = Accounts.User |> Ash.Changeset.for_create(:register_with_password, %{ email: "register#{System.unique_integer([:positive])}@example.com", password: "testpassword123" }) - |> Ash.Changeset.set_context(%{private: %{ash_authentication?: true}}) - - {:ok, user} = Ash.create(changeset, domain: Mv.Accounts) + |> Ash.create() assert user.email - - # Verify that default "Mitglied" role was assigned - {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, authorize?: false) - assert user_with_role.role != nil - assert user_with_role.role.name == "Mitglied" end - test "register_with_rauthy works without actor via AshAuthentication bypass" do - # Test that AshAuthentication bypass allows OIDC registration without actor + test "register_with_rauthy works with OIDC user_info" do + # OIDC registration should work (AshAuthentication bypass) user_info = %{ "sub" => "oidc_sub_#{System.unique_integer([:positive])}", "email" => "oidc#{System.unique_integer([:positive])}@example.com" @@ -373,24 +355,20 @@ defmodule Mv.Accounts.UserPoliciesTest do oauth_tokens = %{access_token: "token", refresh_token: "refresh"} - changeset = + {:ok, user} = Accounts.User |> Ash.Changeset.for_create(:register_with_rauthy, %{ user_info: user_info, oauth_tokens: oauth_tokens }) - |> Ash.Changeset.set_context(%{private: %{ash_authentication?: true}}) - - {:ok, user} = Ash.create(changeset) + |> Ash.create() assert user.email assert user.oidc_id == user_info["sub"] end - test "sign_in_with_rauthy works without actor via AshAuthentication bypass" do - # First create a user with OIDC ID (using system_actor for setup) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - + test "sign_in_with_rauthy works with OIDC user_info" do + # First create a user with OIDC ID user_info_create = %{ "sub" => "oidc_sub_#{System.unique_integer([:positive])}", "email" => "oidc#{System.unique_integer([:positive])}@example.com" @@ -404,18 +382,16 @@ defmodule Mv.Accounts.UserPoliciesTest do user_info: user_info_create, oauth_tokens: oauth_tokens }) - |> Ash.create(actor: system_actor) + |> Ash.create() - # Now test sign_in_with_rauthy without actor (should work via AshAuthentication bypass) - query = + # Now test sign_in_with_rauthy (should work via AshAuthentication bypass) + {:ok, signed_in_user} = Accounts.User |> Ash.Query.for_read(:sign_in_with_rauthy, %{ user_info: user_info_create, oauth_tokens: oauth_tokens }) - |> Ash.Query.set_context(%{private: %{ash_authentication?: true}}) - - {:ok, signed_in_user} = Ash.read_one(query) + |> Ash.read_one() assert signed_in_user.id == user.id end @@ -427,4 +403,22 @@ defmodule Mv.Accounts.UserPoliciesTest do # when called through the proper authentication flow (sign_in, token refresh, etc.). # Integration tests that use actual JWT tokens cover this functionality. end + + describe "test environment bypass (NoActor)" do + test "operations without actor are allowed in test environment" do + # In test environment, NoActor check should allow operations + {:ok, user} = + Accounts.User + |> Ash.Changeset.for_create(:create_user, %{ + email: "noactor#{System.unique_integer([:positive])}@example.com" + }) + |> Ash.create() + + assert user.email + + # Read should also work + {:ok, fetched_user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts) + assert fetched_user.id == user.id + end + end end diff --git a/test/mv/authorization/actor_test.exs b/test/mv/authorization/actor_test.exs index 9fba86e..e542301 100644 --- a/test/mv/authorization/actor_test.exs +++ b/test/mv/authorization/actor_test.exs @@ -7,17 +7,12 @@ defmodule Mv.Authorization.ActorTest do alias Mv.Accounts alias Mv.Authorization.Actor - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "ensure_loaded/1" do test "returns nil when actor is nil" do assert Actor.ensure_loaded(nil) == nil end - test "returns actor as-is when role is already loaded", %{actor: actor} do + test "returns actor as-is when role is already loaded" do # Create user with role {:ok, user} = Accounts.User @@ -25,10 +20,10 @@ defmodule Mv.Authorization.ActorTest do email: "test#{System.unique_integer([:positive])}@example.com", password: "testpassword123" }) - |> Ash.create(actor: actor) + |> Ash.create() # Load role - {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor) + {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts) # Should return as-is (no additional load) result = Actor.ensure_loaded(user_with_role) @@ -36,7 +31,7 @@ defmodule Mv.Authorization.ActorTest do assert result.role != %Ash.NotLoaded{} end - test "loads role when it's NotLoaded", %{actor: actor} do + test "loads role when it's NotLoaded" do # Create a role first {:ok, role} = Mv.Authorization.Role @@ -45,7 +40,7 @@ defmodule Mv.Authorization.ActorTest do description: "Test role", permission_set_name: "own_data" }) - |> Ash.create(actor: actor) + |> Ash.create() # Create user with role {:ok, user} = @@ -54,18 +49,18 @@ defmodule Mv.Authorization.ActorTest do email: "test#{System.unique_integer([:positive])}@example.com", password: "testpassword123" }) - |> Ash.create(actor: actor) + |> Ash.create() # Assign role to user {:ok, user_with_role} = user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove) - |> Ash.update(actor: actor) + |> Ash.update() # Fetch user again WITHOUT loading role (simulates "role not preloaded" scenario) {:ok, user_without_role_loaded} = - Ash.get(Accounts.User, user_with_role.id, domain: Mv.Accounts, actor: actor) + Ash.get(Accounts.User, user_with_role.id, domain: Mv.Accounts) # User has role as NotLoaded (relationship not preloaded) assert match?(%Ash.NotLoaded{}, user_without_role_loaded.role) diff --git a/test/mv/authorization/checks/has_permission_fail_closed_test.exs b/test/mv/authorization/checks/has_permission_fail_closed_test.exs index 36ddbd2..822e5aa 100644 --- a/test/mv/authorization/checks/has_permission_fail_closed_test.exs +++ b/test/mv/authorization/checks/has_permission_fail_closed_test.exs @@ -36,8 +36,7 @@ defmodule Mv.Authorization.Checks.HasPermissionFailClosedTest do |> Ash.Query.new() |> Ash.Query.filter_input(deny_filter) - {:ok, results} = - Ash.read(query, domain: Mv.Membership, authorize?: false) + {:ok, results} = Ash.read(query, domain: Mv.Membership, authorize?: false) # Assert: deny-filter must match nothing assert results == [] diff --git a/test/mv/authorization/checks/no_actor_test.exs b/test/mv/authorization/checks/no_actor_test.exs new file mode 100644 index 0000000..35205a6 --- /dev/null +++ b/test/mv/authorization/checks/no_actor_test.exs @@ -0,0 +1,52 @@ +defmodule Mv.Authorization.Checks.NoActorTest do + @moduledoc """ + Tests for the NoActor Ash Policy Check. + + This check allows actions without an actor ONLY in test environment. + In production/dev, all operations without an actor are denied. + """ + use ExUnit.Case, async: true + + alias Mv.Authorization.Checks.NoActor + + describe "match?/3" do + test "returns true when actor is nil in test environment" do + # In test environment (config :allow_no_actor_bypass = true), NoActor allows operations + result = NoActor.match?(nil, %{}, []) + assert result == true + end + + test "returns false when actor is present" do + actor = %{id: "user-123"} + result = NoActor.match?(actor, %{}, []) + assert result == false + end + + test "uses compile-time config (not runtime Mix.env)" do + # The @allow_no_actor_bypass is set via Application.compile_env at compile time + # In test.exs: config :mv, :allow_no_actor_bypass, true + # In prod/dev: not set (defaults to false) + # This ensures the check is release-safe (no runtime Mix.env dependency) + result = NoActor.match?(nil, %{}, []) + + # In test environment (as compiled), should allow + assert result == true + + # Note: We cannot test "production mode" here because the flag is compile-time. + # Production safety is guaranteed by: + # 1. Config only set in test.exs + # 2. Default is false (fail-closed) + # 3. No runtime environment checks + end + end + + describe "describe/1" do + test "returns description based on compile-time config" do + description = NoActor.describe([]) + assert is_binary(description) + + # In test environment (compiled with :allow_no_actor_bypass = true) + assert description =~ "test environment" + end + end +end diff --git a/test/mv/authorization/role_test.exs b/test/mv/authorization/role_test.exs index b7aa632..b263455 100644 --- a/test/mv/authorization/role_test.exs +++ b/test/mv/authorization/role_test.exs @@ -6,11 +6,6 @@ defmodule Mv.Authorization.RoleTest do alias Mv.Authorization - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "permission_set_name validation" do test "accepts valid permission set names" do attrs = %{ @@ -47,7 +42,7 @@ defmodule Mv.Authorization.RoleTest do end describe "system role deletion protection" do - test "prevents deletion of system roles", %{actor: actor} do + test "prevents deletion of system roles" do # is_system_role is not settable via public API, so we use Ash.Changeset directly changeset = Mv.Authorization.Role @@ -57,7 +52,7 @@ defmodule Mv.Authorization.RoleTest do }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - {:ok, system_role} = Ash.create(changeset, actor: actor) + {:ok, system_role} = Ash.create(changeset) assert {:error, %Ash.Error.Invalid{errors: errors}} = Authorization.destroy_role(system_role) diff --git a/test/mv/helpers/system_actor_test.exs b/test/mv/helpers/system_actor_test.exs index af28443..751f5c5 100644 --- a/test/mv/helpers/system_actor_test.exs +++ b/test/mv/helpers/system_actor_test.exs @@ -43,55 +43,51 @@ defmodule Mv.Helpers.SystemActorTest do # Helper function to ensure system user exists with admin role defp ensure_system_user(admin_role) do - # Use authorize?: false for bootstrap operations case Accounts.User |> Ash.Query.filter(email == ^"system@mila.local") - |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!(authorize?: false) - |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) + |> Ash.update!() + |> Ash.load!(:role, domain: Mv.Accounts) _ -> Accounts.create_user!(%{email: "system@mila.local"}, upsert?: true, - upsert_identity: :unique_email, - authorize?: false + upsert_identity: :unique_email ) |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!(authorize?: false) - |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) + |> Ash.update!() + |> Ash.load!(:role, domain: Mv.Accounts) end end # Helper function to ensure admin user exists with admin role defp ensure_admin_user(admin_role) do - # Use authorize?: false for bootstrap operations admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" case Accounts.User |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!(authorize?: false) - |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) + |> Ash.update!() + |> Ash.load!(:role, domain: Mv.Accounts) _ -> Accounts.create_user!(%{email: admin_email}, upsert?: true, - upsert_identity: :unique_email, - authorize?: false + upsert_identity: :unique_email ) |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!(authorize?: false) - |> Ash.load!(:role, domain: Mv.Accounts, authorize?: false) + |> Ash.update!() + |> Ash.load!(:role, domain: Mv.Accounts) end end @@ -118,13 +114,11 @@ defmodule Mv.Helpers.SystemActorTest do test "falls back to admin user if system user doesn't exist", %{admin_user: _admin_user} do # Delete system user if it exists - system_actor = SystemActor.get_system_actor() - case Accounts.User |> Ash.Query.filter(email == ^"system@mila.local") - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok @@ -157,13 +151,11 @@ defmodule Mv.Helpers.SystemActorTest do test "creates system user in test environment if none exists", %{admin_role: _admin_role} do # In test environment, system actor should auto-create if missing # Delete all users to test auto-creation - system_actor = SystemActor.get_system_actor() - case Accounts.User |> Ash.Query.filter(email == ^"system@mila.local") - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok @@ -171,13 +163,11 @@ defmodule Mv.Helpers.SystemActorTest do admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" - system_actor = SystemActor.get_system_actor() - case Accounts.User |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok @@ -221,13 +211,11 @@ defmodule Mv.Helpers.SystemActorTest do test "returns error tuple when system actor cannot be loaded" do # Delete all users to force error - system_actor = SystemActor.get_system_actor() - case Accounts.User |> Ash.Query.filter(email == ^"system@mila.local") - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok @@ -235,13 +223,11 @@ defmodule Mv.Helpers.SystemActorTest do admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" - system_actor = SystemActor.get_system_actor() - case Accounts.User |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok @@ -265,56 +251,39 @@ defmodule Mv.Helpers.SystemActorTest do end describe "edge cases" do - test "raises error if admin user has invalid role (role loading fails)", %{ - admin_user: admin_user - } do - # Delete system user to force fallback to admin user - system_actor = SystemActor.get_system_actor() + test "raises error if admin user has no role", %{admin_user: admin_user} do + # Remove role from admin user + admin_user + |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.manage_relationship(:role, nil, type: :append_and_remove) + |> Ash.update!() + # Delete system user to force fallback case Accounts.User |> Ash.Query.filter(email == ^"system@mila.local") - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok end - # Test that NOT NULL + FK constraints prevent setting role_id to NULL - # We verify this by attempting to set role_id to NULL and expecting a constraint violation - admin_user_id = Ecto.UUID.cast!(admin_user.id) - admin_user_id_binary = Ecto.UUID.dump!(admin_user_id) + SystemActor.invalidate_cache() - # Attempting to set role_id to NULL should fail due to NOT NULL constraint - assert_raise Postgrex.Error, - ~r/null value in column.*role_id.*violates not-null constraint/i, - fn -> - Ecto.Adapters.SQL.query!( - Mv.Repo, - """ - UPDATE users - SET role_id = NULL - WHERE id = $1::uuid - """, - [admin_user_id_binary] - ) - end - - # Note: With NOT NULL + FK constraints, we can't test the "no role" case directly - # because the database prevents it. This is the desired behavior - the constraints - # guarantee that role_id is always valid. + # Should raise error because admin user has no role + assert_raise RuntimeError, ~r/System actor must have a role assigned/, fn -> + SystemActor.get_system_actor() + end end test "handles concurrent calls without race conditions" do # Delete system user and admin user to force creation - system_actor = SystemActor.get_system_actor() - case Accounts.User |> Ash.Query.filter(email == ^"system@mila.local") - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok @@ -322,13 +291,11 @@ defmodule Mv.Helpers.SystemActorTest do admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" - system_actor = SystemActor.get_system_actor() - case Accounts.User |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts, actor: system_actor) do + |> Ash.read_one(domain: Mv.Accounts) do {:ok, user} when not is_nil(user) -> - Ash.destroy!(user, domain: Mv.Accounts, actor: system_actor) + Ash.destroy!(user, domain: Mv.Accounts) _ -> :ok @@ -363,13 +330,11 @@ defmodule Mv.Helpers.SystemActorTest do permission_set_name: "read_only" }) - system_actor = SystemActor.get_system_actor() - # Assign wrong role to system user system_user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, read_only_role, type: :append_and_remove) - |> Ash.update!(actor: system_actor) + |> Ash.update!() SystemActor.invalidate_cache() @@ -379,32 +344,19 @@ defmodule Mv.Helpers.SystemActorTest do end end - test "raises error if system user has invalid role (role loading fails)", %{ - system_user: system_user - } do - # Test that NOT NULL + FK constraints prevent setting role_id to NULL - # We verify this by attempting to set role_id to NULL and expecting a constraint violation - system_user_id = Ecto.UUID.cast!(system_user.id) - system_user_id_binary = Ecto.UUID.dump!(system_user_id) + test "raises error if system user has no role", %{system_user: system_user} do + # Remove role from system user + system_user + |> Ash.Changeset.for_update(:update, %{}) + |> Ash.Changeset.manage_relationship(:role, nil, type: :append_and_remove) + |> Ash.update!() - # Attempting to set role_id to NULL should fail due to NOT NULL constraint - assert_raise Postgrex.Error, - ~r/null value in column.*role_id.*violates not-null constraint/i, - fn -> - Ecto.Adapters.SQL.query!( - Mv.Repo, - """ - UPDATE users - SET role_id = NULL - WHERE id = $1::uuid - """, - [system_user_id_binary] - ) - end + SystemActor.invalidate_cache() - # Note: With NOT NULL + FK constraints, we can't test the "no role" case directly - # because the database prevents it. This is the desired behavior - the constraints - # guarantee that role_id is always valid. + # Should raise error because system user has no role + assert_raise RuntimeError, ~r/System actor must have a role assigned/, fn -> + SystemActor.get_system_actor() + end end end end diff --git a/test/mv/membership/import/member_csv_test.exs b/test/mv/membership/import/member_csv_test.exs index 778e82b..98943d5 100644 --- a/test/mv/membership/import/member_csv_test.exs +++ b/test/mv/membership/import/member_csv_test.exs @@ -73,33 +73,25 @@ defmodule Mv.Membership.Import.MemberCSVTest do end describe "process_chunk/4" do - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - - test "function exists and accepts chunk_rows_with_lines, column_map, custom_field_map, and opts", - %{ - actor: actor - } do + test "function exists and accepts chunk_rows_with_lines, column_map, custom_field_map, and opts" do chunk_rows_with_lines = [{2, %{member: %{email: "john@example.com"}, custom: %{}}}] column_map = %{email: 0} custom_field_map = %{} - opts = [actor: actor] + opts = [] # This will fail until the function is implemented result = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert match?({:ok, _}, result) or match?({:error, _}, result) end - test "creates member successfully with valid data", %{actor: actor} do + test "creates member successfully with valid data" do chunk_rows_with_lines = [ {2, %{member: %{email: "john@example.com", first_name: "John"}, custom: %{}}} ] column_map = %{email: 0, first_name: 1} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -109,19 +101,18 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert chunk_result.errors == [] # Verify member was created - system_actor = Mv.Helpers.SystemActor.get_system_actor() - members = Mv.Membership.list_members!(actor: system_actor) + members = Mv.Membership.list_members!() assert Enum.any?(members, &(&1.email == "john@example.com")) end - test "returns error for invalid email", %{actor: actor} do + test "returns error for invalid email" do chunk_rows_with_lines = [ {2, %{member: %{email: "invalid-email"}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -138,14 +129,14 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert error.message != "" end - test "returns error for missing email", %{actor: actor} do + test "returns error for missing email" do chunk_rows_with_lines = [ {2, %{member: %{}, custom: %{}}} ] column_map = %{} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -160,14 +151,14 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert is_binary(error.message) end - test "returns error for whitespace-only email", %{actor: actor} do + test "returns error for whitespace-only email" do chunk_rows_with_lines = [ {3, %{member: %{email: " "}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -181,12 +172,10 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert error.field == :email end - test "returns error for duplicate email", %{actor: actor} do + test "returns error for duplicate email" do # Create existing member first {:ok, _existing} = - Mv.Membership.create_member(%{email: "duplicate@example.com", first_name: "Existing"}, - actor: actor - ) + Mv.Membership.create_member(%{email: "duplicate@example.com", first_name: "Existing"}) chunk_rows_with_lines = [ {2, %{member: %{email: "duplicate@example.com", first_name: "New"}, custom: %{}}} @@ -194,7 +183,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0, first_name: 1} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -209,7 +198,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert error.message =~ "email" or error.message =~ "duplicate" or error.message =~ "unique" end - test "creates member with custom field values", %{actor: actor} do + test "creates member with custom field values" do # Create custom field first {:ok, custom_field} = Mv.Membership.CustomField @@ -217,7 +206,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do name: "Phone", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() chunk_rows_with_lines = [ {2, @@ -234,7 +223,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do to_string(custom_field.id) => %{id: custom_field.id, value_type: custom_field.value_type} } - opts = [custom_field_lookup: custom_field_lookup, actor: actor] + opts = [custom_field_lookup: custom_field_lookup] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -243,7 +232,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert chunk_result.failed == 0 # Verify member and custom field value were created - members = Mv.Membership.list_members!(actor: actor) + members = Mv.Membership.list_members!() member = Enum.find(members, &(&1.email == "withcustom@example.com")) assert member != nil @@ -254,7 +243,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert cfv.value.value == "123-456-7890" end - test "handles multiple rows with mixed success and failure", %{actor: actor} do + test "handles multiple rows with mixed success and failure" do chunk_rows_with_lines = [ {2, %{member: %{email: "valid1@example.com"}, custom: %{}}}, {3, %{member: %{email: "invalid-email"}, custom: %{}}}, @@ -263,7 +252,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -279,7 +268,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert is_binary(error.message) end - test "preserves CSV line numbers in errors", %{actor: actor} do + test "preserves CSV line numbers in errors" do chunk_rows_with_lines = [ {5, %{member: %{email: "invalid"}, custom: %{}}}, {10, %{member: %{email: "also-invalid"}, custom: %{}}} @@ -287,7 +276,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -300,11 +289,11 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert 10 in line_numbers end - test "returns {:ok, chunk_result} on success", %{actor: actor} do + test "returns {:ok, chunk_result} on success" do chunk_rows_with_lines = [{2, %{member: %{email: "test@example.com"}, custom: %{}}}] column_map = %{email: 0} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -318,11 +307,11 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert is_list(chunk_result.errors) end - test "returns {:ok, _} with zero counts for empty chunk", %{actor: actor} do + test "returns {:ok, _} with zero counts for empty chunk" do chunk_rows_with_lines = [] column_map = %{} custom_field_map = %{} - opts = [actor: actor] + opts = [] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -337,7 +326,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert function_exported?(MemberCSV, :process_chunk, 4) end - test "error capping collects exactly 50 errors", %{actor: actor} do + test "error capping collects exactly 50 errors" do # Create 50 rows with invalid emails chunk_rows_with_lines = 1..50 @@ -347,7 +336,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [existing_error_count: 0, max_errors: 50, actor: actor] + opts = [existing_error_count: 0, max_errors: 50] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -357,9 +346,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert length(chunk_result.errors) == 50 end - test "error capping collects only first 50 errors when more than 50 errors occur", %{ - actor: actor - } do + test "error capping collects only first 50 errors when more than 50 errors occur" do # Create 60 rows with invalid emails chunk_rows_with_lines = 1..60 @@ -369,7 +356,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [existing_error_count: 0, max_errors: 50, actor: actor] + opts = [existing_error_count: 0, max_errors: 50] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -379,7 +366,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert length(chunk_result.errors) == 50 end - test "error capping respects existing_error_count", %{actor: actor} do + test "error capping respects existing_error_count" do # Create 30 rows with invalid emails chunk_rows_with_lines = 1..30 @@ -389,7 +376,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [existing_error_count: 25, max_errors: 50, actor: actor] + opts = [existing_error_count: 25, max_errors: 50] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -400,7 +387,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert length(chunk_result.errors) == 25 end - test "error capping collects no errors when limit already reached", %{actor: actor} do + test "error capping collects no errors when limit already reached" do # Create 10 rows with invalid emails chunk_rows_with_lines = 1..10 @@ -410,7 +397,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [existing_error_count: 50, max_errors: 50, actor: actor] + opts = [existing_error_count: 50, max_errors: 50] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -420,7 +407,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert chunk_result.errors == [] end - test "error capping with mixed success and failure", %{actor: actor} do + test "error capping with mixed success and failure" do # Create 100 rows: 30 valid, 70 invalid valid_rows = 1..30 @@ -438,7 +425,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [existing_error_count: 0, max_errors: 50, actor: actor] + opts = [existing_error_count: 0, max_errors: 50] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) @@ -449,7 +436,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do assert length(chunk_result.errors) == 50 end - test "error capping with custom max_errors", %{actor: actor} do + test "error capping with custom max_errors" do # Create 20 rows with invalid emails chunk_rows_with_lines = 1..20 @@ -459,7 +446,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do column_map = %{email: 0} custom_field_map = %{} - opts = [existing_error_count: 0, max_errors: 10, actor: actor] + opts = [existing_error_count: 0, max_errors: 10] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) diff --git a/test/mv/membership/member_policies_test.exs b/test/mv/membership/member_policies_test.exs index 0bbe1c1..69b0e22 100644 --- a/test/mv/membership/member_policies_test.exs +++ b/test/mv/membership/member_policies_test.exs @@ -16,23 +16,15 @@ defmodule Mv.Membership.MemberPoliciesTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a role with a specific permission set - defp create_role_with_permission_set(permission_set_name, actor) do + defp create_role_with_permission_set(permission_set_name) do 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: actor - ) do + case Authorization.create_role(%{ + name: role_name, + description: "Test role for #{permission_set_name}", + permission_set_name: permission_set_name + }) do {:ok, role} -> role {:error, error} -> raise "Failed to create role: #{inspect(error)}" end @@ -40,9 +32,9 @@ defmodule Mv.Membership.MemberPoliciesTest do # Helper to create a user with a specific permission set # Returns user with role preloaded (required for authorization) - defp create_user_with_permission_set(permission_set_name, actor) do + defp create_user_with_permission_set(permission_set_name) do # Create role with permission set - role = create_role_with_permission_set(permission_set_name, actor) + role = create_role_with_permission_set(permission_set_name) # Create user {:ok, user} = @@ -51,28 +43,28 @@ defmodule Mv.Membership.MemberPoliciesTest do email: "user#{System.unique_integer([:positive])}@example.com", password: "testpassword123" }) - |> Ash.create(actor: actor) + |> 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(actor: actor) + |> Ash.update() # Reload user with role preloaded (critical for authorization!) - {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor) + {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts) user_with_role end # Helper to create an admin user (for creating test fixtures) - defp create_admin_user(actor) do - create_user_with_permission_set("admin", actor) + defp create_admin_user do + create_user_with_permission_set("admin") end # Helper to create a member linked to a user - defp create_linked_member_for_user(user, actor) do - admin = create_admin_user(actor) + defp create_linked_member_for_user(user) do + admin = create_admin_user() # Create member # NOTE: We need to ensure the member is actually persisted to the database @@ -104,8 +96,8 @@ defmodule Mv.Membership.MemberPoliciesTest do end # Helper to create an unlinked member (no user relationship) - defp create_unlinked_member(actor) do - admin = create_admin_user(actor) + defp create_unlinked_member do + admin = create_admin_user() {:ok, member} = Membership.Member @@ -120,16 +112,14 @@ defmodule Mv.Membership.MemberPoliciesTest do end describe "own_data permission set (Mitglied)" do - setup %{actor: actor} do - user = create_user_with_permission_set("own_data", actor) - linked_member = create_linked_member_for_user(user, actor) - unlinked_member = create_unlinked_member(actor) + setup do + user = create_user_with_permission_set("own_data") + linked_member = create_linked_member_for_user(user) + unlinked_member = create_unlinked_member() # Reload user to get updated member_id - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) - - {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) + {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts) %{user: user, linked_member: linked_member, unlinked_member: unlinked_member} end @@ -175,10 +165,7 @@ defmodule Mv.Membership.MemberPoliciesTest do end end - test "list members returns only linked member", %{ - user: user, - linked_member: linked_member - } do + test "list members returns only linked member", %{user: user, linked_member: linked_member} do {:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership) # Should only return the linked member (scope :linked filters) @@ -198,10 +185,7 @@ defmodule Mv.Membership.MemberPoliciesTest do end end - test "cannot destroy member (returns forbidden)", %{ - user: user, - linked_member: linked_member - } do + test "cannot destroy member (returns forbidden)", %{user: user, linked_member: linked_member} do assert_raise Ash.Error.Forbidden, fn -> Ash.destroy!(linked_member, actor: user) end @@ -209,14 +193,13 @@ defmodule Mv.Membership.MemberPoliciesTest do end describe "read_only permission set (Vorstand/Buchhaltung)" do - setup %{actor: actor} do - user = create_user_with_permission_set("read_only", actor) - linked_member = create_linked_member_for_user(user, actor) - unlinked_member = create_unlinked_member(actor) + setup do + user = create_user_with_permission_set("read_only") + linked_member = create_linked_member_for_user(user) + unlinked_member = create_unlinked_member() # Reload user to get updated member_id - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) %{user: user, linked_member: linked_member, unlinked_member: unlinked_member} end @@ -234,10 +217,7 @@ defmodule Mv.Membership.MemberPoliciesTest do assert unlinked_member.id in member_ids end - test "can read individual member", %{ - user: user, - unlinked_member: unlinked_member - } do + test "can read individual member", %{user: user, unlinked_member: unlinked_member} do {:ok, member} = Ash.get(Membership.Member, unlinked_member.id, actor: user, domain: Mv.Membership) @@ -278,14 +258,13 @@ defmodule Mv.Membership.MemberPoliciesTest do end describe "normal_user permission set (Kassenwart)" do - setup %{actor: actor} do - user = create_user_with_permission_set("normal_user", actor) - linked_member = create_linked_member_for_user(user, actor) - unlinked_member = create_unlinked_member(actor) + setup do + user = create_user_with_permission_set("normal_user") + linked_member = create_linked_member_for_user(user) + unlinked_member = create_unlinked_member() # Reload user to get updated member_id - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) %{user: user, linked_member: linked_member, unlinked_member: unlinked_member} end @@ -336,14 +315,13 @@ defmodule Mv.Membership.MemberPoliciesTest do end describe "admin permission set" do - setup %{actor: actor} do - user = create_user_with_permission_set("admin", actor) - linked_member = create_linked_member_for_user(user, actor) - unlinked_member = create_unlinked_member(actor) + setup do + user = create_user_with_permission_set("admin") + linked_member = create_linked_member_for_user(user) + unlinked_member = create_unlinked_member() # Reload user to get updated member_id - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) %{user: user, linked_member: linked_member, unlinked_member: unlinked_member} end @@ -383,10 +361,7 @@ defmodule Mv.Membership.MemberPoliciesTest do assert updated_member.first_name == "Updated" end - test "can destroy any member", %{ - user: user, - unlinked_member: unlinked_member - } do + test "can destroy any member", %{user: user, unlinked_member: unlinked_member} do :ok = Ash.destroy(unlinked_member, actor: user) # Verify member is deleted @@ -395,24 +370,19 @@ defmodule Mv.Membership.MemberPoliciesTest do end describe "special case: user can always READ linked member" do - setup %{actor: _actor} do - # Note: The special case policy only applies to :read actions. - # Updates are handled by HasPermission with :linked scope (if permission exists). - :ok - end + # Note: The special case policy only applies to :read actions. + # Updates are handled by HasPermission with :linked scope (if permission exists). - test "read_only user can read linked member (via special case bypass)", %{actor: actor} do + test "read_only user can read linked member (via special case bypass)" do # read_only has Member.read scope :all, but the special case ensures # users can ALWAYS read their linked member, even if they had no read permission. # This test verifies the special case works independently of permission sets. - user = create_user_with_permission_set("read_only", actor) - linked_member = create_linked_member_for_user(user, actor) + user = create_user_with_permission_set("read_only") + linked_member = create_linked_member_for_user(user) # Reload user to get updated member_id - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) - - {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) + {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts) # Should succeed (special case bypass policy for :read takes precedence) {:ok, member} = @@ -421,17 +391,15 @@ defmodule Mv.Membership.MemberPoliciesTest do assert member.id == linked_member.id end - test "own_data user can read linked member (via special case bypass)", %{actor: actor} do + test "own_data user can read linked member (via special case bypass)" do # own_data has Member.read scope :linked, but the special case ensures # users can ALWAYS read their linked member regardless of permission set. - user = create_user_with_permission_set("own_data", actor) - linked_member = create_linked_member_for_user(user, actor) + user = create_user_with_permission_set("own_data") + linked_member = create_linked_member_for_user(user) # Reload user to get updated member_id - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) - - {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) + {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts) # Should succeed (special case bypass policy for :read takes precedence) {:ok, member} = @@ -440,19 +408,15 @@ defmodule Mv.Membership.MemberPoliciesTest do assert member.id == linked_member.id end - test "own_data user can update linked member (via HasPermission :linked scope)", %{ - actor: actor - } do + test "own_data user can update linked member (via HasPermission :linked scope)" do # Update is NOT handled by special case - it's handled by HasPermission # with :linked scope. own_data has Member.update scope :linked. - user = create_user_with_permission_set("own_data", actor) - linked_member = create_linked_member_for_user(user, actor) + user = create_user_with_permission_set("own_data") + linked_member = create_linked_member_for_user(user) # Reload user to get updated member_id - {:ok, user} = - Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor) - - {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor) + {:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role]) + {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts) # Should succeed via HasPermission check (not special case) {:ok, updated_member} = diff --git a/test/mv/membership_fees/cycle_generator_edge_cases_test.exs b/test/mv/membership_fees/cycle_generator_edge_cases_test.exs index d4899a3..85eb406 100644 --- a/test/mv/membership_fees/cycle_generator_edge_cases_test.exs +++ b/test/mv/membership_fees/cycle_generator_edge_cases_test.exs @@ -19,13 +19,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -36,12 +31,12 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a member. Note: If membership_fee_type_id is provided, # cycles will be auto-generated during creation in test environment. - defp create_member(attrs, actor) do + defp create_member(attrs) do default_attrs = %{ first_name: "Test", last_name: "User", @@ -52,7 +47,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a member and explicitly generate cycles with a fixed "today" date. @@ -61,7 +56,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do # Note: We first create the member without fee_type_id, then assign it via update, # which triggers the after_action hook. However, we then explicitly regenerate # cycles with the fixed "today" date to ensure consistency. - defp create_member_with_cycles(attrs, today, actor) do + defp create_member_with_cycles(attrs, today) do # Extract membership_fee_type_id if present fee_type_id = Map.get(attrs, :membership_fee_type_id) @@ -69,14 +64,14 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do attrs_without_fee_type = Map.delete(attrs, :membership_fee_type_id) member = - create_member(attrs_without_fee_type, actor) + create_member(attrs_without_fee_type) # Assign fee type if provided (this will trigger auto-generation with real today) member = if fee_type_id do member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type_id}) - |> Ash.update!(actor: actor) + |> Ash.update!() else member end @@ -85,8 +80,8 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do # This ensures the test uses the fixed date, not the real current date if fee_type_id && member.join_date do # Delete any existing cycles first to ensure clean state - existing_cycles = get_member_cycles(member.id, actor) - Enum.each(existing_cycles, &Ash.destroy!(&1, actor: actor)) + existing_cycles = get_member_cycles(member.id) + Enum.each(existing_cycles, &Ash.destroy!(&1)) # Generate cycles with fixed "today" date {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today) @@ -96,91 +91,85 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end # Helper to get cycles for a member - defp get_member_cycles(member_id, actor) do + defp get_member_cycles(member_id) do MembershipFeeCycle |> Ash.Query.filter(member_id == ^member_id) |> Ash.Query.sort(cycle_start: :asc) - |> Ash.read!(actor: actor) + |> Ash.read!() end # Helper to set up settings - defp setup_settings(include_joining_cycle, actor) do + defp setup_settings(include_joining_cycle) do {:ok, settings} = Mv.Membership.get_settings() settings |> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle}) - |> Ash.update!(actor: actor) + |> Ash.update!() end describe "member joins today" do - test "current cycle is generated (yearly)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "current cycle is generated (yearly)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-06-15] # Create member WITHOUT fee type first to avoid auto-generation with real today member = - create_member( - %{ - join_date: today, - membership_fee_start_date: ~D[2024-01-01] - }, - actor - ) + create_member(%{ + join_date: today, + membership_fee_start_date: ~D[2024-01-01] + }) # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Explicitly generate cycles with fixed "today" date {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have the current year's cycle cycle_years = Enum.map(cycles, & &1.cycle_start.year) assert 2024 in cycle_years end - test "current cycle is generated (monthly)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :monthly}, actor) + test "current cycle is generated (monthly)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :monthly}) today = ~D[2024-06-15] # Create member WITHOUT fee type first to avoid auto-generation with real today member = - create_member( - %{ - join_date: today, - membership_fee_start_date: ~D[2024-06-01] - }, - actor - ) + create_member(%{ + join_date: today, + membership_fee_start_date: ~D[2024-06-01] + }) # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Explicitly generate cycles with fixed "today" date {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have June 2024 cycle assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-06-01] end) end - test "current cycle is generated (quarterly)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :quarterly}, actor) + test "current cycle is generated (quarterly)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :quarterly}) today = ~D[2024-05-15] @@ -192,12 +181,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2024-04-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have Q2 2024 cycle assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-04-01] end) @@ -205,9 +193,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end describe "member left yesterday" do - test "no future cycles are generated", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "no future cycles are generated" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-06-15] yesterday = Date.add(today, -1) @@ -221,12 +209,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2022-01-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() # 2024 should be included because the member was still active during that cycle @@ -238,24 +225,21 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do refute 2025 in cycle_years end - test "exit during first month of year stops at that year (monthly)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :monthly}, actor) + test "exit during first month of year stops at that year (monthly)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :monthly}) # Create member - cycles will be auto-generated member = - create_member( - %{ - join_date: ~D[2024-01-15], - exit_date: ~D[2024-03-15], - membership_fee_type_id: fee_type.id, - membership_fee_start_date: ~D[2024-01-01] - }, - actor - ) + create_member(%{ + join_date: ~D[2024-01-15], + exit_date: ~D[2024-03-15], + membership_fee_type_id: fee_type.id, + membership_fee_start_date: ~D[2024-01-01] + }) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_months = Enum.map(cycles, & &1.cycle_start.month) |> Enum.sort() assert 1 in cycle_months @@ -269,21 +253,18 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end describe "member has no cycles initially" do - test "returns error when fee type is not assigned", %{actor: actor} do - setup_settings(true, actor) + test "returns error when fee type is not assigned" do + setup_settings(true) # Create member WITHOUT fee type (no auto-generation) member = - create_member( - %{ - join_date: ~D[2022-03-15], - membership_fee_start_date: ~D[2022-01-01] - }, - actor - ) + create_member(%{ + join_date: ~D[2022-03-15], + membership_fee_start_date: ~D[2022-01-01] + }) # Verify no cycles exist initially - initial_cycles = get_member_cycles(member.id, actor) + initial_cycles = get_member_cycles(member.id) assert initial_cycles == [] # Trying to generate cycles without fee type should return error @@ -291,9 +272,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do assert result == {:error, :no_membership_fee_type} end - test "generates all cycles when member is created with fee type", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "generates all cycles when member is created with fee type" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-06-15] @@ -305,12 +286,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2022-01-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have generated all cycles from 2022 to 2024 (3 cycles) cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() @@ -323,19 +303,16 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end describe "member has existing cycles" do - test "generates from last cycle (not duplicating existing)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "generates from last cycle (not duplicating existing)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create member WITHOUT fee type first member = - create_member( - %{ - join_date: ~D[2022-03-15], - membership_fee_start_date: ~D[2022-01-01] - }, - actor - ) + create_member(%{ + join_date: ~D[2022-03-15], + membership_fee_start_date: ~D[2022-01-01] + }) # Manually create an existing cycle for 2022 MembershipFeeCycle @@ -346,20 +323,20 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do amount: fee_type.amount, status: :paid }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Now assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Explicitly generate cycles with fixed "today" date today = ~D[2024-06-15] {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today) # Check all cycles - all_cycles = get_member_cycles(member.id, actor) + all_cycles = get_member_cycles(member.id) all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq() # Should have 2022 (manually created), 2023 and 2024 (auto-generated) @@ -373,9 +350,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end describe "year boundary handling" do - test "cycles span across year boundaries correctly (yearly)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "cycles span across year boundaries correctly (yearly)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-06-15] @@ -387,12 +364,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2023-01-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() # Should have 2023 and 2024 @@ -400,9 +376,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do assert 2024 in cycle_years end - test "cycles span across year boundaries correctly (quarterly)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :quarterly}, actor) + test "cycles span across year boundaries correctly (quarterly)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :quarterly}) today = ~D[2024-12-15] @@ -414,21 +390,20 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2024-10-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date) # Should have Q4 2024 assert ~D[2024-10-01] in cycle_starts end - test "December to January transition (monthly)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :monthly}, actor) + test "December to January transition (monthly)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :monthly}) today = ~D[2024-12-31] @@ -440,12 +415,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2024-12-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date) # Should have Dec 2024 @@ -454,9 +428,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end describe "leap year handling" do - test "February cycles in leap year", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :monthly}, actor) + test "February cycles in leap year" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :monthly}) today = ~D[2024-03-15] @@ -469,12 +443,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2024-02-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have February 2024 cycle feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-02-01] end) @@ -482,9 +455,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do assert feb_cycle != nil end - test "February cycles in non-leap year", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :monthly}, actor) + test "February cycles in non-leap year" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :monthly}) today = ~D[2023-03-15] @@ -497,12 +470,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2023-02-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have February 2023 cycle feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2023-02-01] end) @@ -510,9 +482,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do assert feb_cycle != nil end - test "yearly cycle in leap year", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "yearly cycle in leap year" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-12-31] @@ -524,12 +496,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2024-01-01] }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) # Should have 2024 cycle cycle_2024 = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-01-01] end) @@ -539,9 +510,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end describe "include_joining_cycle variations" do - test "include_joining_cycle = true starts from joining cycle", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "include_joining_cycle = true starts from joining cycle" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-06-15] @@ -554,21 +525,20 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id # membership_fee_start_date will be auto-calculated }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() # Should include 2023 (joining year) assert 2023 in cycle_years end - test "include_joining_cycle = false starts from next cycle", %{actor: actor} do - setup_settings(false, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "include_joining_cycle = false starts from next cycle" do + setup_settings(false) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-06-15] @@ -581,12 +551,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id # membership_fee_start_date will be auto-calculated }, - today, - actor + today ) # Check all cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() # Should NOT include 2023 (joining year) @@ -598,22 +567,17 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do end describe "inactive member processing" do - test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members", %{ - actor: actor - } do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create an inactive member (left in 2023) WITHOUT fee type initially # This simulates a member that was created before the fee system existed member = - create_member( - %{ - join_date: ~D[2021-03-15], - exit_date: ~D[2023-06-15] - }, - actor - ) + create_member(%{ + join_date: ~D[2021-03-15], + exit_date: ~D[2023-06-15] + }) # Now assign fee type (simulating a retroactive assignment) member = @@ -622,7 +586,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2021-01-01] }) - |> Ash.update!(actor: actor) + |> Ash.update!() # Run batch generation with a "today" date after the member left today = ~D[2024-06-15] @@ -632,7 +596,7 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do assert results.total >= 1 # Check the member's cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq() # Should have 2021, 2022, 2023 (exit year included) @@ -644,9 +608,9 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do refute 2024 in cycle_years end - test "exit_date on cycle_start still generates that cycle", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "exit_date on cycle_start still generates that cycle" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) today = ~D[2024-12-31] @@ -660,12 +624,11 @@ defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do membership_fee_type_id: fee_type.id, membership_fee_start_date: ~D[2022-01-01] }, - today, - actor + today ) # Check cycles - cycles = get_member_cycles(member.id, actor) + cycles = get_member_cycles(member.id) cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() # 2024 should be included because exit_date == cycle_start means diff --git a/test/mv/membership_fees/cycle_generator_test.exs b/test/mv/membership_fees/cycle_generator_test.exs index 1863312..e6988da 100644 --- a/test/mv/membership_fees/cycle_generator_test.exs +++ b/test/mv/membership_fees/cycle_generator_test.exs @@ -11,13 +11,8 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - # Helper to create a membership fee type - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -28,11 +23,11 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a member without triggering cycle generation - defp create_member_without_cycles(attrs, actor) do + defp create_member_without_cycles(attrs) do default_attrs = %{ first_name: "Test", last_name: "User", @@ -43,53 +38,50 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to set up settings with specific include_joining_cycle value - defp setup_settings(include_joining_cycle, actor) do + defp setup_settings(include_joining_cycle) do {:ok, settings} = Mv.Membership.get_settings() settings |> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle}) - |> Ash.update!(actor: actor) + |> Ash.update!() end # Helper to get cycles for a member - defp get_member_cycles(member_id, actor) do + defp get_member_cycles(member_id) do MembershipFeeCycle |> Ash.Query.filter(member_id == ^member_id) |> Ash.Query.sort(cycle_start: :asc) - |> Ash.read!(actor: actor) + |> Ash.read!() end describe "generate_cycles_for_member/2" do - test "generates cycles from start date to today", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "generates cycles from start date to today" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create member WITHOUT fee type first to avoid auto-generation member = - create_member_without_cycles( - %{ - join_date: ~D[2022-03-15], - membership_fee_start_date: ~D[2022-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2022-03-15], + membership_fee_start_date: ~D[2022-01-01] + }) # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Explicitly generate cycles with fixed "today" date to avoid date dependency today = ~D[2024-06-15] {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today) # Verify cycles were generated - all_cycles = get_member_cycles(member.id, actor) + all_cycles = get_member_cycles(member.id) cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq() # With include_joining_cycle=true and join_date=2022-03-15, @@ -100,19 +92,16 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do assert 2024 in cycle_years end - test "generates cycles from last existing cycle", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "generates cycles from last existing cycle" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create member without fee type first to avoid auto-generation member = - create_member_without_cycles( - %{ - join_date: ~D[2022-03-15], - membership_fee_start_date: ~D[2022-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2022-03-15], + membership_fee_start_date: ~D[2022-01-01] + }) # Manually create a cycle for 2022 MembershipFeeCycle @@ -123,13 +112,13 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do amount: fee_type.amount, status: :paid }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Now assign fee type to member member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Generate cycles with specific "today" date today = ~D[2024-06-15] @@ -141,20 +130,17 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do assert 2022 not in new_cycle_years end - test "respects left_at boundary (stops generation)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "respects left_at boundary (stops generation)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) member = - create_member_without_cycles( - %{ - join_date: ~D[2022-03-15], - exit_date: ~D[2023-06-15], - membership_fee_type_id: fee_type.id, - membership_fee_start_date: ~D[2022-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2022-03-15], + exit_date: ~D[2023-06-15], + membership_fee_type_id: fee_type.id, + membership_fee_start_date: ~D[2022-01-01] + }) # Generate cycles with specific "today" date far in the future today = ~D[2025-06-15] @@ -168,19 +154,16 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do assert 2025 not in cycle_years end - test "skips existing cycles (idempotent)", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "skips existing cycles (idempotent)" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) member = - create_member_without_cycles( - %{ - join_date: ~D[2023-03-15], - membership_fee_type_id: fee_type.id, - membership_fee_start_date: ~D[2023-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2023-03-15], + membership_fee_type_id: fee_type.id, + membership_fee_start_date: ~D[2023-01-01] + }) today = ~D[2024-06-15] @@ -194,43 +177,37 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do assert second_cycles == [] end - test "does not fill gaps when cycles were deleted", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "does not fill gaps when cycles were deleted" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create member without fee type first to control which cycles exist member = - create_member_without_cycles( - %{ - join_date: ~D[2020-03-15], - membership_fee_start_date: ~D[2020-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2020-03-15], + membership_fee_start_date: ~D[2020-01-01] + }) # Manually create cycles for 2020, 2021, 2022, 2023 for year <- [2020, 2021, 2022, 2023] do MembershipFeeCycle - |> Ash.Changeset.for_create( - :create, - %{ - cycle_start: Date.new!(year, 1, 1), - member_id: member.id, - membership_fee_type_id: fee_type.id, - amount: fee_type.amount, - status: :unpaid - } - ) - |> Ash.create!(actor: actor) + |> Ash.Changeset.for_create(:create, %{ + cycle_start: Date.new!(year, 1, 1), + member_id: member.id, + membership_fee_type_id: fee_type.id, + amount: fee_type.amount, + status: :unpaid + }) + |> Ash.create!() end # Delete the 2021 cycle (create a gap) cycle_2021 = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^~D[2021-01-01]) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() - Ash.destroy!(cycle_2021, actor: actor) + Ash.destroy!(cycle_2021) # Now assign fee type to member (this triggers generation) # Since cycles already exist (2020, 2022, 2023), the generator will @@ -238,10 +215,10 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Verify gap was NOT filled and new cycles were generated from last existing - all_cycles = get_member_cycles(member.id, actor) + all_cycles = get_member_cycles(member.id) all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() # 2021 should NOT exist (gap was not filled) @@ -257,23 +234,20 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do assert 2025 in all_cycle_years end - test "sets correct amount from membership fee type", %{actor: actor} do - setup_settings(true, actor) + test "sets correct amount from membership fee type" do + setup_settings(true) amount = Decimal.new("75.50") - fee_type = create_fee_type(%{interval: :yearly, amount: amount}, actor) + fee_type = create_fee_type(%{interval: :yearly, amount: amount}) member = - create_member_without_cycles( - %{ - join_date: ~D[2024-03-15], - membership_fee_type_id: fee_type.id, - membership_fee_start_date: ~D[2024-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2024-03-15], + membership_fee_type_id: fee_type.id, + membership_fee_start_date: ~D[2024-01-01] + }) # Verify cycles were generated with correct amount - all_cycles = get_member_cycles(member.id, actor) + all_cycles = get_member_cycles(member.id) refute Enum.empty?(all_cycles), "Expected cycles to be generated" # All cycles should have the correct amount @@ -282,24 +256,21 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do end) end - test "handles NULL membership_fee_start_date by calculating from join_date", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :quarterly}, actor) + test "handles NULL membership_fee_start_date by calculating from join_date" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :quarterly}) # Create member without membership_fee_start_date - it will be auto-calculated # and cycles will be auto-generated member = - create_member_without_cycles( - %{ - join_date: ~D[2024-02-15], - membership_fee_type_id: fee_type.id - # No membership_fee_start_date - should be calculated - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2024-02-15], + membership_fee_type_id: fee_type.id + # No membership_fee_start_date - should be calculated + }) # Verify cycles were auto-generated - all_cycles = get_member_cycles(member.id, actor) + all_cycles = get_member_cycles(member.id) # With include_joining_cycle=true and join_date=2024-02-15 (quarterly), # start_date should be 2024-01-01 (Q1 start) @@ -313,34 +284,28 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do assert first_cycle_start == ~D[2024-01-01] end - test "returns error when member has no membership_fee_type", %{actor: actor} do + test "returns error when member has no membership_fee_type" do # Create member without fee type - no auto-generation will occur member = - create_member_without_cycles( - %{ - join_date: ~D[2024-03-15] - # No membership_fee_type_id - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2024-03-15] + # No membership_fee_type_id + }) {:error, reason} = CycleGenerator.generate_cycles_for_member(member.id) assert reason == :no_membership_fee_type end - test "returns error when member has no join_date", %{actor: actor} do - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "returns error when member has no join_date" do + fee_type = create_fee_type(%{interval: :yearly}) # Create member without join_date - no auto-generation will occur # (after_action hook checks for join_date) member = - create_member_without_cycles( - %{ - membership_fee_type_id: fee_type.id - # No join_date - }, - actor - ) + create_member_without_cycles(%{ + membership_fee_type_id: fee_type.id + # No join_date + }) {:error, reason} = CycleGenerator.generate_cycles_for_member(member.id) assert reason == :no_join_date @@ -392,30 +357,24 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do end describe "generate_cycles_for_all_members/1" do - test "generates cycles for multiple members", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "generates cycles for multiple members" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) # Create multiple members _member1 = - create_member_without_cycles( - %{ - join_date: ~D[2024-01-15], - membership_fee_type_id: fee_type.id, - membership_fee_start_date: ~D[2024-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2024-01-15], + membership_fee_type_id: fee_type.id, + membership_fee_start_date: ~D[2024-01-01] + }) _member2 = - create_member_without_cycles( - %{ - join_date: ~D[2024-02-15], - membership_fee_type_id: fee_type.id, - membership_fee_start_date: ~D[2024-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2024-02-15], + membership_fee_type_id: fee_type.id, + membership_fee_start_date: ~D[2024-01-01] + }) today = ~D[2024-06-15] {:ok, results} = CycleGenerator.generate_cycles_for_all_members(today: today) @@ -428,19 +387,16 @@ defmodule Mv.MembershipFees.CycleGeneratorTest do end describe "lock mechanism" do - test "prevents concurrent generation for same member", %{actor: actor} do - setup_settings(true, actor) - fee_type = create_fee_type(%{interval: :yearly}, actor) + test "prevents concurrent generation for same member" do + setup_settings(true) + fee_type = create_fee_type(%{interval: :yearly}) member = - create_member_without_cycles( - %{ - join_date: ~D[2022-03-15], - membership_fee_type_id: fee_type.id, - membership_fee_start_date: ~D[2022-01-01] - }, - actor - ) + create_member_without_cycles(%{ + join_date: ~D[2022-03-15], + membership_fee_type_id: fee_type.id, + membership_fee_start_date: ~D[2022-01-01] + }) today = ~D[2024-06-15] diff --git a/test/mv_web/controllers/oidc_e2e_flow_test.exs b/test/mv_web/controllers/oidc_e2e_flow_test.exs index fbd59d2..3b4a22f 100644 --- a/test/mv_web/controllers/oidc_e2e_flow_test.exs +++ b/test/mv_web/controllers/oidc_e2e_flow_test.exs @@ -8,13 +8,8 @@ defmodule MvWeb.OidcE2EFlowTest do use MvWeb.ConnCase, async: true require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "E2E: New OIDC user registration" do - test "new user can register via OIDC", %{conn: _conn, actor: actor} do + test "new user can register via OIDC", %{conn: _conn} do # Simulate OIDC callback for brand new user user_info = %{ "sub" => "new_oidc_user_123", @@ -23,13 +18,10 @@ defmodule MvWeb.OidcE2EFlowTest do # Call register action result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert {:ok, new_user} = result assert to_string(new_user.email) == "newuser@example.com" @@ -38,20 +30,17 @@ defmodule MvWeb.OidcE2EFlowTest do # Verify user can be found by oidc_id {:ok, [found_user]} = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert found_user.id == new_user.id end end describe "E2E: Existing OIDC user sign-in" do - test "existing OIDC user can sign in and email updates", %{conn: _conn, actor: actor} do + test "existing OIDC user can sign in and email updates", %{conn: _conn} do # Create OIDC user user = create_test_user(%{ @@ -67,13 +56,10 @@ defmodule MvWeb.OidcE2EFlowTest do # Register (upsert) with new email {:ok, updated_user} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: updated_user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: updated_user_info, + oauth_tokens: %{} + }) # Same user, updated email assert updated_user.id == user.id @@ -84,7 +70,7 @@ defmodule MvWeb.OidcE2EFlowTest do describe "E2E: OIDC with existing password account (Email Collision)" do test "OIDC registration with password account email triggers PasswordVerificationRequired", - %{conn: _conn, actor: actor} do + %{conn: _conn} do # Step 1: Create a password-only user password_user = create_test_user(%{ @@ -100,13 +86,10 @@ defmodule MvWeb.OidcE2EFlowTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Step 3: Should fail with PasswordVerificationRequired assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -123,7 +106,7 @@ defmodule MvWeb.OidcE2EFlowTest do end test "full E2E flow: OIDC collision -> password verification -> account linked", - %{conn: _conn, actor: actor} do + %{conn: _conn} do # Step 1: Create password user password_user = create_test_user(%{ @@ -139,13 +122,10 @@ defmodule MvWeb.OidcE2EFlowTest do } {:error, %Ash.Error.Invalid{errors: errors}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Extract the error password_error = @@ -162,12 +142,12 @@ defmodule MvWeb.OidcE2EFlowTest do {:ok, linked_user} = Mv.Accounts.User |> Ash.Query.filter(id == ^password_user.id) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:link_oidc_id, %{ oidc_id: user_info["sub"], oidc_user_info: user_info }) - |> Ash.update(actor: actor) + |> Ash.update() # Verify account is now linked assert linked_user.id == password_user.id @@ -178,20 +158,17 @@ defmodule MvWeb.OidcE2EFlowTest do # Step 5: User can now sign in via OIDC {:ok, [signed_in_user]} = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert signed_in_user.id == password_user.id assert signed_in_user.oidc_id == "oidc_link_888" end test "E2E: OIDC collision with different email at provider updates email after linking", - %{conn: _conn, actor: actor} do + %{conn: _conn} do # Password user with old email password_user = create_test_user(%{ @@ -222,12 +199,12 @@ defmodule MvWeb.OidcE2EFlowTest do {:ok, linked_user} = Mv.Accounts.User |> Ash.Query.filter(id == ^password_user.id) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:link_oidc_id, %{ oidc_id: updated_user_info["sub"], oidc_user_info: updated_user_info }) - |> Ash.update(actor: actor) + |> Ash.update() # Email should be updated to match OIDC provider assert to_string(linked_user.email) == "new@example.com" @@ -236,10 +213,7 @@ defmodule MvWeb.OidcE2EFlowTest do end describe "E2E: OIDC with linked member" do - test "E2E: email sync to member when linking OIDC to password account", %{ - conn: _conn, - actor: actor - } do + test "E2E: email sync to member when linking OIDC to password account", %{conn: _conn} do # Create member member = Ash.Seed.seed!(Mv.Membership.Member, %{ @@ -265,13 +239,10 @@ defmodule MvWeb.OidcE2EFlowTest do # Collision detected {:error, %Ash.Error.Invalid{}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # After password verification, link OIDC with NEW email updated_user_info = %{ @@ -282,27 +253,24 @@ defmodule MvWeb.OidcE2EFlowTest do {:ok, linked_user} = Mv.Accounts.User |> Ash.Query.filter(id == ^password_user.id) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:link_oidc_id, %{ oidc_id: updated_user_info["sub"], oidc_user_info: updated_user_info }) - |> Ash.update(actor: actor) + |> Ash.update() # User email updated assert to_string(linked_user.email) == "newmember@example.com" # Member email should be synced - {:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id) assert to_string(updated_member.email) == "newmember@example.com" end end describe "E2E: Security scenarios" do - test "E2E: password-only user cannot be accessed via OIDC without password", %{ - conn: _conn, - actor: actor - } do + test "E2E: password-only user cannot be accessed via OIDC without password", %{conn: _conn} do # Create password user _password_user = create_test_user(%{ @@ -319,13 +287,10 @@ defmodule MvWeb.OidcE2EFlowTest do # Sign-in should fail (no matching oidc_id) result = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) case result do {:ok, []} -> @@ -340,23 +305,17 @@ defmodule MvWeb.OidcE2EFlowTest do # Registration should trigger password requirement {:error, %Ash.Error.Invalid{errors: errors}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert Enum.any?(errors, fn err -> match?(%Mv.Accounts.User.Errors.PasswordVerificationRequired{}, err) end) end - test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{ - conn: _conn, - actor: actor - } do + test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{conn: _conn} do # User linked to OIDC provider A _user = create_test_user(%{ @@ -372,13 +331,10 @@ defmodule MvWeb.OidcE2EFlowTest do # Should trigger hard error (not PasswordVerificationRequired) {:error, %Ash.Error.Invalid{errors: errors}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Should have hard error about "already linked to a different OIDC account" assert Enum.any?(errors, fn @@ -395,10 +351,7 @@ defmodule MvWeb.OidcE2EFlowTest do end) end - test "E2E: empty string oidc_id is treated as password-only account", %{ - conn: _conn, - actor: actor - } do + test "E2E: empty string oidc_id is treated as password-only account", %{conn: _conn} do # User with empty oidc_id _password_user = create_test_user(%{ @@ -414,13 +367,10 @@ defmodule MvWeb.OidcE2EFlowTest do } {:error, %Ash.Error.Invalid{errors: errors}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Should require password (empty string = no OIDC) assert Enum.any?(errors, fn err -> @@ -430,38 +380,32 @@ defmodule MvWeb.OidcE2EFlowTest do end describe "E2E: Error scenarios" do - test "E2E: OIDC registration without oidc_id fails", %{conn: _conn, actor: actor} do + test "E2E: OIDC registration without oidc_id fails", %{conn: _conn} do user_info = %{ "preferred_username" => "noid@example.com" } {:error, %Ash.Error.Invalid{errors: errors}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert Enum.any?(errors, fn err -> match?(%Ash.Error.Changes.InvalidChanges{}, err) end) end - test "E2E: OIDC registration without email fails", %{conn: _conn, actor: actor} do + test "E2E: OIDC registration without email fails", %{conn: _conn} do user_info = %{ "sub" => "noemail_123" } {:error, %Ash.Error.Invalid{errors: errors}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert Enum.any?(errors, fn err -> match?(%Ash.Error.Changes.Required{field: :email}, err) diff --git a/test/mv_web/controllers/oidc_email_update_test.exs b/test/mv_web/controllers/oidc_email_update_test.exs index b486b71..53a6514 100644 --- a/test/mv_web/controllers/oidc_email_update_test.exs +++ b/test/mv_web/controllers/oidc_email_update_test.exs @@ -5,13 +5,8 @@ defmodule MvWeb.OidcEmailUpdateTest do """ use MvWeb.ConnCase, async: true - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "OIDC user updates email to available email" do - test "should succeed and update email", %{actor: actor} do + test "should succeed and update email" do # Create OIDC user {:ok, oidc_user} = Mv.Accounts.User @@ -19,7 +14,7 @@ defmodule MvWeb.OidcEmailUpdateTest do email: "original@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_123") - |> Ash.create(actor: actor) + |> Ash.create() # User logs in via OIDC with NEW email user_info = %{ @@ -28,13 +23,10 @@ defmodule MvWeb.OidcEmailUpdateTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should succeed and email should be updated assert {:ok, updated_user} = result @@ -45,7 +37,7 @@ defmodule MvWeb.OidcEmailUpdateTest do end describe "OIDC user updates email to email of passwordless user" do - test "should fail with clear error message", %{actor: actor} do + test "should fail with clear error message" do # Create OIDC user {:ok, _oidc_user} = Mv.Accounts.User @@ -53,7 +45,7 @@ defmodule MvWeb.OidcEmailUpdateTest do email: "oidcuser@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_456") - |> Ash.create(actor: actor) + |> Ash.create() # Create passwordless user with target email {:ok, _passwordless_user} = @@ -61,7 +53,7 @@ defmodule MvWeb.OidcEmailUpdateTest do |> Ash.Changeset.for_create(:create_user, %{ email: "taken@example.com" }) - |> Ash.create(actor: actor) + |> Ash.create() # OIDC user tries to update email to taken email user_info = %{ @@ -70,13 +62,10 @@ defmodule MvWeb.OidcEmailUpdateTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should fail with email update conflict error assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -99,7 +88,7 @@ defmodule MvWeb.OidcEmailUpdateTest do end describe "OIDC user updates email to email of password-protected user" do - test "should fail with clear error message", %{actor: actor} do + test "should fail with clear error message" do # Create OIDC user {:ok, _oidc_user} = Mv.Accounts.User @@ -107,7 +96,7 @@ defmodule MvWeb.OidcEmailUpdateTest do email: "oidcuser2@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_789") - |> Ash.create(actor: actor) + |> Ash.create() # Create password user with target email (explicitly NO oidc_id) password_user = @@ -117,14 +106,14 @@ defmodule MvWeb.OidcEmailUpdateTest do }) # Ensure it's a password-only user - {:ok, password_user} = Ash.reload(password_user, actor: actor) + {:ok, password_user} = Ash.reload(password_user) assert not is_nil(password_user.hashed_password) # Force oidc_id to be nil to avoid any confusion {:ok, password_user} = password_user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.force_change_attribute(:oidc_id, nil) - |> Ash.update(actor: actor) + |> Ash.update() assert is_nil(password_user.oidc_id) @@ -135,13 +124,10 @@ defmodule MvWeb.OidcEmailUpdateTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should fail with email update conflict error assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -164,7 +150,7 @@ defmodule MvWeb.OidcEmailUpdateTest do end describe "OIDC user updates email to email of different OIDC user" do - test "should fail with clear error message about different OIDC account", %{actor: actor} do + test "should fail with clear error message about different OIDC account" do # Create first OIDC user {:ok, _oidc_user1} = Mv.Accounts.User @@ -172,7 +158,7 @@ defmodule MvWeb.OidcEmailUpdateTest do email: "oidcuser1@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_aaa") - |> Ash.create(actor: actor) + |> Ash.create() # Create second OIDC user with target email {:ok, _oidc_user2} = @@ -181,7 +167,7 @@ defmodule MvWeb.OidcEmailUpdateTest do email: "oidcuser2@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_bbb") - |> Ash.create(actor: actor) + |> Ash.create() # First OIDC user tries to update email to second user's email user_info = %{ @@ -190,13 +176,10 @@ defmodule MvWeb.OidcEmailUpdateTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should fail with "already linked to different OIDC account" error assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -218,14 +201,14 @@ defmodule MvWeb.OidcEmailUpdateTest do end describe "New OIDC user registration scenarios (for comparison)" do - test "new OIDC user with email of passwordless user triggers linking flow", %{actor: actor} do + test "new OIDC user with email of passwordless user triggers linking flow" do # Create passwordless user {:ok, passwordless_user} = Mv.Accounts.User |> Ash.Changeset.for_create(:create_user, %{ email: "passwordless@example.com" }) - |> Ash.create(actor: actor) + |> Ash.create() # New OIDC user tries to register user_info = %{ @@ -234,13 +217,10 @@ defmodule MvWeb.OidcEmailUpdateTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should trigger PasswordVerificationRequired (linking flow) assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -254,7 +234,7 @@ defmodule MvWeb.OidcEmailUpdateTest do end) end - test "new OIDC user with email of existing OIDC user shows hard error", %{actor: actor} do + test "new OIDC user with email of existing OIDC user shows hard error" do # Create existing OIDC user {:ok, _existing_oidc_user} = Mv.Accounts.User @@ -262,7 +242,7 @@ defmodule MvWeb.OidcEmailUpdateTest do email: "existing@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_existing") - |> Ash.create(actor: actor) + |> Ash.create() # New OIDC user tries to register with same email user_info = %{ @@ -271,13 +251,10 @@ defmodule MvWeb.OidcEmailUpdateTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should fail with "already linked to different OIDC account" error assert {:error, %Ash.Error.Invalid{errors: errors}} = result diff --git a/test/mv_web/controllers/oidc_integration_test.exs b/test/mv_web/controllers/oidc_integration_test.exs index 650158a..bc12196 100644 --- a/test/mv_web/controllers/oidc_integration_test.exs +++ b/test/mv_web/controllers/oidc_integration_test.exs @@ -4,11 +4,6 @@ defmodule MvWeb.OidcIntegrationTest do # Test OIDC callback scenarios by directly calling the actions # This simulates what happens during real OIDC authentication - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "OIDC sign-in scenarios" do test "existing OIDC user with unchanged email can sign in" do # Create user with OIDC ID @@ -25,16 +20,11 @@ defmodule MvWeb.OidcIntegrationTest do } # Test sign_in_with_rauthy action directly - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, [found_user]} = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert found_user.id == user.id assert to_string(found_user.email) == "existing@example.com" @@ -49,15 +39,10 @@ defmodule MvWeb.OidcIntegrationTest do } # Test register_with_rauthy action - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - case Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) do + case Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) do {:ok, new_user} -> assert to_string(new_user.email) == "newuser@example.com" assert new_user.oidc_id == "brand_new_oidc_456" @@ -88,16 +73,11 @@ defmodule MvWeb.OidcIntegrationTest do } # Should NOT find any user (security requirement) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Either returns empty list OR authentication error - both mean "user not found" case result do @@ -127,16 +107,11 @@ defmodule MvWeb.OidcIntegrationTest do "preferred_username" => "oidc.user@example.com" } - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, [found_user]} = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: correct_user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: correct_user_info, + oauth_tokens: %{} + }) assert found_user.id == user.id @@ -147,13 +122,10 @@ defmodule MvWeb.OidcIntegrationTest do } result = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: wrong_user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: wrong_user_info, + oauth_tokens: %{} + }) # Either returns empty list OR authentication error - both mean "user not found" case result do @@ -182,16 +154,11 @@ defmodule MvWeb.OidcIntegrationTest do "preferred_username" => "empty.oidc@example.com" } - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.read_sign_in_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.read_sign_in_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Either returns empty list OR authentication error - both mean "user not found" case result do @@ -222,16 +189,11 @@ defmodule MvWeb.OidcIntegrationTest do "preferred_username" => "conflict@example.com" } - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Should fail with hard error (not PasswordVerificationRequired) # This prevents someone with OIDC provider B from taking over an account @@ -258,16 +220,11 @@ defmodule MvWeb.OidcIntegrationTest do "preferred_username" => "nosub@example.com" } - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert {:error, %Ash.Error.Invalid{ @@ -282,16 +239,11 @@ defmodule MvWeb.OidcIntegrationTest do "sub" => "noemail_oidc_123" } - system_actor = Mv.Helpers.SystemActor.get_system_actor() - result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -312,16 +264,11 @@ defmodule MvWeb.OidcIntegrationTest do "preferred_username" => "new@example.com" } - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, user} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert user.id == existing_user.id assert to_string(user.email) == "new@example.com" @@ -334,16 +281,11 @@ defmodule MvWeb.OidcIntegrationTest do "preferred_username" => "altid@example.com" } - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, user} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: system_actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert user.oidc_id == "alt_oidc_id_123" assert to_string(user.email) == "altid@example.com" diff --git a/test/mv_web/controllers/oidc_password_linking_test.exs b/test/mv_web/controllers/oidc_password_linking_test.exs index e9e3876..a898f95 100644 --- a/test/mv_web/controllers/oidc_password_linking_test.exs +++ b/test/mv_web/controllers/oidc_password_linking_test.exs @@ -8,15 +8,9 @@ defmodule MvWeb.OidcPasswordLinkingTest do use MvWeb.ConnCase, async: true require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "OIDC login with existing email (no oidc_id) - Email Collision" do @tag :test_proposal - test "OIDC register with existing password user email fails with PasswordVerificationRequired", - %{actor: actor} do + test "OIDC register with existing password user email fails with PasswordVerificationRequired" do # Create password-only user existing_user = create_test_user(%{ @@ -32,13 +26,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Should fail with PasswordVerificationRequired error assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -56,7 +47,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do end @tag :test_proposal - test "PasswordVerificationRequired error contains necessary context", %{actor: actor} do + test "PasswordVerificationRequired error contains necessary context" do existing_user = create_test_user(%{ email: "test@example.com", @@ -70,13 +61,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do } {:error, %Ash.Error.Invalid{errors: errors}} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) password_error = Enum.find(errors, fn err -> @@ -90,7 +78,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do end @tag :test_proposal - test "after successful password verification, oidc_id can be set", %{actor: actor} do + test "after successful password verification, oidc_id can be set" do # Create password user user = create_test_user(%{ @@ -109,12 +97,12 @@ defmodule MvWeb.OidcPasswordLinkingTest do {:ok, updated_user} = Mv.Accounts.User |> Ash.Query.filter(id == ^user.id) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:link_oidc_id, %{ oidc_id: user_info["sub"], oidc_user_info: user_info }) - |> Ash.update(actor: actor) + |> Ash.update() assert updated_user.id == user.id assert updated_user.oidc_id == "linked_oidc_555" @@ -124,7 +112,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do end @tag :test_proposal - test "password verification with wrong password keeps oidc_id as nil", %{actor: actor} do + test "password verification with wrong password keeps oidc_id as nil" do # This test verifies that if password verification fails, # the oidc_id should NOT be set @@ -143,7 +131,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do # before link_oidc_id is called, so here we just verify the user state # User should still have no oidc_id (no linking happened) - {:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor) + {:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id) assert is_nil(unchanged_user.oidc_id) assert unchanged_user.hashed_password == user.hashed_password end @@ -151,7 +139,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do describe "OIDC login with email of user having different oidc_id - Account Conflict" do @tag :test_proposal - test "OIDC register with email of user having different oidc_id fails", %{actor: actor} do + test "OIDC register with email of user having different oidc_id fails" do # User already linked to OIDC provider A _existing_user = create_test_user(%{ @@ -167,13 +155,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Should fail - cannot link different OIDC account to same email assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -186,7 +171,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do end @tag :test_proposal - test "existing OIDC user email remains unchanged when oidc_id matches", %{actor: actor} do + test "existing OIDC user email remains unchanged when oidc_id matches" do user = create_test_user(%{ email: "oidc@example.com", @@ -201,13 +186,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do # This should work via upsert {:ok, updated_user} = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) assert updated_user.id == user.id assert updated_user.oidc_id == "oidc_stable_789" @@ -217,7 +199,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do describe "Email update during OIDC linking" do @tag :test_proposal - test "linking OIDC to password account updates email if different in OIDC", %{actor: actor} do + test "linking OIDC to password account updates email if different in OIDC" do # Password user with old email user = create_test_user(%{ @@ -236,19 +218,19 @@ defmodule MvWeb.OidcPasswordLinkingTest do {:ok, updated_user} = Mv.Accounts.User |> Ash.Query.filter(id == ^user.id) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:link_oidc_id, %{ oidc_id: user_info["sub"], oidc_user_info: user_info }) - |> Ash.update(actor: actor) + |> Ash.update() assert updated_user.oidc_id == "oidc_link_999" assert to_string(updated_user.email) == "newemail@example.com" end @tag :test_proposal - test "email change during linking triggers member email sync", %{actor: actor} do + test "email change during linking triggers member email sync" do # Create member member = Ash.Seed.seed!(Mv.Membership.Member, %{ @@ -275,25 +257,25 @@ defmodule MvWeb.OidcPasswordLinkingTest do {:ok, updated_user} = Mv.Accounts.User |> Ash.Query.filter(id == ^user.id) - |> Ash.read_one!(actor: actor) + |> Ash.read_one!() |> Ash.Changeset.for_update(:link_oidc_id, %{ oidc_id: user_info["sub"], oidc_user_info: user_info }) - |> Ash.update(actor: actor) + |> Ash.update() # Verify user email changed assert to_string(updated_user.email) == "newemail@example.com" # Verify member email was synced - {:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor) + {:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id) assert to_string(updated_member.email) == "newemail@example.com" end end describe "Edge cases" do @tag :test_proposal - test "user with empty string oidc_id is treated as password-only user", %{actor: actor} do + test "user with empty string oidc_id is treated as password-only user" do _user = create_test_user(%{ email: "empty@example.com", @@ -308,13 +290,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{} + }) # Should trigger PasswordVerificationRequired (empty string = no OIDC) assert {:error, %Ash.Error.Invalid{errors: errors}} = result @@ -328,7 +307,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do end @tag :test_proposal - test "cannot link same oidc_id to multiple users", %{actor: actor} do + test "cannot link same oidc_id to multiple users" do # User 1 with OIDC _user1 = create_test_user(%{ @@ -344,7 +323,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do email: "user2@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "shared_oidc_333") - |> Ash.create(actor: actor) + |> Ash.create() # Should fail due to unique constraint on oidc_id assert match?({:error, %Ash.Error.Invalid{}}, result) @@ -358,16 +337,14 @@ defmodule MvWeb.OidcPasswordLinkingTest do end describe "OIDC login with passwordless user - Requires Linking Flow" do - test "user without password and without oidc_id triggers PasswordVerificationRequired", %{ - actor: actor - } do + test "user without password and without oidc_id triggers PasswordVerificationRequired" do # Create user without password (e.g., invited user) {:ok, existing_user} = Mv.Accounts.User |> Ash.Changeset.for_create(:create_user, %{ email: "invited@example.com" }) - |> Ash.create(actor: actor) + |> Ash.create() # Verify user has no password and no oidc_id assert is_nil(existing_user.hashed_password) @@ -395,14 +372,14 @@ defmodule MvWeb.OidcPasswordLinkingTest do end) end - test "user without password but WITH password later requires verification", %{actor: actor} do + test "user without password but WITH password later requires verification" do # Create user without password first {:ok, user} = Mv.Accounts.User |> Ash.Changeset.for_create(:create_user, %{ email: "added-password@example.com" }) - |> Ash.create(actor: actor) + |> Ash.create() # User sets password later (using admin action) {:ok, user_with_password} = @@ -410,7 +387,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do |> Ash.Changeset.for_update(:admin_set_password, %{ password: "newpassword123" }) - |> Ash.update(actor: actor) + |> Ash.update() assert not is_nil(user_with_password.hashed_password) @@ -421,13 +398,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should fail with PasswordVerificationRequired assert {:error, %Ash.Error.Invalid{}} = result @@ -440,7 +414,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do end describe "OIDC login with different oidc_id - Hard Error" do - test "user with different oidc_id cannot be linked (hard error)", %{actor: actor} do + test "user with different oidc_id cannot be linked (hard error)" do # Create user with existing OIDC ID {:ok, existing_user} = Mv.Accounts.User @@ -448,7 +422,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do email: "already-linked@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999") - |> Ash.create(actor: actor) + |> Ash.create() assert existing_user.oidc_id == "original_oidc_999" @@ -459,13 +433,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should fail with hard error (not PasswordVerificationRequired) assert {:error, %Ash.Error.Invalid{}} = result @@ -488,7 +459,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do end) end - test "cannot link different oidc_id even with password verification", %{actor: actor} do + test "cannot link different oidc_id even with password verification" do # Create user with password AND existing OIDC ID existing_user = create_test_user(%{ @@ -507,13 +478,10 @@ defmodule MvWeb.OidcPasswordLinkingTest do } result = - Mv.Accounts.create_register_with_rauthy( - %{ - user_info: user_info, - oauth_tokens: %{"access_token" => "test_token"} - }, - actor: actor - ) + Mv.Accounts.create_register_with_rauthy(%{ + user_info: user_info, + oauth_tokens: %{"access_token" => "test_token"} + }) # Should fail - cannot link different OIDC ID assert {:error, %Ash.Error.Invalid{}} = result diff --git a/test/mv_web/controllers/oidc_passwordless_linking_test.exs b/test/mv_web/controllers/oidc_passwordless_linking_test.exs index 1b5753f..9da66ac 100644 --- a/test/mv_web/controllers/oidc_passwordless_linking_test.exs +++ b/test/mv_web/controllers/oidc_passwordless_linking_test.exs @@ -7,20 +7,15 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do """ use MvWeb.ConnCase, async: true - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "Passwordless user - Automatic linking via special action" do - test "passwordless user can be linked via link_passwordless_oidc action", %{actor: actor} do + test "passwordless user can be linked via link_passwordless_oidc action" do # Create user without password (e.g., invited user) {:ok, existing_user} = Mv.Accounts.User |> Ash.Changeset.for_create(:create_user, %{ email: "invited@example.com" }) - |> Ash.create(actor: actor) + |> Ash.create() # Verify user has no password and no oidc_id assert is_nil(existing_user.hashed_password) @@ -36,7 +31,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do "preferred_username" => "invited@example.com" } }) - |> Ash.update(actor: actor) + |> Ash.update() # User should now have oidc_id linked assert linked_user.oidc_id == "auto_link_oidc_123" @@ -52,22 +47,20 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do }, oauth_tokens: %{"access_token" => "test_token"} }) - |> Ash.read_one(actor: actor) + |> Ash.read_one() assert {:ok, signed_in_user} = result assert signed_in_user.id == existing_user.id end - test "passwordless user triggers PasswordVerificationRequired for linking flow", %{ - actor: actor - } do + test "passwordless user triggers PasswordVerificationRequired for linking flow" do # Create passwordless user {:ok, existing_user} = Mv.Accounts.User |> Ash.Changeset.for_create(:create_user, %{ email: "passwordless@example.com" }) - |> Ash.create(actor: actor) + |> Ash.create() assert is_nil(existing_user.hashed_password) assert is_nil(existing_user.oidc_id) @@ -102,7 +95,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do end describe "User with different OIDC ID - Hard Error" do - test "user with different oidc_id gets hard error, not password verification", %{actor: actor} do + test "user with different oidc_id gets hard error, not password verification" do # Create user with existing OIDC ID {:ok, _existing_user} = Mv.Accounts.User @@ -110,7 +103,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do email: "already-linked@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999") - |> Ash.create(actor: actor) + |> Ash.create() # Try to register with same email but different OIDC ID user_info = %{ @@ -145,7 +138,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do end) end - test "passwordless user with different oidc_id also gets hard error", %{actor: actor} do + test "passwordless user with different oidc_id also gets hard error" do # Create passwordless user with OIDC ID {:ok, existing_user} = Mv.Accounts.User @@ -153,7 +146,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do email: "passwordless-linked@example.com" }) |> Ash.Changeset.force_change_attribute(:oidc_id, "first_oidc_777") - |> Ash.create(actor: actor) + |> Ash.create() assert is_nil(existing_user.hashed_password) assert existing_user.oidc_id == "first_oidc_777" diff --git a/test/mv_web/helpers/membership_fee_helpers_test.exs b/test/mv_web/helpers/membership_fee_helpers_test.exs index d5b0571..6d6d35c 100644 --- a/test/mv_web/helpers/membership_fee_helpers_test.exs +++ b/test/mv_web/helpers/membership_fee_helpers_test.exs @@ -9,11 +9,6 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do alias MvWeb.Helpers.MembershipFeeHelpers alias Mv.MembershipFees.CalendarCycles - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "format_currency/1" do test "formats decimal amount correctly" do assert MembershipFeeHelpers.format_currency(Decimal.new("60.00")) == "60,00 €" @@ -68,7 +63,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do end describe "get_last_completed_cycle/2" do - test "returns last completed cycle for member", %{actor: actor} do + test "returns last completed cycle for member" do # Create test data fee_type = Mv.MembershipFees.MembershipFeeType @@ -77,7 +72,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do amount: Decimal.new("50.00"), interval: :yearly }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Create member without fee type first to avoid auto-generation member = @@ -88,21 +83,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2022-01-01] }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Assign fee type after member creation (this may generate cycles, but we'll create our own) member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Delete any auto-generated cycles first cycles = Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: actor) + |> Ash.read!() - Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end) + Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end) # Create cycles manually _cycle_2022 = @@ -114,7 +109,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do membership_fee_type_id: fee_type.id, status: :paid }) - |> Ash.create!(actor: actor) + |> Ash.create!() cycle_2023 = Mv.MembershipFees.MembershipFeeCycle @@ -125,7 +120,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do membership_fee_type_id: fee_type.id, status: :paid }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Load cycles with membership_fee_type relationship member = @@ -140,7 +135,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do assert last_cycle.id == cycle_2023.id end - test "returns nil if no cycles exist", %{actor: actor} do + test "returns nil if no cycles exist" do fee_type = Mv.MembershipFees.MembershipFeeType |> Ash.Changeset.for_create(:create, %{ @@ -148,7 +143,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do amount: Decimal.new("50.00"), interval: :yearly }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Create member without fee type first member = @@ -158,21 +153,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do last_name: "Member", email: "test#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Delete any auto-generated cycles cycles = Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: actor) + |> Ash.read!() - Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end) + Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end) # Load cycles and fee type (will be empty) member = @@ -186,7 +181,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do end describe "get_current_cycle/2" do - test "returns current cycle for member", %{actor: actor} do + test "returns current cycle for member" do fee_type = Mv.MembershipFees.MembershipFeeType |> Ash.Changeset.for_create(:create, %{ @@ -194,7 +189,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do amount: Decimal.new("50.00"), interval: :yearly }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Create member without fee type first member = @@ -205,21 +200,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do email: "test#{System.unique_integer([:positive])}@example.com", join_date: ~D[2023-01-01] }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: actor) + |> Ash.update!() # Delete any auto-generated cycles cycles = Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: actor) + |> Ash.read!() - Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end) + Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end) today = Date.utc_today() current_year_start = %{today | month: 1, day: 1} @@ -233,7 +228,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do membership_fee_type_id: fee_type.id, status: :unpaid }) - |> Ash.create!(actor: actor) + |> Ash.create!() # Load cycles with membership_fee_type relationship member = diff --git a/test/mv_web/live/custom_field_live/deletion_test.exs b/test/mv_web/live/custom_field_live/deletion_test.exs index 9610b24..a35c06c 100644 --- a/test/mv_web/live/custom_field_live/deletion_test.exs +++ b/test/mv_web/live/custom_field_live/deletion_test.exs @@ -19,8 +19,6 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create admin user for testing {:ok, user} = Mv.Accounts.User @@ -28,7 +26,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do email: "admin#{System.unique_integer([:positive])}@mv.local", password: "testpassword123" }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = log_in_user(build_conn(), user) %{conn: conn, user: user} @@ -158,16 +156,14 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do # Should show success message assert render(view) =~ "Data field deleted successfully" - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Custom field should be gone from database - assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: system_actor) + assert {:error, _} = Ash.get(CustomField, custom_field.id) # Custom field value should also be gone (CASCADE) - assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id, actor: system_actor) + assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id) # Member should still exist - assert {:ok, _} = Ash.get(Member, member.id, actor: system_actor) + assert {:ok, _} = Ash.get(Member, member.id) end test "button remains disabled and custom field not deleted when slug doesn't match", %{ @@ -192,8 +188,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do assert html =~ ~r/disabled(?:=""|(?!\w))/ # Custom field should still exist since deletion couldn't proceed - system_actor = Mv.Helpers.SystemActor.get_system_actor() - assert {:ok, _} = Ash.get(CustomField, custom_field.id, actor: system_actor) + assert {:ok, _} = Ash.get(CustomField, custom_field.id) end end @@ -219,45 +214,38 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do refute has_element?(view, "#delete-custom-field-modal") # Custom field should still exist - system_actor = Mv.Helpers.SystemActor.get_system_actor() - assert {:ok, _} = Ash.get(CustomField, custom_field.id, actor: system_actor) + assert {:ok, _} = Ash.get(CustomField, custom_field.id) end end # Helper functions defp create_member do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Test", last_name: "User#{System.unique_integer([:positive])}", email: "test#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() end defp create_custom_field(name, value_type) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - CustomField |> Ash.Changeset.for_create(:create, %{ name: "#{name}_#{System.unique_integer([:positive])}", value_type: value_type }) - |> Ash.create(actor: system_actor) + |> Ash.create() end defp create_custom_field_value(member, custom_field, value) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member.id, custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => value} }) - |> Ash.create(actor: system_actor) + |> Ash.create() end defp log_in_user(conn, user) do diff --git a/test/mv_web/live/membership_fee_type_live/form_test.exs b/test/mv_web/live/membership_fee_type_live/form_test.exs index 9398403..8576f6f 100644 --- a/test/mv_web/live/membership_fee_type_live/form_test.exs +++ b/test/mv_web/live/membership_fee_type_live/form_test.exs @@ -12,8 +12,6 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do require Ash.Query setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create admin user {:ok, user} = Mv.Accounts.User @@ -21,7 +19,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do email: "admin#{System.unique_integer([:positive])}@mv.local", password: "testpassword123" }) - |> Ash.create(actor: system_actor) + |> Ash.create() authenticated_conn = conn_with_password_user(conn, user) %{conn: authenticated_conn, user: user} @@ -29,8 +27,6 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do # Helper to create a membership fee type defp create_fee_type(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -41,13 +37,11 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a member defp create_member(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ first_name: "Test", last_name: "Member", @@ -58,7 +52,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end describe "create form" do diff --git a/test/mv_web/live/membership_fee_type_live/index_test.exs b/test/mv_web/live/membership_fee_type_live/index_test.exs index 302814d..9c5ad55 100644 --- a/test/mv_web/live/membership_fee_type_live/index_test.exs +++ b/test/mv_web/live/membership_fee_type_live/index_test.exs @@ -15,8 +15,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do # No custom setup needed # Helper to create a membership fee type - # Uses admin_user to test permissions (UI-/Permissions-nah) - defp create_fee_type(attrs, admin_user) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -27,7 +26,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: admin_user) + |> Ash.create!() end # Helper to create a member @@ -49,21 +48,12 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do end describe "list display" do - test "displays all membership fee types with correct data", %{ - conn: conn, - current_user: admin_user - } do + test "displays all membership fee types with correct data", %{conn: conn} do _fee_type1 = - create_fee_type( - %{name: "Regular", amount: Decimal.new("60.00"), interval: :yearly}, - admin_user - ) + create_fee_type(%{name: "Regular", amount: Decimal.new("60.00"), interval: :yearly}) _fee_type2 = - create_fee_type( - %{name: "Reduced", amount: Decimal.new("30.00"), interval: :yearly}, - admin_user - ) + create_fee_type(%{name: "Reduced", amount: Decimal.new("30.00"), interval: :yearly}) {:ok, _view, html} = live(conn, "/membership_fee_types") @@ -75,7 +65,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do end test "member count column shows correct count", %{conn: conn, current_user: admin_user} do - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + fee_type = create_fee_type(%{interval: :yearly}) # Create 3 members with this fee type Enum.each(1..3, fn _ -> @@ -98,8 +88,8 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do assert to == "/membership_fee_types/new" end - test "edit button per row navigates to edit form", %{conn: conn, current_user: admin_user} do - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + test "edit button per row navigates to edit form", %{conn: conn} do + fee_type = create_fee_type(%{interval: :yearly}) {:ok, view, _html} = live(conn, "/membership_fee_types") @@ -114,7 +104,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do describe "delete functionality" do test "delete button disabled if type is in use", %{conn: conn, current_user: admin_user} do - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + fee_type = create_fee_type(%{interval: :yearly}) create_member(%{membership_fee_type_id: fee_type.id}, admin_user) {:ok, _view, html} = live(conn, "/membership_fee_types") @@ -123,8 +113,8 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do assert html =~ "disabled" || html =~ "cursor-not-allowed" end - test "delete button works if type is not in use", %{conn: conn, current_user: admin_user} do - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + test "delete button works if type is not in use", %{conn: conn} do + fee_type = create_fee_type(%{interval: :yearly}) # No members assigned {:ok, view, _html} = live(conn, "/membership_fee_types") @@ -134,12 +124,9 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do |> element("button[phx-click='delete'][phx-value-id='#{fee_type.id}']") |> render_click() - # Type should be deleted - use admin_user to test permissions + # Type should be deleted assert {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{}]}} = - Ash.get(MembershipFeeType, fee_type.id, - domain: Mv.MembershipFees, - actor: admin_user - ) + Ash.get(MembershipFeeType, fee_type.id, domain: Mv.MembershipFees) end end diff --git a/test/mv_web/live/profile_navigation_test.exs b/test/mv_web/live/profile_navigation_test.exs index b104900..cac6802 100644 --- a/test/mv_web/live/profile_navigation_test.exs +++ b/test/mv_web/live/profile_navigation_test.exs @@ -2,11 +2,6 @@ defmodule MvWeb.ProfileNavigationTest do use MvWeb.ConnCase, async: true import Phoenix.LiveViewTest - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "profile navigation" do test "clicking profile button redirects to current user profile", %{conn: conn} do # Setup: Create and login a user @@ -65,7 +60,7 @@ defmodule MvWeb.ProfileNavigationTest do end describe "profile navigation with OIDC user" do - test "shows correct profile data for OIDC user", %{conn: conn, actor: actor} do + test "shows correct profile data for OIDC user", %{conn: conn} do # Setup: Create OIDC user with sub claim user_info = %{ "sub" => "oidc_123", @@ -83,7 +78,7 @@ defmodule MvWeb.ProfileNavigationTest do user_info: user_info, oauth_tokens: oauth_tokens }) - |> Ash.create!(domain: Mv.Accounts, actor: actor) + |> Ash.create!(domain: Mv.Accounts) # Login user via OIDC conn = sign_in_user_via_oidc(conn, user) @@ -99,10 +94,7 @@ defmodule MvWeb.ProfileNavigationTest do assert html =~ "Not enabled" end - test "profile navigation works across different authentication methods", %{ - conn: conn, - actor: actor - } do + test "profile navigation works across different authentication methods", %{conn: conn} do # Create password user password_user = create_test_user(%{ @@ -127,7 +119,7 @@ defmodule MvWeb.ProfileNavigationTest do user_info: user_info, oauth_tokens: oauth_tokens }) - |> Ash.create!(domain: Mv.Accounts, actor: actor) + |> Ash.create!(domain: Mv.Accounts) # Test with password user conn_password = conn_with_password_user(conn, password_user) diff --git a/test/mv_web/live/role_live/show_test.exs b/test/mv_web/live/role_live/show_test.exs index 4931058..2c56347 100644 --- a/test/mv_web/live/role_live/show_test.exs +++ b/test/mv_web/live/role_live/show_test.exs @@ -35,7 +35,7 @@ defmodule MvWeb.RoleLive.ShowTest do end # Helper to create admin user with admin role - defp create_admin_user(conn, actor) do + defp create_admin_user(conn) do # Create admin role admin_role = case Authorization.list_roles() do @@ -69,17 +69,17 @@ defmodule MvWeb.RoleLive.ShowTest do email: "admin#{System.unique_integer([:positive])}@mv.local", password: "testpassword123" }) - |> Ash.create(actor: actor) + |> Ash.create() # Assign admin role using manage_relationship {:ok, user} = user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update(actor: actor) + |> Ash.update() # Load role for authorization checks (must be loaded for can?/3 to work) - user_with_role = Ash.load!(user, :role, domain: Mv.Accounts, actor: actor) + user_with_role = Ash.load!(user, :role, domain: Mv.Accounts) # Store user with role in session for LiveView conn = conn_with_password_user(conn, user_with_role) @@ -88,9 +88,8 @@ defmodule MvWeb.RoleLive.ShowTest do describe "mount and display" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, _user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor} + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} end test "mounts successfully with valid role ID", %{conn: conn} do @@ -136,7 +135,7 @@ defmodule MvWeb.RoleLive.ShowTest do assert html =~ gettext("Permission Set") end - test "displays system role badge when is_system_role is true", %{conn: conn, actor: actor} do + test "displays system role badge when is_system_role is true", %{conn: conn} do system_role = Role |> Ash.Changeset.for_create(:create_role, %{ @@ -144,7 +143,7 @@ defmodule MvWeb.RoleLive.ShowTest do permission_set_name: "own_data" }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - |> Ash.create!(actor: actor) + |> Ash.create!() {:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}") @@ -173,9 +172,8 @@ defmodule MvWeb.RoleLive.ShowTest do describe "navigation" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, _user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor} + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} end test "back button navigates to role list", %{conn: conn} do @@ -211,9 +209,8 @@ defmodule MvWeb.RoleLive.ShowTest do describe "error handling" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, _user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor} + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} end test "redirects to role list with error for invalid role ID", %{conn: conn} do @@ -229,12 +226,11 @@ defmodule MvWeb.RoleLive.ShowTest do describe "delete functionality" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, _user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor} + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} end - test "delete button is not shown for system roles", %{conn: conn, actor: actor} do + test "delete button is not shown for system roles", %{conn: conn} do system_role = Role |> Ash.Changeset.for_create(:create_role, %{ @@ -242,7 +238,7 @@ defmodule MvWeb.RoleLive.ShowTest do permission_set_name: "own_data" }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - |> Ash.create!(actor: actor) + |> Ash.create!() {:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}") @@ -262,9 +258,8 @@ defmodule MvWeb.RoleLive.ShowTest do describe "page title" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, _user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor} + {conn, _user, _admin_role} = create_admin_user(conn) + %{conn: conn} end test "sets correct page title", %{conn: conn} do diff --git a/test/mv_web/live/role_live_test.exs b/test/mv_web/live/role_live_test.exs index d3db337..792cbac 100644 --- a/test/mv_web/live/role_live_test.exs +++ b/test/mv_web/live/role_live_test.exs @@ -26,7 +26,7 @@ defmodule MvWeb.RoleLiveTest do end # Helper to create admin user with admin role - defp create_admin_user(conn, actor) do + defp create_admin_user(conn) do # Create admin role admin_role = case Authorization.list_roles() do @@ -60,17 +60,17 @@ defmodule MvWeb.RoleLiveTest do email: "admin#{System.unique_integer([:positive])}@mv.local", password: "testpassword123" }) - |> Ash.create(actor: actor) + |> Ash.create() # Assign admin role using manage_relationship {:ok, user} = user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update(actor: actor) + |> Ash.update() # Load role for authorization checks (must be loaded for can?/3 to work) - user_with_role = Ash.load!(user, :role, domain: Mv.Accounts, actor: actor) + user_with_role = Ash.load!(user, :role, domain: Mv.Accounts) # Store user with role in session for LiveView conn = conn_with_password_user(conn, user_with_role) @@ -78,14 +78,14 @@ defmodule MvWeb.RoleLiveTest do end # Helper to create non-admin user - defp create_non_admin_user(conn, actor) do + defp create_non_admin_user(conn) do {:ok, user} = Mv.Accounts.User |> Ash.Changeset.for_create(:register_with_password, %{ email: "user#{System.unique_integer([:positive])}@mv.local", password: "testpassword123" }) - |> Ash.create(actor: actor) + |> Ash.create() conn = conn_with_password_user(conn, user) {conn, user} @@ -93,9 +93,8 @@ defmodule MvWeb.RoleLiveTest do describe "index page" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor, user: user} + {conn, user, _admin_role} = create_admin_user(conn) + %{conn: conn, user: user} end test "mounts successfully", %{conn: conn} do @@ -122,7 +121,7 @@ defmodule MvWeb.RoleLiveTest do assert html =~ role.permission_set_name end - test "shows system role badge", %{conn: conn, actor: actor} do + test "shows system role badge", %{conn: conn} do _system_role = Role |> Ash.Changeset.for_create(:create_role, %{ @@ -130,14 +129,14 @@ defmodule MvWeb.RoleLiveTest do permission_set_name: "own_data" }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - |> Ash.create!(actor: actor) + |> Ash.create!() {:ok, _view, html} = live(conn, "/admin/roles") assert html =~ "System Role" || html =~ "system" end - test "delete button disabled for system roles", %{conn: conn, actor: actor} do + test "delete button disabled for system roles", %{conn: conn} do system_role = Role |> Ash.Changeset.for_create(:create_role, %{ @@ -145,7 +144,7 @@ defmodule MvWeb.RoleLiveTest do permission_set_name: "own_data" }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - |> Ash.create!(actor: actor) + |> Ash.create!() {:ok, view, _html} = live(conn, "/admin/roles") @@ -192,9 +191,8 @@ defmodule MvWeb.RoleLiveTest do describe "show page" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor, user: user} + {conn, user, _admin_role} = create_admin_user(conn) + %{conn: conn, user: user} end test "mounts with valid role ID", %{conn: conn} do @@ -217,7 +215,7 @@ defmodule MvWeb.RoleLiveTest do assert match?({:error, {:redirect, %{to: "/admin/roles"}}}, result) end - test "shows system role badge if is_system_role is true", %{conn: conn, actor: actor} do + test "shows system role badge if is_system_role is true", %{conn: conn} do system_role = Role |> Ash.Changeset.for_create(:create_role, %{ @@ -225,7 +223,7 @@ defmodule MvWeb.RoleLiveTest do permission_set_name: "own_data" }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - |> Ash.create!(actor: actor) + |> Ash.create!() {:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}") @@ -235,9 +233,8 @@ defmodule MvWeb.RoleLiveTest do describe "form - create" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor, user: user} + {conn, user, _admin_role} = create_admin_user(conn) + %{conn: conn, user: user} end test "mounts successfully", %{conn: conn} do @@ -309,10 +306,9 @@ defmodule MvWeb.RoleLiveTest do describe "form - edit" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, user, _admin_role} = create_admin_user(conn, system_actor) + {conn, user, _admin_role} = create_admin_user(conn) role = create_role() - %{conn: conn, actor: system_actor, user: user, role: role} + %{conn: conn, user: user, role: role} end test "mounts with valid role ID", %{conn: conn, role: role} do @@ -351,7 +347,7 @@ defmodule MvWeb.RoleLiveTest do assert updated_role.name == "Updated Role Name" end - test "updates system role's permission_set_name", %{conn: conn, actor: actor} do + test "updates system role's permission_set_name", %{conn: conn} do system_role = Role |> Ash.Changeset.for_create(:create_role, %{ @@ -359,7 +355,7 @@ defmodule MvWeb.RoleLiveTest do permission_set_name: "own_data" }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - |> Ash.create!(actor: actor) + |> Ash.create!() {:ok, view, _html} = live(conn, "/admin/roles/#{system_role.id}/edit?return_to=show") @@ -383,9 +379,8 @@ defmodule MvWeb.RoleLiveTest do describe "delete functionality" do setup %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {conn, user, _admin_role} = create_admin_user(conn, system_actor) - %{conn: conn, actor: system_actor, user: user} + {conn, user, _admin_role} = create_admin_user(conn) + %{conn: conn, user: user} end test "deletes non-system role", %{conn: conn} do @@ -405,7 +400,7 @@ defmodule MvWeb.RoleLiveTest do Authorization.get_role(role.id) end - test "fails to delete system role with error message", %{conn: conn, actor: actor} do + test "fails to delete system role with error message", %{conn: conn} do system_role = Role |> Ash.Changeset.for_create(:create_role, %{ @@ -413,7 +408,7 @@ defmodule MvWeb.RoleLiveTest do permission_set_name: "own_data" }) |> Ash.Changeset.force_change_attribute(:is_system_role, true) - |> Ash.create!(actor: actor) + |> Ash.create!() {:ok, view, html} = live(conn, "/admin/roles") @@ -433,13 +428,8 @@ defmodule MvWeb.RoleLiveTest do end describe "authorization" do - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - - test "only admin can access /admin/roles", %{conn: conn, actor: actor} do - {conn, _user} = create_non_admin_user(conn, actor) + test "only admin can access /admin/roles", %{conn: conn} do + {conn, _user} = create_non_admin_user(conn) # Non-admin should be redirected or see error # Note: Authorization is checked via can_access_page? which returns false @@ -453,8 +443,8 @@ defmodule MvWeb.RoleLiveTest do assert html =~ "Listing Roles" || html =~ "Roles" end - test "admin can access /admin/roles", %{conn: conn, actor: actor} do - {conn, _user, _admin_role} = create_admin_user(conn, actor) + test "admin can access /admin/roles", %{conn: conn} do + {conn, _user, _admin_role} = create_admin_user(conn) {:ok, _view, _html} = live(conn, "/admin/roles") end diff --git a/test/mv_web/live/user_live/show_test.exs b/test/mv_web/live/user_live/show_test.exs index 3551fdf..054640c 100644 --- a/test/mv_web/live/user_live/show_test.exs +++ b/test/mv_web/live/user_live/show_test.exs @@ -64,8 +64,6 @@ defmodule MvWeb.UserLive.ShowTest do end test "displays linked member when present", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create member {:ok, member} = Member @@ -74,7 +72,7 @@ defmodule MvWeb.UserLive.ShowTest do last_name: "Smith", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create user and link to member user = create_test_user(%{email: "user@example.com"}) @@ -83,7 +81,7 @@ defmodule MvWeb.UserLive.ShowTest do user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:member, member, type: :append_and_remove) - |> Ash.update(actor: system_actor) + |> Ash.update() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/users/#{user.id}") diff --git a/test/mv_web/member_live/form_error_handling_test.exs b/test/mv_web/member_live/form_error_handling_test.exs index 07a3cfe..859402e 100644 --- a/test/mv_web/member_live/form_error_handling_test.exs +++ b/test/mv_web/member_live/form_error_handling_test.exs @@ -12,8 +12,6 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do describe "error handling - flash messages" do test "shows flash message when member creation fails with validation error", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create a member with the same email to trigger uniqueness error {:ok, _existing_member} = Member @@ -22,7 +20,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do last_name: "Member", email: "duplicate@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, view, _html} = live(conn, "/members/new") @@ -75,8 +73,6 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do end test "shows flash message when member update fails", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create a member to edit {:ok, member} = Member @@ -85,7 +81,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do last_name: "Member", email: "original@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create another member with different email {:ok, _other_member} = @@ -95,7 +91,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do last_name: "Member", email: "other@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, view, _html} = live(conn, "/members/#{member.id}/edit") diff --git a/test/mv_web/member_live/form_membership_fee_type_test.exs b/test/mv_web/member_live/form_membership_fee_type_test.exs index 911a4ce..4293e67 100644 --- a/test/mv_web/member_live/form_membership_fee_type_test.exs +++ b/test/mv_web/member_live/form_membership_fee_type_test.exs @@ -12,8 +12,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do require Ash.Query # Helper to create a membership fee type - # Uses admin_user to test permissions (UI-/Permissions-nah) - defp create_fee_type(attrs, admin_user) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -24,12 +23,11 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: admin_user) + |> Ash.create!() end # Helper to create a member - # Uses admin_user to test permissions (UI-/Permissions-nah) - defp create_member(attrs, admin_user) do + defp create_member(attrs) do default_attrs = %{ first_name: "Test", last_name: "Member", @@ -40,7 +38,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: admin_user) + |> Ash.create!() end describe "membership fee type dropdown" do @@ -52,9 +50,9 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do html =~ "Beitragsart" end - test "shows available types", %{conn: conn, current_user: admin_user} do - _fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly}, admin_user) - _fee_type2 = create_fee_type(%{name: "Type 2", interval: :yearly}, admin_user) + test "shows available types", %{conn: conn} do + _fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly}) + _fee_type2 = create_fee_type(%{name: "Type 2", interval: :yearly}) {:ok, _view, html} = live(conn, "/members/new") @@ -62,14 +60,11 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do assert html =~ "Type 2" end - test "filters to same interval types if member has type", %{ - conn: conn, - current_user: admin_user - } do - yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly}, admin_user) - _monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly}, admin_user) + test "filters to same interval types if member has type", %{conn: conn} do + yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly}) + _monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly}) - member = create_member(%{membership_fee_type_id: yearly_type.id}, admin_user) + member = create_member(%{membership_fee_type_id: yearly_type.id}) {:ok, _view, html} = live(conn, "/members/#{member.id}/edit") @@ -78,11 +73,11 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do refute html =~ "Monthly Type" end - test "shows warning if different interval selected", %{conn: conn, current_user: admin_user} do - yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly}, admin_user) - monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly}, admin_user) + test "shows warning if different interval selected", %{conn: conn} do + yearly_type = create_fee_type(%{name: "Yearly Type", interval: :yearly}) + monthly_type = create_fee_type(%{name: "Monthly Type", interval: :monthly}) - member = create_member(%{membership_fee_type_id: yearly_type.id}, admin_user) + member = create_member(%{membership_fee_type_id: yearly_type.id}) {:ok, _view, html} = live(conn, "/members/#{member.id}/edit") @@ -93,11 +88,11 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do assert html =~ yearly_type.id end - test "warning cleared if same interval selected", %{conn: conn, current_user: admin_user} do - yearly_type1 = create_fee_type(%{name: "Yearly Type 1", interval: :yearly}, admin_user) - yearly_type2 = create_fee_type(%{name: "Yearly Type 2", interval: :yearly}, admin_user) + test "warning cleared if same interval selected", %{conn: conn} do + yearly_type1 = create_fee_type(%{name: "Yearly Type 1", interval: :yearly}) + yearly_type2 = create_fee_type(%{name: "Yearly Type 2", interval: :yearly}) - member = create_member(%{membership_fee_type_id: yearly_type1.id}, admin_user) + member = create_member(%{membership_fee_type_id: yearly_type1.id}) {:ok, view, _html} = live(conn, "/members/#{member.id}/edit") @@ -110,8 +105,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do refute html =~ "Warning" || html =~ "Warnung" end - test "form saves with selected membership fee type", %{conn: conn, current_user: admin_user} do - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + test "form saves with selected membership fee type", %{conn: conn} do + fee_type = create_fee_type(%{interval: :yearly}) {:ok, view, _html} = live(conn, "/members/new") @@ -127,18 +122,18 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do |> form("#member-form", form_data) |> render_submit() - # Verify member was created with fee type - use admin_user to test permissions + # Verify member was created with fee type member = Member |> Ash.Query.filter(email == ^form_data["member[email]"]) - |> Ash.read_one!(actor: admin_user) + |> Ash.read_one!() assert member.membership_fee_type_id == fee_type.id end - test "new members get default membership fee type", %{conn: conn, current_user: admin_user} do + test "new members get default membership fee type", %{conn: conn} do # Set default fee type in settings - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + fee_type = create_fee_type(%{interval: :yearly}) {:ok, settings} = Mv.Membership.get_settings() @@ -146,7 +141,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: fee_type.id }) - |> Ash.update!(actor: admin_user) + |> Ash.update!() {:ok, view, _html} = live(conn, "/members/new") @@ -161,7 +156,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do conn: conn, current_user: admin_user } do - # Create custom field - use admin_user to test permissions + # Create custom field custom_field = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ @@ -169,11 +164,11 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do value_type: :string, required: false }) - |> Ash.create!(actor: admin_user) + |> Ash.create!() # Create two fee types with same interval - fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly}, admin_user) - fee_type2 = create_fee_type(%{name: "Type 2", interval: :yearly}, admin_user) + fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly}) + fee_type2 = create_fee_type(%{name: "Type 2", interval: :yearly}) # Create member with fee type 1 and custom field value member = @@ -208,7 +203,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do end test "union/typed values roundtrip correctly", %{conn: conn, current_user: admin_user} do - # Create date custom field - use admin_user to test permissions + # Create date custom field custom_field = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ @@ -216,9 +211,9 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do value_type: :date, required: false }) - |> Ash.create!(actor: admin_user) + |> Ash.create!() - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + fee_type = create_fee_type(%{interval: :yearly}) # Create member with date custom field value member = @@ -255,7 +250,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do end test "removing custom field values works correctly", %{conn: conn, current_user: admin_user} do - # Create custom field - use admin_user to test permissions + # Create custom field custom_field = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ @@ -263,9 +258,9 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do value_type: :string, required: false }) - |> Ash.create!(actor: admin_user) + |> Ash.create!() - fee_type = create_fee_type(%{interval: :yearly}, admin_user) + fee_type = create_fee_type(%{interval: :yearly}) # Create member with custom field value member = diff --git a/test/mv_web/member_live/index/membership_fee_status_test.exs b/test/mv_web/member_live/index/membership_fee_status_test.exs index 331375e..c56e80c 100644 --- a/test/mv_web/member_live/index/membership_fee_status_test.exs +++ b/test/mv_web/member_live/index/membership_fee_status_test.exs @@ -13,8 +13,6 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do # Helper to create a membership fee type defp create_fee_type(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -25,13 +23,11 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a member defp create_member(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ first_name: "Test", last_name: "Member", @@ -42,15 +38,13 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a cycle # Note: Does not delete existing cycles - tests should manage their own test data # If cleanup is needed, it should be done in setup or explicitly in the test defp create_cycle(member, fee_type, attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ cycle_start: ~D[2023-01-01], amount: Decimal.new("50.00"), @@ -63,7 +57,7 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end describe "load_cycles_for_members/2" do @@ -81,8 +75,7 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do |> Ash.Query.filter(id in [^member1.id, ^member2.id]) |> MembershipFeeStatus.load_cycles_for_members() - system_actor = Mv.Helpers.SystemActor.get_system_actor() - members = Ash.read!(query, actor: system_actor) + members = Ash.read!(query) assert length(members) == 2 @@ -101,21 +94,19 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do # Create member without fee type to avoid auto-generation member = create_member(%{}) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: system_actor) + |> Ash.update!() # Delete any auto-generated cycles cycles = Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() - Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end) + Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end) # Create cycles with dates that ensure 2023 is last completed # Use a fixed "today" date in 2024 to make 2023 the last completed @@ -146,21 +137,19 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do # Create member without fee type to avoid auto-generation member = create_member(%{}) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: system_actor) + |> Ash.update!() # Delete any auto-generated cycles cycles = Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() - Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end) + Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end) # Create cycles - use current year for current cycle today = Date.utc_today() @@ -187,21 +176,19 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do # Create member without fee type to avoid auto-generation member = create_member(%{}) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Assign fee type member = member |> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id}) - |> Ash.update!(actor: system_actor) + |> Ash.update!() # Delete any auto-generated cycles cycles = Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() - Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end) + Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end) # Load cycles and fee type first (will be empty) member = diff --git a/test/mv_web/member_live/index_custom_fields_accessibility_test.exs b/test/mv_web/member_live/index_custom_fields_accessibility_test.exs index 571555e..149d441 100644 --- a/test/mv_web/member_live/index_custom_fields_accessibility_test.exs +++ b/test/mv_web/member_live/index_custom_fields_accessibility_test.exs @@ -14,8 +14,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test member {:ok, member} = Member @@ -24,7 +22,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field with show_in_overview: true {:ok, field} = @@ -34,7 +32,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field value {:ok, _cfv} = @@ -44,7 +42,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => "A001"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{member: member, field: field} end diff --git a/test/mv_web/member_live/index_custom_fields_display_test.exs b/test/mv_web/member_live/index_custom_fields_display_test.exs index 287a915..b720099 100644 --- a/test/mv_web/member_live/index_custom_fields_display_test.exs +++ b/test/mv_web/member_live/index_custom_fields_display_test.exs @@ -17,8 +17,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test members {:ok, member1} = Member @@ -27,7 +25,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member2} = Member @@ -36,7 +34,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do last_name: "Brown", email: "bob@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom fields {:ok, field_show_string} = @@ -46,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field_hide} = CustomField @@ -55,7 +53,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do value_type: :string, show_in_overview: false }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field_show_integer} = CustomField @@ -64,7 +62,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do value_type: :integer, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field_show_boolean} = CustomField @@ -73,7 +71,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do value_type: :boolean, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field_show_date} = CustomField @@ -82,7 +80,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do value_type: :date, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field_show_email} = CustomField @@ -91,7 +89,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do value_type: :email, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field values for member1 {:ok, _cfv1} = @@ -101,7 +99,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do custom_field_id: field_show_string.id, value: %{"_union_type" => "string", "_union_value" => "+49123456789"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = CustomFieldValue @@ -110,7 +108,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do custom_field_id: field_show_integer.id, value: %{"_union_type" => "integer", "_union_value" => 12_345} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv3} = CustomFieldValue @@ -119,7 +117,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do custom_field_id: field_show_boolean.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv4} = CustomFieldValue @@ -128,7 +126,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do custom_field_id: field_show_date.id, value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv5} = CustomFieldValue @@ -137,7 +135,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do custom_field_id: field_show_email.id, value: %{"_union_type" => "email", "_union_value" => "alice.private@example.com"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create hidden custom field value (should not be displayed) {:ok, _cfv_hidden} = @@ -147,7 +145,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do custom_field_id: field_hide.id, value: %{"_union_type" => "string", "_union_value" => "Internal note"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{ member1: member1, diff --git a/test/mv_web/member_live/index_custom_fields_edge_cases_test.exs b/test/mv_web/member_live/index_custom_fields_edge_cases_test.exs index cdf26f1..d526556 100644 --- a/test/mv_web/member_live/index_custom_fields_edge_cases_test.exs +++ b/test/mv_web/member_live/index_custom_fields_edge_cases_test.exs @@ -13,8 +13,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do alias Mv.Membership.{CustomField, Member} test "displays custom field column even when no members have values", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test members without custom field values {:ok, _member1} = Member @@ -23,7 +21,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _member2} = Member @@ -32,7 +30,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do last_name: "Brown", email: "bob@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field with show_in_overview: true but no values {:ok, field} = @@ -42,7 +40,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, "/members") @@ -52,8 +50,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do end test "displays very long custom field values correctly", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test member {:ok, member} = Member @@ -62,7 +58,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field {:ok, field} = @@ -72,7 +68,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create very long value (but within limits) long_value = String.duplicate("A", 500) @@ -84,7 +80,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => long_value} }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, "/members") @@ -95,8 +91,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do end test "handles multiple custom fields with show_in_overview correctly", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test member {:ok, member} = Member @@ -105,7 +99,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create multiple custom fields with show_in_overview: true {:ok, field1} = @@ -115,7 +109,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field2} = CustomField @@ -124,7 +118,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field3} = CustomField @@ -133,7 +127,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create values for all fields {:ok, _cfv1} = @@ -143,7 +137,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do custom_field_id: field1.id, value: %{"_union_type" => "string", "_union_value" => "Value1"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = Mv.Membership.CustomFieldValue @@ -152,7 +146,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do custom_field_id: field2.id, value: %{"_union_type" => "string", "_union_value" => "Value2"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv3} = Mv.Membership.CustomFieldValue @@ -161,7 +155,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do custom_field_id: field3.id, value: %{"_union_type" => "string", "_union_value" => "Value3"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, "/members") diff --git a/test/mv_web/member_live/index_custom_fields_sorting_test.exs b/test/mv_web/member_live/index_custom_fields_sorting_test.exs index 88f225f..21b0c9f 100644 --- a/test/mv_web/member_live/index_custom_fields_sorting_test.exs +++ b/test/mv_web/member_live/index_custom_fields_sorting_test.exs @@ -16,8 +16,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test members {:ok, member1} = Member @@ -26,7 +24,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member2} = Member @@ -35,7 +33,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Brown", email: "bob@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member3} = Member @@ -44,7 +42,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Clark", email: "charlie@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field with show_in_overview: true {:ok, field_string} = @@ -54,7 +52,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, field_integer} = CustomField @@ -63,7 +61,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do value_type: :integer, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field values {:ok, _cfv1} = @@ -73,7 +71,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field_string.id, value: %{"_union_type" => "string", "_union_value" => "A001"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = CustomFieldValue @@ -82,7 +80,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field_string.id, value: %{"_union_type" => "string", "_union_value" => "C003"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv3} = CustomFieldValue @@ -91,7 +89,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field_string.id, value: %{"_union_type" => "string", "_union_value" => "B002"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv4} = CustomFieldValue @@ -100,7 +98,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field_integer.id, value: %{"_union_type" => "integer", "_union_value" => 10} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv5} = CustomFieldValue @@ -109,7 +107,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field_integer.id, value: %{"_union_type" => "integer", "_union_value" => 30} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv6} = CustomFieldValue @@ -118,7 +116,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field_integer.id, value: %{"_union_type" => "integer", "_union_value" => 20} }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{ member1: member1, @@ -238,8 +236,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do end test "NULL values and empty strings are always sorted last (ASC)", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create additional members with NULL and empty string values {:ok, member_with_value} = Member @@ -248,7 +244,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "withvalue@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member_with_empty} = Member @@ -257,7 +253,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "withempty@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member_with_null} = Member @@ -266,7 +262,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "withnull@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member_with_another_value} = Member @@ -275,7 +271,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "another@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field {:ok, field} = @@ -285,7 +281,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create values: one with actual value, one with empty string, one with NULL (no value), another with value {:ok, _cfv1} = @@ -295,7 +291,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => "Zebra"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = CustomFieldValue @@ -304,7 +300,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => ""} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # member_with_null has no custom field value (NULL) @@ -315,7 +311,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => "Apple"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = conn_with_oidc_user(conn) @@ -351,8 +347,6 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do end test "NULL values and empty strings are always sorted last (DESC)", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create additional members with NULL and empty string values {:ok, member_with_value} = Member @@ -361,7 +355,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "withvalue@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member_with_empty} = Member @@ -370,7 +364,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "withempty@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member_with_null} = Member @@ -379,7 +373,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "withnull@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member_with_another_value} = Member @@ -388,7 +382,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do last_name: "Test", email: "another@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field {:ok, field} = @@ -398,7 +392,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create values: one with actual value, one with empty string, one with NULL (no value), another with value {:ok, _cfv1} = @@ -408,7 +402,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => "Apple"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = CustomFieldValue @@ -417,7 +411,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => ""} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # member_with_null has no custom field value (NULL) @@ -428,7 +422,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do custom_field_id: field.id, value: %{"_union_type" => "string", "_union_value" => "Zebra"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() conn = conn_with_oidc_user(conn) diff --git a/test/mv_web/member_live/index_field_visibility_test.exs b/test/mv_web/member_live/index_field_visibility_test.exs index d471a23..05fa768 100644 --- a/test/mv_web/member_live/index_field_visibility_test.exs +++ b/test/mv_web/member_live/index_field_visibility_test.exs @@ -19,8 +19,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test members {:ok, member1} = Member @@ -31,7 +29,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do street: "Main St", city: "Berlin" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member2} = Member @@ -42,7 +40,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do street: "Second St", city: "Hamburg" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field {:ok, custom_field} = @@ -52,7 +50,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do value_type: :string, show_in_overview: true }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create custom field values {:ok, _cfv1} = @@ -62,7 +60,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do custom_field_id: custom_field.id, value: "M001" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = CustomFieldValue @@ -71,7 +69,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do custom_field_id: custom_field.id, value: "M002" }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{ member1: member1, diff --git a/test/mv_web/member_live/index_member_fields_display_test.exs b/test/mv_web/member_live/index_member_fields_display_test.exs index ca6ffb0..c6fd39f 100644 --- a/test/mv_web/member_live/index_member_fields_display_test.exs +++ b/test/mv_web/member_live/index_member_fields_display_test.exs @@ -6,8 +6,6 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do alias Mv.Membership.Member setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, member1} = Member |> Ash.Changeset.for_create(:create_member, %{ @@ -20,7 +18,7 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do city: "Berlin", join_date: ~D[2020-01-15] }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, member2} = Member @@ -29,7 +27,7 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do last_name: "Brown", email: "bob@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() %{ member1: member1, diff --git a/test/mv_web/member_live/index_membership_fee_status_test.exs b/test/mv_web/member_live/index_membership_fee_status_test.exs index 043c5cb..a189873 100644 --- a/test/mv_web/member_live/index_membership_fee_status_test.exs +++ b/test/mv_web/member_live/index_membership_fee_status_test.exs @@ -14,8 +14,6 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do # Helper to create a membership fee type defp create_fee_type(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -26,13 +24,11 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a member defp create_member(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ first_name: "Test", last_name: "Member", @@ -43,20 +39,18 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a cycle defp create_cycle(member, fee_type, attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Delete any auto-generated cycles first to avoid conflicts existing_cycles = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() - Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end) + Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end) default_attrs = %{ cycle_start: ~D[2023-01-01], @@ -70,7 +64,7 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end describe "status column display" do @@ -178,18 +172,16 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do member2 = create_member(%{first_name: "PaidMember", membership_fee_type_id: fee_type.id}) create_cycle(member2, fee_type, %{cycle_start: ~D[2023-01-01], status: :paid}) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Verify cycles exist in database cycles1 = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member1.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() cycles2 = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member2.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() refute Enum.empty?(cycles1) refute Enum.empty?(cycles2) @@ -214,18 +206,16 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do member2 = create_member(%{first_name: "PaidCurrent", membership_fee_type_id: fee_type.id}) create_cycle(member2, fee_type, %{cycle_start: current_year_start, status: :paid}) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Verify cycles exist in database cycles1 = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member1.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() cycles2 = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member2.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() refute Enum.empty?(cycles1) refute Enum.empty?(cycles2) diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index 0624c77..3391b86 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -7,7 +7,7 @@ defmodule MvWeb.MemberLive.IndexTest do alias Mv.MembershipFees.MembershipFeeCycle # Helper to create a membership fee type (shared across all tests) - defp create_fee_type(attrs, actor) do + defp create_fee_type(attrs) do default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -18,18 +18,18 @@ defmodule MvWeb.MemberLive.IndexTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end # Helper to create a cycle (shared across all tests) - defp create_cycle(member, fee_type, attrs, actor) do + defp create_cycle(member, fee_type, attrs) do # Delete any auto-generated cycles first to avoid conflicts existing_cycles = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: actor) + |> Ash.read!() - Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end) + Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end) default_attrs = %{ cycle_start: ~D[2023-01-01], @@ -43,7 +43,7 @@ defmodule MvWeb.MemberLive.IndexTest do MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end test "shows translated title in German", %{conn: conn} do @@ -266,18 +266,13 @@ defmodule MvWeb.MemberLive.IndexTest do end test "can delete a member without error", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create a test member first {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Test", - last_name: "User", - email: "test@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Test", + last_name: "User", + email: "test@example.com" + }) conn = conn_with_oidc_user(conn) {:ok, index_view, _html} = live(conn, "/members") @@ -299,38 +294,27 @@ defmodule MvWeb.MemberLive.IndexTest do describe "copy_emails feature" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test members {:ok, member1} = - Mv.Membership.create_member( - %{ - first_name: "Max", - last_name: "Mustermann", - email: "max@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Max", + last_name: "Mustermann", + email: "max@example.com" + }) {:ok, member2} = - Mv.Membership.create_member( - %{ - first_name: "Erika", - last_name: "Musterfrau", - email: "erika@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Erika", + last_name: "Musterfrau", + email: "erika@example.com" + }) {:ok, member3} = - Mv.Membership.create_member( - %{ - first_name: "Hans", - last_name: "Müller-Lüdenscheidt", - email: "hans@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Hans", + last_name: "Müller-Lüdenscheidt", + email: "hans@example.com" + }) %{member1: member1, member2: member2, member3: member3} end @@ -410,8 +394,7 @@ defmodule MvWeb.MemberLive.IndexTest do render_click(view, "select_member", %{"id" => member1.id}) # Delete the member from the database - system_actor = Mv.Helpers.SystemActor.get_system_actor() - Ash.destroy!(member1, actor: system_actor) + Ash.destroy!(member1) # Trigger copy_emails event directly - selection still contains the deleted ID # but the member is no longer in @members list after reload @@ -451,17 +434,12 @@ defmodule MvWeb.MemberLive.IndexTest do conn = conn_with_oidc_user(conn) # Create a member with known data - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, test_member} = - Mv.Membership.create_member( - %{ - first_name: "Test", - last_name: "Format", - email: "test.format@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Test", + last_name: "Format", + email: "test.format@example.com" + }) {:ok, view, _html} = live(conn, "/members") @@ -522,8 +500,8 @@ defmodule MvWeb.MemberLive.IndexTest do end describe "cycle status filter" do - # Helper to create a member (only used in this describe block) - defp create_member(attrs, actor) do + # Helper to create a member + defp create_member(attrs) do default_attrs = %{ first_name: "Test", last_name: "Member", @@ -534,49 +512,32 @@ defmodule MvWeb.MemberLive.IndexTest do Mv.Membership.Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: actor) + |> Ash.create!() end test "filter shows only members with paid status in last cycle", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) - fee_type = create_fee_type(%{interval: :yearly}, system_actor) + fee_type = create_fee_type(%{interval: :yearly}) today = Date.utc_today() last_year_start = Date.new!(today.year - 1, 1, 1) # Member with paid last cycle paid_member = - create_member( - %{ - first_name: "PaidLast", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "PaidLast", + membership_fee_type_id: fee_type.id + }) - create_cycle( - paid_member, - fee_type, - %{cycle_start: last_year_start, status: :paid}, - system_actor - ) + create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid}) # Member with unpaid last cycle unpaid_member = - create_member( - %{ - first_name: "UnpaidLast", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "UnpaidLast", + membership_fee_type_id: fee_type.id + }) - create_cycle( - unpaid_member, - fee_type, - %{cycle_start: last_year_start, status: :unpaid}, - system_actor - ) + create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid}) {:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid") @@ -585,45 +546,28 @@ defmodule MvWeb.MemberLive.IndexTest do end test "filter shows only members with unpaid status in last cycle", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) - fee_type = create_fee_type(%{interval: :yearly}, system_actor) + fee_type = create_fee_type(%{interval: :yearly}) today = Date.utc_today() last_year_start = Date.new!(today.year - 1, 1, 1) # Member with paid last cycle paid_member = - create_member( - %{ - first_name: "PaidLast", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "PaidLast", + membership_fee_type_id: fee_type.id + }) - create_cycle( - paid_member, - fee_type, - %{cycle_start: last_year_start, status: :paid}, - system_actor - ) + create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid}) # Member with unpaid last cycle unpaid_member = - create_member( - %{ - first_name: "UnpaidLast", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "UnpaidLast", + membership_fee_type_id: fee_type.id + }) - create_cycle( - unpaid_member, - fee_type, - %{cycle_start: last_year_start, status: :unpaid}, - system_actor - ) + create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid}) {:ok, _view, html} = live(conn, "/members?cycle_status_filter=unpaid") @@ -632,45 +576,28 @@ defmodule MvWeb.MemberLive.IndexTest do end test "filter shows only members with paid status in current cycle", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) - fee_type = create_fee_type(%{interval: :yearly}, system_actor) + fee_type = create_fee_type(%{interval: :yearly}) today = Date.utc_today() current_year_start = Date.new!(today.year, 1, 1) # Member with paid current cycle paid_member = - create_member( - %{ - first_name: "PaidCurrent", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "PaidCurrent", + membership_fee_type_id: fee_type.id + }) - create_cycle( - paid_member, - fee_type, - %{cycle_start: current_year_start, status: :paid}, - system_actor - ) + create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid}) # Member with unpaid current cycle unpaid_member = - create_member( - %{ - first_name: "UnpaidCurrent", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "UnpaidCurrent", + membership_fee_type_id: fee_type.id + }) - create_cycle( - unpaid_member, - fee_type, - %{cycle_start: current_year_start, status: :unpaid}, - system_actor - ) + create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid}) {:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid&show_current_cycle=true") @@ -679,45 +606,28 @@ defmodule MvWeb.MemberLive.IndexTest do end test "filter shows only members with unpaid status in current cycle", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) - fee_type = create_fee_type(%{interval: :yearly}, system_actor) + fee_type = create_fee_type(%{interval: :yearly}) today = Date.utc_today() current_year_start = Date.new!(today.year, 1, 1) # Member with paid current cycle paid_member = - create_member( - %{ - first_name: "PaidCurrent", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "PaidCurrent", + membership_fee_type_id: fee_type.id + }) - create_cycle( - paid_member, - fee_type, - %{cycle_start: current_year_start, status: :paid}, - system_actor - ) + create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid}) # Member with unpaid current cycle unpaid_member = - create_member( - %{ - first_name: "UnpaidCurrent", - membership_fee_type_id: fee_type.id - }, - system_actor - ) + create_member(%{ + first_name: "UnpaidCurrent", + membership_fee_type_id: fee_type.id + }) - create_cycle( - unpaid_member, - fee_type, - %{cycle_start: current_year_start, status: :unpaid}, - system_actor - ) + create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid}) {:ok, _view, html} = live(conn, "/members?cycle_status_filter=unpaid&show_current_cycle=true") @@ -1121,7 +1031,7 @@ defmodule MvWeb.MemberLive.IndexTest do end # Helper to create a member with a boolean custom field value - defp create_member_with_boolean_value(member_attrs, custom_field, value, actor) do + defp create_member_with_boolean_value(member_attrs, custom_field, value) do {:ok, member} = Mv.Membership.Member |> Ash.Changeset.for_create( @@ -1133,7 +1043,7 @@ defmodule MvWeb.MemberLive.IndexTest do } |> Map.merge(member_attrs) ) - |> Ash.create(actor: actor) + |> Ash.create() {:ok, _cfv} = Mv.Membership.CustomFieldValue @@ -1142,18 +1052,17 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: custom_field.id, value: %{"_union_type" => "boolean", "_union_value" => value} }) - |> Ash.create(actor: actor) + |> Ash.create() # Reload member with custom field values member - |> Ash.load!(:custom_field_values, actor: actor) + |> Ash.load!(:custom_field_values) end # Tests for get_boolean_custom_field_value/2 test "get_boolean_custom_field_value extracts true from Ash.Union format", %{conn: _conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() - member = create_member_with_boolean_value(%{}, boolean_field, true, system_actor) + member = create_member_with_boolean_value(%{}, boolean_field, true) # Test the function (will fail until implemented) result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field) @@ -1162,9 +1071,8 @@ defmodule MvWeb.MemberLive.IndexTest do end test "get_boolean_custom_field_value extracts false from Ash.Union format", %{conn: _conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() - member = create_member_with_boolean_value(%{}, boolean_field, false, system_actor) + member = create_member_with_boolean_value(%{}, boolean_field, false) result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field) @@ -1173,7 +1081,6 @@ defmodule MvWeb.MemberLive.IndexTest do test "get_boolean_custom_field_value extracts true from map format with _union_type and _union_value keys", %{conn: _conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() {:ok, member} = @@ -1183,7 +1090,7 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "test.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create CustomFieldValue with map format (Ash expects _union_type and _union_value) {:ok, _cfv} = @@ -1193,10 +1100,10 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Reload member with custom field values - member = member |> Ash.load!(:custom_field_values, actor: system_actor) + member = member |> Ash.load!(:custom_field_values) result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field) @@ -1206,7 +1113,6 @@ defmodule MvWeb.MemberLive.IndexTest do test "get_boolean_custom_field_value returns nil when no CustomFieldValue exists", %{ conn: _conn } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() {:ok, member} = @@ -1216,10 +1122,10 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "test.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Member has no custom field value for this field - member = member |> Ash.load!(:custom_field_values, actor: system_actor) + member = member |> Ash.load!(:custom_field_values) result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field) @@ -1229,7 +1135,6 @@ defmodule MvWeb.MemberLive.IndexTest do test "get_boolean_custom_field_value returns nil when CustomFieldValue has nil value", %{ conn: _conn } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() {:ok, member} = @@ -1239,7 +1144,7 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "test.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create CustomFieldValue with nil value (edge case) {:ok, _cfv} = @@ -1249,9 +1154,9 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field.id, value: nil }) - |> Ash.create(actor: system_actor) + |> Ash.create() - member = member |> Ash.load!(:custom_field_values, actor: system_actor) + member = member |> Ash.load!(:custom_field_values) result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field) @@ -1261,7 +1166,6 @@ defmodule MvWeb.MemberLive.IndexTest do test "get_boolean_custom_field_value returns nil for non-boolean CustomFieldValue", %{ conn: _conn } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() string_field = create_string_custom_field() boolean_field = create_boolean_custom_field() @@ -1272,7 +1176,7 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "test.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Create string custom field value (not boolean) {:ok, _cfv} = @@ -1282,9 +1186,9 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "test"} }) - |> Ash.create(actor: system_actor) + |> Ash.create() - member = member |> Ash.load!(:custom_field_values, actor: system_actor) + member = member |> Ash.load!(:custom_field_values) # Try to get boolean value from string field - should return nil result = MvWeb.MemberLive.Index.get_boolean_custom_field_value(member, boolean_field) @@ -1295,24 +1199,13 @@ defmodule MvWeb.MemberLive.IndexTest do # Tests for apply_boolean_custom_field_filters/2 test "apply_boolean_custom_field_filters filters members with true value and excludes false/without values", %{conn: _conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() member_with_true = - create_member_with_boolean_value( - %{first_name: "TrueMember"}, - boolean_field, - true, - system_actor - ) + create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true) member_with_false = - create_member_with_boolean_value( - %{first_name: "FalseMember"}, - boolean_field, - false, - system_actor - ) + create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false) {:ok, member_without_value} = Mv.Membership.Member @@ -1321,10 +1214,9 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "novalue.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() - member_without_value = - member_without_value |> Ash.load!(:custom_field_values, actor: system_actor) + member_without_value = member_without_value |> Ash.load!(:custom_field_values) members = [member_with_true, member_with_false, member_without_value] filters = %{to_string(boolean_field.id) => true} @@ -1345,24 +1237,13 @@ defmodule MvWeb.MemberLive.IndexTest do test "apply_boolean_custom_field_filters filters members with false value and excludes true/without values", %{conn: _conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() member_with_true = - create_member_with_boolean_value( - %{first_name: "TrueMember"}, - boolean_field, - true, - system_actor - ) + create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true) member_with_false = - create_member_with_boolean_value( - %{first_name: "FalseMember"}, - boolean_field, - false, - system_actor - ) + create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false) {:ok, member_without_value} = Mv.Membership.Member @@ -1371,10 +1252,9 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "novalue.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() - member_without_value = - member_without_value |> Ash.load!(:custom_field_values, actor: system_actor) + member_without_value = member_without_value |> Ash.load!(:custom_field_values) members = [member_with_true, member_with_false, member_without_value] filters = %{to_string(boolean_field.id) => false} @@ -1396,24 +1276,10 @@ defmodule MvWeb.MemberLive.IndexTest do test "apply_boolean_custom_field_filters returns all members when filter map is empty", %{ conn: _conn } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() - member1 = - create_member_with_boolean_value( - %{first_name: "Member1"}, - boolean_field, - true, - system_actor - ) - - member2 = - create_member_with_boolean_value( - %{first_name: "Member2"}, - boolean_field, - false, - system_actor - ) + member1 = create_member_with_boolean_value(%{first_name: "Member1"}, boolean_field, true) + member2 = create_member_with_boolean_value(%{first_name: "Member2"}, boolean_field, false) members = [member1, member2] filters = %{} @@ -1436,7 +1302,6 @@ defmodule MvWeb.MemberLive.IndexTest do test "apply_boolean_custom_field_filters applies multiple filters with AND logic", %{ conn: _conn } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field1 = create_boolean_custom_field(%{name: "Field1"}) boolean_field2 = create_boolean_custom_field(%{name: "Field2"}) @@ -1448,7 +1313,7 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "bothtrue.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv1} = Mv.Membership.CustomFieldValue @@ -1457,7 +1322,7 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field1.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = Mv.Membership.CustomFieldValue @@ -1466,9 +1331,9 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field2.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() - member_both_true = member_both_true |> Ash.load!(:custom_field_values, actor: system_actor) + member_both_true = member_both_true |> Ash.load!(:custom_field_values) # Member with field1 = true, field2 = false {:ok, member_mixed} = @@ -1478,7 +1343,7 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "mixed.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv3} = Mv.Membership.CustomFieldValue @@ -1487,7 +1352,7 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field1.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv4} = Mv.Membership.CustomFieldValue @@ -1496,9 +1361,9 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field2.id, value: %{"_union_type" => "boolean", "_union_value" => false} }) - |> Ash.create(actor: system_actor) + |> Ash.create() - member_mixed = member_mixed |> Ash.load!(:custom_field_values, actor: system_actor) + member_mixed = member_mixed |> Ash.load!(:custom_field_values) members = [member_both_true, member_mixed] @@ -1524,17 +1389,10 @@ defmodule MvWeb.MemberLive.IndexTest do test "apply_boolean_custom_field_filters ignores filter with non-existent custom field ID", %{ conn: _conn } do - system_actor = Mv.Helpers.SystemActor.get_system_actor() boolean_field = create_boolean_custom_field() fake_id = Ecto.UUID.generate() - member = - create_member_with_boolean_value( - %{first_name: "Member"}, - boolean_field, - true, - system_actor - ) + member = create_member_with_boolean_value(%{first_name: "Member"}, boolean_field, true) members = [member] filters = %{fake_id => true} @@ -1554,25 +1412,14 @@ defmodule MvWeb.MemberLive.IndexTest do # Integration tests for boolean custom field filters in load_members test "boolean filter integration filters members by boolean custom field value via URL parameter", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) boolean_field = create_boolean_custom_field() _member_with_true = - create_member_with_boolean_value( - %{first_name: "TrueMember"}, - boolean_field, - true, - system_actor - ) + create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true) _member_with_false = - create_member_with_boolean_value( - %{first_name: "FalseMember"}, - boolean_field, - false, - system_actor - ) + create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false) {:ok, _member_without_value} = Mv.Membership.Member @@ -1581,7 +1428,7 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "novalue.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Test true filter {:ok, _view, html_true} = @@ -1601,10 +1448,9 @@ defmodule MvWeb.MemberLive.IndexTest do end test "boolean filter integration works together with cycle_status_filter", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) boolean_field = create_boolean_custom_field() - fee_type = create_fee_type(%{interval: :yearly}, system_actor) + fee_type = create_fee_type(%{interval: :yearly}) today = Date.utc_today() last_year_start = Date.new!(today.year - 1, 1, 1) @@ -1617,7 +1463,7 @@ defmodule MvWeb.MemberLive.IndexTest do email: "paidtrue.member.#{System.unique_integer([:positive])}@example.com", membership_fee_type_id: fee_type.id }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv} = Mv.Membership.CustomFieldValue @@ -1626,14 +1472,9 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() - create_cycle( - member_paid_true, - fee_type, - %{cycle_start: last_year_start, status: :paid}, - system_actor - ) + create_cycle(member_paid_true, fee_type, %{cycle_start: last_year_start, status: :paid}) # Member with true boolean value but unpaid status {:ok, member_unpaid_true} = @@ -1644,7 +1485,7 @@ defmodule MvWeb.MemberLive.IndexTest do email: "unpaidtrue.member.#{System.unique_integer([:positive])}@example.com", membership_fee_type_id: fee_type.id }) - |> Ash.create(actor: system_actor) + |> Ash.create() {:ok, _cfv2} = Mv.Membership.CustomFieldValue @@ -1653,14 +1494,9 @@ defmodule MvWeb.MemberLive.IndexTest do custom_field_id: boolean_field.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) - |> Ash.create(actor: system_actor) + |> Ash.create() - create_cycle( - member_unpaid_true, - fee_type, - %{cycle_start: last_year_start, status: :unpaid}, - system_actor - ) + create_cycle(member_unpaid_true, fee_type, %{cycle_start: last_year_start, status: :unpaid}) # Test both filters together {:ok, _view, html} = @@ -1672,25 +1508,14 @@ defmodule MvWeb.MemberLive.IndexTest do end test "boolean filter integration works together with search query", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) boolean_field = create_boolean_custom_field() _member_with_true = - create_member_with_boolean_value( - %{first_name: "TrueMember"}, - boolean_field, - true, - system_actor - ) + create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true) _member_with_false = - create_member_with_boolean_value( - %{first_name: "FalseMember"}, - boolean_field, - false, - system_actor - ) + create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false) # Test search + boolean filter {:ok, _view, html} = @@ -1702,27 +1527,16 @@ defmodule MvWeb.MemberLive.IndexTest do end test "boolean filter works even when custom field is not visible in overview", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) # Create boolean field with show_in_overview: false boolean_field = create_boolean_custom_field(%{show_in_overview: false}) _member_with_true = - create_member_with_boolean_value( - %{first_name: "TrueMember"}, - boolean_field, - true, - system_actor - ) + create_member_with_boolean_value(%{first_name: "TrueMember"}, boolean_field, true) _member_with_false = - create_member_with_boolean_value( - %{first_name: "FalseMember"}, - boolean_field, - false, - system_actor - ) + create_member_with_boolean_value(%{first_name: "FalseMember"}, boolean_field, false) {:ok, _member_without_value} = Mv.Membership.Member @@ -1731,7 +1545,7 @@ defmodule MvWeb.MemberLive.IndexTest do last_name: "Member", email: "novalue.member.#{System.unique_integer([:positive])}@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() # Test that filter works even though field is not visible in overview {:ok, _view, html_true} = @@ -1776,7 +1590,6 @@ defmodule MvWeb.MemberLive.IndexTest do end test "boolean filter performance with 150 members", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = conn_with_oidc_user(conn) boolean_field = create_boolean_custom_field() @@ -1789,8 +1602,7 @@ defmodule MvWeb.MemberLive.IndexTest do email: "truemember#{i}@example.com" }, boolean_field, - true, - system_actor + true ) end) @@ -1802,8 +1614,7 @@ defmodule MvWeb.MemberLive.IndexTest do email: "falsemember#{i}@example.com" }, boolean_field, - false, - system_actor + false ) end) diff --git a/test/mv_web/member_live/membership_fee_integration_test.exs b/test/mv_web/member_live/membership_fee_integration_test.exs index 2636419..9358c70 100644 --- a/test/mv_web/member_live/membership_fee_integration_test.exs +++ b/test/mv_web/member_live/membership_fee_integration_test.exs @@ -14,8 +14,6 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do # Helper to create a membership fee type defp create_fee_type(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -26,13 +24,11 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a member defp create_member(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ first_name: "Test", last_name: "Member", @@ -43,7 +39,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end describe "end-to-end workflows" do @@ -79,13 +75,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do |> render_click() # Verify status changed - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - updated_cycle = - Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id), - actor: system_actor - ) - + updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id)) assert updated_cycle.status == :paid end end @@ -125,14 +115,13 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do fee_type = create_fee_type(%{interval: :yearly}) # Update settings - system_actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, settings} = Mv.Membership.get_settings() settings |> Ash.Changeset.for_update(:update_membership_fee_settings, %{ default_membership_fee_type_id: fee_type.id }) - |> Ash.update!(actor: system_actor) + |> Ash.update!() # Create new member {:ok, view, _html} = live(conn, "/members/new") @@ -149,12 +138,10 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do |> render_submit() # Verify member got default type - system_actor = Mv.Helpers.SystemActor.get_system_actor() - member = Member |> Ash.Query.filter(email == ^form_data["member[email]"]) - |> Ash.read_one!(actor: system_actor) + |> Ash.read_one!() assert member.membership_fee_type_id == fee_type.id end @@ -163,8 +150,6 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do fee_type = create_fee_type(%{interval: :yearly}) member = create_member(%{membership_fee_type_id: fee_type.id}) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - cycle = MembershipFeeCycle |> Ash.Changeset.for_create(:create, %{ @@ -174,7 +159,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do membership_fee_type_id: fee_type.id, status: :unpaid }) - |> Ash.create!(actor: system_actor) + |> Ash.create!() {:ok, view, _html} = live(conn, "/members/#{member.id}") @@ -202,8 +187,6 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do fee_type = create_fee_type(%{interval: :yearly}) member = create_member(%{membership_fee_type_id: fee_type.id}) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - cycle = MembershipFeeCycle |> Ash.Changeset.for_create(:create, %{ @@ -213,7 +196,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do membership_fee_type_id: fee_type.id, status: :unpaid }) - |> Ash.create!(actor: system_actor) + |> Ash.create!() {:ok, view, _html} = live(conn, "/members/#{member.id}") @@ -233,13 +216,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do |> render_submit() # Verify amount updated - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - updated_cycle = - Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id), - actor: system_actor - ) - + updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id)) assert updated_cycle.amount == Decimal.new("75.00") end end diff --git a/test/mv_web/member_live/show_membership_fees_test.exs b/test/mv_web/member_live/show_membership_fees_test.exs index e41f02f..d0402c3 100644 --- a/test/mv_web/member_live/show_membership_fees_test.exs +++ b/test/mv_web/member_live/show_membership_fees_test.exs @@ -14,8 +14,6 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do # Helper to create a membership fee type defp create_fee_type(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ name: "Test Fee Type #{System.unique_integer([:positive])}", amount: Decimal.new("50.00"), @@ -26,13 +24,11 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do MembershipFeeType |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a member defp create_member(attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - default_attrs = %{ first_name: "Test", last_name: "Member", @@ -43,20 +39,18 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do Member |> Ash.Changeset.for_create(:create_member, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end # Helper to create a cycle defp create_cycle(member, fee_type, attrs) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Delete any auto-generated cycles first to avoid conflicts existing_cycles = MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: system_actor) + |> Ash.read!() - Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end) + Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end) default_attrs = %{ cycle_start: ~D[2023-01-01], @@ -70,7 +64,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do MembershipFeeCycle |> Ash.Changeset.for_create(:create, attrs) - |> Ash.create!(actor: system_actor) + |> Ash.create!() end describe "cycles table display" do @@ -167,13 +161,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do |> render_click() # Verify cycle is now paid - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - updated_cycle = - Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id), - actor: system_actor - ) - + updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id)) assert updated_cycle.status == :paid end @@ -198,13 +186,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do |> render_click() # Verify cycle is now suspended - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - updated_cycle = - Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id), - actor: system_actor - ) - + updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id)) assert updated_cycle.status == :suspended end @@ -229,13 +211,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do |> render_click() # Verify cycle is now unpaid - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - updated_cycle = - Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id), - actor: system_actor - ) - + updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id)) assert updated_cycle.status == :unpaid end end diff --git a/test/mv_web/member_live/show_test.exs b/test/mv_web/member_live/show_test.exs index d2c6e55..fdcfebb 100644 --- a/test/mv_web/member_live/show_test.exs +++ b/test/mv_web/member_live/show_test.exs @@ -21,8 +21,6 @@ defmodule MvWeb.MemberLive.ShowTest do alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create test member {:ok, member} = Member @@ -31,16 +29,15 @@ defmodule MvWeb.MemberLive.ShowTest do last_name: "Anderson", email: "alice@example.com" }) - |> Ash.create(actor: system_actor) + |> Ash.create() - %{member: member, actor: system_actor} + %{member: member} end describe "custom fields section visibility (Issue #282)" do test "displays Custom Fields section even when member has no custom field values", %{ conn: conn, - member: member, - actor: actor + member: member } do # Create a custom field but no value for the member {:ok, custom_field} = @@ -49,7 +46,7 @@ defmodule MvWeb.MemberLive.ShowTest do name: "phone_mobile", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -66,8 +63,7 @@ defmodule MvWeb.MemberLive.ShowTest do test "displays Custom Fields section with multiple custom fields, some without values", %{ conn: conn, - member: member, - actor: actor + member: member } do # Create multiple custom fields {:ok, field1} = @@ -76,7 +72,7 @@ defmodule MvWeb.MemberLive.ShowTest do name: "phone_mobile", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() {:ok, field2} = CustomField @@ -84,7 +80,7 @@ defmodule MvWeb.MemberLive.ShowTest do name: "membership_number", value_type: :integer }) - |> Ash.create(actor: actor) + |> Ash.create() # Create value only for first field {:ok, _cfv} = @@ -94,7 +90,7 @@ defmodule MvWeb.MemberLive.ShowTest do custom_field_id: field1.id, value: %{"_union_type" => "string", "_union_value" => "+49123456789"} }) - |> Ash.create(actor: actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -115,19 +111,18 @@ defmodule MvWeb.MemberLive.ShowTest do test "does not display Custom Fields section when no custom fields exist", %{ conn: conn, - member: member, - actor: actor + member: member } do # Ensure no custom fields exist for this test # This ensures test isolation even if previous tests created custom fields - existing_custom_fields = Ash.read!(CustomField, actor: actor) + existing_custom_fields = Ash.read!(CustomField) for cf <- existing_custom_fields do - Ash.destroy!(cf, actor: actor) + Ash.destroy!(cf) end # Verify no custom fields exist - assert Ash.read!(CustomField, actor: actor) == [] + assert Ash.read!(CustomField) == [] conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -138,14 +133,14 @@ defmodule MvWeb.MemberLive.ShowTest do end describe "custom field value formatting" do - test "formats string custom field values", %{conn: conn, member: member, actor: actor} do + test "formats string custom field values", %{conn: conn, member: member} do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "phone_mobile", value_type: :string }) - |> Ash.create(actor: actor) + |> Ash.create() {:ok, _cfv} = CustomFieldValue @@ -154,7 +149,7 @@ defmodule MvWeb.MemberLive.ShowTest do custom_field_id: custom_field.id, value: %{"_union_type" => "string", "_union_value" => "+49123456789"} }) - |> Ash.create(actor: actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -162,18 +157,14 @@ defmodule MvWeb.MemberLive.ShowTest do assert html =~ "+49123456789" end - test "formats email custom field values as mailto links", %{ - conn: conn, - member: member, - actor: actor - } do + test "formats email custom field values as mailto links", %{conn: conn, member: member} do {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "private_email", value_type: :email }) - |> Ash.create(actor: actor) + |> Ash.create() {:ok, _cfv} = CustomFieldValue @@ -182,7 +173,7 @@ defmodule MvWeb.MemberLive.ShowTest do custom_field_id: custom_field.id, value: %{"_union_type" => "email", "_union_value" => "private@example.com"} }) - |> Ash.create(actor: actor) + |> Ash.create() conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") diff --git a/test/mv_web/user_live/form_member_dropdown_test.exs b/test/mv_web/user_live/form_member_dropdown_test.exs index c4387ce..0e93d4d 100644 --- a/test/mv_web/user_live/form_member_dropdown_test.exs +++ b/test/mv_web/user_live/form_member_dropdown_test.exs @@ -70,17 +70,12 @@ defmodule MvWeb.UserLive.FormMemberDropdownTest do test "links user and member with identical email successfully", %{conn: conn} do conn = setup_admin_conn(conn) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, member} = - Membership.create_member( - %{ - first_name: "David", - last_name: "Miller", - email: "david@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "David", + last_name: "Miller", + email: "david@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -111,17 +106,12 @@ defmodule MvWeb.UserLive.FormMemberDropdownTest do test "shows member with same email in dropdown", %{conn: conn} do conn = setup_admin_conn(conn) - system_actor = Mv.Helpers.SystemActor.get_system_actor() - {:ok, _member} = - Membership.create_member( - %{ - first_name: "Emma", - last_name: "Davis", - email: "emma@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Emma", + last_name: "Davis", + email: "emma@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -145,18 +135,13 @@ defmodule MvWeb.UserLive.FormMemberDropdownTest do # Helper functions defp create_unlinked_members(count) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - for i <- 1..count do {:ok, member} = - Membership.create_member( - %{ - first_name: "FirstName#{i}", - last_name: "LastName#{i}", - email: "member#{i}@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "FirstName#{i}", + last_name: "LastName#{i}", + email: "member#{i}@example.com" + }) member end diff --git a/test/mv_web/user_live/form_member_search_test.exs b/test/mv_web/user_live/form_member_search_test.exs index e45df49..b2644f3 100644 --- a/test/mv_web/user_live/form_member_search_test.exs +++ b/test/mv_web/user_live/form_member_search_test.exs @@ -18,18 +18,14 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do describe "fuzzy search" do test "finds member with exact name", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) {:ok, _member} = - Membership.create_member( - %{ - first_name: "Jonathan", - last_name: "Smith", - email: "jonathan.smith@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Jonathan", + last_name: "Smith", + email: "jonathan.smith@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -45,18 +41,14 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do end test "finds member with typo (Jon finds Jonathan)", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) {:ok, _member} = - Membership.create_member( - %{ - first_name: "Jonathan", - last_name: "Smith", - email: "jonathan.smith@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Jonathan", + last_name: "Smith", + email: "jonathan.smith@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -73,18 +65,14 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do end test "finds member with partial substring", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) {:ok, _member} = - Membership.create_member( - %{ - first_name: "Alexander", - last_name: "Williams", - email: "alex@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Alexander", + last_name: "Williams", + email: "alex@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -99,18 +87,14 @@ defmodule MvWeb.UserLive.FormMemberSearchTest do end test "shows partial match with similar names", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) {:ok, _member} = - Membership.create_member( - %{ - first_name: "Johnny", - last_name: "Doeson", - email: "johnny@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Johnny", + last_name: "Doeson", + email: "johnny@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") diff --git a/test/mv_web/user_live/form_member_selection_test.exs b/test/mv_web/user_live/form_member_selection_test.exs index 2ee3caa..74810df 100644 --- a/test/mv_web/user_live/form_member_selection_test.exs +++ b/test/mv_web/user_live/form_member_selection_test.exs @@ -19,18 +19,14 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do describe "member selection" do test "input field shows selected member name", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) {:ok, member} = - Membership.create_member( - %{ - first_name: "Alice", - last_name: "Johnson", - email: "alice@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Alice", + last_name: "Johnson", + email: "alice@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -51,18 +47,14 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do end test "confirmation box appears", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) {:ok, member} = - Membership.create_member( - %{ - first_name: "Bob", - last_name: "Williams", - email: "bob@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Bob", + last_name: "Williams", + email: "bob@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -85,18 +77,14 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do end test "hidden input stores member ID", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) {:ok, member} = - Membership.create_member( - %{ - first_name: "Charlie", - last_name: "Brown", - email: "charlie@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Charlie", + last_name: "Brown", + email: "charlie@example.com" + }) {:ok, view, _html} = live(conn, ~p"/users/new") @@ -117,27 +105,20 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do describe "unlink workflow" do test "unlink hides dropdown", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) # Create user with linked member {:ok, member} = - Membership.create_member( - %{ - first_name: "Frank", - last_name: "Wilson", - email: "frank@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Frank", + last_name: "Wilson", + email: "frank@example.com" + }) {:ok, user} = - Accounts.create_user( - %{ - email: "frank@example.com", - member: %{id: member.id} - }, - actor: system_actor - ) + Accounts.create_user(%{ + email: "frank@example.com", + member: %{id: member.id} + }) {:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit") @@ -153,27 +134,20 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do end test "unlink shows warning", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) # Create user with linked member {:ok, member} = - Membership.create_member( - %{ - first_name: "Grace", - last_name: "Taylor", - email: "grace@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Grace", + last_name: "Taylor", + email: "grace@example.com" + }) {:ok, user} = - Accounts.create_user( - %{ - email: "grace@example.com", - member: %{id: member.id} - }, - actor: system_actor - ) + Accounts.create_user(%{ + email: "grace@example.com", + member: %{id: member.id} + }) {:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit") @@ -190,27 +164,20 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do end test "unlink disables input", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) # Create user with linked member {:ok, member} = - Membership.create_member( - %{ - first_name: "Henry", - last_name: "Anderson", - email: "henry@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Henry", + last_name: "Anderson", + email: "henry@example.com" + }) {:ok, user} = - Accounts.create_user( - %{ - email: "henry@example.com", - member: %{id: member.id} - }, - actor: system_actor - ) + Accounts.create_user(%{ + email: "henry@example.com", + member: %{id: member.id} + }) {:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit") @@ -226,27 +193,20 @@ defmodule MvWeb.UserLive.FormMemberSelectionTest do end test "save re-enables member selection", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() conn = setup_admin_conn(conn) # Create user with linked member {:ok, member} = - Membership.create_member( - %{ - first_name: "Isabel", - last_name: "Martinez", - email: "isabel@example.com" - }, - actor: system_actor - ) + Membership.create_member(%{ + first_name: "Isabel", + last_name: "Martinez", + email: "isabel@example.com" + }) {:ok, user} = - Accounts.create_user( - %{ - email: "isabel@example.com", - member: %{id: member.id} - }, - actor: system_actor - ) + Accounts.create_user(%{ + email: "isabel@example.com", + member: %{id: member.id} + }) {:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit") diff --git a/test/mv_web/user_live/form_test.exs b/test/mv_web/user_live/form_test.exs index ed309fb..334dedd 100644 --- a/test/mv_web/user_live/form_test.exs +++ b/test/mv_web/user_live/form_test.exs @@ -75,14 +75,11 @@ defmodule MvWeb.UserLive.FormTest do |> form("#user-form", user: %{email: "storetest@example.com"}) |> render_submit() - system_actor = Mv.Helpers.SystemActor.get_system_actor() - user = Ash.get!( Mv.Accounts.User, [email: Ash.CiString.new("storetest@example.com")], - domain: Mv.Accounts, - actor: system_actor + domain: Mv.Accounts ) assert to_string(user.email) == "storetest@example.com" @@ -104,14 +101,11 @@ defmodule MvWeb.UserLive.FormTest do ) |> render_submit() - system_actor = Mv.Helpers.SystemActor.get_system_actor() - user = Ash.get!( Mv.Accounts.User, [email: Ash.CiString.new("passwordstoretest@example.com")], - domain: Mv.Accounts, - actor: system_actor + domain: Mv.Accounts ) assert user.hashed_password != nil @@ -187,8 +181,7 @@ defmodule MvWeb.UserLive.FormTest do assert_redirected(view, "/users") - system_actor = Mv.Helpers.SystemActor.get_system_actor() - updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor) + updated_user = Ash.reload!(user, domain: Mv.Accounts) assert to_string(updated_user.email) == "new@example.com" assert updated_user.hashed_password == original_password end @@ -211,8 +204,7 @@ defmodule MvWeb.UserLive.FormTest do assert_redirected(view, "/users") - system_actor = Mv.Helpers.SystemActor.get_system_actor() - updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor) + updated_user = Ash.reload!(user, domain: Mv.Accounts) assert updated_user.hashed_password != original_password assert String.starts_with?(updated_user.hashed_password, "$2b$") end @@ -293,24 +285,17 @@ defmodule MvWeb.UserLive.FormTest do describe "member linking - display" do test "shows linked member with unlink button when user has member", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create member {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "John", - last_name: "Doe", - email: "john@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john@example.com" + }) # Create user linked to member user = create_test_user(%{email: "user@example.com"}) - - {:ok, _updated_user} = - Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor) + {:ok, _updated_user} = Mv.Accounts.update_user(user, %{member: %{id: member.id}}) # Load form {:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit") @@ -337,18 +322,13 @@ defmodule MvWeb.UserLive.FormTest do describe "member linking - workflow" do test "selecting member and saving links member to user", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create unlinked member {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Jane", - last_name: "Smith", - email: "jane@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Jane", + last_name: "Smith", + email: "jane@example.com" + }) # Create user without member user = create_test_user(%{email: "user@example.com"}) @@ -365,35 +345,22 @@ defmodule MvWeb.UserLive.FormTest do assert_redirected(view, "/users") # Verify member is linked - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - updated_user = - Ash.get!(Mv.Accounts.User, user.id, - domain: Mv.Accounts, - actor: system_actor, - load: [:member] - ) - + updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member]) assert updated_user.member.id == member.id end test "unlinking member and saving removes member from user", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create member {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Bob", - last_name: "Wilson", - email: "bob@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Bob", + last_name: "Wilson", + email: "bob@example.com" + }) # Create user linked to member user = create_test_user(%{email: "user@example.com"}) - {:ok, _} = Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor) + {:ok, _} = Mv.Accounts.update_user(user, %{member: %{id: member.id}}) {:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit") @@ -408,15 +375,7 @@ defmodule MvWeb.UserLive.FormTest do assert_redirected(view, "/users") # Verify member is unlinked - system_actor = Mv.Helpers.SystemActor.get_system_actor() - - updated_user = - Ash.get!(Mv.Accounts.User, user.id, - domain: Mv.Accounts, - actor: system_actor, - load: [:member] - ) - + updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member]) assert is_nil(updated_user.member) end end diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs index 41c198d..360ef72 100644 --- a/test/mv_web/user_live/index_test.exs +++ b/test/mv_web/user_live/index_test.exs @@ -407,24 +407,17 @@ defmodule MvWeb.UserLive.IndexTest do describe "member linking display" do test "displays linked member name in user list", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create member {:ok, member} = - Mv.Membership.create_member( - %{ - first_name: "Alice", - last_name: "Johnson", - email: "alice@example.com" - }, - actor: system_actor - ) + Mv.Membership.create_member(%{ + first_name: "Alice", + last_name: "Johnson", + email: "alice@example.com" + }) # Create user linked to member user = create_test_user(%{email: "user@example.com"}) - - {:ok, _updated_user} = - Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor) + {:ok, _updated_user} = Mv.Accounts.update_user(user, %{member: %{id: member.id}}) # Create another user without member _unlinked_user = create_test_user(%{email: "unlinked@example.com"}) diff --git a/test/seeds_test.exs b/test/seeds_test.exs index 67b376e..c28eab9 100644 --- a/test/seeds_test.exs +++ b/test/seeds_test.exs @@ -3,42 +3,37 @@ defmodule Mv.SeedsTest do require Ash.Query - setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - %{actor: system_actor} - end - describe "Seeds script" do - test "runs successfully without errors", %{actor: actor} do + test "runs successfully without errors" do # Run the seeds script - should not raise any errors assert Code.eval_file("priv/repo/seeds.exs") # Basic smoke test: ensure some data was created - {:ok, users} = Ash.read(Mv.Accounts.User, actor: actor) - {:ok, members} = Ash.read(Mv.Membership.Member, actor: actor) - {:ok, custom_fields} = Ash.read(Mv.Membership.CustomField, actor: actor) + {:ok, users} = Ash.read(Mv.Accounts.User) + {:ok, members} = Ash.read(Mv.Membership.Member) + {:ok, custom_fields} = Ash.read(Mv.Membership.CustomField) assert not Enum.empty?(users), "Seeds should create at least one user" assert not Enum.empty?(members), "Seeds should create at least one member" assert not Enum.empty?(custom_fields), "Seeds should create at least one custom field" end - test "can be run multiple times (idempotent)", %{actor: actor} do + test "can be run multiple times (idempotent)" do # Run seeds first time assert Code.eval_file("priv/repo/seeds.exs") # Count records - {:ok, users_count_1} = Ash.read(Mv.Accounts.User, actor: actor) - {:ok, members_count_1} = Ash.read(Mv.Membership.Member, actor: actor) - {:ok, custom_fields_count_1} = Ash.read(Mv.Membership.CustomField, actor: actor) + {:ok, users_count_1} = Ash.read(Mv.Accounts.User) + {:ok, members_count_1} = Ash.read(Mv.Membership.Member) + {:ok, custom_fields_count_1} = Ash.read(Mv.Membership.CustomField) # Run seeds second time - should not raise errors assert Code.eval_file("priv/repo/seeds.exs") # Count records again - should be the same (upsert, not duplicate) - {:ok, users_count_2} = Ash.read(Mv.Accounts.User, actor: actor) - {:ok, members_count_2} = Ash.read(Mv.Membership.Member, actor: actor) - {:ok, custom_fields_count_2} = Ash.read(Mv.Membership.CustomField, actor: actor) + {:ok, users_count_2} = Ash.read(Mv.Accounts.User) + {:ok, members_count_2} = Ash.read(Mv.Membership.Member) + {:ok, custom_fields_count_2} = Ash.read(Mv.Membership.CustomField) assert length(users_count_1) == length(users_count_2), "Users count should remain same after re-running seeds" @@ -50,12 +45,12 @@ defmodule Mv.SeedsTest do "CustomFields count should remain same after re-running seeds" end - test "at least one member has no membership fee type assigned", %{actor: actor} do + test "at least one member has no membership fee type assigned" do # Run the seeds script assert Code.eval_file("priv/repo/seeds.exs") # Get all members - {:ok, members} = Ash.read(Mv.Membership.Member, actor: actor) + {:ok, members} = Ash.read(Mv.Membership.Member) # At least one member should have no membership_fee_type_id members_without_fee_type = @@ -65,13 +60,13 @@ defmodule Mv.SeedsTest do "At least one member should have no membership fee type assigned" end - test "each membership fee type has at least one member", %{actor: actor} do + test "each membership fee type has at least one member" do # Run the seeds script assert Code.eval_file("priv/repo/seeds.exs") # Get all fee types and members - {:ok, fee_types} = Ash.read(Mv.MembershipFees.MembershipFeeType, actor: actor) - {:ok, members} = Ash.read(Mv.Membership.Member, actor: actor) + {:ok, fee_types} = Ash.read(Mv.MembershipFees.MembershipFeeType) + {:ok, members} = Ash.read(Mv.Membership.Member) # Group members by fee type (excluding nil) members_by_fee_type = @@ -88,12 +83,12 @@ defmodule Mv.SeedsTest do end) end - test "members with fee types have cycles with various statuses", %{actor: actor} do + test "members with fee types have cycles with various statuses" do # Run the seeds script assert Code.eval_file("priv/repo/seeds.exs") # Get all members with fee types - {:ok, members} = Ash.read(Mv.Membership.Member, actor: actor) + {:ok, members} = Ash.read(Mv.Membership.Member) members_with_fee_types = members @@ -109,7 +104,7 @@ defmodule Mv.SeedsTest do |> Enum.flat_map(fn member -> Mv.MembershipFees.MembershipFeeCycle |> Ash.Query.filter(member_id == ^member.id) - |> Ash.read!(actor: actor) + |> Ash.read!() end) |> Enum.map(& &1.status) @@ -121,140 +116,4 @@ defmodule Mv.SeedsTest do assert :suspended in all_cycle_statuses, "At least one cycle should be suspended" end end - - describe "Authorization roles (from seeds)" do - test "creates all 5 authorization roles with correct permission sets" do - # Run seeds once for this test - Code.eval_file("priv/repo/seeds.exs") - {:ok, roles} = Ash.read(Mv.Authorization.Role, domain: Mv.Authorization, authorize?: false) - - assert length(roles) >= 5, "Should have at least 5 roles" - - # Check each role - role_configs = [ - {"Mitglied", "own_data", true}, - {"Vorstand", "read_only", false}, - {"Kassenwart", "normal_user", false}, - {"Buchhaltung", "read_only", false}, - {"Admin", "admin", false} - ] - - Enum.each(role_configs, fn {name, perm_set, is_system} -> - role = Enum.find(roles, &(&1.name == name)) - assert role, "Role #{name} should exist" - assert role.permission_set_name == perm_set - assert role.is_system_role == is_system - end) - end - - test "Mitglied role is marked as system role" do - Code.eval_file("priv/repo/seeds.exs") - - {:ok, mitglied} = - Mv.Authorization.Role - |> Ash.Query.filter(name == "Mitglied") - |> Ash.read_one(domain: Mv.Authorization, authorize?: false) - - assert mitglied.is_system_role == true - end - - test "all roles have valid permission_set_names" do - Code.eval_file("priv/repo/seeds.exs") - - {:ok, roles} = Ash.read(Mv.Authorization.Role, domain: Mv.Authorization, authorize?: false) - - valid_sets = - Mv.Authorization.PermissionSets.all_permission_sets() - |> Enum.map(&Atom.to_string/1) - - Enum.each(roles, fn role -> - assert role.permission_set_name in valid_sets, - "Role #{role.name} has invalid permission_set_name: #{role.permission_set_name}" - end) - end - - test "assigns Admin role to ADMIN_EMAIL user" do - Code.eval_file("priv/repo/seeds.exs") - - admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" - - {:ok, admin_user} = - Mv.Accounts.User - |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts, authorize?: false) - - assert admin_user != nil, "Admin user should exist after seeds run" - - {:ok, admin_user_with_role} = - Ash.load(admin_user, :role, domain: Mv.Accounts, authorize?: false) - - assert admin_user_with_role.role != nil, "Admin user should have a role assigned" - assert admin_user_with_role.role.name == "Admin" - assert admin_user_with_role.role.permission_set_name == "admin" - end - end - - describe "Authorization role assignment" do - test "does not change role of users who already have a role" do - # Seeds once (creates Admin with Admin role) - Code.eval_file("priv/repo/seeds.exs") - - admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost" - - {:ok, admin_user} = - Mv.Accounts.User - |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts, authorize?: false) - - assert admin_user != nil, "Admin user should exist after seeds run" - - {:ok, admin_user_with_role} = - Ash.load(admin_user, :role, domain: Mv.Accounts, authorize?: false) - - assert admin_user_with_role.role != nil, "Admin user should have a role assigned" - original_role_id = admin_user_with_role.role_id - assert admin_user_with_role.role.name == "Admin" - - # Seeds again - Code.eval_file("priv/repo/seeds.exs") - - # Admin reloaded - {:ok, admin_reloaded} = - Mv.Accounts.User - |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts, authorize?: false) - - assert admin_reloaded != nil, "Admin user should still exist after re-running seeds" - - {:ok, admin_reloaded_with_role} = - Ash.load(admin_reloaded, :role, domain: Mv.Accounts, authorize?: false) - - assert admin_reloaded_with_role.role != nil, - "Admin user should still have a role after re-running seeds" - - assert admin_reloaded_with_role.role_id == original_role_id - assert admin_reloaded_with_role.role.name == "Admin" - end - - test "role creation is idempotent" do - Code.eval_file("priv/repo/seeds.exs") - - {:ok, roles_1} = - Ash.read(Mv.Authorization.Role, domain: Mv.Authorization, authorize?: false) - - Code.eval_file("priv/repo/seeds.exs") - - {:ok, roles_2} = - Ash.read(Mv.Authorization.Role, domain: Mv.Authorization, authorize?: false) - - assert length(roles_1) == length(roles_2), - "Role count should remain same after re-running seeds" - - # Each role should appear exactly once - role_names = Enum.map(roles_2, & &1.name) - - assert length(role_names) == length(Enum.uniq(role_names)), - "Each role name should appear exactly once" - end - end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 290b3ac..3b2a5ed 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -115,16 +115,15 @@ defmodule MvWeb.ConnCase do # Create admin role and assign it admin_role = Mv.Fixtures.role_fixture("admin") - system_actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, user} = user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update(actor: system_actor) + |> Ash.update() # Load role for authorization - user_with_role = Ash.load!(user, :role, domain: Mv.Accounts, actor: system_actor) + user_with_role = Ash.load!(user, :role, domain: Mv.Accounts) sign_in_user_via_oidc(conn, user_with_role) end diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 630125c..4ba75ef 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -16,8 +16,6 @@ defmodule Mv.DataCase do use ExUnit.CaseTemplate - require Ash.Query - using do quote do alias Mv.Repo @@ -31,10 +29,6 @@ defmodule Mv.DataCase do setup tags do Mv.DataCase.setup_sandbox(tags) - # Ensure "Mitglied" role exists for default role assignment to work in tests - # Note: This runs in every test because each test runs in a sandboxed database. - # The check is fast (single query) and idempotent (skips if role exists). - Mv.DataCase.ensure_default_role() :ok end @@ -48,36 +42,6 @@ defmodule Mv.DataCase do pid end - @doc """ - Ensures the default "Mitglied" role exists in the test database. - - This is necessary because the role_id attribute's default function expects this role to exist. - Tests run in sandbox mode, so the role needs to be created for each test. - """ - def ensure_default_role do - # Check if "Mitglied" role already exists - case Mv.Authorization.Role.get_mitglied_role() do - {:ok, nil} -> - # Create the role if it doesn't exist - Mv.Authorization.Role - |> Ash.Changeset.for_create(:create_role_with_system_flag, %{ - name: "Mitglied", - description: "Default member role with access to own data only", - permission_set_name: "own_data", - is_system_role: true - }) - |> Ash.create!(authorize?: false, domain: Mv.Authorization) - - {:ok, _role} -> - # Role already exists, do nothing - :ok - - {:error, _error} -> - # Ignore errors (e.g., in tests that don't need roles) - :ok - end - end - @doc """ A helper that transforms changeset errors into a map of messages. diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 23d4aa7..d474764 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -9,8 +9,6 @@ defmodule Mv.Fixtures do @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 @@ -27,15 +25,13 @@ defmodule Mv.Fixtures do """ def member_fixture(attrs \\ %{}) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - attrs |> Enum.into(%{ first_name: "Test", last_name: "Member", email: "test#{System.unique_integer([:positive])}@example.com" }) - |> Mv.Membership.create_member(actor: system_actor) + |> Mv.Membership.create_member() |> case do {:ok, member} -> member {:error, error} -> raise "Failed to create member: #{inspect(error)}" @@ -45,11 +41,6 @@ defmodule Mv.Fixtures do @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 @@ -66,13 +57,11 @@ defmodule Mv.Fixtures do """ def user_fixture(attrs \\ %{}) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - attrs |> Enum.into(%{ email: "user#{System.unique_integer([:positive])}@example.com" }) - |> Mv.Accounts.create_user(actor: system_actor) + |> Mv.Accounts.create_user() |> case do {:ok, user} -> user {:error, error} -> raise "Failed to create user: #{inspect(error)}" @@ -108,8 +97,6 @@ defmodule Mv.Fixtures do @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") @@ -123,17 +110,13 @@ defmodule Mv.Fixtures do """ def role_fixture(permission_set_name) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}" - case Mv.Authorization.create_role( - %{ - name: role_name, - description: "Test role for #{permission_set_name}", - permission_set_name: permission_set_name - }, - actor: system_actor - ) do + case Mv.Authorization.create_role(%{ + name: role_name, + description: "Test role for #{permission_set_name}", + permission_set_name: permission_set_name + }) do {:ok, role} -> role {:error, error} -> raise "Failed to create role: #{inspect(error)}" end @@ -157,8 +140,6 @@ defmodule Mv.Fixtures do """ def user_with_role_fixture(permission_set_name \\ "admin", user_attrs \\ %{}) do - system_actor = Mv.Helpers.SystemActor.get_system_actor() - # Create role with permission set role = role_fixture(permission_set_name) @@ -168,57 +149,7 @@ defmodule Mv.Fixtures do |> Enum.into(%{ email: "user#{System.unique_integer([:positive])}@example.com" }) - |> Mv.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: Mv.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() + |> Mv.Accounts.create_user() # Assign role to user {:ok, user} = @@ -227,7 +158,7 @@ defmodule Mv.Fixtures do |> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove) |> Ash.update() - # Reload user with role preloaded + # Reload user with role preloaded (critical for authorization!) {:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts) user_with_role end