towerops/test/towerops_web/plugs/api_auth_test.exs
mayor 8ee39a2d36
Add comprehensive tests for API authentication and authorization
Implemented tests for critical security modules with significant coverage improvements:
- ApiAuth plug: 0% → 100% (12 tests)
- Permissions module: 0% → 85% (32 tests)
- Devices API controller: 0% → 95% (22 tests)

Enhanced test fixtures to support membership roles and device creation with proper organization scoping.
2026-02-05 16:39:01 -06:00

195 lines
5.5 KiB
Elixir

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