defmodule Mv.ReleaseTest do @moduledoc """ Tests for release tasks (e.g. seed_admin/0). These tests verify that the admin user is created or updated from ENV (ADMIN_EMAIL, ADMIN_PASSWORD / ADMIN_PASSWORD_FILE) in an idempotent way. """ use Mv.DataCase, async: false alias Mv.Accounts alias Mv.Accounts.User alias Mv.Authorization.Role require Ash.Query setup do ensure_admin_role_exists() clear_admin_env() :ok end describe "seed_admin/0" do test "without ADMIN_EMAIL does nothing (idempotent), no user created" do clear_admin_env() user_count_before = count_users() Mv.Release.seed_admin() assert count_users() == user_count_before end test "with ADMIN_EMAIL but without ADMIN_PASSWORD and user does not exist: does not create user" do System.delete_env("ADMIN_PASSWORD") System.delete_env("ADMIN_PASSWORD_FILE") email = "admin-no-password-#{System.unique_integer([:positive])}@test.example.com" System.put_env("ADMIN_EMAIL", email) on_exit(fn -> System.delete_env("ADMIN_EMAIL") end) user_count_before = count_users() Mv.Release.seed_admin() assert count_users() == user_count_before, "seed_admin must not create any user when ADMIN_PASSWORD is unset (expected #{user_count_before}, got #{count_users()})" end test "with ADMIN_EMAIL but without ADMIN_PASSWORD and user exists: leaves user and role unchanged" do email = "existing-admin-#{System.unique_integer([:positive])}@test.example.com" System.put_env("ADMIN_EMAIL", email) on_exit(fn -> System.delete_env("ADMIN_EMAIL") end) {:ok, user} = create_user_with_mitglied_role(email) role_id_before = user.role_id Mv.Release.seed_admin() {:ok, updated} = get_user_by_email(email) assert updated.role_id == role_id_before end test "with ADMIN_EMAIL and ADMIN_PASSWORD: creates user with Admin role and sets password" do email = "new-admin-#{System.unique_integer([:positive])}@test.example.com" password = "SecurePassword123!" System.put_env("ADMIN_EMAIL", email) System.put_env("ADMIN_PASSWORD", password) on_exit(fn -> System.delete_env("ADMIN_EMAIL") System.delete_env("ADMIN_PASSWORD") end) Mv.Release.seed_admin() assert user_exists?(email), "seed_admin must create user when ADMIN_EMAIL and ADMIN_PASSWORD are set" {:ok, user} = get_user_by_email(email) assert user.role_id == admin_role_id() assert user.hashed_password != nil assert AshAuthentication.BcryptProvider.valid?(password, user.hashed_password) end test "with ADMIN_EMAIL and ADMIN_PASSWORD, user already exists: assigns Admin role and updates password" do email = "existing-to-admin-#{System.unique_integer([:positive])}@test.example.com" password = "NewSecurePassword456!" System.put_env("ADMIN_EMAIL", email) System.put_env("ADMIN_PASSWORD", password) on_exit(fn -> System.delete_env("ADMIN_EMAIL") System.delete_env("ADMIN_PASSWORD") end) {:ok, user} = create_user_with_mitglied_role(email) assert user.role_id == mitglied_role_id() old_hashed = user.hashed_password Mv.Release.seed_admin() {:ok, updated} = get_user_by_email(email) assert updated.role_id == admin_role_id() assert updated.hashed_password != nil assert updated.hashed_password != old_hashed assert AshAuthentication.BcryptProvider.valid?(password, updated.hashed_password) end test "with ADMIN_PASSWORD_FILE: reads password from file, same behavior as ADMIN_PASSWORD" do email = "admin-file-#{System.unique_integer([:positive])}@test.example.com" password = "FilePassword789!" tmp = Path.join( System.tmp_dir!(), "mv_admin_password_#{System.unique_integer([:positive])}.txt" ) File.write!(tmp, password) System.put_env("ADMIN_EMAIL", email) System.put_env("ADMIN_PASSWORD_FILE", tmp) on_exit(fn -> System.delete_env("ADMIN_EMAIL") System.delete_env("ADMIN_PASSWORD_FILE") File.rm(tmp) end) Mv.Release.seed_admin() assert user_exists?(email), "seed_admin must create user when ADMIN_PASSWORD_FILE is set" {:ok, user} = get_user_by_email(email) assert AshAuthentication.BcryptProvider.valid?(password, user.hashed_password) end test "called twice: idempotent (no duplicate user, same state)" do email = "idempotent-admin-#{System.unique_integer([:positive])}@test.example.com" password = "IdempotentPassword123!" System.put_env("ADMIN_EMAIL", email) System.put_env("ADMIN_PASSWORD", password) on_exit(fn -> System.delete_env("ADMIN_EMAIL") System.delete_env("ADMIN_PASSWORD") end) Mv.Release.seed_admin() {:ok, user_after_first} = get_user_by_email(email) user_count_after_first = count_users() Mv.Release.seed_admin() assert count_users() == user_count_after_first {:ok, user_after_second} = get_user_by_email(email) assert user_after_second.id == user_after_first.id assert user_after_second.role_id == admin_role_id() end end defp clear_admin_env do System.delete_env("ADMIN_EMAIL") System.delete_env("ADMIN_PASSWORD") System.delete_env("ADMIN_PASSWORD_FILE") end defp ensure_admin_role_exists do case Role |> Ash.Query.filter(name == "Admin") |> Ash.read_one(authorize?: false, domain: Mv.Authorization) do {:ok, nil} -> Role |> Ash.Changeset.for_create(:create_role_with_system_flag, %{ name: "Admin", description: "Administrator with full access", permission_set_name: "admin", is_system_role: false }) |> Ash.create!(authorize?: false, domain: Mv.Authorization) _ -> :ok end end defp admin_role_id do {:ok, role} = Role |> Ash.Query.filter(name == "Admin") |> Ash.read_one(authorize?: false, domain: Mv.Authorization) role.id end defp mitglied_role_id do {:ok, role} = Role.get_mitglied_role() role.id end defp count_users do User |> Ash.read!(authorize?: false, domain: Mv.Accounts) |> length() end defp user_exists?(email) do case get_user_by_email(email) do {:ok, _} -> true {:error, _} -> false end end defp get_user_by_email(email) do User |> Ash.Query.filter(email == ^email) |> Ash.read_one(authorize?: false, domain: Mv.Accounts) end defp create_user_with_mitglied_role(email) do {:ok, _} = Accounts.create_user(%{email: email}, authorize?: false) get_user_by_email(email) end end