From 61d7d8dcbe3ff3ecdb5feb7d27f6e59642a7a532 Mon Sep 17 00:00:00 2001 From: Joel C Date: Mon, 25 May 2020 14:03:46 +0200 Subject: [PATCH] Replace custom admin dashboard with phoenix_live_dashboard Fixes #142 - Still on the same path: `/admin` - There are lasso specific metrics under the Metrics tab - Upgrade phoenix_live_view to latest versionc --- README.md | 2 +- lib/lasso.ex | 131 +++++++++--------- lib/lasso/application.ex | 7 +- lib/lasso_web/controllers/admin_controller.ex | 17 --- lib/lasso_web/router.ex | 11 +- lib/lasso_web/telemetry.ex | 65 +++++++++ mix.exs | 4 + mix.lock | 5 +- test/lasso/lasso_test.exs | 81 ++++++++--- .../controllers/admin_controller_test.exs | 33 ----- .../controllers/page_controller_test.exs | 30 ++++ 11 files changed, 248 insertions(+), 138 deletions(-) delete mode 100644 lib/lasso_web/controllers/admin_controller.ex create mode 100644 lib/lasso_web/telemetry.ex delete mode 100644 test/lasso_web/controllers/admin_controller_test.exs diff --git a/README.md b/README.md index 77b46b6..2f6d471 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ that I'd like to improve. ### Admin area -There is a minimal admin page where you can see some stats about current lassos on [/admin](http://localhost:4000/admin). This page is protected by basic auth, the credentials can be configured in `config.exs`. By default the password is read from the environment variable `ADMIN_PASSWORD`. +There's a Phoenix Live Dashboard available on [/admin](http://localhost:4000/admin). This page is protected by basic auth, the credentials can be configured in `config.exs`. By default the password is read from the environment variable `ADMIN_PASSWORD`. ## Building a release diff --git a/lib/lasso.ex b/lib/lasso.ex index 26fbb17..3dc78fe 100644 --- a/lib/lasso.ex +++ b/lib/lasso.ex @@ -7,11 +7,6 @@ defmodule Lasso do @cache_id Application.get_env(:lasso, Lasso)[:cache_name] @request_limit Application.get_env(:lasso, Lasso)[:max_requests_per_lasso] - @active_lassos_key :active_lassos - @total_lassos_key :total_lassos - - @admin_events Application.get_env(:lasso, Lasso)[:admin_events_topic] - @topic inspect(__MODULE__) def subscribe(uuid) do @@ -21,47 +16,40 @@ defmodule Lasso do @doc """ Create a new lasso that we need to keep track of """ - def create(uuid, opts \\ [update_stats: true]) do - result = ConCache.put(@cache_id, uuid, []) - if opts[:update_stats], do: update_stats() - result - end - - defp update_stats() do - ConCache.update(@cache_id, @active_lassos_key, fn val -> - case val do - nil -> {:ok, 1} - val -> {:ok, val + 1} - end - end) - - ConCache.update(@cache_id, @total_lassos_key, fn val -> - case val do - nil -> {:ok, 1} - val -> {:ok, val + 1} - end - end) - - notify_stats() + def create(uuid, _opts \\ [update_stats: true]) do + with_telemetry_events( + fn -> ConCache.put(@cache_id, uuid, []) end, + {:action, %{action: :create}} + ) end @doc """ Get all the requests for a lasso """ def get(uuid) do - case ConCache.get(@cache_id, uuid) do - nil -> {:error, :no_such_key, uuid} - value -> {:ok, value} - end + with_telemetry_events( + fn -> + case ConCache.get(@cache_id, uuid) do + nil -> {:error, :no_such_key, uuid} + value -> {:ok, value} + end + end, + {:action, %{action: :get}} + ) end @doc """ Append a request to a lasso """ def add(uuid, request) do - with :ok <- update(uuid, request) do - notify_subscribers(uuid, {:request, request}) - end + with_telemetry_events( + fn -> + with :ok <- update(uuid, request) do + notify_subscribers(uuid, {:request, request}) + end + end, + {:request, %{request: request}} + ) end defp notify_subscribers(uuid, data) do @@ -72,18 +60,28 @@ defmodule Lasso do Delete a lasso """ def delete(uuid) do - ConCache.delete(@cache_id, uuid) - notify_subscribers(uuid, :delete) + with_telemetry_events( + fn -> + ConCache.delete(@cache_id, uuid) + notify_subscribers(uuid, :delete) + end, + {:action, %{action: :delete}} + ) end @doc """ Clear all requests for a lasso """ def clear(uuid) do - with {:ok, _} <- get(uuid), - :ok <- create(uuid, update_stats: false) do - notify_subscribers(uuid, :clear) - end + with_telemetry_events( + fn -> + with {:ok, _} <- get(uuid), + :ok <- create(uuid, update_stats: false) do + notify_subscribers(uuid, :clear) + end + end, + {:action, %{action: :clear}} + ) end @doc """ @@ -92,29 +90,8 @@ defmodule Lasso do def cache_callback({:update, _cache_pid, _key, _value}), do: :ok def cache_callback({:delete, _cache_pid, _key}) do - ConCache.update(@cache_id, @active_lassos_key, fn val -> - case val do - nil -> {:ok, 0} - val -> {:ok, max(0, val - 1)} - end - end) - - notify_stats() - end - - @doc """ - Get statistics about lassos - """ - def stats() do - active_lassos = ConCache.get(@cache_id, @active_lassos_key) || 0 - total_lassos = ConCache.get(@cache_id, @total_lassos_key) || 0 - {:ok, %{"active_lassos" => active_lassos, "total_lassos" => total_lassos}} - end - - defp notify_stats() do - with {:ok, stats} <- stats() do - notify_subscribers(@admin_events, {:stats, stats}) - end + emit_stop({:action, %{action: :delete}}, 1) + :ok end defp update(uuid, request) do @@ -129,4 +106,32 @@ defmodule Lasso do end end) end + + defp with_telemetry_events(my_fn, path_meta) do + start_time = emit_start(path_meta) + result = my_fn.() + duration = System.monotonic_time() - start_time + emit_stop(path_meta, duration) + result + end + + defp emit_start({path, meta}) do + start_time_mono = System.monotonic_time() + + :telemetry.execute( + [:lasso, path, :start], + %{system_time: System.system_time()}, + meta + ) + + start_time_mono + end + + defp emit_stop({path, meta}, duration) do + :telemetry.execute( + [:lasso, path, :stop], + %{duration: duration}, + meta + ) + end end diff --git a/lib/lasso/application.ex b/lib/lasso/application.ex index 429ce08..2929c9a 100644 --- a/lib/lasso/application.ex +++ b/lib/lasso/application.ex @@ -1,12 +1,9 @@ defmodule Lasso.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications @moduledoc false use Application def start(_type, _args) do - # List all child processes to be supervised children = [ {ConCache, [ @@ -17,10 +14,8 @@ defmodule Lasso.Application do callback: &Lasso.cache_callback/1 ]}, {Phoenix.PubSub, name: Lasso.PubSub}, - # Start the endpoint when the application starts + LassoWeb.Telemetry, LassoWeb.Endpoint - # Starts a worker by calling: Lasso.Worker.start_link(arg) - # {Lasso.Worker, arg}, ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/lasso_web/controllers/admin_controller.ex b/lib/lasso_web/controllers/admin_controller.ex deleted file mode 100644 index c793dcd..0000000 --- a/lib/lasso_web/controllers/admin_controller.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule LassoWeb.AdminController do - use LassoWeb, :controller - alias Phoenix.LiveView.Controller - require Logger - - plug BasicAuth, use_config: {:basic_auth, :admin_area} - - def index(conn, _params) do - with {:ok, stats} <- Lasso.stats() do - Controller.live_render( - conn, - LassoWeb.AdminLiveView, - session: stats - ) - end - end -end diff --git a/lib/lasso_web/router.ex b/lib/lasso_web/router.ex index 0d9b44d..b1dd723 100644 --- a/lib/lasso_web/router.ex +++ b/lib/lasso_web/router.ex @@ -1,6 +1,7 @@ defmodule LassoWeb.Router do use LassoWeb, :router import Phoenix.LiveView.Router + import Phoenix.LiveDashboard.Router pipeline :browser do plug :accepts, ["html"] @@ -18,11 +19,14 @@ defmodule LassoWeb.Router do plug :accepts, ["json"] end + pipeline :admins_only do + plug BasicAuth, use_config: {:basic_auth, :admin_area} + end + scope "/", LassoWeb do pipe_through :browser get "/", PageController, :index - get "/admin", AdminController, :index delete "/admin/lasso/:uuid", LassoViewController, :delete post "/admin/lasso/", LassoViewController, :new @@ -30,6 +34,11 @@ defmodule LassoWeb.Router do get "/lasso/:uuid/view", LassoViewController, :show end + scope "/admin" do + pipe_through [:browser, :admins_only] + live_dashboard "/", metrics: LassoWeb.Telemetry + end + # The `get` here gets flagged by Sobelow as a potential for "CSRF via Action Reuse" # It does not matter if a user can trigger the post request in this scenario. scope "/lasso", LassoWeb do diff --git a/lib/lasso_web/telemetry.ex b/lib/lasso_web/telemetry.ex new file mode 100644 index 0000000..6627d8a --- /dev/null +++ b/lib/lasso_web/telemetry.ex @@ -0,0 +1,65 @@ +defmodule LassoWeb.Telemetry do + @moduledoc """ + Metrics for the admin live dashboard + """ + + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + + # Lasso specifics + summary("lasso.action.stop.duration", + tags: [:action], + tag_values: &action_tag_value/1, + unit: {:native, :millisecond} + ), + counter("lasso.action.stop.duration", tags: [:action], tag_values: &action_tag_value/1), + counter("lasso.request.stop.duration", tags: [:method], tag_values: &request_method_value/1), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [] + end + + defp action_tag_value(metadata) do + Map.take(metadata, [:action]) + end + + defp request_method_value(metadata) do + Map.take(metadata.request, [:method]) + end +end diff --git a/mix.exs b/mix.exs index 961daba..e00c304 100644 --- a/mix.exs +++ b/mix.exs @@ -44,6 +44,10 @@ defmodule Lasso.MixProject do {:elixir_uuid, "~> 1.2"}, {:con_cache, "~> 0.13"}, {:basic_auth, "~> 2.2"}, + {:phoenix_live_dashboard, "~> 0.2"}, + {:telemetry_poller, "~> 0.4"}, + {:telemetry_metrics, "~> 0.4"}, + {:telemetry, "~> 0.4"}, {:credo, "~> 1.1", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.8", only: :dev, runtime: false}, {:floki, "~> 0.26", only: [:test], runtime: false} diff --git a/mix.lock b/mix.lock index 62bdf4d..d729a32 100644 --- a/mix.lock +++ b/mix.lock @@ -14,8 +14,9 @@ "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"}, "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.4", "3080e8a89bab3ec08d4dd9a6858dfa24af9334464aae78c83e58a2db37c6f983", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.12.0 or ~> 0.13.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1c89595ef60f1b76ac07705e73f001823af451491792a4b0d5b2b2a3789b0a00"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.12.1", "42f591c781edbf9fab921319076b7ac635d43aa23e6748d2644563326236d7e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.16 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "585321e98df1cd5943e370b9784e950a37ca073744eb534660c9048967c52ab6"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.13.0", "dec006b3da4ab164283d5bebe960724eb4d19cd0ed553e05fb99b260233e200f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "bd6f13b666fa9bfeca88b013db20414c693d5a5e6d19b1fc2602c282d626ed8e"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, "plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"}, "plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"}, @@ -23,4 +24,6 @@ "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, "sobelow": {:hex, :sobelow, "0.10.2", "00e91208046d3b434f9f08779fe0ca7c6d6595b7fa33b289e792dffa6dde8081", [:mix], [], "hexpm", "e30fc994330cf6f485c1c4f2fb7c4b2d403557d0e101c6e5329fd17a58e55a7e"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"}, + "telemetry_poller": {:hex, :telemetry_poller, "0.5.0", "4770888ef85599ead39c7f51d6b4b62306e602d96c69b2625d54dea3d9a5204b", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69e4e8e65b0ae077c9e14cd5f42c7cc486de0e07ac6e3409e6f0e52699a7872c"}, } diff --git a/test/lasso/lasso_test.exs b/test/lasso/lasso_test.exs index b03ca18..abbfe7c 100644 --- a/test/lasso/lasso_test.exs +++ b/test/lasso/lasso_test.exs @@ -5,6 +5,7 @@ defmodule Lasso.LassoTest do # Ensure the cache is cleared before each test Supervisor.terminate_child(Lasso.Supervisor, ConCache) Supervisor.restart_child(Lasso.Supervisor, ConCache) + :ok end @@ -74,32 +75,80 @@ defmodule Lasso.LassoTest do end end - describe "stats/0" do - test "returns active lassos" do + describe "telemetry events" do + setup :attach_telemetry_handlers + + test "on create" do Lasso.create("1") - Lasso.create("2") - Lasso.create("3") - Lasso.delete("2") - assert {:ok, %{"active_lassos" => 2}} = Lasso.stats() + assert_receive {:telemetry_event, [:lasso, :action, :start], %{system_time: _}, + %{action: :create}} + + assert_receive {:telemetry_event, [:lasso, :action, :stop], %{duration: _}, + %{action: :create}} end - test "returns total lassos" do + test "on get" do Lasso.create("1") - Lasso.create("2") - Lasso.create("3") - Lasso.delete("2") + Lasso.get("1") - assert {:ok, %{"total_lassos" => 3}} = Lasso.stats() + assert_receive {:telemetry_event, [:lasso, :action, :start], %{system_time: _}, + %{action: :get}} + + assert_receive {:telemetry_event, [:lasso, :action, :stop], %{duration: _}, %{action: :get}} end - test "is not affected by clear/1" do - lasso_id = "stats" + test "on delete" do + Lasso.create("1") + Lasso.delete("1") - Lasso.create(lasso_id) - Lasso.clear(lasso_id) + assert_receive {:telemetry_event, [:lasso, :action, :start], %{system_time: _}, + %{action: :delete}} + + assert_receive {:telemetry_event, [:lasso, :action, :stop], %{duration: _}, + %{action: :delete}} + end + + test "on clear" do + Lasso.create("1") + Lasso.clear("1") + + assert_receive {:telemetry_event, [:lasso, :action, :start], %{system_time: _}, + %{action: :clear}} - assert Lasso.stats() == {:ok, %{"total_lassos" => 1, "active_lassos" => 1}} + assert_receive {:telemetry_event, [:lasso, :action, :stop], %{duration: _}, + %{action: :clear}} end + + test "on request" do + request = %Lasso.Request{method: "GET"} + Lasso.create("1") + Lasso.add("1", request) + + assert_receive {:telemetry_event, [:lasso, :request, :start], %{system_time: _}, + %{request: request}} + + assert_receive {:telemetry_event, [:lasso, :request, :stop], %{duration: _}, + %{request: request}} + end + end + + defp attach_telemetry_handlers(%{test: test}) do + self = self() + + :ok = + :telemetry.attach_many( + "telementry-handler-#{test}", + [ + [:lasso, :action, :start], + [:lasso, :action, :stop], + [:lasso, :request, :start], + [:lasso, :request, :stop] + ], + fn name, measurements, metadata, _ -> + send(self, {:telemetry_event, name, measurements, metadata}) + end, + nil + ) end end diff --git a/test/lasso_web/controllers/admin_controller_test.exs b/test/lasso_web/controllers/admin_controller_test.exs deleted file mode 100644 index 5b8b822..0000000 --- a/test/lasso_web/controllers/admin_controller_test.exs +++ /dev/null @@ -1,33 +0,0 @@ -defmodule LassoWeb.AdminControllerTest do - use LassoWeb.ConnCase - - describe "GET /admin" do - test "returns 401 without credentials", %{conn: conn} do - conn = get(conn, "/admin") - assert response(conn, 401) =~ "Unauthorized" - end - - test "returns 401 with incorrect credentials", %{conn: conn} do - conn = - conn - |> using_basic_auth("root", "foo") - |> get("/admin") - - assert response(conn, 401) =~ "Unauthorized" - end - - test "returns 200 with correct credentials", %{conn: conn} do - conn = - conn - |> using_basic_auth("admin", "test") - |> get("/admin") - - assert response(conn, 200) =~ "Admin" - end - end - - defp using_basic_auth(conn, username, password) do - header_content = "Basic " <> Base.encode64("#{username}:#{password}") - put_req_header(conn, "authorization", header_content) - end -end diff --git a/test/lasso_web/controllers/page_controller_test.exs b/test/lasso_web/controllers/page_controller_test.exs index b9e09b6..a748cb5 100644 --- a/test/lasso_web/controllers/page_controller_test.exs +++ b/test/lasso_web/controllers/page_controller_test.exs @@ -5,4 +5,34 @@ defmodule LassoWeb.PageControllerTest do conn = get(conn, "/") assert html_response(conn, 200) =~ "Lasso" end + + describe "GET /admin" do + test "returns 401 without credentials", %{conn: conn} do + conn = get(conn, "/admin") + assert response(conn, 401) =~ "Unauthorized" + end + + test "returns 401 with incorrect credentials", %{conn: conn} do + conn = + conn + |> using_basic_auth("root", "foo") + |> get("/admin") + + assert response(conn, 401) =~ "Unauthorized" + end + + test "returns 200 with correct credentials", %{conn: conn} do + conn = + conn + |> using_basic_auth("admin", "test") + |> get("/admin") + + assert redirected_to(conn, 302) == "/admin/nonode%40nohost" + end + end + + defp using_basic_auth(conn, username, password) do + header_content = "Basic " <> Base.encode64("#{username}:#{password}") + put_req_header(conn, "authorization", header_content) + end end