berrypod/test/berrypod/clients/printful_http_test.exs
jamey a45e85ef4c
All checks were successful
deploy / deploy (push) Successful in 1m10s
add Printful client test coverage with Req.Test stubs
Wire up Req.Test plug for the Printful HTTP client so tests can stub
responses. Adds HTTP-level tests for the client, provider integration
tests, and mockup enricher tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:20:49 +00:00

337 lines
10 KiB
Elixir

defmodule Berrypod.Clients.PrintfulHttpTest do
@moduledoc """
Tests the Printful HTTP client against Req.Test stubs.
Exercises URL construction, auth headers, response unwrapping, and error handling.
"""
use ExUnit.Case, async: true
alias Berrypod.Clients.Printful
setup do
Process.put(:printful_api_key, "test_token_abc")
Req.Test.stub(Printful, &route/1)
:ok
end
# =============================================================================
# Response unwrapping
# =============================================================================
describe "v1 response unwrapping" do
test "unwraps result key from v1 responses" do
assert {:ok, [%{"id" => 456}]} = Printful.list_sync_products()
end
test "get_sync_product returns nested data" do
assert {:ok, data} = Printful.get_sync_product(456)
assert data["sync_product"]["name"] == "Test T-Shirt"
end
end
describe "v2 response unwrapping" do
test "unwraps data key from v2 responses" do
assert {:ok, stores} = Printful.get_stores()
assert [%{"id" => 123, "name" => "Test Store"}] = stores
end
test "get_order returns v2 data" do
assert {:ok, order} = Printful.get_order(12345)
assert order["status"] == "draft"
end
end
# =============================================================================
# Error handling
# =============================================================================
describe "error handling" do
test "returns error tuple on 4xx" do
Req.Test.stub(Printful, fn conn ->
Req.Test.json(conn |> Plug.Conn.put_status(404), %{"error" => "Not found"})
end)
assert {:error, {404, %{"error" => "Not found"}}} = Printful.get("/v2/orders/999")
end
test "returns error tuple on 5xx" do
Req.Test.stub(Printful, fn conn ->
Req.Test.json(conn |> Plug.Conn.put_status(500), %{"error" => "Internal error"})
end)
assert {:error, {500, _}} = Printful.get("/v2/stores")
end
test "post returns error on 422" do
Req.Test.stub(Printful, fn conn ->
Req.Test.json(
conn |> Plug.Conn.put_status(422),
%{"error" => "Invalid address"}
)
end)
assert {:error, {422, %{"error" => "Invalid address"}}} =
Printful.create_order(%{test: true})
end
end
# =============================================================================
# Auth headers
# =============================================================================
describe "auth headers" do
test "includes Bearer token" do
Req.Test.stub(Printful, fn conn ->
[auth] = Plug.Conn.get_req_header(conn, "authorization")
assert auth == "Bearer test_token_abc"
Req.Test.json(conn, %{"data" => []})
end)
assert {:ok, _} = Printful.get_stores()
end
test "includes X-PF-Store-Id when set" do
Process.put(:printful_store_id, 99999)
Req.Test.stub(Printful, fn conn ->
[store_id] = Plug.Conn.get_req_header(conn, "x-pf-store-id")
assert store_id == "99999"
Req.Test.json(conn, %{"code" => 200, "result" => []})
end)
assert {:ok, _} = Printful.list_sync_products()
after
Process.delete(:printful_store_id)
end
test "omits X-PF-Store-Id when not set" do
Process.delete(:printful_store_id)
Req.Test.stub(Printful, fn conn ->
assert Plug.Conn.get_req_header(conn, "x-pf-store-id") == []
Req.Test.json(conn, %{"data" => []})
end)
assert {:ok, _} = Printful.get_stores()
end
end
# =============================================================================
# Specific endpoints
# =============================================================================
describe "get_stores/0" do
test "calls GET /v2/stores" do
Req.Test.stub(Printful, fn conn ->
assert conn.method == "GET"
assert conn.request_path == "/v2/stores"
Req.Test.json(conn, %{"data" => [%{"id" => 1}]})
end)
assert {:ok, [%{"id" => 1}]} = Printful.get_stores()
end
end
describe "get_store_id/0" do
test "returns first store id" do
assert {:ok, 123} = Printful.get_store_id()
end
test "returns error when no stores" do
Req.Test.stub(Printful, fn conn ->
Req.Test.json(conn, %{"data" => []})
end)
assert {:error, :no_stores} = Printful.get_store_id()
end
end
describe "list_sync_products/1" do
test "passes offset and limit as query params" do
Req.Test.stub(Printful, fn conn ->
assert conn.query_string == "offset=40&limit=20"
Req.Test.json(conn, %{"code" => 200, "result" => []})
end)
assert {:ok, []} = Printful.list_sync_products(offset: 40)
end
test "defaults to offset 0 and limit 20" do
Req.Test.stub(Printful, fn conn ->
assert conn.query_string == "offset=0&limit=20"
Req.Test.json(conn, %{"code" => 200, "result" => []})
end)
assert {:ok, []} = Printful.list_sync_products()
end
end
describe "calculate_shipping/2" do
test "sends recipient and items in POST body" do
Req.Test.stub(Printful, fn conn ->
assert conn.method == "POST"
assert conn.request_path == "/v2/shipping-rates"
{:ok, body, _conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
assert decoded["recipient"]["country_code"] == "GB"
assert length(decoded["order_items"]) == 1
Req.Test.json(conn, %{
"data" => [%{"shipping" => "STANDARD", "rate" => "4.99", "currency" => "USD"}]
})
end)
recipient = %{country_code: "GB"}
items = [%{source: "catalog", catalog_variant_id: 474, quantity: 1}]
assert {:ok, [rate]} = Printful.calculate_shipping(recipient, items)
assert rate["rate"] == "4.99"
end
end
describe "create_order/1 and confirm_order/1" do
test "create_order sends POST to /v2/orders" do
Req.Test.stub(Printful, fn conn ->
assert conn.method == "POST"
assert conn.request_path == "/v2/orders"
Req.Test.json(conn, %{"data" => %{"id" => 12345, "status" => "draft"}})
end)
assert {:ok, %{"id" => 12345}} = Printful.create_order(%{external_id: "SS-001"})
end
test "confirm_order sends POST to /v2/orders/:id/confirmation" do
Req.Test.stub(Printful, fn conn ->
assert conn.method == "POST"
assert conn.request_path == "/v2/orders/12345/confirmation"
Req.Test.json(conn, %{"data" => %{"id" => 12345, "status" => "pending"}})
end)
assert {:ok, %{"status" => "pending"}} = Printful.confirm_order(12345)
end
end
describe "get_order_shipments/1" do
test "calls GET /v2/orders/:id/shipments" do
Req.Test.stub(Printful, fn conn ->
assert conn.request_path == "/v2/orders/12345/shipments"
Req.Test.json(conn, %{
"data" => [%{"tracking_number" => "1Z999", "tracking_url" => "https://ups.com/1Z999"}]
})
end)
assert {:ok, [shipment]} = Printful.get_order_shipments(12345)
assert shipment["tracking_number"] == "1Z999"
end
end
describe "setup_webhooks/2" do
test "sends POST to /v2/webhooks with url and events" do
Req.Test.stub(Printful, fn conn ->
assert conn.method == "POST"
assert conn.request_path == "/v2/webhooks"
{:ok, body, _conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
assert decoded["url"] =~ "https://example.com"
assert is_list(decoded["events"])
Req.Test.json(conn, %{"data" => %{"url" => decoded["url"]}})
end)
assert {:ok, _} =
Printful.setup_webhooks("https://example.com/webhooks", ["package_shipped"])
end
end
describe "mockup generator" do
test "create_mockup_generator_task sends POST to /mockup-generator/create-task/:id" do
Req.Test.stub(Printful, fn conn ->
assert conn.method == "POST"
assert conn.request_path == "/mockup-generator/create-task/71"
Req.Test.json(conn, %{
"code" => 200,
"result" => %{"task_key" => "gt-abc123", "status" => "pending"}
})
end)
assert {:ok, %{"task_key" => "gt-abc123"}} =
Printful.create_mockup_generator_task(71, %{variant_ids: [4011]})
end
test "get_mockup_generator_task polls by task key" do
Req.Test.stub(Printful, fn conn ->
assert conn.query_string =~ "task_key=gt-abc123"
Req.Test.json(conn, %{
"code" => 200,
"result" => %{"status" => "completed", "mockups" => []}
})
end)
assert {:ok, %{"status" => "completed"}} =
Printful.get_mockup_generator_task("gt-abc123")
end
end
describe "delete/1" do
test "delete_webhooks calls DELETE /v2/webhooks" do
Req.Test.stub(Printful, fn conn ->
assert conn.method == "DELETE"
assert conn.request_path == "/v2/webhooks"
Plug.Conn.send_resp(conn, 204, "")
end)
assert {:ok, nil} = Printful.delete_webhooks()
end
end
# =============================================================================
# Default stub router — handles all standard routes for basic tests
# =============================================================================
defp route(%Plug.Conn{method: "GET", request_path: "/v2/stores"} = conn) do
Req.Test.json(conn, %{"data" => [%{"id" => 123, "name" => "Test Store"}]})
end
defp route(%Plug.Conn{method: "GET", request_path: "/v2/orders/" <> rest} = conn) do
cond do
String.contains?(rest, "/shipments") ->
Req.Test.json(conn, %{"data" => []})
true ->
Req.Test.json(conn, %{
"data" => %{"id" => 12345, "status" => "draft", "external_id" => "SS-001"}
})
end
end
defp route(%Plug.Conn{method: "GET", request_path: "/store/products/" <> _id} = conn) do
Req.Test.json(conn, %{
"code" => 200,
"result" => %{
"sync_product" => %{"id" => 456, "name" => "Test T-Shirt", "thumbnail_url" => nil},
"sync_variants" => []
}
})
end
defp route(%Plug.Conn{method: "GET", request_path: "/store/products"} = conn) do
Req.Test.json(conn, %{"code" => 200, "result" => [%{"id" => 456}]})
end
defp route(%Plug.Conn{method: "GET"} = conn) do
Req.Test.json(conn, %{"data" => %{}})
end
defp route(%Plug.Conn{method: "POST"} = conn) do
Req.Test.json(conn, %{"data" => %{}})
end
defp route(%Plug.Conn{method: "DELETE"} = conn) do
Plug.Conn.send_resp(conn, 204, "")
end
end