From 7eb63a0176bfc48151597678c42f7ed16df18c84 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 9 May 2026 13:07:41 -0500 Subject: [PATCH] test: GaiiaWebhookController auth/signature error branches --- .../api/v1/gaiia_webhook_controller_test.exs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/test/towerops_web/controllers/api/v1/gaiia_webhook_controller_test.exs b/test/towerops_web/controllers/api/v1/gaiia_webhook_controller_test.exs index db351152..fafba339 100644 --- a/test/towerops_web/controllers/api/v1/gaiia_webhook_controller_test.exs +++ b/test/towerops_web/controllers/api/v1/gaiia_webhook_controller_test.exs @@ -161,5 +161,82 @@ defmodule ToweropsWeb.Api.V1.GaiiaWebhookControllerTest do assert json_response(conn, 400)["error"] =~ "Missing" end + + test "returns 404 when no Gaiia integration is configured for org", %{conn: conn} do + orphan_user = user_fixture() + orphan_org = organization_fixture(orphan_user.id) + # No integration created for orphan_org + + conn = post_webhook(conn, orphan_org.id, "test.event", %{}) + assert json_response(conn, 404)["error"] =~ "not found" + end + + test "returns 403 when integration exists but webhook_secret is missing", + %{conn: conn} do + user = user_fixture() + org = organization_fixture(user.id) + + {:ok, _integration} = + Integrations.create_integration(org.id, %{ + provider: "gaiia", + enabled: true, + credentials: %{"api_key" => "key"} + }) + + body = Jason.encode!(%{"event" => "test", "data" => %{}}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post(~p"/api/v1/webhooks/gaiia/#{org.id}", body) + + assert json_response(conn, 403)["error"] =~ "Webhook secret not configured" + end + + test "returns 401 when signature timestamp has expired", %{conn: conn, org: org} do + body = Jason.encode!(%{"event" => "test", "data" => %{}}) + # 1 hour ago — well past the 5-minute window + timestamp = DateTime.to_unix(DateTime.utc_now()) - 3600 + signed_payload = "#{timestamp}.#{body}" + + signature = + :hmac + |> :crypto.mac(:sha256, @webhook_secret, signed_payload) + |> Base.encode16(case: :lower) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("x-gaiia-webhook-signature", "t=#{timestamp},v1=#{signature}") + |> post(~p"/api/v1/webhooks/gaiia/#{org.id}", body) + + assert json_response(conn, 401)["error"] =~ "Invalid webhook signature" + end + + test "returns 401 when signature header is malformed (no v1)", + %{conn: conn, org: org} do + body = Jason.encode!(%{"event" => "test", "data" => %{}}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("x-gaiia-webhook-signature", "garbage-no-pair") + |> post(~p"/api/v1/webhooks/gaiia/#{org.id}", body) + + assert json_response(conn, 401)["error"] =~ "Invalid webhook signature" + end + + test "returns 401 when signature timestamp is unparseable", + %{conn: conn, org: org} do + body = Jason.encode!(%{"event" => "test", "data" => %{}}) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("x-gaiia-webhook-signature", "t=notanumber,v1=deadbeef") + |> post(~p"/api/v1/webhooks/gaiia/#{org.id}", body) + + assert json_response(conn, 401)["error"] =~ "Invalid webhook signature" + end end end