LatticeStripe.TestHelpers.TestClock (LatticeStripe v1.7.7)

Copy Markdown View Source

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 current frozen_time and idle
  • :advancing — an advance/4 call 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

  • update — Stripe Test Clocks are immutable after creation. To advance their time, use advance/4 (lands in Plan 13-04). To change metadata or name, delete and re-create.
  • search — Stripe's Test Clock API does not expose a /search endpoint. Use list/3 with client-side filtering if needed.

Summary

Types

t()

A Stripe Test Clock object.

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

t()

@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

advance(client, id, frozen_time, opts \\ [])

@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{} struct
  • id — Test Clock id ("clock_...")
  • frozen_time — Unix timestamp (integer) to advance TO
  • opts — Per-request overrides

Returns

  • {:ok, %TestClock{status: :advancing}} (usually) on success
  • {:error, %LatticeStripe.Error{}} on failure

advance!(client, id, frozen_time, opts \\ [])

@spec advance!(LatticeStripe.Client.t(), String.t(), integer(), keyword()) ::
  t() | no_return()

Bang variant of advance/4. Returns %TestClock{} on success, raises LatticeStripe.Error on failure.

advance_and_wait(client, id, frozen_time, opts \\ [])

@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)) where delay starts 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/3 error 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

advance_and_wait!(client, id, frozen_time, opts \\ [])

@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).

cleanup_tagged(client, opts \\ [])

@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 -- true to actually delete, false to 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}}

create(client, params \\ %{}, opts \\ [])

@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: :metadata is 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"
})

create!(c, p \\ %{}, o \\ [])

delete(client, id, opts \\ [])

@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.

delete!(c, id, o \\ [])

from_map(map)

@spec from_map(map()) :: t()

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.

list(client, params \\ %{}, opts \\ [])

@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.

list!(c, p \\ %{}, o \\ [])

retrieve(client, id, opts \\ [])

@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.

retrieve!(c, id, o \\ [])

stream!(client, params \\ %{}, opts \\ [])

@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).