From 9fd617e45a302d25e811ab672889f00db166901d Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 2 Feb 2026 09:48:37 +0100 Subject: [PATCH 1/7] tests: add tests for config --- .../mv_web/live/global_settings_live_test.exs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/mv_web/live/global_settings_live_test.exs b/test/mv_web/live/global_settings_live_test.exs index f217311..1ea1d12 100644 --- a/test/mv_web/live/global_settings_live_test.exs +++ b/test/mv_web/live/global_settings_live_test.exs @@ -687,5 +687,42 @@ defmodule MvWeb.GlobalSettingsLiveTest do # Check that file input has accept attribute for CSV assert html =~ ~r/accept=["'][^"']*csv["']/i or html =~ "CSV files only" end + + test "configured row limit is enforced", %{conn: conn} do + # Business rule: CSV import respects configured row limits + # Test that a custom limit (500) is enforced, not just the default (1000) + original_config = Application.get_env(:mv, :csv_import, []) + + try do + Application.put_env(:mv, :csv_import, [max_rows: 500]) + + {:ok, view, _html} = live(conn, ~p"/settings") + + # Generate CSV with 501 rows (exceeding custom limit of 500) + header = "first_name;last_name;email;street;postal_code;city\n" + + rows = + for i <- 1..501 do + "Row#{i};Last#{i};email#{i}@example.com;Street#{i};12345;City#{i}\n" + end + + large_csv = header <> Enum.join(rows) + + # Simulate file upload using helper function + upload_csv_file(view, large_csv, "too_many_rows_custom.csv") + + view + |> form("#csv-upload-form", %{}) + |> render_submit() + + html = render(view) + # Business rule: import should be rejected when exceeding configured limit + assert html =~ "exceeds" or html =~ "maximum" or html =~ "limit" or + html =~ "Failed to prepare" + after + # Restore original config + Application.put_env(:mv, :csv_import, original_config) + end + end end end -- 2.47.2 From 3f551c5f8d47695f23b094f3100c73b6815478c0 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 2 Feb 2026 09:49:13 +0100 Subject: [PATCH 2/7] feat: add configs for impor tlimits --- config/config.exs | 6 ++++ lib/mv/config.ex | 46 +++++++++++++++++++++++++ lib/mv_web/live/global_settings_live.ex | 7 ++-- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/config/config.exs b/config/config.exs index cc338b2..6dfb1d1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -51,6 +51,12 @@ config :mv, generators: [timestamp_type: :utc_datetime], ash_domains: [Mv.Membership, Mv.Accounts, Mv.MembershipFees, Mv.Authorization] +# CSV Import configuration +config :mv, csv_import: [ + max_file_size_mb: 10, + max_rows: 1000 +] + # Configures the endpoint config :mv, MvWeb.Endpoint, url: [host: "localhost"], diff --git a/lib/mv/config.ex b/lib/mv/config.ex index 5e6ba90..edf8428 100644 --- a/lib/mv/config.ex +++ b/lib/mv/config.ex @@ -21,4 +21,50 @@ defmodule Mv.Config do def sql_sandbox? do Application.get_env(:mv, :sql_sandbox, false) end + + @doc """ + Returns the maximum file size for CSV imports in bytes. + + Reads the `max_file_size_mb` value from the CSV import configuration + and converts it to bytes. + + ## Returns + + - Maximum file size in bytes (default: 10_485_760 bytes = 10 MB) + + ## Examples + + iex> Mv.Config.csv_import_max_file_size_bytes() + 10_485_760 + """ + @spec csv_import_max_file_size_bytes() :: non_neg_integer() + def csv_import_max_file_size_bytes do + max_file_size_mb = get_csv_import_config(:max_file_size_mb, 10) + max_file_size_mb * 1024 * 1024 + end + + @doc """ + Returns the maximum number of rows allowed in CSV imports. + + Reads the `max_rows` value from the CSV import configuration. + + ## Returns + + - Maximum number of rows (default: 1000) + + ## Examples + + iex> Mv.Config.csv_import_max_rows() + 1000 + """ + @spec csv_import_max_rows() :: pos_integer() + def csv_import_max_rows do + get_csv_import_config(:max_rows, 1000) + end + + # Helper function to get CSV import config values + defp get_csv_import_config(key, default) do + Application.get_env(:mv, :csv_import, []) + |> Keyword.get(key, default) + end end diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index bd0036b..aa41cd5 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -54,8 +54,6 @@ defmodule MvWeb.GlobalSettingsLive do on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} # CSV Import configuration constants - # 10 MB - @max_file_size_bytes 10_485_760 @max_errors 50 @impl true @@ -82,7 +80,7 @@ defmodule MvWeb.GlobalSettingsLive do |> allow_upload(:csv_file, accept: ~w(.csv), max_entries: 1, - max_file_size: @max_file_size_bytes, + max_file_size: Config.csv_import_max_file_size_bytes(), auto_upload: true ) @@ -409,7 +407,8 @@ defmodule MvWeb.GlobalSettingsLive do # Processes CSV upload and starts import defp process_csv_upload(socket) do with {:ok, content} <- consume_and_read_csv(socket), - {:ok, import_state} <- MemberCSV.prepare(content) do + {:ok, import_state} <- + MemberCSV.prepare(content, max_rows: Config.csv_import_max_rows()) do start_import(socket, import_state) else {:error, reason} when is_binary(reason) -> -- 2.47.2 From d61a939debf907bfabdc8aedfb3b6b5435907689 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 2 Feb 2026 09:50:47 +0100 Subject: [PATCH 3/7] formatting --- config/config.exs | 9 +++++---- test/mv_web/live/global_settings_live_test.exs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index 6dfb1d1..64f3604 100644 --- a/config/config.exs +++ b/config/config.exs @@ -52,10 +52,11 @@ config :mv, ash_domains: [Mv.Membership, Mv.Accounts, Mv.MembershipFees, Mv.Authorization] # CSV Import configuration -config :mv, csv_import: [ - max_file_size_mb: 10, - max_rows: 1000 -] +config :mv, + csv_import: [ + max_file_size_mb: 10, + max_rows: 1000 + ] # Configures the endpoint config :mv, MvWeb.Endpoint, diff --git a/test/mv_web/live/global_settings_live_test.exs b/test/mv_web/live/global_settings_live_test.exs index 1ea1d12..0926681 100644 --- a/test/mv_web/live/global_settings_live_test.exs +++ b/test/mv_web/live/global_settings_live_test.exs @@ -694,7 +694,7 @@ defmodule MvWeb.GlobalSettingsLiveTest do original_config = Application.get_env(:mv, :csv_import, []) try do - Application.put_env(:mv, :csv_import, [max_rows: 500]) + Application.put_env(:mv, :csv_import, max_rows: 500) {:ok, view, _html} = live(conn, ~p"/settings") -- 2.47.2 From e74154581c31034be095f6d0671a76e62f2b9573 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 2 Feb 2026 10:10:02 +0100 Subject: [PATCH 4/7] feat: changes UI info based on config for limits --- lib/mv/config.ex | 19 +++++++++++++++++++ lib/mv_web/live/global_settings_live.ex | 8 +++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/mv/config.ex b/lib/mv/config.ex index edf8428..98a5b65 100644 --- a/lib/mv/config.ex +++ b/lib/mv/config.ex @@ -62,6 +62,25 @@ defmodule Mv.Config do get_csv_import_config(:max_rows, 1000) end + @doc """ + Returns the maximum file size for CSV imports in megabytes. + + Reads the `max_file_size_mb` value from the CSV import configuration. + + ## Returns + + - Maximum file size in megabytes (default: 10) + + ## Examples + + iex> Mv.Config.csv_import_max_file_size_mb() + 10 + """ + @spec csv_import_max_file_size_mb() :: pos_integer() + def csv_import_max_file_size_mb do + get_csv_import_config(:max_file_size_mb, 10) + end + # Helper function to get CSV import config values defp get_csv_import_config(key, default) do Application.get_env(:mv, :csv_import, []) diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex index aa41cd5..29cd3f3 100644 --- a/lib/mv_web/live/global_settings_live.ex +++ b/lib/mv_web/live/global_settings_live.ex @@ -34,8 +34,8 @@ defmodule MvWeb.GlobalSettingsLive do ### Limits - - Maximum file size: 10 MB - - Maximum rows: 1,000 rows (excluding header) + - Maximum file size: configurable via `config :mv, csv_import: [max_file_size_mb: ...]` + - Maximum rows: configurable via `config :mv, csv_import: [max_rows: ...]` (excluding header) - Processing: chunks of 200 rows - Errors: capped at 50 per import @@ -74,6 +74,8 @@ defmodule MvWeb.GlobalSettingsLive do |> assign(:import_status, :idle) |> assign(:locale, locale) |> assign(:max_errors, @max_errors) + |> assign(:csv_import_max_rows, Config.csv_import_max_rows()) + |> assign(:csv_import_max_file_size_mb, Config.csv_import_max_file_size_mb()) |> assign_form() # Configure file upload with auto-upload enabled # Files are uploaded automatically when selected, no need for manual trigger @@ -198,7 +200,7 @@ defmodule MvWeb.GlobalSettingsLive do /> -- 2.47.2 From b6d53d2826752a532445f317bb38f13380a17a36 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 2 Feb 2026 10:22:05 +0100 Subject: [PATCH 5/7] refactor: add test to seperate async false module --- .../live/global_settings_live_config_test.exs | 73 +++++++++++++++++++ .../mv_web/live/global_settings_live_test.exs | 37 ---------- 2 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 test/mv_web/live/global_settings_live_config_test.exs diff --git a/test/mv_web/live/global_settings_live_config_test.exs b/test/mv_web/live/global_settings_live_config_test.exs new file mode 100644 index 0000000..c940594 --- /dev/null +++ b/test/mv_web/live/global_settings_live_config_test.exs @@ -0,0 +1,73 @@ +defmodule MvWeb.GlobalSettingsLiveConfigTest do + @moduledoc """ + Tests for GlobalSettingsLive that modify global Application configuration. + + These tests run with `async: false` to prevent race conditions when + modifying global Application environment variables (Application.put_env). + This follows the same pattern as Mv.ConfigTest. + """ + use MvWeb.ConnCase, async: false + import Phoenix.LiveViewTest + + # Helper function to upload CSV file in tests + defp upload_csv_file(view, csv_content, filename \\ "test_import.csv") do + view + |> file_input("#csv-upload-form", :csv_file, [ + %{ + last_modified: System.system_time(:second), + name: filename, + content: csv_content, + size: byte_size(csv_content), + type: "text/csv" + } + ]) + |> render_upload(filename) + end + + describe "CSV Import - Configuration Tests" do + setup %{conn: conn} do + # Ensure admin user + admin_user = Mv.Fixtures.user_with_role_fixture("admin") + conn = MvWeb.ConnCase.conn_with_password_user(conn, admin_user) + + {:ok, conn: conn, admin_user: admin_user} + end + + test "configured row limit is enforced", %{conn: conn} do + # Business rule: CSV import respects configured row limits + # Test that a custom limit (500) is enforced, not just the default (1000) + original_config = Application.get_env(:mv, :csv_import, []) + + try do + Application.put_env(:mv, :csv_import, max_rows: 500) + + {:ok, view, _html} = live(conn, ~p"/settings") + + # Generate CSV with 501 rows (exceeding custom limit of 500) + header = "first_name;last_name;email;street;postal_code;city\n" + + rows = + for i <- 1..501 do + "Row#{i};Last#{i};email#{i}@example.com;Street#{i};12345;City#{i}\n" + end + + large_csv = header <> Enum.join(rows) + + # Simulate file upload using helper function + upload_csv_file(view, large_csv, "too_many_rows_custom.csv") + + view + |> form("#csv-upload-form", %{}) + |> render_submit() + + html = render(view) + # Business rule: import should be rejected when exceeding configured limit + assert html =~ "exceeds" or html =~ "maximum" or html =~ "limit" or + html =~ "Failed to prepare" + after + # Restore original config + Application.put_env(:mv, :csv_import, original_config) + end + end + end +end diff --git a/test/mv_web/live/global_settings_live_test.exs b/test/mv_web/live/global_settings_live_test.exs index 0926681..f217311 100644 --- a/test/mv_web/live/global_settings_live_test.exs +++ b/test/mv_web/live/global_settings_live_test.exs @@ -687,42 +687,5 @@ defmodule MvWeb.GlobalSettingsLiveTest do # Check that file input has accept attribute for CSV assert html =~ ~r/accept=["'][^"']*csv["']/i or html =~ "CSV files only" end - - test "configured row limit is enforced", %{conn: conn} do - # Business rule: CSV import respects configured row limits - # Test that a custom limit (500) is enforced, not just the default (1000) - original_config = Application.get_env(:mv, :csv_import, []) - - try do - Application.put_env(:mv, :csv_import, max_rows: 500) - - {:ok, view, _html} = live(conn, ~p"/settings") - - # Generate CSV with 501 rows (exceeding custom limit of 500) - header = "first_name;last_name;email;street;postal_code;city\n" - - rows = - for i <- 1..501 do - "Row#{i};Last#{i};email#{i}@example.com;Street#{i};12345;City#{i}\n" - end - - large_csv = header <> Enum.join(rows) - - # Simulate file upload using helper function - upload_csv_file(view, large_csv, "too_many_rows_custom.csv") - - view - |> form("#csv-upload-form", %{}) - |> render_submit() - - html = render(view) - # Business rule: import should be rejected when exceeding configured limit - assert html =~ "exceeds" or html =~ "maximum" or html =~ "limit" or - html =~ "Failed to prepare" - after - # Restore original config - Application.put_env(:mv, :csv_import, original_config) - end - end end end -- 2.47.2 From 4997819c7380270b2f9b7953728dce9a32c1398d Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 2 Feb 2026 10:22:21 +0100 Subject: [PATCH 6/7] feat: validate config --- lib/mv/config.ex | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/mv/config.ex b/lib/mv/config.ex index 98a5b65..007309a 100644 --- a/lib/mv/config.ex +++ b/lib/mv/config.ex @@ -85,5 +85,35 @@ defmodule Mv.Config do defp get_csv_import_config(key, default) do Application.get_env(:mv, :csv_import, []) |> Keyword.get(key, default) + |> parse_and_validate_integer(default) + end + + # Parses and validates integer configuration values. + # + # Accepts: + # - Integer values (passed through) + # - String integers (e.g., "1000") - parsed to integer + # - Invalid values (e.g., "abc", nil) - falls back to default + # + # Always clamps the result to a minimum of 1 to ensure positive values. + # + # Note: We don't log warnings for unparseable values because: + # - These functions may be called frequently (e.g., on every request) + # - Logging would create excessive log spam + # - The fallback to default provides a safe behavior + # - Configuration errors should be caught during deployment/testing + defp parse_and_validate_integer(value, _default) when is_integer(value) do + max(1, value) + end + + defp parse_and_validate_integer(value, default) when is_binary(value) do + case Integer.parse(value) do + {int, _remainder} -> max(1, int) + :error -> default + end + end + + defp parse_and_validate_integer(_value, default) do + default end end -- 2.47.2 From ce6240133d329a0d064c393ad78834ea8574ac3d Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 2 Feb 2026 10:23:49 +0100 Subject: [PATCH 7/7] i18n: update translations --- priv/gettext/de/LC_MESSAGES/default.po | 10 +++++----- priv/gettext/default.pot | 10 +++++----- priv/gettext/en/LC_MESSAGES/default.po | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index d650aa2..fa126c1 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -1980,11 +1980,6 @@ msgstr " (Datenfeld: %{field})" msgid "CSV File" msgstr "CSV Datei" -#: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "CSV files only, maximum 10 MB" -msgstr "Nur CSV Dateien, maximal 10 MB" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format msgid "Custom fields must be created in Mila before importing CSV files with custom field columns" @@ -2272,3 +2267,8 @@ msgstr "Nicht berechtigt." #, elixir-autogen, elixir-format msgid "Could not load data fields. Please check your permissions." msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfen Sie Ihre Berechtigungen." + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "CSV files only, maximum %{size} MB" +msgstr "Nur CSV Dateien, maximal %{size} MB" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 98f9d7b..b0e74ab 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -1981,11 +1981,6 @@ msgstr "" msgid "CSV File" msgstr "" -#: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "CSV files only, maximum 10 MB" -msgstr "" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format msgid "Custom fields must be created in Mila before importing CSV files with custom field columns" @@ -2273,3 +2268,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Could not load data fields. Please check your permissions." msgstr "" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format +msgid "CSV files only, maximum %{size} MB" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 95a3c3a..6d3013d 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -1981,11 +1981,6 @@ msgstr "" msgid "CSV File" msgstr "" -#: lib/mv_web/live/global_settings_live.ex -#, elixir-autogen, elixir-format -msgid "CSV files only, maximum 10 MB" -msgstr "" - #: lib/mv_web/live/global_settings_live.ex #, elixir-autogen, elixir-format msgid "Custom fields must be created in Mila before importing CSV files with custom field columns" @@ -2273,3 +2268,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Could not load data fields. Please check your permissions." msgstr "" + +#: lib/mv_web/live/global_settings_live.ex +#, elixir-autogen, elixir-format, fuzzy +msgid "CSV files only, maximum %{size} MB" +msgstr "" -- 2.47.2