Operations on Stripe Test Clock objects.
Test Clocks let you simulate the passage of time in Stripe test mode —
useful for exercising subscription renewals, invoice cycles, and billing
lifecycle events without waiting real-world time. This module is the
low-level SDK wrapper over POST /v1/test_helpers/test_clocks and sibling
endpoints; for an ergonomic ExUnit helper built on top of it, see
LatticeStripe.Testing.TestClock.
Account limit
Stripe enforces a hard limit of 100 test clocks per account. If you
hit the limit, create/2 returns an error. The
LatticeStripe.Testing.TestClock user-facing helper registers every
created clock with a per-test ExUnit owner that cleans up on test exit,
and mix lattice_stripe.test_clock.cleanup backstops SIGKILL/crash
scenarios.
Metadata support (A-13g)
Verified against the Stripe OpenAPI spec (spec3.sdk.json) and
stripe/stripe-mock:latest on 2026-04-11: POST /v1/test_helpers/test_clocks
does NOT accept a metadata parameter, and the test_helpers.test_clock
object schema has no metadata field. The request body schema exposes
only expand, frozen_time, and name; the object schema exposes
created, deletes_after, frozen_time, id, livemode, name,
object, status, and status_details.
Consequence: this struct intentionally omits :metadata.
LatticeStripe.Testing.TestClock (Plan 13-05) falls back to
Owner-only tracking plus an age-based Mix task for cleanup rather than
tagging clocks with a metadata marker.
Status values
:ready— clock is at its currentfrozen_timeand idle:advancing— anadvance/4call is in progress server-side:internal_failure— a server-side advancement failed; the clock is unusable and should be deleted
Unknown server-returned status strings are passed through as raw strings
(String.t()) for forward compatibility; tests should pattern-match
against the known atoms or use to_string/1 for display.
Deletion cascades
Deleting a test clock cascades: every Customer attached to the clock is deleted, every Subscription attached is canceled. Do not attach production-critical fixtures to a clock you intend to delete.
Typical usage
frozen_time = System.system_time(:second)
{:ok, clock} = LatticeStripe.TestHelpers.TestClock.create(client, %{frozen_time: frozen_time})
{:ok, ready} =
LatticeStripe.TestHelpers.TestClock.advance_and_wait(
client,
clock.id,
frozen_time + 86_400 * 30
)
assert ready.status == :ready
{:ok, _} = LatticeStripe.TestHelpers.TestClock.delete(client, clock.id)For a high-level ExUnit experience (automatic cleanup, setup callbacks,
customer linkage), use LatticeStripe.Testing.TestClock instead.
Operations not supported by the Stripe API
Summary
Functions
Advances a Test Clock to a new frozen_time.
Bang variant of advance/4. Returns %TestClock{} on success, raises
LatticeStripe.Error on failure.
Advances a Test Clock and polls until it reaches status: :ready.
Bang variant of advance_and_wait/4. Returns %TestClock{} on success,
raises LatticeStripe.Error on failure (timeout or internal_failure).
Lists and optionally deletes test clocks older than a threshold.
Creates a new Stripe Test Clock.
Deletes a Test Clock by id. DELETE /v1/test_helpers/test_clocks/:id.
Converts a decoded Stripe API map to a %TestClock{} struct.
Lists Test Clocks with optional filters. GET /v1/test_helpers/test_clocks.
Retrieves a Test Clock by id. GET /v1/test_helpers/test_clocks/:id.
Streams Test Clocks lazily via cursor pagination.
Types
@type t() :: %LatticeStripe.TestHelpers.TestClock{ created: integer() | nil, deleted: boolean(), deletes_after: integer() | nil, extra: map(), frozen_time: integer() | nil, id: String.t() | nil, livemode: boolean() | nil, name: String.t() | nil, object: String.t(), status: :ready | :advancing | :internal_failure | String.t() | nil, status_details: map() | nil }
A Stripe Test Clock object.
See the Stripe Test Clock API for field definitions.
Functions
@spec advance(LatticeStripe.Client.t(), String.t(), integer(), keyword()) :: {:ok, t()} | {:error, LatticeStripe.Error.t()}
Advances a Test Clock to a new frozen_time.
Sends POST /v1/test_helpers/test_clocks/:id/advance. The returned
clock will typically have status: :advancing — the server processes
the advancement asynchronously. Use advance_and_wait/4 (or the bang
variant) if you need to block until the clock reaches :ready.
Stripe enforces a maximum advancement of roughly two billing intervals of the shortest attached subscription. Advancing further in a single call returns a 400 error; advance in chunks instead.
Parameters
client— A%LatticeStripe.Client{}structid— Test Clock id ("clock_...")frozen_time— Unix timestamp (integer) to advance TOopts— Per-request overrides
Returns
{:ok, %TestClock{status: :advancing}}(usually) on success{:error, %LatticeStripe.Error{}}on failure
Bang variant of advance/4. Returns %TestClock{} on success, raises
LatticeStripe.Error on failure.
@spec advance_and_wait(LatticeStripe.Client.t(), String.t(), integer(), keyword()) :: {:ok, t()} | {:error, LatticeStripe.Error.t()}
Advances a Test Clock and polls until it reaches status: :ready.
This is the differentiating helper for Stripe test-clock workflows: you
almost never want to call advance/4 directly in a test, because the
clock returns status: :advancing and you have to poll for completion
yourself. advance_and_wait/4 does the advance + the polling + the
terminal-failure detection + a well-behaved timeout, returning either
a ready clock or a typed %LatticeStripe.Error{}.
Polling strategy
- First poll has zero delay — catches already-ready clocks and stripe-mock's instant fixture without waiting 500ms.
- Exponential backoff with full jitter, floored at 500ms. Subsequent
sleeps are
max(500, :rand.uniform(delay))wheredelaystarts at 500ms, multiplies by 1.5 each iteration, and caps at 5000ms. Stripe's docs warn about tight-loop rate limits on test clocks; the 500ms floor is non-negotiable. - Monotonic deadline. The timeout uses
System.monotonic_time/1, not system time — NTP adjustments during long test runs do not cause premature timeouts. - Default timeout: 60 seconds. Override via
opts[:timeout](milliseconds).
Errors
- Timeout — returns
{:error, %LatticeStripe.Error{type: :test_clock_timeout, raw_body: %{"clock_id" => _, "last_status" => _, "attempts" => _, "elapsed_ms" => _}}} - Internal failure — returns
{:error, %LatticeStripe.Error{type: :test_clock_failed, raw_body: %{"clock_id" => _, "last_status" => "internal_failure", "attempts" => _}}}— Stripe entered a terminal failure state, retrying will not help. - HTTP failure during poll — the underlying
retrieve/3error propagates unchanged.
Telemetry
Emits [:lattice_stripe, :test_clock, :advance_and_wait, :start] and
[..., :stop] via :telemetry.span/3. Stop metadata includes
%{clock_id:, status:, attempts:, outcome: :ok | :error}. Gated by
client.telemetry_enabled.
Options
:timeout— total deadline in ms (default 60_000):initial_interval— first non-zero sleep in ms (default 500, clamped to 500 floor):max_interval— maximum sleep in ms (default 5_000):multiplier— per-iteration growth factor (default 1.5)
Example
{:ok, ready} =
LatticeStripe.TestHelpers.TestClock.advance_and_wait(
client,
clock.id,
System.system_time(:second) + 86_400 * 30
)
assert ready.status == :ready
@spec advance_and_wait!(LatticeStripe.Client.t(), String.t(), integer(), keyword()) :: t() | no_return()
Bang variant of advance_and_wait/4. Returns %TestClock{} on success,
raises LatticeStripe.Error on failure (timeout or internal_failure).
@spec cleanup_tagged( LatticeStripe.Client.t(), keyword() ) :: {:ok, term()}
Lists and optionally deletes test clocks older than a threshold.
This is the shared deletion core used by both
the Owner GenServer's cleanup callback (per-test) and
mix lattice_stripe.test_clock.cleanup (backstop). See those callers
for the user-facing entry points.
Metadata limitation (A-13g)
Stripe's Test Clock API does not support metadata, so this
function cannot filter by a LatticeStripe-specific marker. It filters
by age only. This means the Mix task cannot distinguish
LatticeStripe-managed clocks from user-created ones. The primary
cleanup path (Owner + on_exit) is unaffected.
Options
:older_than_ms-- only consider clocks older than N milliseconds (default:3_600_000= 1 hour):delete--trueto actually delete,falseto return candidates only (default:false):name_prefix-- optional string prefix filter on clock name (e.g.,"lattice_stripe_test")
Returns
delete: false--{:ok, [%TestClock{}, ...]}delete: true--{:ok, %{deleted: n, failed: n, total_matched: n}}
@spec create(LatticeStripe.Client.t(), map(), keyword()) :: {:ok, t()} | {:error, LatticeStripe.Error.t()}
Creates a new Stripe Test Clock.
Sends POST /v1/test_helpers/test_clocks with the given params and returns
the new clock as a typed t().
Parameters
client— A%LatticeStripe.Client{}struct.params— Map of Stripe API params. Required::frozen_time(integer unix timestamp). Optional::name(string). Note::metadatais NOT accepted by the Stripe Test Clock API (see "Metadata support" above).opts— Per-request overrides (e.g.,:idempotency_key).
Example
{:ok, clock} = LatticeStripe.TestHelpers.TestClock.create(client, %{
frozen_time: System.system_time(:second),
name: "renewal-test"
})
@spec delete(LatticeStripe.Client.t(), String.t(), keyword()) :: {:ok, t()} | {:error, LatticeStripe.Error.t()}
Deletes a Test Clock by id. DELETE /v1/test_helpers/test_clocks/:id.
This cascades: every Customer attached to the clock is deleted, every Subscription canceled. See module docs.
Converts a decoded Stripe API map to a %TestClock{} struct.
Maps all known Stripe test clock fields. Any unrecognized fields are
collected into the extra map so no data is silently lost.
Per D-03, the status field is atomized via a whitelist: "ready" →
:ready, "advancing" → :advancing, "internal_failure" →
:internal_failure. Unknown values pass through as raw strings for
forward compatibility with future Stripe enum additions.
@spec list(LatticeStripe.Client.t(), map(), keyword()) :: {:ok, LatticeStripe.Response.t()} | {:error, LatticeStripe.Error.t()}
Lists Test Clocks with optional filters. GET /v1/test_helpers/test_clocks.
@spec retrieve(LatticeStripe.Client.t(), String.t(), keyword()) :: {:ok, t()} | {:error, LatticeStripe.Error.t()}
Retrieves a Test Clock by id. GET /v1/test_helpers/test_clocks/:id.
@spec stream!(LatticeStripe.Client.t(), map(), keyword()) :: Enumerable.t()
Streams Test Clocks lazily via cursor pagination.
Returns a Stream that yields %TestClock{} items. Matches the
Phase 12 resource pattern (LatticeStripe.List.stream!/2 + Stream.map).