DpExchange.Coinbase.Fake (DpExchangeCoinbase v0.1.27)

Copy Markdown View Source

An in-process Coinbase, for a consumer's tier-1 tests and for the conformance suite's active-endpoint assertion.

It is not a mock. Nothing is stubbed, no expectation is recorded, and no call is verified. It is a real implementation of DpExchange.Core.Venue that answers from memory instead of from the network, and it runs the same conformance suite as the real adapter.

Two rules, from thirteen real bug reports about a comparable fake

Eleven of those thirteen were the fake diverging from the real client. Six were loud — the fake rejecting what the real thing accepts — which costs time and nothing else. Three were silent, and those are the ones this is designed against.

Less capable is allowed. Differently capable is not. Where this cannot answer, it returns an error. It never returns an empty success for something unsupported: the original answered {:ok, []} to an unsupported query and dropped clauses it could not parse, so callers got plausible wrong answers rather than failures.

It never rewrites a value the caller supplied. The original discarded the caller's timestamp and substituted the current clock, landing points written 900 seconds apart microseconds apart.

It models Coinbase's refusals, not just its successes

A fake where everything works proves only half the contract. This one refuses a symbol it does not carry with {:refused, :not_listed} — permanent, distinct from a transient error — and refuses a timeframe Coinbase does not serve, including 12h, which the shared vocabulary models and this venue does not.

It also enforces the 350-candle boundary as a refusal rather than a truncation, because that is what the venue does: 351 candles requested returns zero and an error, not the first 350.

Failure injection

Every function below with a real success path (not an unconditional Venue.not_supported()) checks DpExchange.Core.FakeInjection.next_outcome/1 or /2 first — a queued or always-set outcome from FakeInjection.queue_failures/2,3 or fail_always/2,3 short-circuits the fake's normal logic and is returned as-is. See that module for the full contract, and DpExchange.Robinhood.Fake for the same wiring applied to a venue whose Fake also gates credentials.

subscribe/2, unsubscribe/2 and update_symbols/2 are NOT wired — each takes a list of symbols in one call, and "this one symbol in the batch fails, the rest succeed" is a case whole-call injection cannot express. coverage/1 and subscribe_notices/1 are not wired either: both are local bookkeeping reads/writes that always succeed by construction, not a call standing in for one the real venue could refuse.

No credential-bypass mode here. Unlike DpExchange.Robinhood.Fake, this fake has no central credential check to bypass — most functions below never inspect credentials at all (an existing gap this wiring does not change; see dp-exchange-core issue #14). Adding a check here that the fake never had would be a behaviour change beyond this task's scope, not a wiring exercise.

Summary

Functions

coverage_by_kind(opts \\ [])

@spec coverage_by_kind(keyword()) :: %{
  required(DpExchange.Core.Capabilities.data_kind()) => %{
    required(String.t()) => atom()
  }
}

coverage/1, split by kind — see DpExchange.Coinbase.coverage_by_kind/1.

subscribe/2 above only ever pushes a DpExchange.Core.Types.Quote (it never synthesises a DpExchange.Core.Types.OrderBook), so every symbol this fake reports arrives under :quotes and :quotes alone — anything else would claim delivery this fake never actually sends, which is exactly the "differently capable" divergence this module's own moduledoc forbids. Less capable than the real venue here is honest; a fabricated :order_book entry would not be.