AuroraMeter.StorageCase (Aurora Meter v1.0.0)

View Source

The conformance suite every AuroraMeter.Storage adapter should pass.

Aurora Meter ships one adapter, AuroraMeter.Storage.Ecto. Writing another one is supported, and this is how you find out whether yours is correct before it is asked to hold money. use it inside an ExUnit case that already knows how to reach your storage:

defmodule MyApp.RiakStorageTest do
  use ExUnit.Case, async: false
  use AuroraMeter.StorageCase, adapter: MyApp.RiakStorage
end

It installs your adapter as config :aurora_meter, :storage for the duration of each test and restores whatever was there before, then drives it through AuroraMeter.Storage's own dispatchers rather than calling your module directly: what a host observes is what is asserted.

Options

  • :adapter (required) the module under test.
  • :checkout {module, function} called with no arguments in setup, for an adapter that needs a connection checked out first. The Ecto adapter under the SQL sandbox does; an in-memory one will not.
  • :tenant_prefix a string the generated tenant keys start with, so your own cleanup can find them. Default "storage_case".

What it asserts

An adapter that declares :durable_events must record, deduplicate by identity, refuse a changed payload under an identity it already holds, keep its totals arithmetic exact, and not project a duplicate a second time. An adapter that declares nothing must answer {:error, {:unsupported, _}} from every durable dispatcher. Both pass this suite: declining the work is a supported answer, and the difference between declining it and faking it is the difference between a caller that can handle the situation and one that cannot.

Each assertion lives in a public function of this module and the generated test blocks are one line each, so a failure names the function you can read rather than a line inside a macro expansion.

It does not assert anything about counters or history: those callbacks predate this suite and the existing adapter tests cover them. It does assert AuroraMeter.Storage.list_subscriptions/2, which does not predate it: a keyset page that repeats or skips a row is a subscription billed twice or not at all, and that is not something an adapter author should have to discover in production.

Summary

Functions

The projection generation reads currently resolve to.

The capability vocabulary an adapter may draw from.

The projected total for one generation, %{quantity: 0, events: 0} when the row is absent, without going through the active-generation indirection.

Installs the adapter for one test and returns the context the assertions take.

Puts the projection state back to one active generation with nothing being built, for a suite that announced one.

Functions

active_generation()

@spec active_generation() :: non_neg_integer()

The projection generation reads currently resolve to.

The suite restores it after the generation test, because it is one row for the whole installation.

capabilities()

@spec capabilities() :: [AuroraMeter.Storage.capability()]

The capability vocabulary an adapter may draw from.

correction(context, event_id, original_event_id, quantity)

@spec correction(map(), String.t(), String.t(), pos_integer() | :remaining) ::
  AuroraMeter.Storage.correction_entry()

One AuroraMeter.Storage.correction_entry/0 for the suite.

There is no payload_hash here on purpose: a correction's canonical tuple carries the original's feature, occurrence instant and dimensions, so only an adapter that has read the original can compute it. An adapter that invents its own definition of "the same correction" fails the duplicate and conflict assertions above.

entry(context, event_id, quantity)

One AuroraMeter.Storage.event_entry/0 for the suite, already canonical.

The payload_hash is computed with AuroraMeter.Events.Canonical, which is what the facade does, so an adapter cannot pass by inventing its own definition of "the same payload".

generation_total(ctx, generation)

@spec generation_total(map(), integer()) :: %{quantity: integer(), events: integer()}

The projected total for one generation, %{quantity: 0, events: 0} when the row is absent, without going through the active-generation indirection.

install!(adapter, checkout, prefix)

@spec install!(module(), {module(), atom()} | nil, String.t()) :: map()

Installs the adapter for one test and returns the context the assertions take.

Restores the previous :storage configuration in on_exit, including deleting the key when it was absent.

restore_projection!(active)

@spec restore_projection!(non_neg_integer()) :: :ok

Puts the projection state back to one active generation with nothing being built, for a suite that announced one.