From 64e9edec5a648c6522a52a934e487e40251084da Mon Sep 17 00:00:00 2001 From: njms Date: Fri, 9 Jun 2023 10:03:11 -0700 Subject: [PATCH] Implement tests for all token actions --- lib/hostas/denizen.ex | 1 + lib/hostas/token.ex | 11 +- .../controllers/auth/token_controller.ex | 8 +- lib/hostas_web/router.ex | 4 +- .../auth/token_controller_test.exs | 125 +++++++++++++++++- 5 files changed, 134 insertions(+), 15 deletions(-) diff --git a/lib/hostas/denizen.ex b/lib/hostas/denizen.ex index b0c9bdc..ba19cf3 100644 --- a/lib/hostas/denizen.ex +++ b/lib/hostas/denizen.ex @@ -27,5 +27,6 @@ defmodule Hostas.Denizen do %{password_hash: hash} = Bcrypt.add_hash(password) change(changeset, password: hash) end + defp hash_password(changeset), do: changeset end diff --git a/lib/hostas/token.ex b/lib/hostas/token.ex index 10bf1c8..307f558 100644 --- a/lib/hostas/token.ex +++ b/lib/hostas/token.ex @@ -29,16 +29,19 @@ defmodule Hostas.Token do end @doc """ - Creates a token + Creates a token for denizen with the id `denizen_id` that expires in + `duration` days. """ - def new(denizen_id) do + def new(denizen_id, duration \\ @duration_days) do # Create a random token token = Base.encode64(:crypto.strong_rand_bytes(256)) # Calculate the time of expiry {:ok, time_now} = DateTime.now("Etc/UTC") - expiry = DateTime.add(time_now, @duration_days, :day) - |> DateTime.truncate(:second) + + expiry = + DateTime.add(time_now, duration, :day) + |> DateTime.truncate(:second) # Register the token Repo.insert(changeset(%Token{}, %{denizen_id: denizen_id, token: token, expires: expiry})) diff --git a/lib/hostas_web/controllers/auth/token_controller.ex b/lib/hostas_web/controllers/auth/token_controller.ex index 7593d99..29734d2 100644 --- a/lib/hostas_web/controllers/auth/token_controller.ex +++ b/lib/hostas_web/controllers/auth/token_controller.ex @@ -11,9 +11,11 @@ defmodule HostasWeb.Auth.TokenController do provides the correct password """ def create(conn, %{"handle" => handle, "password" => given_password}) do - case Repo.one(from d in Denizen, - where: d.handle == ^handle, - select: %{id: d.id, password: d.password}) do + case Repo.one( + from d in Denizen, + where: d.handle == ^handle, + select: %{id: d.id, password: d.password} + ) do nil -> conn |> put_status(404) diff --git a/lib/hostas_web/router.ex b/lib/hostas_web/router.ex index fee567f..9fea092 100644 --- a/lib/hostas_web/router.ex +++ b/lib/hostas_web/router.ex @@ -26,8 +26,8 @@ defmodule HostasWeb.Router do # create, verify, renew, revoke post "/auth/token", Auth.TokenController, :create get "/auth/token", Auth.TokenController, :verify - delete "/auth/token", Auth.TokenController, :revoke - get "/auth/token/renew", Auth.TokenController, :renew + delete "/auth/token/:id", Auth.TokenController, :revoke + get "/auth/token/:id/renew", Auth.TokenController, :renew end # Enable LiveDashboard and Swoosh mailbox preview in development diff --git a/test/hostas_web/controllers/auth/token_controller_test.exs b/test/hostas_web/controllers/auth/token_controller_test.exs index 7400b2c..be0fecb 100644 --- a/test/hostas_web/controllers/auth/token_controller_test.exs +++ b/test/hostas_web/controllers/auth/token_controller_test.exs @@ -2,19 +2,23 @@ defmodule HostasWeb.Auth.TokenControllerTest do use HostasWeb.ConnCase # For testing with Ecto + import Ecto.Query, only: [from: 2] alias Hostas.Repo alias Hostas.Denizen + alias Hostas.Token - @denizen_data %{handle: "testuser", name: "Test User", password: "password"} - - defp create_denizen, do: Repo.insert!(Denizen.changeset(%Denizen{}, @denizen_data)) + defp create_denizen(handle \\ "denizen") do + Repo.insert!( + Denizen.changeset(%Denizen{}, %{handle: handle, name: "Test Denizen", password: "password"}) + ) + end describe "token create" do test "creates a token", %{conn: conn} do create_denizen() - conn = post(conn, ~p"/hostapi/auth/token", %{handle: "testuser", password: "password"}) + conn = post(conn, ~p"/hostapi/auth/token", %{handle: "denizen", password: "password"}) assert Map.has_key?(json_response(conn, 201), "token") assert Map.has_key?(json_response(conn, 201), "expires") end @@ -22,12 +26,12 @@ defmodule HostasWeb.Auth.TokenControllerTest do test "fails due to password mismatch", %{conn: conn} do create_denizen() - conn = post(conn, ~p"/hostapi/auth/token", %{handle: "testuser", password: "incorrect"}) + conn = post(conn, ~p"/hostapi/auth/token", %{handle: "denizen", password: "incorrect"}) assert json_response(conn, 401)["error"] == "Password mismatch" end test "fails due to non-existant denizen", %{conn: conn} do - conn = post(conn, ~p"/hostapi/auth/token", %{handle: "testuser", password: "password"}) + conn = post(conn, ~p"/hostapi/auth/token", %{handle: "denizen", password: "password"}) assert json_response(conn, 404)["error"] == "No user with handle testuser" end @@ -36,4 +40,113 @@ defmodule HostasWeb.Auth.TokenControllerTest do assert json_response(conn, 422)["error"] == "Missing required parameters" end end + + describe "token verification" do + test "succeeds", %{conn: conn} do + {:ok, struct} = Token.new(create_denizen().id) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct.token}") + |> get(~p"/hostapi/auth/token") + + assert Map.has_key?(json_response(conn, 200), "expires") + end + + test "fails because of expiry", %{conn: conn} do + {:ok, struct} = Token.new(create_denizen().id, -10) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct.token}") + |> get(~p"/hostapi/auth/token") + + assert json_response(conn, 401)["error"] == "Token expired" + end + + test "fails because of unrecognized credentials", %{conn: conn} do + conn = + conn + |> put_req_header("authorization", "Bearer unknown_credential") + |> get(~p"/hostapi/auth/token") + + assert json_response(conn, 200)["error"] == "Token expired" + end + end + + describe "token revocation" do + test "succeeds", %{conn: conn} do + {:ok, struct} = Token.new(create_denizen().id) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct.token}") + |> delete(~p"/hostapi/auth/token/#{struct.id}") + + assert json_response(conn, 200) + end + + test "fails because it's someone else's token", %{conn: conn} do + {:ok, struct1} = Token.new(create_denizen("denizen1").id) + {:ok, struct2} = Token.new(create_denizen("denizen2").id) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct1.token}") + |> delete(~p"/hostapi/auth/token/#{struct2.id}") + + assert json_response(conn, 404)["error"] == "Token not found" + end + + test "fails because the token doesn't exist", %{conn: conn} do + {:ok, struct} = Token.new(create_denizen().id) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct.token}") + |> delete(~p"/hostapi/auth/token/#{struct.id + 1}") + + assert json_response(conn, 404)["error"] == "Token not found" + end + end + + describe "token renewal" do + test "succeeds", %{conn: conn} do + {:ok, struct} = Token.new(create_denizen().id) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct.token}") + |> get(~p"/hostapi/auth/token/${struct.id}/renew") + + assert Map.has_key?(json_response(conn, 200), "token") + assert Map.has_key?(json_response(conn, 200), "expires") + assert not Repo.exists?(from t in Token, where: t.id == ^struct.id) + end + + test "fails because it's not the user's token", %{conn: conn} do + {:ok, struct1} = Token.new(create_denizen("denizen1").id) + {:ok, struct2} = Token.new(create_denizen("denizen2").id) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct1.token}") + |> get(~p"/hostapi/auth/token/#{struct2.id}/renew") + + assert json_response(conn, 404)["error"] == "Token not found" + assert Repo.exists?(from t in Token, where: t.id == ^struct2.id) + end + + test "fails because it doesn't exist", %{conn: conn} do + {:ok, struct} = Token.new(create_denizen().id) + + conn = + conn + |> put_req_header("authorization", "Bearer #{struct.token}") + |> get(~p"/hostapi/auth/token/#{struct.id + 1}/renew") + + assert json_response(conn, 404)["error"] == "Token not found" + assert Repo.exists?(from t in Token, where: t.id == ^struct.id) + end + end end