From 7118782a2dabe20fef50afe52e2ca357c5f61251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 21 Aug 2025 14:09:03 +0200 Subject: [PATCH 0001/1181] Add seed data for members --- priv/repo/seeds.exs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 306b627..cb38969 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -47,3 +47,48 @@ end Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity: :unique_email) |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) |> Ash.update!() + +# Create sample members for testing +for member_attrs <- [ + %{ + first_name: "Hans", + last_name: "Müller", + email: "hans.mueller@example.de", + birth_date: ~D[1985-06-15], + join_date: ~D[2023-01-15], + paid: true, + phone_number: "+49301234567", + city: "München", + street: "Hauptstraße", + house_number: "42", + postal_code: "80331" + }, + %{ + first_name: "Greta", + last_name: "Schmidt", + email: "greta.schmidt@example.de", + birth_date: ~D[1990-03-22], + join_date: ~D[2023-02-01], + paid: false, + phone_number: "+49309876543", + city: "Hamburg", + street: "Lindenstraße", + house_number: "17", + postal_code: "20095", + notes: "Interessiert an Fortgeschrittenen-Kursen" + }, + %{ + first_name: "Friedrich", + last_name: "Wagner", + email: "friedrich.wagner@example.de", + birth_date: ~D[1978-11-08], + join_date: ~D[2022-11-10], + paid: true, + phone_number: "+49301122334", + city: "Berlin", + street: "Kastanienallee", + house_number: "8" + } + ] do + Membership.create_member!(member_attrs) +end From c6be9b510412f3870659f3f991ffefc7f771e956 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 4 Sep 2025 13:54:57 +0200 Subject: [PATCH 0002/1181] feat: add playwright and a11y audit and example test --- Justfile | 5 +- assets/package-lock.json | 59 +++++++++++++++++++ assets/package.json | 5 ++ config/test.exs | 16 ++++- mix.exs | 17 ++++++ mix.lock | 4 ++ test-results/.last-run.json | 4 ++ test/e2e/a11y/a11y_test.exs | 23 ++++++++ test/test_helper.exs | 1 + test/{ => unit}/membership/member_test.exs | 0 .../controllers/auth_controller_test.exs | 0 .../mv_web/controllers/error_html_test.exs | 0 .../mv_web/controllers/error_json_test.exs | 0 .../controllers/oidc_integration_test.exs | 0 .../controllers/page_controller_test.exs | 0 test/{ => unit}/mv_web/locale_test.exs | 0 .../mv_web/member_live/index_test.exs | 1 + .../{ => unit}/mv_web/user_live/form_test.exs | 0 .../mv_web/user_live/index_test.exs | 0 19 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 assets/package-lock.json create mode 100644 assets/package.json create mode 100644 test-results/.last-run.json create mode 100644 test/e2e/a11y/a11y_test.exs rename test/{ => unit}/membership/member_test.exs (100%) rename test/{ => unit}/mv_web/controllers/auth_controller_test.exs (100%) rename test/{ => unit}/mv_web/controllers/error_html_test.exs (100%) rename test/{ => unit}/mv_web/controllers/error_json_test.exs (100%) rename test/{ => unit}/mv_web/controllers/oidc_integration_test.exs (100%) rename test/{ => unit}/mv_web/controllers/page_controller_test.exs (100%) rename test/{ => unit}/mv_web/locale_test.exs (100%) rename test/{ => unit}/mv_web/member_live/index_test.exs (99%) rename test/{ => unit}/mv_web/user_live/form_test.exs (100%) rename test/{ => unit}/mv_web/user_live/index_test.exs (100%) diff --git a/Justfile b/Justfile index 1874b67..ece0d49 100644 --- a/Justfile +++ b/Justfile @@ -35,8 +35,11 @@ audit: mix deps.audit mix hex.audit +# first run unit test and after that run e2e test (especially for accessibility) test: install-dependencies start-database - mix test + mix test.unit + mix test.e2e + format: mix format diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..d8dee79 --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,59 @@ +{ + "name": "assets", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "playwright": "^1.55.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..3f52f4c --- /dev/null +++ b/assets/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "playwright": "^1.55.0" + } +} diff --git a/config/test.exs b/config/test.exs index bcb55eb..1d2e567 100644 --- a/config/test.exs +++ b/config/test.exs @@ -19,7 +19,8 @@ config :mv, Mv.Repo, config :mv, MvWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 4002], secret_key_base: "Qbc/hcosiQzgfgMMPVs2slKjY2oqiqhpQHsV3twL9dN5GVDzsmsMWC1L/BZAU3Fd", - server: false + # Set to true for playwright + server: true # In test we don't send emails config :mv, Mv.Mailer, adapter: Swoosh.Adapters.Test @@ -45,3 +46,16 @@ config :mv, :token_signing_secret, "test_secret_key_for_ash_authentication_token config :mv, :session_identifier, :unsafe config :mv, :require_token_presence_for_authentication, false + +# Playwright config +config :phoenix_test, + endpoint: MvWeb.Endpoint, + otp_app: :mv, + playwright: [ + browser: :firefox, #:chromium + headless: System.get_env("PW_HEADLESS", "true") in ~w(t true), + js_logger: false, + screenshot: System.get_env("PW_SCREENSHOT", "false") in ~w(t true), + trace: System.get_env("PW_TRACE", "false") in ~w(t true), + browser_launch_timeout: 10_000 + ] diff --git a/mix.exs b/mix.exs index 7d1797b..42e660d 100644 --- a/mix.exs +++ b/mix.exs @@ -11,11 +11,24 @@ defmodule Mv.MixProject do consolidate_protocols: Mix.env() != :dev, compilers: [:phoenix_live_view] ++ Mix.compilers(), aliases: aliases(), + preferred_cli_env: preferred_cli_env(), deps: deps(), listeners: [Phoenix.CodeReloader] ] end + # Specifies which environment to be set for which alias / tasks + defp preferred_cli_env do + [ + # Standard‑Mix‑Task + test: :test, + + # Aliases + "test.unit": :test, + "test.e2e": :test, + ] + end + # Configuration for the OTP application. # # Type `mix help compile.app` for more information. @@ -75,6 +88,8 @@ defmodule Mv.MixProject do {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.14", only: [:dev, :test], runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:phoenix_test_playwright, "~> 0.4", only: :test, runtime: false}, + {:a11y_audit, "~> 0.2.3", only: :test}, {:ecto_commons, "~> 0.3"} ] end @@ -91,6 +106,8 @@ defmodule Mv.MixProject do "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ash.setup --quiet", "test"], + "test.unit": ["ash.setup --quiet","test test/unit"], + "test.e2e": ["test test/e2e"], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], "assets.build": ["tailwind mv", "esbuild mv"], "assets.deploy": [ diff --git a/mix.lock b/mix.lock index 46c9f3f..a5bae03 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "a11y_audit": {:hex, :a11y_audit, "0.2.3", "4a041eeeb9ae87967b30526b8bb9e4a1a3b0136b6e0f1324a029c47cc0938a21", [:mix], [{:hound, "~> 1.1", [hex: :hound, repo: "hexpm", optional: true]}, {:wallaby, "~> 0.30", [hex: :wallaby, repo: "hexpm", optional: true]}], "hexpm", "748ed5dcca3c4a20db2b4a71b3ca2130c66dcacb48cfabf1d70d4fde80eddc27"}, "ash": {:hex, :ash, "3.5.34", "e79e82dc3e3e66fb54a598eeba5feca2d1c3af6a0e752a3378cbad8d7a47dc6f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.4 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.65 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cbf0a4d0ec1b6525b0782e4f5509c55dad446d657c635ceffe55f78a59132cd"}, "ash_admin": {:hex, :ash_admin, "0.13.16", "6b30487e88b0a47b2da1c508b157be6d86b954ba464a01d412e6d5e047a53ad5", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "07a03d761b2029d8b1fefad815eb3cc525532ae9d440e7ca3f5c9f4c1ecb5d17"}, "ash_authentication": {:hex, :ash_authentication, "4.9.9", "23ec61bedc3157c258ece622c6f0f6a7645df275ff5e794d513cc6e8798471eb", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "ab8bd1277ff570425346dcf22dd14a059d9bbce0c28d24964b60e51fabaddda8"}, @@ -29,6 +30,7 @@ "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, + "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, @@ -57,6 +59,8 @@ "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.8", "d283d5e047e6c013182a3833e99ff33942e3a8076f9f984c337ea04cc53e8206", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6184cf1e82fe6627d40cfa62236133099438513710d30358f4c085c16ecb84b4"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "phoenix_test": {:hex, :phoenix_test, "0.7.1", "0de2b8a7b7cb5ca3bf422211eb544b15cef1ed7c62ac9fb6806a304cee2624a7", [:mix], [{:floki, ">= 0.30.0", [hex: :floki, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, ">= 1.0.0", [hex: :mime, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.7.10", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "e5de115d48f22af9d9e9a31ffcf063407adf07163a20abb02d61180975f46622"}, + "phoenix_test_playwright": {:hex, :phoenix_test_playwright, "0.7.1", "20992d444992f94e5b824d388cb9d849c6e42246a7568b604f8df40854b9e17d", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_ecto, "~> 4.5", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_test, "~> 0.6", [hex: :phoenix_test, repo: "hexpm", optional: false]}], "hexpm", "90266ce5b5d72a244e9a3bd00462e2450fcb0499f683b2ea0e59223e586fb6de"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/test/e2e/a11y/a11y_test.exs b/test/e2e/a11y/a11y_test.exs new file mode 100644 index 0000000..26f5556 --- /dev/null +++ b/test/e2e/a11y/a11y_test.exs @@ -0,0 +1,23 @@ +defmodule MvWeb.A11yTest do + use PhoenixTest.Playwright.Case, async: true + + alias PhoenixTest.Playwright.Frame + + test "is accessible", %{conn: conn} do + conn = conn_with_oidc_user(conn) + conn = Plug.Test.init_test_session(conn, locale: "de") + {:ok, _view, html} = live(conn, "/members") + conn + #|> visit("/members") + |> unwrap(&assert_a11y/1) + end + + defp assert_a11y(%{frame_id: frame_id}) do + Frame.evaluate(frame_id, A11yAudit.JS.axe_core()) + + frame_id + |> Frame.evaluate("axe.run()") + |> A11yAudit.Results.from_json() + |> A11yAudit.Assertions.assert_no_violations() + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index a52775b..5b88589 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,3 @@ ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Mv.Repo, :manual) +Application.put_env(:phoenix_test, :base_url, MvWeb.Endpoint.url()) diff --git a/test/membership/member_test.exs b/test/unit/membership/member_test.exs similarity index 100% rename from test/membership/member_test.exs rename to test/unit/membership/member_test.exs diff --git a/test/mv_web/controllers/auth_controller_test.exs b/test/unit/mv_web/controllers/auth_controller_test.exs similarity index 100% rename from test/mv_web/controllers/auth_controller_test.exs rename to test/unit/mv_web/controllers/auth_controller_test.exs diff --git a/test/mv_web/controllers/error_html_test.exs b/test/unit/mv_web/controllers/error_html_test.exs similarity index 100% rename from test/mv_web/controllers/error_html_test.exs rename to test/unit/mv_web/controllers/error_html_test.exs diff --git a/test/mv_web/controllers/error_json_test.exs b/test/unit/mv_web/controllers/error_json_test.exs similarity index 100% rename from test/mv_web/controllers/error_json_test.exs rename to test/unit/mv_web/controllers/error_json_test.exs diff --git a/test/mv_web/controllers/oidc_integration_test.exs b/test/unit/mv_web/controllers/oidc_integration_test.exs similarity index 100% rename from test/mv_web/controllers/oidc_integration_test.exs rename to test/unit/mv_web/controllers/oidc_integration_test.exs diff --git a/test/mv_web/controllers/page_controller_test.exs b/test/unit/mv_web/controllers/page_controller_test.exs similarity index 100% rename from test/mv_web/controllers/page_controller_test.exs rename to test/unit/mv_web/controllers/page_controller_test.exs diff --git a/test/mv_web/locale_test.exs b/test/unit/mv_web/locale_test.exs similarity index 100% rename from test/mv_web/locale_test.exs rename to test/unit/mv_web/locale_test.exs diff --git a/test/mv_web/member_live/index_test.exs b/test/unit/mv_web/member_live/index_test.exs similarity index 99% rename from test/mv_web/member_live/index_test.exs rename to test/unit/mv_web/member_live/index_test.exs index e3e77dc..04bbb3a 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/unit/mv_web/member_live/index_test.exs @@ -2,6 +2,7 @@ defmodule MvWeb.MemberLive.IndexTest do use MvWeb.ConnCase, async: true import Phoenix.LiveViewTest + test "shows translated title in German", %{conn: conn} do conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "de") diff --git a/test/mv_web/user_live/form_test.exs b/test/unit/mv_web/user_live/form_test.exs similarity index 100% rename from test/mv_web/user_live/form_test.exs rename to test/unit/mv_web/user_live/form_test.exs diff --git a/test/mv_web/user_live/index_test.exs b/test/unit/mv_web/user_live/index_test.exs similarity index 100% rename from test/mv_web/user_live/index_test.exs rename to test/unit/mv_web/user_live/index_test.exs From a3746dfaaa5aa9e32a7b6dd82dad0d494d86e5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 11 Sep 2025 11:49:23 +0200 Subject: [PATCH 0003/1181] Explicitly require ash authentication settings Previously, we'd rely on defaults for configuring user token authentication. With these changes, we explicitly require :session_identifier and :require_token_presence_for_authentication to be configured in the application environment to make sure the system is configured the way it should be. --- lib/accounts/user.ex | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 9294526..b085407 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -19,16 +19,15 @@ defmodule Mv.Accounts.User do Currently password and SSO with Rauthy as OIDC provider """ authentication do - session_identifier Application.compile_env(:mv, :session_identifier, :jti) + session_identifier Application.compile_env!(:mv, :session_identifier) tokens do enabled? true token_resource Mv.Accounts.Token - require_token_presence_for_authentication? Application.compile_env( + require_token_presence_for_authentication? Application.compile_env!( :mv, - :require_token_presence_for_authentication, - false + :require_token_presence_for_authentication ) store_all_tokens? true From dd03000428aa82090ad23a1c20f31ea02a4b37f6 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 17 Sep 2025 13:34:14 +0200 Subject: [PATCH 0004/1181] chore: adds tsvector to members --- lib/membership/member.ex | 2 + ...0250912085235_AddSearchVectorToMembers.exs | 60 ++++++ .../repo/members/20250912085235.json | 199 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 priv/repo/migrations/20250912085235_AddSearchVectorToMembers.exs create mode 100644 priv/resource_snapshots/repo/members/20250912085235.json diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 583f173..14e8708 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -166,6 +166,8 @@ defmodule Mv.Membership.Member do attribute :postal_code, :string do allow_nil? true end + + attribute :search_vector, AshPostgres.Tsvector, writable?: false, public?: false, select_by_default?: false end relationships do diff --git a/priv/repo/migrations/20250912085235_AddSearchVectorToMembers.exs b/priv/repo/migrations/20250912085235_AddSearchVectorToMembers.exs new file mode 100644 index 0000000..126f369 --- /dev/null +++ b/priv/repo/migrations/20250912085235_AddSearchVectorToMembers.exs @@ -0,0 +1,60 @@ +defmodule Mv.Repo.Migrations.AddSearchVectorToMembers do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:members) do + add :search_vector, :tsvector + end + + execute(""" + CREATE INDEX members_search_vector_idx + ON members + USING GIN (search_vector) + """) + + # Eigene Trigger-Funktion mit Gewichtung + execute(""" + CREATE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$ + BEGIN + NEW.search_vector := + setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') || + setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') || + setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') || + setweight(to_tsvector('simple', coalesce(NEW.birth_date::text, '')), 'C') || + setweight(to_tsvector('simple', coalesce(NEW.phone_number, '')), 'C') || + setweight(to_tsvector('simple', coalesce(NEW.join_date::text, '')), 'D') || + setweight(to_tsvector('simple', coalesce(NEW.exit_date::text, '')), 'D') || + setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') || + setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') || + setweight(to_tsvector('simple', coalesce(NEW.street, '')), 'C') || + setweight(to_tsvector('simple', coalesce(NEW.house_number::text, '')), 'C') || + setweight(to_tsvector('simple', coalesce(NEW.postal_code::text, '')), 'C'); + RETURN NEW; + END + $$ LANGUAGE plpgsql; + """) + + execute(""" + CREATE TRIGGER update_search_vector + BEFORE INSERT OR UPDATE ON members + FOR EACH ROW + EXECUTE FUNCTION members_search_vector_trigger() + """) + end + + def down do + execute("DROP TRIGGER IF EXISTS update_search_vector ON members") + execute("DROP FUNCTION IF EXISTS members_search_vector_trigger()") + execute("DROP INDEX IF EXISTS members_search_vector_idx") + + alter table(:members) do + remove :search_vector + end + end +end diff --git a/priv/resource_snapshots/repo/members/20250912085235.json b/priv/resource_snapshots/repo/members/20250912085235.json new file mode 100644 index 0000000..a8b86da --- /dev/null +++ b/priv/resource_snapshots/repo/members/20250912085235.json @@ -0,0 +1,199 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v7()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "first_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "last_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "email", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "birth_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "paid", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "phone_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "join_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "exit_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "notes", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "street", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "house_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "postal_code", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "search_vectors", + "type": "tsvector" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "3B162FD69B92BF8258DB56BA0CBB6108FBE996B1F7231C5F2D9EC53D956EFC75", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "members" +} \ No newline at end of file From 78588cbad9c667bd9990f34b63dffaa46844a272 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 17 Sep 2025 14:36:13 +0200 Subject: [PATCH 0005/1181] feat: adds SearchBar Live Component --- .../live/components/search_bar_component.ex | 62 +++++++++++++++++++ lib/mv_web/live/member_live/index.ex | 28 +++++++++ lib/mv_web/live/member_live/index.html.heex | 7 +++ 3 files changed, 97 insertions(+) create mode 100644 lib/mv_web/live/components/search_bar_component.ex diff --git a/lib/mv_web/live/components/search_bar_component.ex b/lib/mv_web/live/components/search_bar_component.ex new file mode 100644 index 0000000..b1c4a10 --- /dev/null +++ b/lib/mv_web/live/components/search_bar_component.ex @@ -0,0 +1,62 @@ +defmodule MvWeb.Components.SearchBarComponent do + @moduledoc """ + Provides the SearchBar Live-Component. + + - uses the DaisyUI search input field + - sends search_changed event to parent live view with a query + """ + use MvWeb, :live_component + + @impl true + def update(assigns, socket) do + socket = + socket + |> assign_new(:query, fn -> "" end) + |> assign_new(:placeholder, fn -> gettext("Search...") end) + + {:ok, socket} + end + + @impl true + def render(assigns) do + ~H""" +
+ +
+ """ + end + + @impl true + # Function to handle the search + def handle_event("search", %{"query" => q}, socket) do + # Forward a high level message to the parent + send(self(), {:search_changed, q}) + {:noreply, assign(socket, :query, q)} + end +end diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 476abd1..d238436 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -1,5 +1,7 @@ defmodule MvWeb.MemberLive.Index do use MvWeb, :live_view + import Ash.Expr + import Ash.Query import MvWeb.TableComponents @impl true @@ -10,12 +12,38 @@ defmodule MvWeb.MemberLive.Index do {:ok, socket |> assign(:page_title, gettext("Members")) + |> assign(:query, "") |> assign(:sort_field, :first_name) |> assign(:sort_order, :asc) |> assign(:members, sorted) |> assign(:selected_members, [])} end + # ----------------------------------------------------------------- + # Receive messages from any toolbar component + # ----------------------------------------------------------------- + + # Function to handle search + @impl true + def handle_info({:search_changed, q}, socket) do + members = + Mv.Membership.Member + |> Ash.Query.filter( + expr(fragment("search_vector @@ plainto_tsquery('simple', ?)", ^q)) + ) + |> Ash.read!() + + IO.inspect(members) + {:noreply, + socket + |> assign(:query, q) + |> assign(:members, members)} + end + + # ----------------------------------------------------------------- + # Handle Events + # ----------------------------------------------------------------- + @impl true def handle_event("delete", %{"id" => id}, socket) do member = Ash.get!(Mv.Membership.Member, id) diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index fc38889..aa7a820 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -8,6 +8,13 @@ + <.live_component + module={MvWeb.Components.SearchBarComponent} + id="search-bar" + query={@query} + placeholder={gettext("Search...")} + /> + <.table id="members" rows={@members} From 53f6b62289b42444d04d3d1ddb6f1fdae7f7797d Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 17 Sep 2025 14:36:50 +0200 Subject: [PATCH 0006/1181] test: updated tests for member and search bar --- .../components/search_bar_component_test.exs | 34 +++++++++++++++++++ test/mv_web/member_live/index_test.exs | 13 +++++++ 2 files changed, 47 insertions(+) create mode 100644 test/mv_web/components/search_bar_component_test.exs diff --git a/test/mv_web/components/search_bar_component_test.exs b/test/mv_web/components/search_bar_component_test.exs new file mode 100644 index 0000000..d84f78e --- /dev/null +++ b/test/mv_web/components/search_bar_component_test.exs @@ -0,0 +1,34 @@ +defmodule MvWeb.Components.SearchBarComponentTest do + use MvWeb.ConnCase, async: true + import Phoenix.LiveViewTest + + describe "SearchBarComponent" do + test "renders with placeholder", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + assert has_element?(view, "input[placeholder='Search...']") + end + + test "updates query when user types", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # simulate search input and check that correct user is listed + html = + view + |> element("form[role=search]") + |> render_change(%{"query" => "Friedrich"}) + + assert html =~ "Friedrich" + + # simulate search input and check that not matching user is not shown + html = + view + |> element("form[role=search]") + |> render_change(%{"query" => "Greta"}) + + refute html =~ "Friedrich" + end + end +end diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index e3e77dc..e10c685 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -73,4 +73,17 @@ defmodule MvWeb.MemberLive.IndexTest do assert has_element?(index_view, "#flash-group", "Member create successfully") end + + test "handle_info(:search_changed) updates assigns with search results", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + send(view.pid, {:search_changed, "Friedrich"}) + + # State aus dem LiveView-Prozess holen + state = :sys.get_state(view.pid) + + assert state.socket.assigns.query == "Friedrich" + assert is_list(state.socket.assigns.members) + end end From 02b30847893767202f46c05ccbd0a7c2fb13e113 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 17 Sep 2025 14:37:04 +0200 Subject: [PATCH 0007/1181] formatting --- lib/membership/member.ex | 5 ++++- lib/mv_web/live/components/search_bar_component.ex | 3 +-- lib/mv_web/live/member_live/index.ex | 9 ++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 14e8708..7fe69da 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -167,7 +167,10 @@ defmodule Mv.Membership.Member do allow_nil? true end - attribute :search_vector, AshPostgres.Tsvector, writable?: false, public?: false, select_by_default?: false + attribute :search_vector, AshPostgres.Tsvector, + writable?: false, + public?: false, + select_by_default?: false end relationships do diff --git a/lib/mv_web/live/components/search_bar_component.ex b/lib/mv_web/live/components/search_bar_component.ex index b1c4a10..dd7341a 100644 --- a/lib/mv_web/live/components/search_bar_component.ex +++ b/lib/mv_web/live/components/search_bar_component.ex @@ -20,8 +20,7 @@ defmodule MvWeb.Components.SearchBarComponent do @impl true def render(assigns) do ~H""" -
+