Add actor parameter to all tests requiring authorization

This commit adds actor: system_actor to all Ash operations in tests that
require authorization.
This commit is contained in:
Moritz 2026-01-23 20:00:24 +01:00
parent 686f69c9e9
commit 0f48a9b15a
Signed by: moritz
GPG key ID: 1020A035E5DD0824
75 changed files with 4686 additions and 2859 deletions

View file

@ -8,8 +8,13 @@ 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} do
test "new user can register via OIDC", %{conn: _conn, actor: actor} do
# Simulate OIDC callback for brand new user
user_info = %{
"sub" => "new_oidc_user_123",
@ -18,10 +23,13 @@ defmodule MvWeb.OidcE2EFlowTest do
# Call register action
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
assert {:ok, new_user} = result
assert to_string(new_user.email) == "newuser@example.com"
@ -30,17 +38,20 @@ 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: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
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} do
test "existing OIDC user can sign in and email updates", %{conn: _conn, actor: actor} do
# Create OIDC user
user =
create_test_user(%{
@ -56,10 +67,13 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: updated_user_info,
oauth_tokens: %{}
},
actor: actor
)
# Same user, updated email
assert updated_user.id == user.id
@ -70,7 +84,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} do
%{conn: _conn, actor: actor} do
# Step 1: Create a password-only user
password_user =
create_test_user(%{
@ -86,10 +100,13 @@ defmodule MvWeb.OidcE2EFlowTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# Step 3: Should fail with PasswordVerificationRequired
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -106,7 +123,7 @@ defmodule MvWeb.OidcE2EFlowTest do
end
test "full E2E flow: OIDC collision -> password verification -> account linked",
%{conn: _conn} do
%{conn: _conn, actor: actor} do
# Step 1: Create password user
password_user =
create_test_user(%{
@ -122,10 +139,13 @@ defmodule MvWeb.OidcE2EFlowTest do
}
{:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# Extract the error
password_error =
@ -142,12 +162,12 @@ defmodule MvWeb.OidcE2EFlowTest do
{:ok, linked_user} =
Mv.Accounts.User
|> Ash.Query.filter(id == ^password_user.id)
|> Ash.read_one!()
|> Ash.read_one!(actor: actor)
|> Ash.Changeset.for_update(:link_oidc_id, %{
oidc_id: user_info["sub"],
oidc_user_info: user_info
})
|> Ash.update()
|> Ash.update(actor: actor)
# Verify account is now linked
assert linked_user.id == password_user.id
@ -158,17 +178,20 @@ 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: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
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} do
%{conn: _conn, actor: actor} do
# Password user with old email
password_user =
create_test_user(%{
@ -199,12 +222,12 @@ defmodule MvWeb.OidcE2EFlowTest do
{:ok, linked_user} =
Mv.Accounts.User
|> Ash.Query.filter(id == ^password_user.id)
|> Ash.read_one!()
|> Ash.read_one!(actor: actor)
|> Ash.Changeset.for_update(:link_oidc_id, %{
oidc_id: updated_user_info["sub"],
oidc_user_info: updated_user_info
})
|> Ash.update()
|> Ash.update(actor: actor)
# Email should be updated to match OIDC provider
assert to_string(linked_user.email) == "new@example.com"
@ -213,7 +236,10 @@ 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} do
test "E2E: email sync to member when linking OIDC to password account", %{
conn: _conn,
actor: actor
} do
# Create member
member =
Ash.Seed.seed!(Mv.Membership.Member, %{
@ -239,10 +265,13 @@ defmodule MvWeb.OidcE2EFlowTest do
# Collision detected
{:error, %Ash.Error.Invalid{}} =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# After password verification, link OIDC with NEW email
updated_user_info = %{
@ -253,24 +282,27 @@ defmodule MvWeb.OidcE2EFlowTest do
{:ok, linked_user} =
Mv.Accounts.User
|> Ash.Query.filter(id == ^password_user.id)
|> Ash.read_one!()
|> Ash.read_one!(actor: actor)
|> Ash.Changeset.for_update(:link_oidc_id, %{
oidc_id: updated_user_info["sub"],
oidc_user_info: updated_user_info
})
|> Ash.update()
|> Ash.update(actor: actor)
# 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)
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
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} do
test "E2E: password-only user cannot be accessed via OIDC without password", %{
conn: _conn,
actor: actor
} do
# Create password user
_password_user =
create_test_user(%{
@ -287,10 +319,13 @@ 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: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
case result do
{:ok, []} ->
@ -305,17 +340,23 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
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} do
test "E2E: user with oidc_id cannot be hijacked by different OIDC provider", %{
conn: _conn,
actor: actor
} do
# User linked to OIDC provider A
_user =
create_test_user(%{
@ -331,10 +372,13 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# Should have hard error about "already linked to a different OIDC account"
assert Enum.any?(errors, fn
@ -351,7 +395,10 @@ defmodule MvWeb.OidcE2EFlowTest do
end)
end
test "E2E: empty string oidc_id is treated as password-only account", %{conn: _conn} do
test "E2E: empty string oidc_id is treated as password-only account", %{
conn: _conn,
actor: actor
} do
# User with empty oidc_id
_password_user =
create_test_user(%{
@ -367,10 +414,13 @@ defmodule MvWeb.OidcE2EFlowTest do
}
{:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# Should require password (empty string = no OIDC)
assert Enum.any?(errors, fn err ->
@ -380,32 +430,38 @@ defmodule MvWeb.OidcE2EFlowTest do
end
describe "E2E: Error scenarios" do
test "E2E: OIDC registration without oidc_id fails", %{conn: _conn} do
test "E2E: OIDC registration without oidc_id fails", %{conn: _conn, actor: actor} 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
assert Enum.any?(errors, fn err ->
match?(%Ash.Error.Changes.InvalidChanges{}, err)
end)
end
test "E2E: OIDC registration without email fails", %{conn: _conn} do
test "E2E: OIDC registration without email fails", %{conn: _conn, actor: actor} do
user_info = %{
"sub" => "noemail_123"
}
{:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
assert Enum.any?(errors, fn err ->
match?(%Ash.Error.Changes.Required{field: :email}, err)

View file

@ -5,8 +5,13 @@ 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" do
test "should succeed and update email", %{actor: actor} do
# Create OIDC user
{:ok, oidc_user} =
Mv.Accounts.User
@ -14,7 +19,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
email: "original@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_123")
|> Ash.create()
|> Ash.create(actor: actor)
# User logs in via OIDC with NEW email
user_info = %{
@ -23,10 +28,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should succeed and email should be updated
assert {:ok, updated_user} = result
@ -37,7 +45,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
end
describe "OIDC user updates email to email of passwordless user" do
test "should fail with clear error message" do
test "should fail with clear error message", %{actor: actor} do
# Create OIDC user
{:ok, _oidc_user} =
Mv.Accounts.User
@ -45,7 +53,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
email: "oidcuser@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_456")
|> Ash.create()
|> Ash.create(actor: actor)
# Create passwordless user with target email
{:ok, _passwordless_user} =
@ -53,7 +61,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
|> Ash.Changeset.for_create(:create_user, %{
email: "taken@example.com"
})
|> Ash.create()
|> Ash.create(actor: actor)
# OIDC user tries to update email to taken email
user_info = %{
@ -62,10 +70,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should fail with email update conflict error
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -88,7 +99,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" do
test "should fail with clear error message", %{actor: actor} do
# Create OIDC user
{:ok, _oidc_user} =
Mv.Accounts.User
@ -96,7 +107,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
email: "oidcuser2@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_789")
|> Ash.create()
|> Ash.create(actor: actor)
# Create password user with target email (explicitly NO oidc_id)
password_user =
@ -106,14 +117,14 @@ defmodule MvWeb.OidcEmailUpdateTest do
})
# Ensure it's a password-only user
{:ok, password_user} = Ash.reload(password_user)
{:ok, password_user} = Ash.reload(password_user, actor: actor)
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()
|> Ash.update(actor: actor)
assert is_nil(password_user.oidc_id)
@ -124,10 +135,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should fail with email update conflict error
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -150,7 +164,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" do
test "should fail with clear error message about different OIDC account", %{actor: actor} do
# Create first OIDC user
{:ok, _oidc_user1} =
Mv.Accounts.User
@ -158,7 +172,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
email: "oidcuser1@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_aaa")
|> Ash.create()
|> Ash.create(actor: actor)
# Create second OIDC user with target email
{:ok, _oidc_user2} =
@ -167,7 +181,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
email: "oidcuser2@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_bbb")
|> Ash.create()
|> Ash.create(actor: actor)
# First OIDC user tries to update email to second user's email
user_info = %{
@ -176,10 +190,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should fail with "already linked to different OIDC account" error
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -201,14 +218,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" do
test "new OIDC user with email of passwordless user triggers linking flow", %{actor: actor} do
# Create passwordless user
{:ok, passwordless_user} =
Mv.Accounts.User
|> Ash.Changeset.for_create(:create_user, %{
email: "passwordless@example.com"
})
|> Ash.create()
|> Ash.create(actor: actor)
# New OIDC user tries to register
user_info = %{
@ -217,10 +234,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should trigger PasswordVerificationRequired (linking flow)
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -234,7 +254,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
end)
end
test "new OIDC user with email of existing OIDC user shows hard error" do
test "new OIDC user with email of existing OIDC user shows hard error", %{actor: actor} do
# Create existing OIDC user
{:ok, _existing_oidc_user} =
Mv.Accounts.User
@ -242,7 +262,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
email: "existing@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "oidc_existing")
|> Ash.create()
|> Ash.create(actor: actor)
# New OIDC user tries to register with same email
user_info = %{
@ -251,10 +271,13 @@ defmodule MvWeb.OidcEmailUpdateTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should fail with "already linked to different OIDC account" error
assert {:error, %Ash.Error.Invalid{errors: errors}} = result

View file

@ -4,6 +4,11 @@ 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
@ -20,11 +25,16 @@ 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: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
assert found_user.id == user.id
assert to_string(found_user.email) == "existing@example.com"
@ -39,10 +49,15 @@ defmodule MvWeb.OidcIntegrationTest do
}
# Test register_with_rauthy action
case Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
}) do
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
{:ok, new_user} ->
assert to_string(new_user.email) == "newuser@example.com"
assert new_user.oidc_id == "brand_new_oidc_456"
@ -73,11 +88,16 @@ 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: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
# Either returns empty list OR authentication error - both mean "user not found"
case result do
@ -107,11 +127,16 @@ 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: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: correct_user_info,
oauth_tokens: %{}
},
actor: system_actor
)
assert found_user.id == user.id
@ -122,10 +147,13 @@ defmodule MvWeb.OidcIntegrationTest do
}
result =
Mv.Accounts.read_sign_in_with_rauthy(%{
user_info: wrong_user_info,
oauth_tokens: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: wrong_user_info,
oauth_tokens: %{}
},
actor: system_actor
)
# Either returns empty list OR authentication error - both mean "user not found"
case result do
@ -154,11 +182,16 @@ 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: %{}
})
Mv.Accounts.read_sign_in_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
# Either returns empty list OR authentication error - both mean "user not found"
case result do
@ -189,11 +222,16 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
# Should fail with hard error (not PasswordVerificationRequired)
# This prevents someone with OIDC provider B from taking over an account
@ -220,11 +258,16 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
assert {:error,
%Ash.Error.Invalid{
@ -239,11 +282,16 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -264,11 +312,16 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
assert user.id == existing_user.id
assert to_string(user.email) == "new@example.com"
@ -281,11 +334,16 @@ 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: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: system_actor
)
assert user.oidc_id == "alt_oidc_id_123"
assert to_string(user.email) == "altid@example.com"

View file

@ -8,9 +8,15 @@ 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" do
test "OIDC register with existing password user email fails with PasswordVerificationRequired",
%{actor: actor} do
# Create password-only user
existing_user =
create_test_user(%{
@ -26,10 +32,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# Should fail with PasswordVerificationRequired error
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -47,7 +56,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
end
@tag :test_proposal
test "PasswordVerificationRequired error contains necessary context" do
test "PasswordVerificationRequired error contains necessary context", %{actor: actor} do
existing_user =
create_test_user(%{
email: "test@example.com",
@ -61,10 +70,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
}
{:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
password_error =
Enum.find(errors, fn err ->
@ -78,7 +90,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
end
@tag :test_proposal
test "after successful password verification, oidc_id can be set" do
test "after successful password verification, oidc_id can be set", %{actor: actor} do
# Create password user
user =
create_test_user(%{
@ -97,12 +109,12 @@ defmodule MvWeb.OidcPasswordLinkingTest do
{:ok, updated_user} =
Mv.Accounts.User
|> Ash.Query.filter(id == ^user.id)
|> Ash.read_one!()
|> Ash.read_one!(actor: actor)
|> Ash.Changeset.for_update(:link_oidc_id, %{
oidc_id: user_info["sub"],
oidc_user_info: user_info
})
|> Ash.update()
|> Ash.update(actor: actor)
assert updated_user.id == user.id
assert updated_user.oidc_id == "linked_oidc_555"
@ -112,7 +124,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
end
@tag :test_proposal
test "password verification with wrong password keeps oidc_id as nil" do
test "password verification with wrong password keeps oidc_id as nil", %{actor: actor} do
# This test verifies that if password verification fails,
# the oidc_id should NOT be set
@ -131,7 +143,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)
{:ok, unchanged_user} = Ash.get(Mv.Accounts.User, user.id, actor: actor)
assert is_nil(unchanged_user.oidc_id)
assert unchanged_user.hashed_password == user.hashed_password
end
@ -139,7 +151,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" do
test "OIDC register with email of user having different oidc_id fails", %{actor: actor} do
# User already linked to OIDC provider A
_existing_user =
create_test_user(%{
@ -155,10 +167,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# Should fail - cannot link different OIDC account to same email
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -171,7 +186,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
end
@tag :test_proposal
test "existing OIDC user email remains unchanged when oidc_id matches" do
test "existing OIDC user email remains unchanged when oidc_id matches", %{actor: actor} do
user =
create_test_user(%{
email: "oidc@example.com",
@ -186,10 +201,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
# This should work via upsert
{:ok, updated_user} =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
assert updated_user.id == user.id
assert updated_user.oidc_id == "oidc_stable_789"
@ -199,7 +217,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" do
test "linking OIDC to password account updates email if different in OIDC", %{actor: actor} do
# Password user with old email
user =
create_test_user(%{
@ -218,19 +236,19 @@ defmodule MvWeb.OidcPasswordLinkingTest do
{:ok, updated_user} =
Mv.Accounts.User
|> Ash.Query.filter(id == ^user.id)
|> Ash.read_one!()
|> Ash.read_one!(actor: actor)
|> Ash.Changeset.for_update(:link_oidc_id, %{
oidc_id: user_info["sub"],
oidc_user_info: user_info
})
|> Ash.update()
|> Ash.update(actor: actor)
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" do
test "email change during linking triggers member email sync", %{actor: actor} do
# Create member
member =
Ash.Seed.seed!(Mv.Membership.Member, %{
@ -257,25 +275,25 @@ defmodule MvWeb.OidcPasswordLinkingTest do
{:ok, updated_user} =
Mv.Accounts.User
|> Ash.Query.filter(id == ^user.id)
|> Ash.read_one!()
|> Ash.read_one!(actor: actor)
|> Ash.Changeset.for_update(:link_oidc_id, %{
oidc_id: user_info["sub"],
oidc_user_info: user_info
})
|> Ash.update()
|> Ash.update(actor: actor)
# 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)
{:ok, updated_member} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
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" do
test "user with empty string oidc_id is treated as password-only user", %{actor: actor} do
_user =
create_test_user(%{
email: "empty@example.com",
@ -290,10 +308,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{}
},
actor: actor
)
# Should trigger PasswordVerificationRequired (empty string = no OIDC)
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
@ -307,7 +328,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
end
@tag :test_proposal
test "cannot link same oidc_id to multiple users" do
test "cannot link same oidc_id to multiple users", %{actor: actor} do
# User 1 with OIDC
_user1 =
create_test_user(%{
@ -323,7 +344,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
email: "user2@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "shared_oidc_333")
|> Ash.create()
|> Ash.create(actor: actor)
# Should fail due to unique constraint on oidc_id
assert match?({:error, %Ash.Error.Invalid{}}, result)
@ -337,14 +358,16 @@ 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" do
test "user without password and without oidc_id triggers PasswordVerificationRequired", %{
actor: actor
} 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()
|> Ash.create(actor: actor)
# Verify user has no password and no oidc_id
assert is_nil(existing_user.hashed_password)
@ -372,14 +395,14 @@ defmodule MvWeb.OidcPasswordLinkingTest do
end)
end
test "user without password but WITH password later requires verification" do
test "user without password but WITH password later requires verification", %{actor: actor} do
# Create user without password first
{:ok, user} =
Mv.Accounts.User
|> Ash.Changeset.for_create(:create_user, %{
email: "added-password@example.com"
})
|> Ash.create()
|> Ash.create(actor: actor)
# User sets password later (using admin action)
{:ok, user_with_password} =
@ -387,7 +410,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
|> Ash.Changeset.for_update(:admin_set_password, %{
password: "newpassword123"
})
|> Ash.update()
|> Ash.update(actor: actor)
assert not is_nil(user_with_password.hashed_password)
@ -398,10 +421,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should fail with PasswordVerificationRequired
assert {:error, %Ash.Error.Invalid{}} = result
@ -414,7 +440,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)" do
test "user with different oidc_id cannot be linked (hard error)", %{actor: actor} do
# Create user with existing OIDC ID
{:ok, existing_user} =
Mv.Accounts.User
@ -422,7 +448,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
email: "already-linked@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999")
|> Ash.create()
|> Ash.create(actor: actor)
assert existing_user.oidc_id == "original_oidc_999"
@ -433,10 +459,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should fail with hard error (not PasswordVerificationRequired)
assert {:error, %Ash.Error.Invalid{}} = result
@ -459,7 +488,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
end)
end
test "cannot link different oidc_id even with password verification" do
test "cannot link different oidc_id even with password verification", %{actor: actor} do
# Create user with password AND existing OIDC ID
existing_user =
create_test_user(%{
@ -478,10 +507,13 @@ defmodule MvWeb.OidcPasswordLinkingTest do
}
result =
Mv.Accounts.create_register_with_rauthy(%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
})
Mv.Accounts.create_register_with_rauthy(
%{
user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"}
},
actor: actor
)
# Should fail - cannot link different OIDC ID
assert {:error, %Ash.Error.Invalid{}} = result

View file

@ -7,15 +7,20 @@ 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" do
test "passwordless user can be linked via link_passwordless_oidc action", %{actor: actor} 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()
|> Ash.create(actor: actor)
# Verify user has no password and no oidc_id
assert is_nil(existing_user.hashed_password)
@ -31,7 +36,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
"preferred_username" => "invited@example.com"
}
})
|> Ash.update()
|> Ash.update(actor: actor)
# User should now have oidc_id linked
assert linked_user.oidc_id == "auto_link_oidc_123"
@ -47,20 +52,22 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
},
oauth_tokens: %{"access_token" => "test_token"}
})
|> Ash.read_one()
|> Ash.read_one(actor: actor)
assert {:ok, signed_in_user} = result
assert signed_in_user.id == existing_user.id
end
test "passwordless user triggers PasswordVerificationRequired for linking flow" do
test "passwordless user triggers PasswordVerificationRequired for linking flow", %{
actor: actor
} do
# Create passwordless user
{:ok, existing_user} =
Mv.Accounts.User
|> Ash.Changeset.for_create(:create_user, %{
email: "passwordless@example.com"
})
|> Ash.create()
|> Ash.create(actor: actor)
assert is_nil(existing_user.hashed_password)
assert is_nil(existing_user.oidc_id)
@ -95,7 +102,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" do
test "user with different oidc_id gets hard error, not password verification", %{actor: actor} do
# Create user with existing OIDC ID
{:ok, _existing_user} =
Mv.Accounts.User
@ -103,7 +110,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
email: "already-linked@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "original_oidc_999")
|> Ash.create()
|> Ash.create(actor: actor)
# Try to register with same email but different OIDC ID
user_info = %{
@ -138,7 +145,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
end)
end
test "passwordless user with different oidc_id also gets hard error" do
test "passwordless user with different oidc_id also gets hard error", %{actor: actor} do
# Create passwordless user with OIDC ID
{:ok, existing_user} =
Mv.Accounts.User
@ -146,7 +153,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
email: "passwordless-linked@example.com"
})
|> Ash.Changeset.force_change_attribute(:oidc_id, "first_oidc_777")
|> Ash.create()
|> Ash.create(actor: actor)
assert is_nil(existing_user.hashed_password)
assert existing_user.oidc_id == "first_oidc_777"

View file

@ -9,6 +9,11 @@ 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 €"
@ -63,7 +68,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
end
describe "get_last_completed_cycle/2" do
test "returns last completed cycle for member" do
test "returns last completed cycle for member", %{actor: actor} do
# Create test data
fee_type =
Mv.MembershipFees.MembershipFeeType
@ -72,7 +77,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
amount: Decimal.new("50.00"),
interval: :yearly
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Create member without fee type first to avoid auto-generation
member =
@ -83,21 +88,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
email: "test#{System.unique_integer([:positive])}@example.com",
join_date: ~D[2022-01-01]
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# 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!()
|> Ash.update!(actor: actor)
# Delete any auto-generated cycles first
cycles =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!()
|> Ash.read!(actor: actor)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
# Create cycles manually
_cycle_2022 =
@ -109,7 +114,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
membership_fee_type_id: fee_type.id,
status: :paid
})
|> Ash.create!()
|> Ash.create!(actor: actor)
cycle_2023 =
Mv.MembershipFees.MembershipFeeCycle
@ -120,7 +125,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
membership_fee_type_id: fee_type.id,
status: :paid
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Load cycles with membership_fee_type relationship
member =
@ -135,7 +140,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
assert last_cycle.id == cycle_2023.id
end
test "returns nil if no cycles exist" do
test "returns nil if no cycles exist", %{actor: actor} do
fee_type =
Mv.MembershipFees.MembershipFeeType
|> Ash.Changeset.for_create(:create, %{
@ -143,7 +148,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
amount: Decimal.new("50.00"),
interval: :yearly
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Create member without fee type first
member =
@ -153,21 +158,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
last_name: "Member",
email: "test#{System.unique_integer([:positive])}@example.com"
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Assign fee type
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Delete any auto-generated cycles
cycles =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!()
|> Ash.read!(actor: actor)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
# Load cycles and fee type (will be empty)
member =
@ -181,7 +186,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
end
describe "get_current_cycle/2" do
test "returns current cycle for member" do
test "returns current cycle for member", %{actor: actor} do
fee_type =
Mv.MembershipFees.MembershipFeeType
|> Ash.Changeset.for_create(:create, %{
@ -189,7 +194,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
amount: Decimal.new("50.00"),
interval: :yearly
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Create member without fee type first
member =
@ -200,21 +205,21 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
email: "test#{System.unique_integer([:positive])}@example.com",
join_date: ~D[2023-01-01]
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Assign fee type
member =
member
|> Ash.Changeset.for_update(:update_member, %{membership_fee_type_id: fee_type.id})
|> Ash.update!()
|> Ash.update!(actor: actor)
# Delete any auto-generated cycles
cycles =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!()
|> Ash.read!(actor: actor)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
today = Date.utc_today()
current_year_start = %{today | month: 1, day: 1}
@ -228,7 +233,7 @@ defmodule MvWeb.Helpers.MembershipFeeHelpersTest do
membership_fee_type_id: fee_type.id,
status: :unpaid
})
|> Ash.create!()
|> Ash.create!(actor: actor)
# Load cycles with membership_fee_type relationship
member =

View file

@ -19,6 +19,8 @@ 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
@ -26,7 +28,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
email: "admin#{System.unique_integer([:positive])}@mv.local",
password: "testpassword123"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = log_in_user(build_conn(), user)
%{conn: conn, user: user}
@ -156,14 +158,16 @@ 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)
assert {:error, _} = Ash.get(CustomField, custom_field.id, actor: system_actor)
# Custom field value should also be gone (CASCADE)
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id)
assert {:error, _} = Ash.get(CustomFieldValue, custom_field_value.id, actor: system_actor)
# Member should still exist
assert {:ok, _} = Ash.get(Member, member.id)
assert {:ok, _} = Ash.get(Member, member.id, actor: system_actor)
end
test "button remains disabled and custom field not deleted when slug doesn't match", %{
@ -188,7 +192,8 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
assert html =~ ~r/disabled(?:=""|(?!\w))/
# Custom field should still exist since deletion couldn't proceed
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
assert {:ok, _} = Ash.get(CustomField, custom_field.id, actor: system_actor)
end
end
@ -214,38 +219,45 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
refute has_element?(view, "#delete-custom-field-modal")
# Custom field should still exist
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
assert {:ok, _} = Ash.get(CustomField, custom_field.id, actor: system_actor)
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()
|> Ash.create(actor: system_actor)
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()
|> Ash.create(actor: system_actor)
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()
|> Ash.create(actor: system_actor)
end
defp log_in_user(conn, user) do

View file

@ -12,6 +12,8 @@ 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
@ -19,7 +21,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
email: "admin#{System.unique_integer([:positive])}@mv.local",
password: "testpassword123"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
authenticated_conn = conn_with_password_user(conn, user)
%{conn: authenticated_conn, user: user}
@ -27,6 +29,8 @@ 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"),
@ -37,11 +41,13 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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",
@ -52,7 +58,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
end
describe "create form" do

View file

@ -2,6 +2,11 @@ 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
@ -60,7 +65,7 @@ defmodule MvWeb.ProfileNavigationTest do
end
describe "profile navigation with OIDC user" do
test "shows correct profile data for OIDC user", %{conn: conn} do
test "shows correct profile data for OIDC user", %{conn: conn, actor: actor} do
# Setup: Create OIDC user with sub claim
user_info = %{
"sub" => "oidc_123",
@ -78,7 +83,7 @@ defmodule MvWeb.ProfileNavigationTest do
user_info: user_info,
oauth_tokens: oauth_tokens
})
|> Ash.create!(domain: Mv.Accounts)
|> Ash.create!(domain: Mv.Accounts, actor: actor)
# Login user via OIDC
conn = sign_in_user_via_oidc(conn, user)
@ -94,7 +99,10 @@ defmodule MvWeb.ProfileNavigationTest do
assert html =~ "Not enabled"
end
test "profile navigation works across different authentication methods", %{conn: conn} do
test "profile navigation works across different authentication methods", %{
conn: conn,
actor: actor
} do
# Create password user
password_user =
create_test_user(%{
@ -119,7 +127,7 @@ defmodule MvWeb.ProfileNavigationTest do
user_info: user_info,
oauth_tokens: oauth_tokens
})
|> Ash.create!(domain: Mv.Accounts)
|> Ash.create!(domain: Mv.Accounts, actor: actor)
# Test with password user
conn_password = conn_with_password_user(conn, password_user)

View file

@ -35,7 +35,7 @@ defmodule MvWeb.RoleLive.ShowTest do
end
# Helper to create admin user with admin role
defp create_admin_user(conn) do
defp create_admin_user(conn, actor) do
# Create admin role
admin_role =
case Authorization.list_roles() do
@ -69,14 +69,14 @@ defmodule MvWeb.RoleLive.ShowTest do
email: "admin#{System.unique_integer([:positive])}@mv.local",
password: "testpassword123"
})
|> Ash.create()
|> Ash.create(actor: actor)
# 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()
|> Ash.update(actor: actor)
# Load role for authorization checks (must be loaded for can?/3 to work)
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts)
@ -88,8 +88,9 @@ defmodule MvWeb.RoleLive.ShowTest do
describe "mount and display" do
setup %{conn: conn} do
{conn, _user, _admin_role} = create_admin_user(conn)
%{conn: conn}
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
%{conn: conn, actor: system_actor}
end
test "mounts successfully with valid role ID", %{conn: conn} do
@ -135,7 +136,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} do
test "displays system role badge when is_system_role is true", %{conn: conn, actor: actor} do
system_role =
Role
|> Ash.Changeset.for_create(:create_role, %{
@ -143,7 +144,7 @@ defmodule MvWeb.RoleLive.ShowTest do
permission_set_name: "own_data"
})
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|> Ash.create!()
|> Ash.create!(actor: actor)
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
@ -172,8 +173,9 @@ defmodule MvWeb.RoleLive.ShowTest do
describe "navigation" do
setup %{conn: conn} do
{conn, _user, _admin_role} = create_admin_user(conn)
%{conn: conn}
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
%{conn: conn, actor: system_actor}
end
test "back button navigates to role list", %{conn: conn} do
@ -209,8 +211,9 @@ defmodule MvWeb.RoleLive.ShowTest do
describe "error handling" do
setup %{conn: conn} do
{conn, _user, _admin_role} = create_admin_user(conn)
%{conn: conn}
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
%{conn: conn, actor: system_actor}
end
test "redirects to role list with error for invalid role ID", %{conn: conn} do
@ -226,11 +229,12 @@ defmodule MvWeb.RoleLive.ShowTest do
describe "delete functionality" do
setup %{conn: conn} do
{conn, _user, _admin_role} = create_admin_user(conn)
%{conn: conn}
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
%{conn: conn, actor: system_actor}
end
test "delete button is not shown for system roles", %{conn: conn} do
test "delete button is not shown for system roles", %{conn: conn, actor: actor} do
system_role =
Role
|> Ash.Changeset.for_create(:create_role, %{
@ -238,7 +242,7 @@ defmodule MvWeb.RoleLive.ShowTest do
permission_set_name: "own_data"
})
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|> Ash.create!()
|> Ash.create!(actor: actor)
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
@ -258,8 +262,9 @@ defmodule MvWeb.RoleLive.ShowTest do
describe "page title" do
setup %{conn: conn} do
{conn, _user, _admin_role} = create_admin_user(conn)
%{conn: conn}
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{conn, _user, _admin_role} = create_admin_user(conn, system_actor)
%{conn: conn, actor: system_actor}
end
test "sets correct page title", %{conn: conn} do

View file

@ -26,7 +26,7 @@ defmodule MvWeb.RoleLiveTest do
end
# Helper to create admin user with admin role
defp create_admin_user(conn) do
defp create_admin_user(conn, actor) do
# Create admin role
admin_role =
case Authorization.list_roles() do
@ -60,14 +60,14 @@ defmodule MvWeb.RoleLiveTest do
email: "admin#{System.unique_integer([:positive])}@mv.local",
password: "testpassword123"
})
|> Ash.create()
|> Ash.create(actor: actor)
# 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()
|> Ash.update(actor: actor)
# Load role for authorization checks (must be loaded for can?/3 to work)
user_with_role = Ash.load!(user, :role, domain: Mv.Accounts)
@ -78,14 +78,14 @@ defmodule MvWeb.RoleLiveTest do
end
# Helper to create non-admin user
defp create_non_admin_user(conn) do
defp create_non_admin_user(conn, actor) 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()
|> Ash.create(actor: actor)
conn = conn_with_password_user(conn, user)
{conn, user}
@ -93,8 +93,9 @@ defmodule MvWeb.RoleLiveTest do
describe "index page" do
setup %{conn: conn} do
{conn, user, _admin_role} = create_admin_user(conn)
%{conn: conn, user: user}
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}
end
test "mounts successfully", %{conn: conn} do
@ -121,7 +122,7 @@ defmodule MvWeb.RoleLiveTest do
assert html =~ role.permission_set_name
end
test "shows system role badge", %{conn: conn} do
test "shows system role badge", %{conn: conn, actor: actor} do
_system_role =
Role
|> Ash.Changeset.for_create(:create_role, %{
@ -129,14 +130,14 @@ defmodule MvWeb.RoleLiveTest do
permission_set_name: "own_data"
})
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|> Ash.create!()
|> Ash.create!(actor: actor)
{:ok, _view, html} = live(conn, "/admin/roles")
assert html =~ "System Role" || html =~ "system"
end
test "delete button disabled for system roles", %{conn: conn} do
test "delete button disabled for system roles", %{conn: conn, actor: actor} do
system_role =
Role
|> Ash.Changeset.for_create(:create_role, %{
@ -144,7 +145,7 @@ defmodule MvWeb.RoleLiveTest do
permission_set_name: "own_data"
})
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|> Ash.create!()
|> Ash.create!(actor: actor)
{:ok, view, _html} = live(conn, "/admin/roles")
@ -191,8 +192,9 @@ defmodule MvWeb.RoleLiveTest do
describe "show page" do
setup %{conn: conn} do
{conn, user, _admin_role} = create_admin_user(conn)
%{conn: conn, user: user}
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}
end
test "mounts with valid role ID", %{conn: conn} do
@ -215,7 +217,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} do
test "shows system role badge if is_system_role is true", %{conn: conn, actor: actor} do
system_role =
Role
|> Ash.Changeset.for_create(:create_role, %{
@ -223,7 +225,7 @@ defmodule MvWeb.RoleLiveTest do
permission_set_name: "own_data"
})
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|> Ash.create!()
|> Ash.create!(actor: actor)
{:ok, _view, html} = live(conn, "/admin/roles/#{system_role.id}")
@ -233,8 +235,9 @@ defmodule MvWeb.RoleLiveTest do
describe "form - create" do
setup %{conn: conn} do
{conn, user, _admin_role} = create_admin_user(conn)
%{conn: conn, user: user}
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}
end
test "mounts successfully", %{conn: conn} do
@ -306,9 +309,10 @@ defmodule MvWeb.RoleLiveTest do
describe "form - edit" do
setup %{conn: conn} do
{conn, user, _admin_role} = create_admin_user(conn)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{conn, user, _admin_role} = create_admin_user(conn, system_actor)
role = create_role()
%{conn: conn, user: user, role: role}
%{conn: conn, actor: system_actor, user: user, role: role}
end
test "mounts with valid role ID", %{conn: conn, role: role} do
@ -347,7 +351,7 @@ defmodule MvWeb.RoleLiveTest do
assert updated_role.name == "Updated Role Name"
end
test "updates system role's permission_set_name", %{conn: conn} do
test "updates system role's permission_set_name", %{conn: conn, actor: actor} do
system_role =
Role
|> Ash.Changeset.for_create(:create_role, %{
@ -355,7 +359,7 @@ defmodule MvWeb.RoleLiveTest do
permission_set_name: "own_data"
})
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|> Ash.create!()
|> Ash.create!(actor: actor)
{:ok, view, _html} = live(conn, "/admin/roles/#{system_role.id}/edit?return_to=show")
@ -379,8 +383,9 @@ defmodule MvWeb.RoleLiveTest do
describe "delete functionality" do
setup %{conn: conn} do
{conn, user, _admin_role} = create_admin_user(conn)
%{conn: conn, user: user}
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}
end
test "deletes non-system role", %{conn: conn} do
@ -400,7 +405,7 @@ defmodule MvWeb.RoleLiveTest do
Authorization.get_role(role.id)
end
test "fails to delete system role with error message", %{conn: conn} do
test "fails to delete system role with error message", %{conn: conn, actor: actor} do
system_role =
Role
|> Ash.Changeset.for_create(:create_role, %{
@ -408,7 +413,7 @@ defmodule MvWeb.RoleLiveTest do
permission_set_name: "own_data"
})
|> Ash.Changeset.force_change_attribute(:is_system_role, true)
|> Ash.create!()
|> Ash.create!(actor: actor)
{:ok, view, html} = live(conn, "/admin/roles")
@ -428,8 +433,13 @@ defmodule MvWeb.RoleLiveTest do
end
describe "authorization" do
test "only admin can access /admin/roles", %{conn: conn} do
{conn, _user} = create_non_admin_user(conn)
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)
# Non-admin should be redirected or see error
# Note: Authorization is checked via can_access_page? which returns false
@ -443,8 +453,8 @@ defmodule MvWeb.RoleLiveTest do
assert html =~ "Listing Roles" || html =~ "Roles"
end
test "admin can access /admin/roles", %{conn: conn} do
{conn, _user, _admin_role} = create_admin_user(conn)
test "admin can access /admin/roles", %{conn: conn, actor: actor} do
{conn, _user, _admin_role} = create_admin_user(conn, actor)
{:ok, _view, _html} = live(conn, "/admin/roles")
end

View file

@ -64,6 +64,8 @@ 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
@ -72,7 +74,7 @@ defmodule MvWeb.UserLive.ShowTest do
last_name: "Smith",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create user and link to member
user = create_test_user(%{email: "user@example.com"})
@ -81,7 +83,7 @@ defmodule MvWeb.UserLive.ShowTest do
user
|> Ash.Changeset.for_update(:update, %{})
|> Ash.Changeset.manage_relationship(:member, member, type: :append_and_remove)
|> Ash.update()
|> Ash.update(actor: system_actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/users/#{user.id}")

View file

@ -12,6 +12,8 @@ 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
@ -20,7 +22,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
last_name: "Member",
email: "duplicate@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members/new")
@ -73,6 +75,8 @@ 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
@ -81,7 +85,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
last_name: "Member",
email: "original@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create another member with different email
{:ok, _other_member} =
@ -91,7 +95,7 @@ defmodule MvWeb.MemberLive.FormErrorHandlingTest do
last_name: "Member",
email: "other@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")

View file

@ -13,6 +13,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest 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"),
@ -23,11 +25,13 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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",
@ -38,7 +42,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
end
describe "membership fee type dropdown" do
@ -123,10 +127,12 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
|> render_submit()
# Verify member was created with fee type
system_actor = Mv.Helpers.SystemActor.get_system_actor()
member =
Member
|> Ash.Query.filter(email == ^form_data["member[email]"])
|> Ash.read_one!()
|> Ash.read_one!(actor: system_actor)
assert member.membership_fee_type_id == fee_type.id
end
@ -135,13 +141,14 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
# Set default fee type in settings
fee_type = create_fee_type(%{interval: :yearly})
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!()
|> Ash.update!(actor: system_actor)
{:ok, view, _html} = live(conn, "/members/new")
@ -156,6 +163,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
conn: conn,
current_user: admin_user
} do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
# Create custom field
custom_field =
Mv.Membership.CustomField
@ -164,7 +173,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
value_type: :string,
required: false
})
|> Ash.create!()
|> Ash.create!(actor: system_actor)
# Create two fee types with same interval
fee_type1 = create_fee_type(%{name: "Type 1", interval: :yearly})
@ -250,6 +259,8 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
end
test "removing custom field values works correctly", %{conn: conn, current_user: admin_user} do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
# Create custom field
custom_field =
Mv.Membership.CustomField
@ -258,7 +269,7 @@ defmodule MvWeb.MemberLive.FormMembershipFeeTypeTest do
value_type: :string,
required: false
})
|> Ash.create!()
|> Ash.create!(actor: system_actor)
fee_type = create_fee_type(%{interval: :yearly})

View file

@ -13,6 +13,8 @@ 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"),
@ -23,11 +25,13 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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",
@ -38,13 +42,15 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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"),
@ -57,7 +63,7 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
MembershipFeeCycle
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
end
describe "load_cycles_for_members/2" do
@ -75,7 +81,8 @@ defmodule MvWeb.MemberLive.Index.MembershipFeeStatusTest do
|> Ash.Query.filter(id in [^member1.id, ^member2.id])
|> MembershipFeeStatus.load_cycles_for_members()
members = Ash.read!(query)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
members = Ash.read!(query, actor: system_actor)
assert length(members) == 2
@ -94,19 +101,21 @@ 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!()
|> Ash.update!(actor: system_actor)
# Delete any auto-generated cycles
cycles =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!()
|> Ash.read!(actor: system_actor)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
# Create cycles with dates that ensure 2023 is last completed
# Use a fixed "today" date in 2024 to make 2023 the last completed
@ -137,19 +146,21 @@ 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!()
|> Ash.update!(actor: system_actor)
# Delete any auto-generated cycles
cycles =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!()
|> Ash.read!(actor: system_actor)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
# Create cycles - use current year for current cycle
today = Date.utc_today()
@ -176,19 +187,21 @@ 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!()
|> Ash.update!(actor: system_actor)
# Delete any auto-generated cycles
cycles =
Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!()
|> Ash.read!(actor: system_actor)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
# Load cycles and fee type first (will be empty)
member =

View file

@ -14,6 +14,8 @@ 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
@ -22,7 +24,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field with show_in_overview: true
{:ok, field} =
@ -32,7 +34,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field value
{:ok, _cfv} =
@ -42,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "A001"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
%{member: member, field: field}
end

View file

@ -17,6 +17,8 @@ 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
@ -25,7 +27,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member2} =
Member
@ -34,7 +36,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
last_name: "Brown",
email: "bob@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom fields
{:ok, field_show_string} =
@ -44,7 +46,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field_hide} =
CustomField
@ -53,7 +55,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
value_type: :string,
show_in_overview: false
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field_show_integer} =
CustomField
@ -62,7 +64,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
value_type: :integer,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field_show_boolean} =
CustomField
@ -71,7 +73,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
value_type: :boolean,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field_show_date} =
CustomField
@ -80,7 +82,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
value_type: :date,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field_show_email} =
CustomField
@ -89,7 +91,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
value_type: :email,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field values for member1
{:ok, _cfv1} =
@ -99,7 +101,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
custom_field_id: field_show_string.id,
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv2} =
CustomFieldValue
@ -108,7 +110,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
custom_field_id: field_show_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 12_345}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv3} =
CustomFieldValue
@ -117,7 +119,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
custom_field_id: field_show_boolean.id,
value: %{"_union_type" => "boolean", "_union_value" => true}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv4} =
CustomFieldValue
@ -126,7 +128,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()
|> Ash.create(actor: system_actor)
{:ok, _cfv5} =
CustomFieldValue
@ -135,7 +137,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()
|> Ash.create(actor: system_actor)
# Create hidden custom field value (should not be displayed)
{:ok, _cfv_hidden} =
@ -145,7 +147,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
custom_field_id: field_hide.id,
value: %{"_union_type" => "string", "_union_value" => "Internal note"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
%{
member1: member1,

View file

@ -13,6 +13,8 @@ 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
@ -21,7 +23,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _member2} =
Member
@ -30,7 +32,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
last_name: "Brown",
email: "bob@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field with show_in_overview: true but no values
{:ok, field} =
@ -40,7 +42,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
@ -50,6 +52,8 @@ 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
@ -58,7 +62,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field
{:ok, field} =
@ -68,7 +72,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create very long value (but within limits)
long_value = String.duplicate("A", 500)
@ -80,7 +84,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => long_value}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
@ -91,6 +95,8 @@ 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
@ -99,7 +105,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create multiple custom fields with show_in_overview: true
{:ok, field1} =
@ -109,7 +115,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field2} =
CustomField
@ -118,7 +124,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field3} =
CustomField
@ -127,7 +133,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create values for all fields
{:ok, _cfv1} =
@ -137,7 +143,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
custom_field_id: field1.id,
value: %{"_union_type" => "string", "_union_value" => "Value1"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv2} =
Mv.Membership.CustomFieldValue
@ -146,7 +152,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
custom_field_id: field2.id,
value: %{"_union_type" => "string", "_union_value" => "Value2"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv3} =
Mv.Membership.CustomFieldValue
@ -155,7 +161,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
custom_field_id: field3.id,
value: %{"_union_type" => "string", "_union_value" => "Value3"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")

View file

@ -16,6 +16,8 @@ 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
@ -24,7 +26,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member2} =
Member
@ -33,7 +35,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Brown",
email: "bob@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member3} =
Member
@ -42,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Clark",
email: "charlie@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field with show_in_overview: true
{:ok, field_string} =
@ -52,7 +54,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, field_integer} =
CustomField
@ -61,7 +63,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
value_type: :integer,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field values
{:ok, _cfv1} =
@ -71,7 +73,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field_string.id,
value: %{"_union_type" => "string", "_union_value" => "A001"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv2} =
CustomFieldValue
@ -80,7 +82,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field_string.id,
value: %{"_union_type" => "string", "_union_value" => "C003"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv3} =
CustomFieldValue
@ -89,7 +91,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field_string.id,
value: %{"_union_type" => "string", "_union_value" => "B002"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv4} =
CustomFieldValue
@ -98,7 +100,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 10}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv5} =
CustomFieldValue
@ -107,7 +109,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 30}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv6} =
CustomFieldValue
@ -116,7 +118,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 20}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
%{
member1: member1,
@ -236,6 +238,8 @@ 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
@ -244,7 +248,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "withvalue@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member_with_empty} =
Member
@ -253,7 +257,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "withempty@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member_with_null} =
Member
@ -262,7 +266,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "withnull@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member_with_another_value} =
Member
@ -271,7 +275,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "another@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field
{:ok, field} =
@ -281,7 +285,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
{:ok, _cfv1} =
@ -291,7 +295,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv2} =
CustomFieldValue
@ -300,7 +304,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => ""}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# member_with_null has no custom field value (NULL)
@ -311,7 +315,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Apple"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = conn_with_oidc_user(conn)
@ -347,6 +351,8 @@ 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
@ -355,7 +361,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "withvalue@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member_with_empty} =
Member
@ -364,7 +370,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "withempty@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member_with_null} =
Member
@ -373,7 +379,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "withnull@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member_with_another_value} =
Member
@ -382,7 +388,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
last_name: "Test",
email: "another@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field
{:ok, field} =
@ -392,7 +398,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
{:ok, _cfv1} =
@ -402,7 +408,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Apple"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv2} =
CustomFieldValue
@ -411,7 +417,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => ""}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# member_with_null has no custom field value (NULL)
@ -422,7 +428,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
})
|> Ash.create()
|> Ash.create(actor: system_actor)
conn = conn_with_oidc_user(conn)

View file

@ -19,6 +19,8 @@ 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
@ -29,7 +31,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
street: "Main St",
city: "Berlin"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member2} =
Member
@ -40,7 +42,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
street: "Second St",
city: "Hamburg"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field
{:ok, custom_field} =
@ -50,7 +52,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
value_type: :string,
show_in_overview: true
})
|> Ash.create()
|> Ash.create(actor: system_actor)
# Create custom field values
{:ok, _cfv1} =
@ -60,7 +62,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
custom_field_id: custom_field.id,
value: "M001"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, _cfv2} =
CustomFieldValue
@ -69,7 +71,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
custom_field_id: custom_field.id,
value: "M002"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
%{
member1: member1,

View file

@ -6,6 +6,8 @@ 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, %{
@ -18,7 +20,7 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
city: "Berlin",
join_date: ~D[2020-01-15]
})
|> Ash.create()
|> Ash.create(actor: system_actor)
{:ok, member2} =
Member
@ -27,7 +29,7 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
last_name: "Brown",
email: "bob@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
%{
member1: member1,

View file

@ -14,6 +14,8 @@ 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"),
@ -24,11 +26,13 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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",
@ -39,18 +43,20 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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!()
|> Ash.read!(actor: system_actor)
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
default_attrs = %{
cycle_start: ~D[2023-01-01],
@ -64,7 +70,7 @@ defmodule MvWeb.MemberLive.IndexMembershipFeeStatusTest do
MembershipFeeCycle
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
end
describe "status column display" do
@ -172,16 +178,18 @@ 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!()
|> Ash.read!(actor: system_actor)
cycles2 =
MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member2.id)
|> Ash.read!()
|> Ash.read!(actor: system_actor)
refute Enum.empty?(cycles1)
refute Enum.empty?(cycles2)
@ -206,16 +214,18 @@ 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!()
|> Ash.read!(actor: system_actor)
cycles2 =
MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member2.id)
|> Ash.read!()
|> Ash.read!(actor: system_actor)
refute Enum.empty?(cycles1)
refute Enum.empty?(cycles2)

View file

@ -266,13 +266,18 @@ 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"
})
Mv.Membership.create_member(
%{
first_name: "Test",
last_name: "User",
email: "test@example.com"
},
actor: system_actor
)
conn = conn_with_oidc_user(conn)
{:ok, index_view, _html} = live(conn, "/members")
@ -294,27 +299,38 @@ 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"
})
Mv.Membership.create_member(
%{
first_name: "Max",
last_name: "Mustermann",
email: "max@example.com"
},
actor: system_actor
)
{:ok, member2} =
Mv.Membership.create_member(%{
first_name: "Erika",
last_name: "Musterfrau",
email: "erika@example.com"
})
Mv.Membership.create_member(
%{
first_name: "Erika",
last_name: "Musterfrau",
email: "erika@example.com"
},
actor: system_actor
)
{:ok, member3} =
Mv.Membership.create_member(%{
first_name: "Hans",
last_name: "Müller-Lüdenscheidt",
email: "hans@example.com"
})
Mv.Membership.create_member(
%{
first_name: "Hans",
last_name: "Müller-Lüdenscheidt",
email: "hans@example.com"
},
actor: system_actor
)
%{member1: member1, member2: member2, member3: member3}
end
@ -394,7 +410,8 @@ defmodule MvWeb.MemberLive.IndexTest do
render_click(view, "select_member", %{"id" => member1.id})
# Delete the member from the database
Ash.destroy!(member1)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
Ash.destroy!(member1, actor: system_actor)
# Trigger copy_emails event directly - selection still contains the deleted ID
# but the member is no longer in @members list after reload
@ -434,12 +451,17 @@ 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"
})
Mv.Membership.create_member(
%{
first_name: "Test",
last_name: "Format",
email: "test.format@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, "/members")
@ -500,8 +522,26 @@ defmodule MvWeb.MemberLive.IndexTest do
end
describe "cycle status filter" do
alias Mv.MembershipFees.MembershipFeeType
alias Mv.MembershipFees.MembershipFeeCycle
# Helper to create a membership fee type
defp create_fee_type(attrs, actor) do
default_attrs = %{
name: "Test Fee Type #{System.unique_integer([:positive])}",
amount: Decimal.new("50.00"),
interval: :yearly
}
attrs = Map.merge(default_attrs, attrs)
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!(actor: actor)
end
# Helper to create a member
defp create_member(attrs) do
defp create_member(attrs, actor) do
default_attrs = %{
first_name: "Test",
last_name: "Member",
@ -512,32 +552,74 @@ defmodule MvWeb.MemberLive.IndexTest do
Mv.Membership.Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: actor)
end
# Helper to create a cycle
defp create_cycle(member, fee_type, attrs, actor) do
# Delete any auto-generated cycles first to avoid conflicts
existing_cycles =
MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id)
|> Ash.read!(actor: actor)
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: actor) end)
default_attrs = %{
cycle_start: ~D[2023-01-01],
amount: Decimal.new("50.00"),
member_id: member.id,
membership_fee_type_id: fee_type.id,
status: :unpaid
}
attrs = Map.merge(default_attrs, attrs)
MembershipFeeCycle
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!(actor: actor)
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})
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
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
})
create_member(
%{
first_name: "PaidLast",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
create_cycle(
paid_member,
fee_type,
%{cycle_start: last_year_start, status: :paid},
system_actor
)
# Member with unpaid last cycle
unpaid_member =
create_member(%{
first_name: "UnpaidLast",
membership_fee_type_id: fee_type.id
})
create_member(
%{
first_name: "UnpaidLast",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
create_cycle(
unpaid_member,
fee_type,
%{cycle_start: last_year_start, status: :unpaid},
system_actor
)
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid")
@ -546,28 +628,45 @@ 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})
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
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
})
create_member(
%{
first_name: "PaidLast",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
create_cycle(
paid_member,
fee_type,
%{cycle_start: last_year_start, status: :paid},
system_actor
)
# Member with unpaid last cycle
unpaid_member =
create_member(%{
first_name: "UnpaidLast",
membership_fee_type_id: fee_type.id
})
create_member(
%{
first_name: "UnpaidLast",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
create_cycle(
unpaid_member,
fee_type,
%{cycle_start: last_year_start, status: :unpaid},
system_actor
)
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=unpaid")
@ -576,28 +675,45 @@ 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})
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
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
})
create_member(
%{
first_name: "PaidCurrent",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
create_cycle(
paid_member,
fee_type,
%{cycle_start: current_year_start, status: :paid},
system_actor
)
# Member with unpaid current cycle
unpaid_member =
create_member(%{
first_name: "UnpaidCurrent",
membership_fee_type_id: fee_type.id
})
create_member(
%{
first_name: "UnpaidCurrent",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
create_cycle(
unpaid_member,
fee_type,
%{cycle_start: current_year_start, status: :unpaid},
system_actor
)
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid&show_current_cycle=true")
@ -606,28 +722,45 @@ 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})
fee_type = create_fee_type(%{interval: :yearly}, system_actor)
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
})
create_member(
%{
first_name: "PaidCurrent",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
create_cycle(
paid_member,
fee_type,
%{cycle_start: current_year_start, status: :paid},
system_actor
)
# Member with unpaid current cycle
unpaid_member =
create_member(%{
first_name: "UnpaidCurrent",
membership_fee_type_id: fee_type.id
})
create_member(
%{
first_name: "UnpaidCurrent",
membership_fee_type_id: fee_type.id
},
system_actor
)
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
create_cycle(
unpaid_member,
fee_type,
%{cycle_start: current_year_start, status: :unpaid},
system_actor
)
{:ok, _view, html} =
live(conn, "/members?cycle_status_filter=unpaid&show_current_cycle=true")

View file

@ -14,6 +14,8 @@ 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"),
@ -24,11 +26,13 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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",
@ -39,7 +43,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
end
describe "end-to-end workflows" do
@ -75,7 +79,13 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|> render_click()
# Verify status changed
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
system_actor = Mv.Helpers.SystemActor.get_system_actor()
updated_cycle =
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
actor: system_actor
)
assert updated_cycle.status == :paid
end
end
@ -115,13 +125,14 @@ 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!()
|> Ash.update!(actor: system_actor)
# Create new member
{:ok, view, _html} = live(conn, "/members/new")
@ -138,10 +149,12 @@ 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!()
|> Ash.read_one!(actor: system_actor)
assert member.membership_fee_type_id == fee_type.id
end
@ -150,6 +163,8 @@ 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, %{
@ -159,7 +174,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
membership_fee_type_id: fee_type.id,
status: :unpaid
})
|> Ash.create!()
|> Ash.create!(actor: system_actor)
{:ok, view, _html} = live(conn, "/members/#{member.id}")
@ -187,6 +202,8 @@ 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, %{
@ -196,7 +213,7 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
membership_fee_type_id: fee_type.id,
status: :unpaid
})
|> Ash.create!()
|> Ash.create!(actor: system_actor)
{:ok, view, _html} = live(conn, "/members/#{member.id}")
@ -216,7 +233,13 @@ defmodule MvWeb.MemberLive.MembershipFeeIntegrationTest do
|> render_submit()
# Verify amount updated
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
system_actor = Mv.Helpers.SystemActor.get_system_actor()
updated_cycle =
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
actor: system_actor
)
assert updated_cycle.amount == Decimal.new("75.00")
end
end

View file

@ -14,6 +14,8 @@ 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"),
@ -24,11 +26,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
MembershipFeeType
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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",
@ -39,18 +43,20 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
Member
|> Ash.Changeset.for_create(:create_member, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
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!()
|> Ash.read!(actor: system_actor)
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle, actor: system_actor) end)
default_attrs = %{
cycle_start: ~D[2023-01-01],
@ -64,7 +70,7 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
MembershipFeeCycle
|> Ash.Changeset.for_create(:create, attrs)
|> Ash.create!()
|> Ash.create!(actor: system_actor)
end
describe "cycles table display" do
@ -161,7 +167,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|> render_click()
# Verify cycle is now paid
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
system_actor = Mv.Helpers.SystemActor.get_system_actor()
updated_cycle =
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
actor: system_actor
)
assert updated_cycle.status == :paid
end
@ -186,7 +198,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|> render_click()
# Verify cycle is now suspended
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
system_actor = Mv.Helpers.SystemActor.get_system_actor()
updated_cycle =
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
actor: system_actor
)
assert updated_cycle.status == :suspended
end
@ -211,7 +229,13 @@ defmodule MvWeb.MemberLive.ShowMembershipFeesTest do
|> render_click()
# Verify cycle is now unpaid
updated_cycle = Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id))
system_actor = Mv.Helpers.SystemActor.get_system_actor()
updated_cycle =
Ash.read_one!(MembershipFeeCycle |> Ash.Query.filter(id == ^cycle.id),
actor: system_actor
)
assert updated_cycle.status == :unpaid
end
end

View file

@ -21,6 +21,8 @@ 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
@ -29,15 +31,16 @@ defmodule MvWeb.MemberLive.ShowTest do
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
|> Ash.create(actor: system_actor)
%{member: member}
%{member: member, actor: system_actor}
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
member: member,
actor: actor
} do
# Create a custom field but no value for the member
{:ok, custom_field} =
@ -46,7 +49,7 @@ defmodule MvWeb.MemberLive.ShowTest do
name: "phone_mobile",
value_type: :string
})
|> Ash.create()
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -63,7 +66,8 @@ defmodule MvWeb.MemberLive.ShowTest do
test "displays Custom Fields section with multiple custom fields, some without values", %{
conn: conn,
member: member
member: member,
actor: actor
} do
# Create multiple custom fields
{:ok, field1} =
@ -72,7 +76,7 @@ defmodule MvWeb.MemberLive.ShowTest do
name: "phone_mobile",
value_type: :string
})
|> Ash.create()
|> Ash.create(actor: actor)
{:ok, field2} =
CustomField
@ -80,7 +84,7 @@ defmodule MvWeb.MemberLive.ShowTest do
name: "membership_number",
value_type: :integer
})
|> Ash.create()
|> Ash.create(actor: actor)
# Create value only for first field
{:ok, _cfv} =
@ -90,7 +94,7 @@ defmodule MvWeb.MemberLive.ShowTest do
custom_field_id: field1.id,
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
})
|> Ash.create()
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -111,18 +115,19 @@ defmodule MvWeb.MemberLive.ShowTest do
test "does not display Custom Fields section when no custom fields exist", %{
conn: conn,
member: member
member: member,
actor: actor
} 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)
existing_custom_fields = Ash.read!(CustomField, actor: actor)
for cf <- existing_custom_fields do
Ash.destroy!(cf)
Ash.destroy!(cf, actor: actor)
end
# Verify no custom fields exist
assert Ash.read!(CustomField) == []
assert Ash.read!(CustomField, actor: actor) == []
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -133,14 +138,14 @@ defmodule MvWeb.MemberLive.ShowTest do
end
describe "custom field value formatting" do
test "formats string custom field values", %{conn: conn, member: member} do
test "formats string custom field values", %{conn: conn, member: member, actor: actor} do
{:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "phone_mobile",
value_type: :string
})
|> Ash.create()
|> Ash.create(actor: actor)
{:ok, _cfv} =
CustomFieldValue
@ -149,7 +154,7 @@ defmodule MvWeb.MemberLive.ShowTest do
custom_field_id: custom_field.id,
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
})
|> Ash.create()
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}")
@ -157,14 +162,18 @@ defmodule MvWeb.MemberLive.ShowTest do
assert html =~ "+49123456789"
end
test "formats email custom field values as mailto links", %{conn: conn, member: member} do
test "formats email custom field values as mailto links", %{
conn: conn,
member: member,
actor: actor
} do
{:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "private_email",
value_type: :email
})
|> Ash.create()
|> Ash.create(actor: actor)
{:ok, _cfv} =
CustomFieldValue
@ -173,7 +182,7 @@ defmodule MvWeb.MemberLive.ShowTest do
custom_field_id: custom_field.id,
value: %{"_union_type" => "email", "_union_value" => "private@example.com"}
})
|> Ash.create()
|> Ash.create(actor: actor)
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, ~p"/members/#{member}")

View file

@ -70,12 +70,17 @@ 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"
})
Membership.create_member(
%{
first_name: "David",
last_name: "Miller",
email: "david@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -106,12 +111,17 @@ 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"
})
Membership.create_member(
%{
first_name: "Emma",
last_name: "Davis",
email: "emma@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -135,13 +145,18 @@ 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"
})
Membership.create_member(
%{
first_name: "FirstName#{i}",
last_name: "LastName#{i}",
email: "member#{i}@example.com"
},
actor: system_actor
)
member
end

View file

@ -18,14 +18,18 @@ 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"
})
Membership.create_member(
%{
first_name: "Jonathan",
last_name: "Smith",
email: "jonathan.smith@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -41,14 +45,18 @@ 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"
})
Membership.create_member(
%{
first_name: "Jonathan",
last_name: "Smith",
email: "jonathan.smith@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -65,14 +73,18 @@ 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"
})
Membership.create_member(
%{
first_name: "Alexander",
last_name: "Williams",
email: "alex@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -87,14 +99,18 @@ 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"
})
Membership.create_member(
%{
first_name: "Johnny",
last_name: "Doeson",
email: "johnny@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")

View file

@ -19,14 +19,18 @@ 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"
})
Membership.create_member(
%{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -47,14 +51,18 @@ 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"
})
Membership.create_member(
%{
first_name: "Bob",
last_name: "Williams",
email: "bob@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -77,14 +85,18 @@ 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"
})
Membership.create_member(
%{
first_name: "Charlie",
last_name: "Brown",
email: "charlie@example.com"
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/new")
@ -105,20 +117,27 @@ 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"
})
Membership.create_member(
%{
first_name: "Frank",
last_name: "Wilson",
email: "frank@example.com"
},
actor: system_actor
)
{:ok, user} =
Accounts.create_user(%{
email: "frank@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "frank@example.com",
member: %{id: member.id}
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
@ -134,20 +153,27 @@ 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"
})
Membership.create_member(
%{
first_name: "Grace",
last_name: "Taylor",
email: "grace@example.com"
},
actor: system_actor
)
{:ok, user} =
Accounts.create_user(%{
email: "grace@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "grace@example.com",
member: %{id: member.id}
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
@ -164,20 +190,27 @@ 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"
})
Membership.create_member(
%{
first_name: "Henry",
last_name: "Anderson",
email: "henry@example.com"
},
actor: system_actor
)
{:ok, user} =
Accounts.create_user(%{
email: "henry@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "henry@example.com",
member: %{id: member.id}
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")
@ -193,20 +226,27 @@ 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"
})
Membership.create_member(
%{
first_name: "Isabel",
last_name: "Martinez",
email: "isabel@example.com"
},
actor: system_actor
)
{:ok, user} =
Accounts.create_user(%{
email: "isabel@example.com",
member: %{id: member.id}
})
Accounts.create_user(
%{
email: "isabel@example.com",
member: %{id: member.id}
},
actor: system_actor
)
{:ok, view, _html} = live(conn, ~p"/users/#{user.id}/edit")

View file

@ -75,11 +75,14 @@ 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
domain: Mv.Accounts,
actor: system_actor
)
assert to_string(user.email) == "storetest@example.com"
@ -101,11 +104,14 @@ 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
domain: Mv.Accounts,
actor: system_actor
)
assert user.hashed_password != nil
@ -181,7 +187,8 @@ defmodule MvWeb.UserLive.FormTest do
assert_redirected(view, "/users")
updated_user = Ash.reload!(user, domain: Mv.Accounts)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor)
assert to_string(updated_user.email) == "new@example.com"
assert updated_user.hashed_password == original_password
end
@ -204,7 +211,8 @@ defmodule MvWeb.UserLive.FormTest do
assert_redirected(view, "/users")
updated_user = Ash.reload!(user, domain: Mv.Accounts)
system_actor = Mv.Helpers.SystemActor.get_system_actor()
updated_user = Ash.reload!(user, domain: Mv.Accounts, actor: system_actor)
assert updated_user.hashed_password != original_password
assert String.starts_with?(updated_user.hashed_password, "$2b$")
end
@ -285,17 +293,24 @@ 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"
})
Mv.Membership.create_member(
%{
first_name: "John",
last_name: "Doe",
email: "john@example.com"
},
actor: system_actor
)
# 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}})
{:ok, _updated_user} =
Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor)
# Load form
{:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit")
@ -322,13 +337,18 @@ 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"
})
Mv.Membership.create_member(
%{
first_name: "Jane",
last_name: "Smith",
email: "jane@example.com"
},
actor: system_actor
)
# Create user without member
user = create_test_user(%{email: "user@example.com"})
@ -345,22 +365,35 @@ defmodule MvWeb.UserLive.FormTest do
assert_redirected(view, "/users")
# Verify member is linked
updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member])
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]
)
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"
})
Mv.Membership.create_member(
%{
first_name: "Bob",
last_name: "Wilson",
email: "bob@example.com"
},
actor: system_actor
)
# Create user linked to member
user = create_test_user(%{email: "user@example.com"})
{:ok, _} = Mv.Accounts.update_user(user, %{member: %{id: member.id}})
{:ok, _} = Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor)
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
@ -375,7 +408,15 @@ defmodule MvWeb.UserLive.FormTest do
assert_redirected(view, "/users")
# Verify member is unlinked
updated_user = Ash.get!(Mv.Accounts.User, user.id, domain: Mv.Accounts, load: [:member])
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]
)
assert is_nil(updated_user.member)
end
end

View file

@ -407,17 +407,24 @@ 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"
})
Mv.Membership.create_member(
%{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com"
},
actor: system_actor
)
# 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}})
{:ok, _updated_user} =
Mv.Accounts.update_user(user, %{member: %{id: member.id}}, actor: system_actor)
# Create another user without member
_unlinked_user = create_test_user(%{email: "unlinked@example.com"})