From e4e6cfdd47e98d38473b44dd07c6ea852d178c4f Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 18 Feb 2026 22:29:05 +0100 Subject: [PATCH] test(vereinfacht): add tests and scope README - Config, Client, SyncContact, Vereinfacht module tests (no real API) - vereinfacht_test_README: document test scope --- test/mv/config_vereinfacht_test.exs | 61 ++++++++++++ .../vereinfacht/changes/sync_contact_test.exs | 92 +++++++++++++++++++ test/mv/vereinfacht/client_test.exs | 50 ++++++++++ test/mv/vereinfacht/vereinfacht_test.exs | 59 ++++++++++++ .../mv/vereinfacht/vereinfacht_test_README.md | 29 ++++++ 5 files changed, 291 insertions(+) create mode 100644 test/mv/config_vereinfacht_test.exs create mode 100644 test/mv/vereinfacht/changes/sync_contact_test.exs create mode 100644 test/mv/vereinfacht/client_test.exs create mode 100644 test/mv/vereinfacht/vereinfacht_test.exs create mode 100644 test/mv/vereinfacht/vereinfacht_test_README.md diff --git a/test/mv/config_vereinfacht_test.exs b/test/mv/config_vereinfacht_test.exs new file mode 100644 index 0000000..08b8104 --- /dev/null +++ b/test/mv/config_vereinfacht_test.exs @@ -0,0 +1,61 @@ +defmodule Mv.ConfigVereinfachtTest do + @moduledoc """ + Tests for Mv.Config Vereinfacht-related helpers. + """ + use Mv.DataCase, async: false + + describe "vereinfacht_env_configured?/0" do + test "returns false when no Vereinfacht ENV variables are set" do + clear_vereinfacht_env() + refute Mv.Config.vereinfacht_env_configured?() + end + + test "returns true when VEREINFACHT_API_URL is set" do + set_vereinfacht_env("VEREINFACHT_API_URL", "https://api.example.com") + assert Mv.Config.vereinfacht_env_configured?() + after + clear_vereinfacht_env() + end + + test "returns true when VEREINFACHT_CLUB_ID is set" do + set_vereinfacht_env("VEREINFACHT_CLUB_ID", "2") + assert Mv.Config.vereinfacht_env_configured?() + after + clear_vereinfacht_env() + end + end + + describe "vereinfacht_configured?/0" do + test "returns false when no config is set" do + clear_vereinfacht_env() + # Settings may have nil for vereinfacht fields + refute Mv.Config.vereinfacht_configured?() + end + end + + describe "vereinfacht_contact_view_url/1" do + test "returns nil when API URL is not configured" do + clear_vereinfacht_env() + assert Mv.Config.vereinfacht_contact_view_url("123") == nil + end + + test "returns URL when API URL is set" do + set_vereinfacht_env("VEREINFACHT_API_URL", "https://api.example.com/api/v1") + + assert Mv.Config.vereinfacht_contact_view_url("42") == + "https://api.example.com/api/v1/finance-contacts/42" + after + clear_vereinfacht_env() + end + end + + defp set_vereinfacht_env(key, value) do + System.put_env(key, value) + end + + defp clear_vereinfacht_env do + System.delete_env("VEREINFACHT_API_URL") + System.delete_env("VEREINFACHT_API_KEY") + System.delete_env("VEREINFACHT_CLUB_ID") + end +end diff --git a/test/mv/vereinfacht/changes/sync_contact_test.exs b/test/mv/vereinfacht/changes/sync_contact_test.exs new file mode 100644 index 0000000..aa102a5 --- /dev/null +++ b/test/mv/vereinfacht/changes/sync_contact_test.exs @@ -0,0 +1,92 @@ +defmodule Mv.Vereinfacht.Changes.SyncContactTest do + @moduledoc """ + Tests for Mv.Vereinfacht.Changes.SyncContact. + + When Vereinfacht is not configured, member create/update should succeed + and vereinfacht_contact_id remains nil. + """ + use Mv.DataCase, async: false + + alias Mv.Membership + + setup do + clear_vereinfacht_env() + :ok + end + + describe "member create when Vereinfacht not configured" do + test "member is created and vereinfacht_contact_id is nil" do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + attrs = %{ + first_name: "Sync", + last_name: "Test", + email: "sync_test_#{System.unique_integer([:positive])}@example.com" + } + + assert {:ok, member} = Membership.create_member(attrs, actor: system_actor) + assert member.vereinfacht_contact_id == nil + end + end + + describe "member update when Vereinfacht not configured" do + test "member is updated and vereinfacht_contact_id is unchanged" do + member = Mv.Fixtures.member_fixture() + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + assert {:ok, updated} = + Membership.update_member(member, %{first_name: "Updated"}, actor: system_actor) + + assert updated.vereinfacht_contact_id == nil + end + end + + describe "when Vereinfacht is configured" do + # Regression: after_transaction callback receives 2 args (changeset, result), not 3. + # If the callback had arity 3, create_member would raise BadArityError. + # Also: Client must send JSON-encoded body (iodata); raw map causes ArgumentError + # when the request is sent. With an unreachable URL we get :econnrefused before + # that, so this test would not catch the iodata bug; a Bypass/stub server would. + test "create_member succeeds and after_transaction runs without error (API may fail)" do + set_vereinfacht_env() + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + attrs = %{ + first_name: "API", + last_name: "Test", + email: "api_test_#{System.unique_integer([:positive])}@example.com" + } + + assert {:ok, member} = Membership.create_member(attrs, actor: system_actor) + assert member.id + # Sync may fail (e.g. connection refused), so contact_id can stay nil + after + clear_vereinfacht_env() + end + + test "update_member succeeds and after_transaction runs without error (API may fail)" do + set_vereinfacht_env() + member = Mv.Fixtures.member_fixture() + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + assert {:ok, updated} = + Membership.update_member(member, %{first_name: "Updated"}, actor: system_actor) + + assert updated.id == member.id + after + clear_vereinfacht_env() + end + end + + defp set_vereinfacht_env do + System.put_env("VEREINFACHT_API_URL", "http://127.0.0.1:1/api/v1") + System.put_env("VEREINFACHT_API_KEY", "test-key") + System.put_env("VEREINFACHT_CLUB_ID", "2") + end + + defp clear_vereinfacht_env do + System.delete_env("VEREINFACHT_API_URL") + System.delete_env("VEREINFACHT_API_KEY") + System.delete_env("VEREINFACHT_CLUB_ID") + end +end diff --git a/test/mv/vereinfacht/client_test.exs b/test/mv/vereinfacht/client_test.exs new file mode 100644 index 0000000..d936adc --- /dev/null +++ b/test/mv/vereinfacht/client_test.exs @@ -0,0 +1,50 @@ +defmodule Mv.Vereinfacht.ClientTest do + @moduledoc """ + Tests for Mv.Vereinfacht.Client. + + Only tests the "not configured" path; no real HTTP calls. Config reads from + ENV first, then from Settings (DB), so we use DataCase so get_settings() is available. + """ + use Mv.DataCase, async: false + + alias Mv.Vereinfacht.Client + + setup do + clear_vereinfacht_env() + :ok + end + + describe "create_contact/1" do + test "returns {:error, :not_configured} when Vereinfacht is not configured" do + member = build_member_struct() + + assert Client.create_contact(member) == {:error, :not_configured} + end + end + + describe "update_contact/2" do + test "returns {:error, :not_configured} when Vereinfacht is not configured" do + member = build_member_struct() + + assert Client.update_contact("123", member) == {:error, :not_configured} + end + end + + defp build_member_struct do + %{ + first_name: "Test", + last_name: "User", + email: "test@example.com", + street: "Street 1", + house_number: "2", + postal_code: "12345", + city: "Berlin" + } + end + + defp clear_vereinfacht_env do + System.delete_env("VEREINFACHT_API_URL") + System.delete_env("VEREINFACHT_API_KEY") + System.delete_env("VEREINFACHT_CLUB_ID") + end +end diff --git a/test/mv/vereinfacht/vereinfacht_test.exs b/test/mv/vereinfacht/vereinfacht_test.exs new file mode 100644 index 0000000..08f73b9 --- /dev/null +++ b/test/mv/vereinfacht/vereinfacht_test.exs @@ -0,0 +1,59 @@ +defmodule Mv.VereinfachtTest do + @moduledoc """ + Tests for Mv.Vereinfacht business logic. + + No real API calls; tests "not configured" path and pure helpers (format_error). + """ + use Mv.DataCase, async: false + + alias Mv.Vereinfacht + + setup do + clear_vereinfacht_env() + :ok + end + + describe "sync_member/1" do + test "returns :ok when Vereinfacht is not configured (no-op)" do + member = Mv.Fixtures.member_fixture() + + assert Vereinfacht.sync_member(member) == :ok + end + end + + describe "sync_members_without_contact/0" do + test "returns {:error, :not_configured} when Vereinfacht is not configured" do + assert Vereinfacht.sync_members_without_contact() == {:error, :not_configured} + end + end + + describe "format_error/1" do + test "formats HTTP error with detail" do + assert Vereinfacht.format_error({:http, 422, "The email field is required."}) == + "Vereinfacht: The email field is required." + end + + test "formats HTTP error without detail" do + assert Vereinfacht.format_error({:http, 500, nil}) == + "Vereinfacht: API error (HTTP 500)." + end + + test "formats request_failed" do + assert Vereinfacht.format_error({:request_failed, %{reason: :econnrefused}}) == + "Vereinfacht: Request failed (e.g. connection error)." + end + + test "formats invalid_response and other terms" do + assert Vereinfacht.format_error({:invalid_response, %{}}) == + "Vereinfacht: Invalid API response." + + assert Vereinfacht.format_error(:timeout) == "Vereinfacht: :timeout" + end + end + + defp clear_vereinfacht_env do + System.delete_env("VEREINFACHT_API_URL") + System.delete_env("VEREINFACHT_API_KEY") + System.delete_env("VEREINFACHT_CLUB_ID") + end +end diff --git a/test/mv/vereinfacht/vereinfacht_test_README.md b/test/mv/vereinfacht/vereinfacht_test_README.md new file mode 100644 index 0000000..993af47 --- /dev/null +++ b/test/mv/vereinfacht/vereinfacht_test_README.md @@ -0,0 +1,29 @@ +# Vereinfacht tests – scope and rationale + +## Constraint: no real API in CI + +Tests do **not** call the real Vereinfacht API or a shared test endpoint. All tests use dummy data and either: + +- Assert behaviour when **Vereinfacht is not configured** (ENV + Settings unset), or +- Run the **full Member/User flow** with a **unreachable URL** (e.g. `http://127.0.0.1:1`) so the HTTP client fails fast (e.g. `:econnrefused`) and we only assert that the application path does not crash. + +## What the tests cover + +| Test file | What it tests | Why it’s enough without an API | +|-----------|----------------|---------------------------------| +| **ConfigVereinfachtTest** | `vereinfacht_env_configured?`, `vereinfacht_configured?`, `vereinfacht_contact_view_url` with ENV set/cleared | Pure config logic; no HTTP. | +| **ClientTest** | `create_contact/1` and `update_contact/2` return `{:error, :not_configured}` when nothing is configured | Ensures the client does not call Req when config is missing. | +| **VereinfachtTest** | `sync_members_without_contact/0` returns `{:error, :not_configured}` when not configured | Ensures bulk sync is a no-op when config is missing. | +| **SyncContactTest** | Member create/update with SyncContact change: not configured → no sync; configured with bad URL → action still succeeds, sync may fail | Ensures the Ash change and after_transaction arity are correct and the action result is not broken by sync failures. | + +## What is *not* tested (and would need a stub or real endpoint) + +- Actual HTTP request shape (body, headers) and response handling (201/200, error codes). +- Persistence of `vereinfacht_contact_id` after a successful create. +- Translation of specific API error payloads into user messages. + +Those would require either a **Bypass** (or similar) stub in front of Req or a dedicated test endpoint; both are out of scope for the current “no real API” setup. + +## Conclusion + +Given the constraint that the API is not called in CI, the tests are **meaningful**: they cover config, “not configured” paths, and integration of SyncContact with Member create/update without crashing. They are **sufficient** for regression safety and refactoring; extending them with a Bypass stub would be an optional next step if we want to assert on request/response shape without hitting the real API.