Powered by AppSignal & Oban Pro

Bunnyx: Basics

livebooks/basics.livemd

Bunnyx: Basics

Section

Reference data, billing, account, API keys, and logging.

Setup

Mix.install([{:bunnyx, path: Path.join(__DIR__, "..")}])

api_key = System.fetch_env!("LB_BUNNY_API_KEY")
client = Bunnyx.new(api_key: api_key, receive_timeout: 60_000)

IO.puts("Client ready")
:ok

Country.list

{:ok, countries} = Bunnyx.Country.list(client)
true = is_list(countries)
true = length(countries) > 100
%{name: name, iso_code: code, is_eu: is_eu} = hd(countries)
true = is_binary(name)
true = is_binary(code)
true = is_boolean(is_eu)

IO.puts("✓ Country.list: #{length(countries)} countries (first: #{name}/#{code}, EU: #{is_eu})")

Region.list

{:ok, regions} = Bunnyx.Region.list(client)
true = is_list(regions)
true = length(regions) > 0
%{id: id, name: name, region_code: code, price_per_gigabyte: price} = hd(regions)
true = is_integer(id)
true = is_number(price)

IO.puts("✓ Region.list: #{length(regions)} regions (first: #{name}/#{code}, $#{price}/GB)")

Statistics.get

{:ok, stats} = Bunnyx.Statistics.get(client)
true = is_integer(stats.total_bandwidth_used)
true = is_integer(stats.total_requests_served)
true = is_number(stats.cache_hit_rate)

IO.puts("✓ Statistics.get: #{stats.total_bandwidth_used} bandwidth, #{stats.total_requests_served} requests, #{stats.cache_hit_rate} cache rate")

Statistics.get with date range

last_month = Date.utc_today() |> Date.add(-30) |> to_string()
today = Date.utc_today() |> to_string()

{:ok, stats} = Bunnyx.Statistics.get(client, date_from: last_month, date_to: today)
true = is_integer(stats.total_bandwidth_used)

IO.puts("✓ Statistics.get (date range): #{stats.total_bandwidth_used} bandwidth last 30 days")

Billing.details

{:ok, billing} = Bunnyx.Billing.details(client)
true = is_number(billing.balance)
true = is_boolean(billing.billing_enabled)
true = is_number(billing.this_month_charges)

IO.puts("✓ Billing.details: balance #{billing.balance}, charges #{billing.this_month_charges}")

Billing.summary

{:ok, summary} = Bunnyx.Billing.summary(client)
true = is_list(summary)

IO.puts("✓ Billing.summary: #{length(summary)} zone summaries")

Billing.pending_payments

{:ok, payments} = Bunnyx.Billing.pending_payments(client)
true = is_list(payments)

IO.puts("✓ Billing.pending_payments: #{length(payments)} pending")

Billing.summary_pdf + invoice_pdf

# PDF downloads need valid billing record IDs — we don't have any, so verify the error path
case Bunnyx.Billing.summary_pdf(client, 999_999) do
  {:ok, pdf} -> IO.puts("✓ Billing.summary_pdf: #{byte_size(pdf)} bytes")
  {:error, e} -> IO.puts("✓ Billing.summary_pdf: verified (#{e.status})")
end

case Bunnyx.Billing.invoice_pdf(client, 999_999) do
  {:ok, pdf} -> IO.puts("✓ Billing.invoice_pdf: #{byte_size(pdf)} bytes")
  {:error, e} -> IO.puts("✓ Billing.invoice_pdf: verified (#{e.status})")
end

ApiKey.list

{:ok, result} = Bunnyx.ApiKey.list(client)
keys = if is_list(result), do: result, else: result.items
true = is_list(keys)

IO.puts("✓ ApiKey.list: #{length(keys)} keys")

Account.affiliate

case Bunnyx.Account.affiliate(client) do
  {:ok, affiliate} ->
    true = is_map(affiliate)
    IO.puts("✓ Account.affiliate: returned")

  {:error, %Bunnyx.Error{message: msg}} ->
    IO.puts("✓ Account.affiliate: not enabled (#{msg})")
end

Account.audit_log

today = Date.utc_today()

{:ok, log} = Bunnyx.Account.audit_log(client, today)
true = is_map(log)

IO.puts("✓ Account.audit_log: returned for #{today}")

Account.search

{:ok, results} = Bunnyx.Account.search(client, "test")
true = is_map(results)

IO.puts("✓ Account.search: returned")

Logging (needs a pull zone)

# Create a temporary pull zone for logging tests
run_id = System.system_time(:second) |> rem(100_000) |> to_string()
pz_name = "bunnyx-int-log-#{run_id}"

{:ok, pz} = Bunnyx.PullZone.create(client, name: pz_name, origin_url: "https://example.com")
IO.puts("  Created temp pull zone #{pz.id} for logging tests")

today = Date.utc_today()

# CDN logs — may be empty for a new zone, that's fine
case Bunnyx.Logging.cdn(client, pz.id, today) do
  {:ok, logs} ->
    IO.puts("✓ Logging.cdn: #{byte_size(logs)} bytes of logs")

  {:error, %Bunnyx.Error{status: status}} ->
    IO.puts("✓ Logging.cdn: no logs available (status #{status}) — expected for new zone")
end

# Origin error logs
case Bunnyx.Logging.origin_errors(client, pz.id, today) do
  {:ok, logs} ->
    IO.puts("✓ Logging.origin_errors: returned")

  {:error, %Bunnyx.Error{status: status}} ->
    IO.puts("✓ Logging.origin_errors: no logs available (status #{status}) — expected for new zone")
end

# Cleanup
{:ok, nil} = Bunnyx.PullZone.delete(client, pz.id)
IO.puts("  Deleted temp pull zone")

Error handling

{:error, error} = Bunnyx.PullZone.get(client, 999_999_999)
%Bunnyx.Error{} = error
true = is_integer(error.status)
true = is_binary(error.message)
true = error.method == :get
true = error.path == "/pullzone/999999999"

IO.puts("✓ Error struct: status=#{error.status}, method=#{error.method}, path=#{error.path}")

ArgumentError on invalid keys

try do
  Bunnyx.PullZone.create(client, bad_key: "value")
  raise "Should have raised"
rescue
  e in ArgumentError ->
    true = e.message =~ "unknown key :bad_key"
    IO.puts("✓ ArgumentError: #{e.message}")
end

Inspect hides secrets

inspected = inspect(client)
false = inspected =~ api_key

IO.puts("✓ Inspect: API key hidden")

Telemetry

ref = make_ref()
pid = self()

:telemetry.attach(
  "int-test-#{inspect(ref)}",
  [:bunnyx, :request, :stop],
  fn _event, measurements, metadata, _ ->
    send(pid, {:telemetry, ref, measurements, metadata})
  end,
  nil
)

Bunnyx.Country.list(client)

receive do
  {:telemetry, ^ref, %{duration: duration}, %{method: :get, status: 200}} ->
    ms = System.convert_time_unit(duration, :native, :millisecond)
    IO.puts("✓ Telemetry: stop event received (#{ms}ms)")
after
  5000 -> raise "Telemetry event not received"
end

:telemetry.detach("int-test-#{inspect(ref)}")

Done

IO.puts("")
IO.puts("═══════════════════════════════════")
IO.puts("  Basics: all passed!")
IO.puts("  Country ✓  Region ✓  Statistics ✓")
IO.puts("  Billing ✓  ApiKey ✓  Account ✓")
IO.puts("  Logging ✓  Error ✓  Telemetry ✓")
IO.puts("═══════════════════════════════════")