mitgliederverwaltung/test/mv_web/live/import_live_test.exs
carla c62b105518
All checks were successful
continuous-integration/drone/push Build is passing
test: updated
2026-02-24 16:00:46 +01:00

279 lines
9.4 KiB
Elixir

defmodule MvWeb.ImportLiveTest do
@moduledoc """
Tests for Import LiveView: authorization (business rule), CSV import integration,
and minimal UI smoke tests. CSV parsing/validation logic is covered by
Mv.Membership.Import.MemberCSVTest; here we verify access control and end-to-end outcomes.
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Mv.Membership
defp put_locale_en(conn), do: Plug.Conn.put_session(conn, "locale", "en")
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
defp submit_import(view), do: view |> form("#csv-upload-form", %{}) |> render_submit()
defp wait_for_import_completion, do: Process.sleep(1000)
# ---------- Business logic: Authorization ----------
describe "Authorization" do
test "non-admin user cannot access import page and sees permission error", %{
conn: conn
} do
member_user = Mv.Fixtures.user_with_role_fixture("own_data")
conn =
conn
|> MvWeb.ConnCase.conn_with_password_user(member_user)
|> put_locale_en()
assert {:error, {:redirect, %{to: redirect_path, flash: %{"error" => msg}}}} =
live(conn, ~p"/admin/import")
assert redirect_path =~ "/users/"
assert msg =~ "don't have permission"
end
test "admin user can access page and run import", %{conn: conn} do
conn = put_locale_en(conn)
csv_content =
Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
|> File.read!()
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content)
submit_import(view)
wait_for_import_completion()
assert has_element?(view, "[data-testid='import-results-panel']")
assert has_element?(view, "[data-testid='import-summary']")
html = render(view)
refute html =~ "Import aborted"
assert html =~ "Successfully inserted"
# Business outcome: two members from fixture were created
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, members} = Membership.list_members(actor: system_actor)
imported =
Enum.filter(members, fn m ->
m.email in ["alice.smith@example.com", "bob.johnson@example.com"]
end)
assert length(imported) == 2
end
end
# ---------- Business logic: Import behaviour (integration) ----------
describe "CSV Import - integration" do
setup %{conn: conn} do
admin_user = Mv.Fixtures.user_with_role_fixture("admin")
conn =
conn
|> MvWeb.ConnCase.conn_with_password_user(admin_user)
|> put_locale_en()
valid_csv =
Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
|> File.read!()
invalid_csv =
Path.join([__DIR__, "..", "..", "fixtures", "invalid_member_import.csv"])
|> File.read!()
unknown_cf_csv =
Path.join([__DIR__, "..", "..", "fixtures", "csv_with_unknown_custom_field.csv"])
|> File.read!()
{:ok,
conn: conn,
valid_csv: valid_csv,
invalid_csv: invalid_csv,
unknown_custom_field_csv: unknown_cf_csv}
end
test "invalid CSV shows user-friendly prepare error", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, "invalid_header\nincomplete_row", "invalid.csv")
submit_import(view)
html = render(view)
assert html =~ "Failed to prepare CSV import"
end
test "invalid rows show errors with correct CSV line numbers", %{
conn: conn,
invalid_csv: csv_content
} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content, "invalid_import.csv")
submit_import(view)
wait_for_import_completion()
assert has_element?(view, "[data-testid='import-results-panel']")
assert has_element?(view, "[data-testid='import-error-list']")
html = render(view)
assert html =~ "Failed"
# Fixture has invalid email on line 2 and missing email on line 3
assert html =~ "Line 2"
assert html =~ "Line 3"
end
test "error list is capped and truncation message is shown", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
header = "first_name;last_name;email;country;city;street;postal_code\n"
invalid_rows =
for i <- 1..100, do: "Row#{i};Last#{i};;Country#{i};City#{i};Street#{i};12345\n"
upload_csv_file(view, header <> Enum.join(invalid_rows), "large_invalid.csv")
submit_import(view)
wait_for_import_completion()
assert has_element?(view, "[data-testid='import-results-panel']")
assert has_element?(view, "[data-testid='import-error-list']")
html = render(view)
assert html =~ "100"
assert html =~ "Error list truncated"
end
test "row limit is enforced (1001 rows rejected)", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
header = "first_name;last_name;email;country;city;street;postal_code\n"
rows =
for i <- 1..1001 do
"Row#{i};Last#{i};email#{i}@example.com;Country#{i};City#{i};Street#{i};12345\n"
end
upload_csv_file(view, header <> Enum.join(rows), "too_many_rows.csv")
submit_import(view)
html = render(view)
assert html =~ "exceeds"
end
test "BOM and semicolon delimiter are accepted", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
csv_content =
Path.join([__DIR__, "..", "..", "fixtures", "csv_with_bom_semicolon.csv"])
|> File.read!()
upload_csv_file(view, csv_content, "bom_import.csv")
submit_import(view)
wait_for_import_completion()
assert has_element?(view, "[data-testid='import-results-panel']")
html = render(view)
assert html =~ "Successfully inserted"
refute html =~ "BOM"
end
test "physical line numbers in errors (empty line does not shift numbering)", %{
conn: conn
} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
csv_content =
Path.join([__DIR__, "..", "..", "fixtures", "csv_with_empty_lines.csv"])
|> File.read!()
upload_csv_file(view, csv_content, "empty_lines.csv")
submit_import(view)
wait_for_import_completion()
assert has_element?(view, "[data-testid='import-error-list']")
html = render(view)
# Invalid row is on physical line 4 (header, valid row, empty line, then invalid)
assert html =~ "Line 4"
end
test "unknown custom field column produces warnings", %{
conn: conn,
unknown_custom_field_csv: csv_content
} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content, "unknown_custom.csv")
submit_import(view)
wait_for_import_completion()
assert has_element?(view, "[data-testid='import-results-panel']")
assert has_element?(view, "[data-testid='import-warnings']")
html = render(view)
assert html =~ "Warnings"
end
end
# ---------- UI (smoke / framework): tagged for exclusion from fast CI ----------
describe "Import page UI" do
@describetag :ui
setup %{conn: conn} do
admin_user = Mv.Fixtures.user_with_role_fixture("admin")
conn =
conn
|> MvWeb.ConnCase.conn_with_password_user(admin_user)
|> put_locale_en()
{:ok, conn: conn}
end
test "page loads and shows import form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
assert has_element?(view, "[data-testid='import-page']")
assert has_element?(view, "[data-testid='csv-upload-form']")
assert has_element?(view, "[data-testid='start-import-button']")
assert has_element?(view, "[data-testid='custom-fields-link']")
end
test "template links and file input are present", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/import")
assert has_element?(view, "a[href*='/templates/member_import_en.csv']")
assert has_element?(view, "a[href*='/templates/member_import_de.csv']")
assert has_element?(view, "label[for='csv_file']")
assert has_element?(view, "#csv_file_help")
assert has_element?(view, "[data-testid='csv-upload-form'] input[type='file']")
end
test "after successful import, progress container has aria-live", %{conn: conn} do
csv_content =
Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
|> File.read!()
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, csv_content)
submit_import(view)
wait_for_import_completion()
assert has_element?(view, "[data-testid='import-progress-container']")
html = render(view)
assert html =~ "aria-live"
end
end
# Skip: LiveView test harness does not reliably support empty/minimal file uploads.
# See docs/csv-member-import-v1.md (Issue #9).
@tag :skip
test "empty CSV shows error", %{conn: conn} do
conn = put_locale_en(conn)
{:ok, view, _html} = live(conn, ~p"/admin/import")
upload_csv_file(view, " ", "empty.csv")
submit_import(view)
html = render(view)
assert html =~ "Failed to prepare"
end
end