2026-02-18 21:23:15 +00:00
|
|
|
defmodule BerrypodWeb.WebhookControllerTest do
|
|
|
|
|
use BerrypodWeb.ConnCase
|
2026-01-31 22:41:15 +00:00
|
|
|
|
2026-02-18 21:23:15 +00:00
|
|
|
import Berrypod.ProductsFixtures
|
2026-01-31 22:41:15 +00:00
|
|
|
|
|
|
|
|
@webhook_secret "test_webhook_secret_123"
|
|
|
|
|
|
|
|
|
|
setup do
|
|
|
|
|
_conn =
|
|
|
|
|
provider_connection_fixture(%{
|
|
|
|
|
provider_type: "printify",
|
|
|
|
|
config: %{"shop_id" => "12345", "webhook_secret" => @webhook_secret}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
:ok
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
describe "POST /webhooks/printify" do
|
|
|
|
|
test "returns 401 without signature", %{conn: conn} do
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> post(~p"/webhooks/printify", %{type: "product:updated"})
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 401)["error"] == "Invalid signature"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "returns 401 with invalid signature", %{conn: conn} do
|
|
|
|
|
body = Jason.encode!(%{type: "product:updated", resource: %{id: "123"}})
|
|
|
|
|
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pfy-signature", "sha256=invalid")
|
|
|
|
|
|> post(~p"/webhooks/printify", body)
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 401)["error"] == "Invalid signature"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "accepts valid signature and returns 200", %{conn: conn} do
|
|
|
|
|
body = Jason.encode!(%{type: "product:updated", resource: %{id: "123", shop_id: "12345"}})
|
|
|
|
|
signature = compute_signature(body, @webhook_secret)
|
|
|
|
|
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pfy-signature", "sha256=#{signature}")
|
|
|
|
|
|> post(~p"/webhooks/printify", body)
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 200)["status"] == "ok"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "handles product:updated event", %{conn: conn} do
|
|
|
|
|
body = Jason.encode!(%{type: "product:updated", resource: %{id: "123", shop_id: "12345"}})
|
|
|
|
|
signature = compute_signature(body, @webhook_secret)
|
|
|
|
|
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pfy-signature", "sha256=#{signature}")
|
|
|
|
|
|> post(~p"/webhooks/printify", body)
|
|
|
|
|
|
|
|
|
|
# Should return 200 even if job processing fails (inline mode tries to execute)
|
|
|
|
|
assert json_response(conn, 200)["status"] == "ok"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "handles product:deleted event", %{conn: conn} do
|
|
|
|
|
body = Jason.encode!(%{type: "product:deleted", resource: %{id: "456"}})
|
|
|
|
|
signature = compute_signature(body, @webhook_secret)
|
|
|
|
|
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pfy-signature", "sha256=#{signature}")
|
|
|
|
|
|> post(~p"/webhooks/printify", body)
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 200)["status"] == "ok"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "returns 401 when no webhook secret configured", %{conn: conn} do
|
|
|
|
|
# Remove the provider connection to simulate no secret
|
2026-02-18 21:23:15 +00:00
|
|
|
Berrypod.Repo.delete_all(Berrypod.Products.ProviderConnection)
|
2026-01-31 22:41:15 +00:00
|
|
|
|
|
|
|
|
body = Jason.encode!(%{type: "product:updated", resource: %{id: "123"}})
|
|
|
|
|
signature = compute_signature(body, @webhook_secret)
|
|
|
|
|
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pfy-signature", "sha256=#{signature}")
|
|
|
|
|
|> post(~p"/webhooks/printify", body)
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 401)["error"] == "Invalid signature"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp compute_signature(body, secret) do
|
|
|
|
|
:crypto.mac(:hmac, :sha256, secret, body)
|
|
|
|
|
|> Base.encode16(case: :lower)
|
|
|
|
|
end
|
2026-02-15 09:32:14 +00:00
|
|
|
|
|
|
|
|
describe "POST /webhooks/printful" do
|
|
|
|
|
@printful_secret "printful_test_secret_456"
|
|
|
|
|
|
|
|
|
|
setup do
|
|
|
|
|
_conn =
|
|
|
|
|
provider_connection_fixture(%{
|
|
|
|
|
provider_type: "printful",
|
|
|
|
|
config: %{"store_id" => "99999", "webhook_secret" => @printful_secret}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
:ok
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "returns 401 without token", %{conn: conn} do
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> post(~p"/webhooks/printful", %{type: "product_updated"})
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 401)["error"] == "Invalid token"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "returns 401 with wrong token", %{conn: conn} do
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pf-webhook-token", "wrong_token")
|
|
|
|
|
|> post(~p"/webhooks/printful", %{type: "product_updated"})
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 401)["error"] == "Invalid token"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "accepts valid token via header", %{conn: conn} do
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pf-webhook-token", @printful_secret)
|
|
|
|
|
|> post(~p"/webhooks/printful", %{type: "product_updated", data: %{}})
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 200)["status"] == "ok"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "accepts valid token via query param", %{conn: conn} do
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> post(~p"/webhooks/printful?token=#{@printful_secret}", %{
|
|
|
|
|
type: "product_updated",
|
|
|
|
|
data: %{}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 200)["status"] == "ok"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "handles unknown event gracefully", %{conn: conn} do
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pf-webhook-token", @printful_secret)
|
|
|
|
|
|> post(~p"/webhooks/printful", %{type: "unknown_event", data: %{}})
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 200)["status"] == "ok"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
test "returns 401 when no webhook secret configured", %{conn: conn} do
|
2026-02-18 21:23:15 +00:00
|
|
|
Berrypod.Repo.delete_all(Berrypod.Products.ProviderConnection)
|
2026-02-15 09:32:14 +00:00
|
|
|
|
|
|
|
|
conn =
|
|
|
|
|
conn
|
|
|
|
|
|> put_req_header("content-type", "application/json")
|
|
|
|
|
|> put_req_header("x-pf-webhook-token", @printful_secret)
|
|
|
|
|
|> post(~p"/webhooks/printful", %{type: "product_updated"})
|
|
|
|
|
|
|
|
|
|
assert json_response(conn, 401)["error"] == "Invalid token"
|
|
|
|
|
end
|
|
|
|
|
end
|
2026-01-31 22:41:15 +00:00
|
|
|
end
|