From b7a83d92985e767f7aa40119c537735e602fac66 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 10 Mar 2026 12:18:36 +0100 Subject: [PATCH] test: add tests for join form settings --- docs/development-progress-log.md | 3 + test/membership/setting_join_form_test.exs | 281 +++++++++++++++++++++ 2 files changed, 284 insertions(+) create mode 100644 test/membership/setting_join_form_test.exs diff --git a/docs/development-progress-log.md b/docs/development-progress-log.md index a86efe6..b2a814b 100644 --- a/docs/development-progress-log.md +++ b/docs/development-progress-log.md @@ -809,6 +809,9 @@ end - **PR review follow-ups (Join confirmation):** Join confirmation email uses `Mailer.deliver/1` and returns `{:ok, email}` \| `{:error, reason}`; domain logs delivery errors but still returns `{:ok, request}` so the user sees success. Comment in `submit_join_request/2` clarifies that the raw token is hashed by `JoinRequest.Changes.SetConfirmationToken`. Cleanup task uses `Ash.bulk_destroy` and logs partial errors without halting. Layout uses assigns `app_name` and `locale` (from config/Gettext) instead of hardcoded "Mila" and `lang="de"`. Production `runtime.exs` sets `:mail_from` from ENV (`MAIL_FROM_NAME`, `MAIL_FROM_EMAIL`). Layout reference unified to `"layout.html"`; redundant `put_layout` removed from senders. - Tests: `join_request_test.exs`, `join_request_submit_email_test.exs`, `join_confirm_controller_test.exs` – all pass. +**Subtask 3 – Admin: Join form settings (TDD tests only):** +- Test file: `test/membership/setting_join_form_test.exs` – TDD tests for join form settings (persistence, validation, allowlist, defaults, robustness). Tests are red until Setting gains `join_form_enabled`, `join_form_field_ids`, `join_form_field_required` and `Mv.Membership.get_join_form_allowlist/0` is implemented. No functionality implemented yet. + ### Test Data Management **Seed Data:** diff --git a/test/membership/setting_join_form_test.exs b/test/membership/setting_join_form_test.exs new file mode 100644 index 0000000..9b15ca4 --- /dev/null +++ b/test/membership/setting_join_form_test.exs @@ -0,0 +1,281 @@ +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.Membership + alias Mv.Helpers.SystemActor + alias Mv.Constants + + 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