defmodule Pleroma.HTTP.BackoffTest do @backoff_cache :http_backoff_cache use Pleroma.DataCase, async: false alias Pleroma.HTTP.Backoff defp within_tolerance?(ttl, expected) do ttl > expected - 15 and ttl < expected + 15 end describe "get/3" do test "should return {:ok, env} when not rate limited" do Tesla.Mock.mock_global(fn %Tesla.Env{url: "https://akkoma.dev/api/v1/instance"} -> {:ok, %Tesla.Env{status: 200, body: "ok"}} end) assert {:ok, env} = Backoff.get("https://akkoma.dev/api/v1/instance") assert env.status == 200 end test "should return {:error, env} when rate limited" do # Shove a value into the cache to simulate a rate limit Cachex.put(@backoff_cache, "akkoma.dev", true) assert {:error, :ratelimit} = Backoff.get("https://akkoma.dev/api/v1/instance") end test "should insert a value into the cache when rate limited" do Tesla.Mock.mock_global(fn %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> {:ok, %Tesla.Env{status: 429, body: "Rate limited"}} end) assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") end test "should insert a value into the cache when rate limited with a 503 response" do Tesla.Mock.mock_global(fn %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> {:ok, %Tesla.Env{status: 503, body: "Rate limited"}} end) assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") end test "should parse the value of x-ratelimit-reset, if present" do ten_minutes_from_now = DateTime.utc_now() |> Timex.shift(minutes: 10) |> DateTime.to_iso8601() Tesla.Mock.mock_global(fn %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> {:ok, %Tesla.Env{ status: 429, body: "Rate limited", headers: [{"x-ratelimit-reset", ten_minutes_from_now}] }} end) assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") {:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev") assert within_tolerance?(ttl, 600) end test "should parse the value of retry-after when it's a timestamp" do ten_minutes_from_now = DateTime.utc_now() |> Timex.shift(minutes: 10) |> DateTime.to_iso8601() Tesla.Mock.mock_global(fn %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> {:ok, %Tesla.Env{ status: 429, body: "Rate limited", headers: [{"retry-after", ten_minutes_from_now}] }} end) assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") {:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev") assert within_tolerance?(ttl, 600) end test "should parse the value of retry-after when it's a number of seconds" do Tesla.Mock.mock_global(fn %Tesla.Env{url: "https://ratelimited.dev/api/v1/instance"} -> {:ok, %Tesla.Env{ status: 429, body: "Rate limited", headers: [{"retry-after", "600"}] }} end) assert {:error, :ratelimit} = Backoff.get("https://ratelimited.dev/api/v1/instance") assert {:ok, true} = Cachex.get(@backoff_cache, "ratelimited.dev") # assert that the value is 10 minutes from now {:ok, ttl} = Cachex.ttl(@backoff_cache, "ratelimited.dev") assert within_tolerance?(ttl, 600) end end end