From e8ba60966590af72703b9d5a6c4056c5a6841c16 Mon Sep 17 00:00:00 2001 From: Luis Araujo Date: Mon, 4 Aug 2025 17:51:44 +0100 Subject: [PATCH] test: add tests for different auth methods (scram-sha-256, password, md5) --- .gitignore | 2 + .../authentication_methods_test.exs | 89 ++++++++++ test/integration/postgres_switching_test.exs | 158 ++++-------------- .../controllers/metrics_controller_test.exs | 4 - .../controllers/tenant_controller_test.exs | 4 - test/support/conn_case.ex | 6 + test/support/postgres_case.ex | 95 +++++++++++ test/support/tenants.ex | 76 +++++++++ 8 files changed, 300 insertions(+), 134 deletions(-) create mode 100644 test/integration/authentication_methods_test.exs create mode 100644 test/support/postgres_case.ex create mode 100644 test/support/tenants.ex diff --git a/.gitignore b/.gitignore index c068007a..6323a90e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ priv/native/* /.pre-commit-config.yaml *.coverdata /tmp + +.devenv/ \ No newline at end of file diff --git a/test/integration/authentication_methods_test.exs b/test/integration/authentication_methods_test.exs new file mode 100644 index 00000000..f93a83f4 --- /dev/null +++ b/test/integration/authentication_methods_test.exs @@ -0,0 +1,89 @@ +defmodule Supavisor.Integration.AuthenticationMethodsTest do + use SupavisorWeb.ConnCase, async: false + use Supavisor.PostgresCase, async: false + + import Supavisor.Support.Tenants + + @moduletag integration_docker: true + + @auth_configs %{ + "scram-sha-256": [ + hostname: "localhost", + port: 6433, + database: "postgres", + username: "postgres", + password: "postgres" + ], + password: [ + hostname: "localhost", + port: 6434, + database: "postgres", + username: "postgres", + password: "postgres", + volume: "./dev/postgres/password/etc/postgresql/pg_hba.conf:/etc/postgresql/pg_hba.conf", + environment: "--auth-host=password" + ] + # md5: [ + # hostname: "localhost", + # port: 6434, + # database: "postgres", + # username: "postgres", + # password: "postgres", + # volume: "./dev/postgres/md5/etc/postgresql/pg_hba.conf:/etc/postgresql/pg_hba.conf", + # environment: "--auth-host=md5" + # ] + } + + for {key, auth_config} <- @auth_configs do + describe "#{key}" do + setup %{conn: conn} do + external_id = unquote(key) + container_name = container_name(external_id) + + cleanup_containers(container_name) + + jwt = gen_token() + + conn = + conn + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "Bearer " <> jwt) + + on_exit(fn -> cleanup_containers(container_name) end) + + {:ok, conn: conn, container_name: container_name, external_id: external_id} + end + + test "starts postgres and connects through proxy", %{ + conn: conn, + container_name: container_name, + external_id: external_id + } do + opts = + Keyword.merge(unquote(auth_config), + container_name: container_name, + external_id: external_id + ) + + start_postgres_container(opts) + create_tenant(conn, opts) + + assert :ok = test_connection(opts) + + stop_postgres_container(container_name) + terminate_tenant(conn, external_id) + end + end + end + + defp test_connection(opts) do + connection_opts = connection_opts(opts) + + assert {:ok, conn} = Postgrex.start_link(connection_opts) + assert {:ok, %{rows: [[_version_string]]}} = Postgrex.query(conn, "SELECT version();", []) + + :ok + end + + defp container_name(internal_id), do: "supavisor-db-#{internal_id}" +end diff --git a/test/integration/postgres_switching_test.exs b/test/integration/postgres_switching_test.exs index 20c6925e..f34cf77d 100644 --- a/test/integration/postgres_switching_test.exs +++ b/test/integration/postgres_switching_test.exs @@ -1,7 +1,8 @@ defmodule Supavisor.Integration.PostgresSwitchingTest do use SupavisorWeb.ConnCase, async: false + use Supavisor.PostgresCase, async: false - alias Supavisor.Jwt.Token + import Supavisor.Support.Tenants @moduletag integration_docker: true @@ -13,7 +14,8 @@ defmodule Supavisor.Integration.PostgresSwitchingTest do @db_host "localhost" setup %{conn: conn} do - cleanup_containers() + containers = [container_name(15), container_name(16)] + cleanup_containers(containers) jwt = gen_token() @@ -22,17 +24,19 @@ defmodule Supavisor.Integration.PostgresSwitchingTest do |> put_req_header("accept", "application/json") |> put_req_header("authorization", "Bearer " <> jwt) - on_exit(fn -> cleanup_containers() end) + on_exit(fn -> cleanup_containers(containers) end) {:ok, conn: conn} end test "PostgreSQL upgrade scenario: 15 -> 16", %{conn: conn} do - start_postgres_container(15) - create_tenant(conn) - assert :ok = test_connection() + opts = build_opts(15, @tenant_name) - stop_postgres_container(15) + start_postgres_container(opts) + create_tenant(conn, opts) + assert :ok = test_connection(opts) + + stop_postgres_container(opts[:container_name]) # Ideally, we shouldn't need to terminate the tenant manually here. # @@ -43,119 +47,35 @@ defmodule Supavisor.Integration.PostgresSwitchingTest do # # Currently, if we don't terminate the tenant (or restart supavisor), # we get authentication errors. - terminate_tenant(conn) + terminate_tenant(conn, @tenant_name) Process.sleep(2000) - start_postgres_container(16) - - assert :ok = test_connection() - end - - defp start_postgres_container(version) do - container_name = container_name(version) - - {_output, 0} = - System.cmd("docker", [ - "run", - "-d", - "--name", - container_name, - "-e", - "POSTGRES_USER=#{@postgres_user}", - "-e", - "POSTGRES_PASSWORD=#{@postgres_password}", - "-e", - "POSTGRES_DB=#{@postgres_db}", - "-p", - "#{@postgres_port}:5432", - "postgres:#{version}" - ]) - - wait_for_postgres() - end - - defp wait_for_postgres(max_attempts \\ 30) do - wait_for_postgres(1, max_attempts) - end - - defp wait_for_postgres(attempt, max_attempts) when attempt != max_attempts do - case System.cmd("pg_isready", [ - "-h", - "localhost", - "-p", - to_string(@postgres_port), - "-U", - @postgres_user, - "-d", - @postgres_db - ]) do - {_, 0} -> - :ok - - _ -> - Process.sleep(1000) - wait_for_postgres(attempt + 1, max_attempts) - end - end - defp wait_for_postgres(_attempt, max_attempts) do - raise "PostgreSQL failed to start within #{max_attempts} seconds" - end + opts = build_opts(16, @tenant_name) + start_postgres_container(opts) - defp stop_postgres_container(version) do - System.cmd("docker", ["stop", container_name(version)]) - System.cmd("docker", ["rm", container_name(version)]) + assert :ok = test_connection(opts) end - defp create_tenant(conn) do - tenant_attrs = %{ - db_host: @db_host, - db_port: @postgres_port, - db_database: @postgres_db, - external_id: @tenant_name, - ip_version: "auto", - enforce_ssl: false, - require_user: false, - auth_query: "SELECT rolname, rolpassword FROM pg_authid WHERE rolname=$1;", - users: [ - %{ - db_user: @postgres_user, - db_password: @postgres_password, - pool_size: 20, - mode_type: "transaction", - is_manager: true - } - ] - } - - conn = put(conn, Routes.tenant_path(conn, :update, @tenant_name), tenant: tenant_attrs) - - case conn.status do - status when status in 200..201 -> - :ok - - _status -> - :ok - end + defp build_opts(version, external_id) do + Keyword.merge(postgres_container_opts(version), + container_name: container_name(version), + external_id: external_id + ) end - defp terminate_tenant(conn) do - _conn = get(conn, Routes.tenant_path(conn, :terminate, @tenant_name)) - :ok - end - - defp test_connection do - proxy_port = Application.fetch_env!(:supavisor, :proxy_port_transaction) - - connection_opts = [ - hostname: @db_host, - port: proxy_port, - database: @postgres_db, - username: "#{@postgres_user}.#{@tenant_name}", + defp postgres_container_opts(version) do + [ + image: "postgres:#{version}", + port: @postgres_port, + user: @postgres_user, password: @postgres_password, - # This is important as otherwise Postgrex may try to reconnect in case of errors. - # We want to avoid that, as it hides connection errors. - backoff: nil + database: @postgres_db, + hostname: @db_host ] + end + + defp test_connection(opts) do + connection_opts = connection_opts(opts) assert {:ok, conn} = Postgrex.start_link(connection_opts) assert {:ok, %{rows: [[_version_string]]}} = Postgrex.query(conn, "SELECT version();", []) @@ -163,19 +83,5 @@ defmodule Supavisor.Integration.PostgresSwitchingTest do :ok end - defp container_name(version) do - "test_postgres_#{version}_switching" - end - - defp cleanup_containers do - [15, 16] - |> Enum.each(fn version -> - System.cmd("docker", ["rm", "-f", container_name(version)], stderr_to_stdout: true) - end) - end - - defp gen_token do - secret = Application.fetch_env!(:supavisor, :api_jwt_secret) - Token.gen!(secret) - end + defp container_name(version), do: "test_postgres_#{version}_switching" end diff --git a/test/supavisor_web/controllers/metrics_controller_test.exs b/test/supavisor_web/controllers/metrics_controller_test.exs index 7edee78b..3bd0f155 100644 --- a/test/supavisor_web/controllers/metrics_controller_test.exs +++ b/test/supavisor_web/controllers/metrics_controller_test.exs @@ -32,8 +32,4 @@ defmodule SupavisorWeb.MetricsControllerTest do defp auth(conn, bearer \\ gen_token()) do put_req_header(conn, "authorization", "Bearer " <> bearer) end - - defp gen_token(secret \\ Application.fetch_env!(:supavisor, :metrics_jwt_secret)) do - Supavisor.Jwt.Token.gen!(secret) - end end diff --git a/test/supavisor_web/controllers/tenant_controller_test.exs b/test/supavisor_web/controllers/tenant_controller_test.exs index 02672727..c026f2d8 100644 --- a/test/supavisor_web/controllers/tenant_controller_test.exs +++ b/test/supavisor_web/controllers/tenant_controller_test.exs @@ -190,8 +190,4 @@ defmodule SupavisorWeb.TenantControllerTest do assert {:ok, nil} = Cachex.get(Supavisor.Cache, {:tenant_cache, external_id, nil}) end - - defp gen_token(secret \\ Application.fetch_env!(:supavisor, :metrics_jwt_secret)) do - Supavisor.Jwt.Token.gen!(secret) - end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 7bc609a0..805b8a5f 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -17,6 +17,8 @@ defmodule SupavisorWeb.ConnCase do use ExUnit.CaseTemplate + alias Supavisor.Jwt.Token + using do quote do # Import conveniences for testing with connections @@ -35,4 +37,8 @@ defmodule SupavisorWeb.ConnCase do Supavisor.DataCase.setup_sandbox(tags) {:ok, conn: Phoenix.ConnTest.build_conn()} end + + def gen_token(secret \\ Application.fetch_env!(:supavisor, :api_jwt_secret)) do + Token.gen!(secret) + end end diff --git a/test/support/postgres_case.ex b/test/support/postgres_case.ex new file mode 100644 index 00000000..e1fb7102 --- /dev/null +++ b/test/support/postgres_case.ex @@ -0,0 +1,95 @@ +defmodule Supavisor.PostgresCase do + @moduledoc """ + Test case for integration tests that require Dockerized Postgres containers. + Provides helpers for starting/stopping containers. + """ + + use ExUnit.CaseTemplate + + @default_postgres_image "postgres:15" + @default_postgres_container_name "test_postgres" + @default_postgres_port 7432 + @default_postgres_user "postgres" + @default_postgres_password "postgres" + @default_postgres_db "postgres" + + using do + quote do + import unquote(__MODULE__) + end + end + + def start_postgres_container(opts) do + image = opts[:image] || @default_postgres_image + container_name = opts[:container_name] || @default_postgres_container_name + port = opts[:port] || @default_postgres_port + user = opts[:username] || @default_postgres_user + password = opts[:password] || @default_postgres_password + db = opts[:database] || @default_postgres_db + volume = opts[:volume] + environment = opts[:environment] + + cmd = [ + "run", + "-d", + "--name", + container_name, + "-e", + "POSTGRES_USER=#{user}", + "-e", + "POSTGRES_PASSWORD=#{password}", + "-e", + "POSTGRES_DB=#{db}", + "-p", + "#{port}:5432" + ] + + cmd = if volume, do: cmd ++ ["-v", volume], else: cmd + cmd = if environment, do: cmd ++ ["-e", "POSTGRES_INITDB_ARGS=#{environment}"], else: cmd + cmd = cmd ++ [image] + + {_output, 0} = System.cmd("docker", cmd) + + wait_for_postgres(%{port: port, username: user, database: db}) + end + + defp wait_for_postgres(opts, max_attempts \\ 30), do: wait_for_postgres(opts, 1, max_attempts) + + defp wait_for_postgres(opts, attempt, max_attempts) when attempt != max_attempts do + case System.cmd("pg_isready", [ + "-h", + "localhost", + "-p", + to_string(opts[:port]), + "-U", + opts[:username], + "-d", + opts[:database] + ]) do + {_, 0} -> + :ok + + _ -> + Process.sleep(1000) + wait_for_postgres(opts, attempt + 1, max_attempts) + end + end + + defp wait_for_postgres(_opts, _attempt, max_attempts) do + raise "PostgreSQL failed to start within #{max_attempts} seconds" + end + + def stop_postgres_container(container_name) do + System.cmd("docker", ["stop", container_name]) + System.cmd("docker", ["rm", container_name]) + :ok + end + + def cleanup_containers(names) when is_list(names) do + Enum.each(names, fn name -> cleanup_containers(name) end) + end + + def cleanup_containers(name) when is_binary(name) do + System.cmd("docker", ["rm", "-f", name], stderr_to_stdout: true) + end +end diff --git a/test/support/tenants.ex b/test/support/tenants.ex new file mode 100644 index 00000000..c7f2243d --- /dev/null +++ b/test/support/tenants.ex @@ -0,0 +1,76 @@ +defmodule Supavisor.Support.Tenants do + @moduledoc """ + Integration test helpers for managing Supavisor tenants. + + This module provides utility functions for creating, terminating, and connecting to tenants + in integration tests. + """ + + import Phoenix.ConnTest + + alias SupavisorWeb.Router.Helpers, as: Routes + + @endpoint SupavisorWeb.Endpoint + + @postgres_port 7432 + @postgres_user "postgres" + @postgres_password "postgres" + @postgres_db "postgres" + @tenant_name "switching_test_tenant" + @db_host "localhost" + + def create_tenant(conn, opts) do + external_id = opts[:external_id] || @tenant_name + + tenant_attrs = %{ + db_host: opts[:hostname] || @db_host, + db_port: opts[:port] || @postgres_port, + db_database: opts[:database] || @postgres_db, + external_id: external_id, + ip_version: "auto", + enforce_ssl: false, + require_user: false, + auth_query: "SELECT rolname, rolpassword FROM pg_authid WHERE rolname=$1;", + users: [ + %{ + db_user: opts[:username] || @postgres_user, + db_password: opts[:password] || @postgres_password, + pool_size: 20, + mode_type: "transaction", + is_manager: true + } + ] + } + + conn = put(conn, Routes.tenant_path(conn, :update, external_id), tenant: tenant_attrs) + + case conn.status do + status when status in 200..201 -> + :ok + + _status -> + :ok + end + end + + def terminate_tenant(conn, external_id) do + _conn = get(conn, Routes.tenant_path(conn, :terminate, external_id)) + :ok + end + + def connection_opts(opts) do + proxy_port = Application.fetch_env!(:supavisor, :proxy_port_transaction) + external_id = opts[:external_id] || @tenant_name + + [ + hostname: opts[:hostname] || @db_host, + port: proxy_port, + database: opts[:database] || @postgres_db, + username: "#{opts[:username] || @postgres_user}.#{external_id}", + password: opts[:password] || @postgres_password, + # This is important as otherwise Postgrex may try to reconnect in case of errors. + # We want to avoid that, as it hides connection errors. + backoff: nil + ] + end +end