defmodule ToweropsWeb.Plugs.ApiAuthTest do use ToweropsWeb.ConnCase, async: true import Towerops.AccountsFixtures import Towerops.OrganizationsFixtures alias Towerops.ApiTokens alias ToweropsWeb.Plugs.ApiAuth describe "call/2 with valid authorization" do setup do user = user_fixture() organization = organization_fixture(user.id) {:ok, {_token, raw_token}} = ApiTokens.create_api_token(%{ organization_id: organization.id, user_id: user.id, name: "Test Token" }) %{ user: user, organization: organization, raw_token: raw_token } end test "authenticates successfully with valid Bearer token", %{ conn: conn, raw_token: raw_token, organization: organization, user: user } do conn = conn |> put_req_header("authorization", "Bearer #{raw_token}") |> ApiAuth.call([]) refute conn.halted assert conn.assigns.current_organization_id == organization.id assert conn.assigns.current_user.id == user.id assert conn.assigns.api_authenticated == true end test "authenticates successfully with token without user_id", %{ conn: conn, organization: organization } do {:ok, {_token, raw_token}} = ApiTokens.create_api_token(%{ organization_id: organization.id, name: "Token Without User" }) conn = conn |> put_req_header("authorization", "Bearer #{raw_token}") |> ApiAuth.call([]) refute conn.halted assert conn.assigns.current_organization_id == organization.id assert is_nil(conn.assigns.current_user) assert conn.assigns.api_authenticated == true end end describe "call/2 with missing authorization" do test "returns 401 when Authorization header is missing", %{conn: conn} do conn = ApiAuth.call(conn, []) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Missing or invalid Authorization header"} end test "returns 401 when Authorization header is empty", %{conn: conn} do conn = conn |> put_req_header("authorization", "") |> ApiAuth.call([]) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Missing or invalid Authorization header"} end end describe "call/2 with invalid authorization format" do test "returns 401 when Authorization header doesn't start with 'Bearer '", %{conn: conn} do conn = conn |> put_req_header("authorization", "Basic dGVzdDp0ZXN0") |> ApiAuth.call([]) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Missing or invalid Authorization header"} end test "returns 401 when Bearer token is malformed", %{conn: conn} do conn = conn |> put_req_header("authorization", "Bearertoken123") |> ApiAuth.call([]) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Missing or invalid Authorization header"} end test "returns 401 when authorization has only 'Bearer' with no token", %{conn: conn} do conn = conn |> put_req_header("authorization", "Bearer ") |> ApiAuth.call([]) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Invalid or expired API token"} end end describe "call/2 with invalid token" do test "returns 401 for non-existent token", %{conn: conn} do conn = conn |> put_req_header("authorization", "Bearer towerops_invalid_token_12345") |> ApiAuth.call([]) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Invalid or expired API token"} end test "returns 401 for randomly generated token", %{conn: conn} do random_token = "towerops_" <> Base.encode64(:crypto.strong_rand_bytes(32)) conn = conn |> put_req_header("authorization", "Bearer #{random_token}") |> ApiAuth.call([]) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Invalid or expired API token"} end end describe "call/2 with expired token" do setup do user = user_fixture() organization = organization_fixture(user.id) # Create an expired token (1 day in the past) expires_at = DateTime.add(DateTime.utc_now(), -1, :day) {:ok, {_token, raw_token}} = ApiTokens.create_api_token(%{ organization_id: organization.id, user_id: user.id, name: "Expired Token", expires_at: expires_at }) %{ user: user, organization: organization, expired_token: raw_token } end test "returns 401 for expired token", %{conn: conn, expired_token: expired_token} do conn = conn |> put_req_header("authorization", "Bearer #{expired_token}") |> ApiAuth.call([]) assert conn.halted assert conn.status == 401 assert json_response(conn, 401) == %{"error" => "Invalid or expired API token"} end end describe "init/1" do test "returns options unchanged" do opts = [some: :option] assert ApiAuth.init(opts) == opts end test "returns empty list unchanged" do assert ApiAuth.init([]) == [] end end end