All checks were successful
deploy / deploy (push) Successful in 1m10s
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>
337 lines
10 KiB
Elixir
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
|