towerops/test/towerops_web/plugs/webhook_auth_test.exs
Graham McIntire c7df6a8569
Add CI-triggered mass agent update webhook
POST /api/v1/webhooks/agent-release triggers all connected agents
to self-update via their existing WebSocket channels. Authenticated
with a shared secret (AGENT_WEBHOOK_SECRET env var).

- Add ReleaseChecker.invalidate_cache/0 for fresh GitHub fetch
- Add Agents.list_updatable_agents/0 (enabled + seen < 10min)
- Add Agents.broadcast_mass_update/0 orchestration function
- Add WebhookAuth plug with timing-safe secret comparison
- Add AgentReleaseWebhookController with :webhook pipeline
- Configure AGENT_WEBHOOK_SECRET in dev/test/runtime configs
2026-02-10 13:40:32 -06:00

67 lines
1.9 KiB
Elixir

defmodule ToweropsWeb.Plugs.WebhookAuthTest do
use ToweropsWeb.ConnCase, async: true
alias ToweropsWeb.Plugs.WebhookAuth
describe "call/2 with valid authorization" do
test "passes through with valid Bearer token", %{conn: conn} do
conn =
conn
|> put_req_header("authorization", "Bearer test-webhook-secret")
|> WebhookAuth.call([])
refute conn.halted
end
end
describe "call/2 with missing authorization" do
test "returns 401 when Authorization header is missing", %{conn: conn} do
conn = WebhookAuth.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", "")
|> WebhookAuth.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" do
test "returns 401 when token does not match", %{conn: conn} do
conn =
conn
|> put_req_header("authorization", "Bearer wrong-secret")
|> WebhookAuth.call([])
assert conn.halted
assert conn.status == 401
assert json_response(conn, 401) == %{"error" => "Invalid webhook secret"}
end
test "returns 401 when using Basic auth instead of Bearer", %{conn: conn} do
conn =
conn
|> put_req_header("authorization", "Basic dGVzdDp0ZXN0")
|> WebhookAuth.call([])
assert conn.halted
assert conn.status == 401
assert json_response(conn, 401) == %{"error" => "Missing or invalid Authorization header"}
end
end
describe "init/1" do
test "returns options unchanged" do
assert WebhookAuth.init([]) == []
end
end
end