defmodule Mv.Membership.SettingJoinFormTest do @moduledoc """ TDD tests for Join Form Settings (onboarding-join-concept subtask 3). These tests define the expected API and behaviour for the "Onboarding / Join" section in global settings. No functionality is implemented yet; tests are expected to fail (red) until: - Setting resource has attributes: `join_form_enabled`, `join_form_field_ids`, `join_form_field_required`, plus validations and accept in the update action. - `Mv.Membership.get_join_form_allowlist/0` is implemented and returns the allowlist for the public join form (subtask 4). """ use Mv.DataCase, async: false alias Mv.Constants alias Mv.Helpers.SystemActor alias Mv.Membership setup do {:ok, settings} = Membership.get_settings() saved_enabled = Map.get(settings, :join_form_enabled) saved_ids = Map.get(settings, :join_form_field_ids) saved_required = Map.get(settings, :join_form_field_required) on_exit(fn -> {:ok, s} = Membership.get_settings() attrs = %{} attrs = if saved_enabled != nil, do: Map.put(attrs, :join_form_enabled, saved_enabled), else: attrs attrs = if saved_ids != nil, do: Map.put(attrs, :join_form_field_ids, saved_ids || []), else: attrs attrs = if saved_required != nil, do: Map.put(attrs, :join_form_field_required, saved_required || %{}), else: attrs if attrs != %{} do Membership.update_settings(s, attrs) end end) :ok end defp update_join_form_settings(settings, attrs) do Membership.update_settings(settings, attrs) end defp error_message(errors, field) when is_atom(field) do errors |> Enum.filter(fn err -> Map.get(err, :field) == field end) |> Enum.map(&Map.get(&1, :message, "")) |> List.first() || "" end # ---- 1. Persistence and loading ---- describe "join form settings persistence and loading" do test "save and load join_form_enabled plus field selection and required flags returns same config" do {:ok, settings} = Membership.get_settings() attrs = %{ join_form_enabled: true, join_form_field_ids: ["email", "first_name"], join_form_field_required: %{"email" => true, "first_name" => false} } assert {:ok, updated} = update_join_form_settings(settings, attrs) assert updated.join_form_enabled == true assert updated.join_form_field_ids == ["email", "first_name"] assert updated.join_form_field_required["email"] == true assert updated.join_form_field_required["first_name"] == false {:ok, reloaded} = Membership.get_settings() assert reloaded.join_form_enabled == true assert reloaded.join_form_field_ids == ["email", "first_name"] assert reloaded.join_form_field_required["email"] == true assert reloaded.join_form_field_required["first_name"] == false end test "repeated save with changed field list overwrites config without leftovers" do {:ok, settings} = Membership.get_settings() assert {:ok, _} = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["email", "first_name"], join_form_field_required: %{"email" => true, "first_name" => false} }) assert {:ok, updated} = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["email", "last_name"], join_form_field_required: %{"email" => true, "last_name" => false} }) assert updated.join_form_field_ids == ["email", "last_name"] assert Map.has_key?(updated.join_form_field_required, "last_name") refute Map.has_key?(updated.join_form_field_required, "first_name") end end # ---- 2. Validation ---- describe "join form settings validation" do test "only existing member fields or custom field ids are accepted; unknown field names rejected or sanitized" do {:ok, settings} = Membership.get_settings() result = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["email", "not_a_member_field"], join_form_field_required: %{"email" => true, "not_a_member_field" => false} }) # Until attributes exist we get NoSuchInput; once implemented we expect validation error assert {:error, _} = result end test "config without email is rejected or email is auto-added and required" do {:ok, settings} = Membership.get_settings() result = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["first_name", "last_name"], join_form_field_required: %{"first_name" => true, "last_name" => false} }) # Either rejected or, when loaded, email must be present and required case result do {:error, _} -> :ok {:ok, updated} -> assert "email" in updated.join_form_field_ids assert updated.join_form_field_required["email"] == true end end test "required false for email is ignored or forced to true when saved" do {:ok, settings} = Membership.get_settings() {:ok, updated} = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["email", "first_name"], join_form_field_required: %{"email" => false, "first_name" => false} }) assert updated.join_form_field_required["email"] == true end test "required flag for field not in join_form_field_ids is rejected or dropped" do {:ok, settings} = Membership.get_settings() result = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["email"], join_form_field_required: %{"email" => true, "first_name" => true} }) case result do {:error, _} -> :ok {:ok, updated} -> refute Map.has_key?(updated.join_form_field_required, "first_name") end end end # ---- 3. Allowlist for join form ---- describe "join form allowlist" do test "allowlist returns configured fields with required/optional when join form enabled" do {:ok, settings} = Membership.get_settings() update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["email", "first_name"], join_form_field_required: %{"email" => true, "first_name" => false} }) allowlist = Membership.get_join_form_allowlist() assert length(allowlist) == 2 email_entry = Enum.find(allowlist, &(&1.id == "email")) first_name_entry = Enum.find(allowlist, &(&1.id == "first_name")) assert email_entry.required == true assert first_name_entry.required == false assert email_entry.type == :member_field assert first_name_entry.type == :member_field end test "allowlist returns empty or defined default when join form disabled" do {:ok, settings} = Membership.get_settings() update_join_form_settings(settings, %{ join_form_enabled: false, join_form_field_ids: ["email", "first_name"], join_form_field_required: %{"email" => true, "first_name" => false} }) allowlist = Membership.get_join_form_allowlist() assert allowlist == [] end @tag :requires_custom_field test "allowlist distinguishes member fields and custom field identifiers" do {:ok, settings} = Membership.get_settings() actor = SystemActor.get_system_actor() {:ok, cf} = Membership.create_custom_field( %{name: "join_cf_#{System.unique_integer([:positive])}", value_type: :string}, actor: actor ) update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: ["email", cf.id], join_form_field_required: %{"email" => true, cf.id => false} }) allowlist = Membership.get_join_form_allowlist() email_entry = Enum.find(allowlist, &(&1.id == "email")) cf_entry = Enum.find(allowlist, &(&1.id == cf.id)) assert email_entry.type == :member_field assert cf_entry.type == :custom_field end end # ---- 4. Defaults and fallback ---- describe "join form defaults and fallback" do test "when no join settings stored, allowlist returns defined default (e.g. disabled, empty list)" do allowlist = Membership.get_join_form_allowlist() # Default: join form disabled → empty allowlist assert is_list(allowlist) assert allowlist == [] || Enum.all?(allowlist, &(is_map(&1) and Map.has_key?(&1, :id))) end test "existing settings without join keys load correctly; new join keys get defaults" do {:ok, settings} = Membership.get_settings() # Ensure other attributes still load assert Map.has_key?(settings, :club_name) # When join keys exist they have sensible defaults join_enabled = Map.get(settings, :join_form_enabled) join_ids = Map.get(settings, :join_form_field_ids) if join_enabled != nil, do: assert(is_boolean(join_enabled)) if join_ids != nil, do: assert(is_list(join_ids)) end end # ---- 5. Authorization (backend: settings update requires authorized actor when policy enforced) ---- # Authorization for the settings page is covered by GlobalSettingsLive and page-permission tests. # If the domain later requires an actor for update_settings, tests here would pass an actor. # ---- 6. Robustness / edge cases ---- describe "join form settings robustness" do test "invalid or unexpected payload structure yields clean error or ignores unknown keys" do {:ok, settings} = Membership.get_settings() result = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: "not_a_list", join_form_field_required: %{} }) assert match?({:error, _}, result) or (match?({:ok, _}, result) && elem(result, 1).join_form_field_ids != "not_a_list") end test "larger but reasonable number of fields saves and loads without error" do {:ok, settings} = Membership.get_settings() all_member = Constants.member_fields() |> Enum.map(&to_string/1) required_map = Map.new(all_member, fn f -> {f, f == "email"} end) assert {:ok, updated} = update_join_form_settings(settings, %{ join_form_enabled: true, join_form_field_ids: all_member, join_form_field_required: required_map }) assert length(updated.join_form_field_ids) == length(all_member) {:ok, reloaded} = Membership.get_settings() assert length(reloaded.join_form_field_ids) == length(all_member) end end end