backendconformance

package
v0.17.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package backendconformance is a reusable test suite that a config backend adapter runs against its own backend to prove it behaves like a first-class layer.

It is the remote-source counterpart of config/conformance, which does the same job for file codecs. A codec plugs into shared file machinery, so that suite can construct everything from a byte sample. A backend owns its whole interaction with a remote system — reading, versioning, staging, committing, watching — so this suite cannot build one itself. The adapter supplies a factory instead (Suite.NewBackend), plus a Control that stands in for another client of the same system: it changes the backing store out of band and re-opens a backend over it.

The trap this exists to make unmissable is the same one config/conformance guards for files, wearing a remote costume: the conflict fingerprint — here a version — must be captured at Load, not at Prepare. A backend that fetches a fresh version inside Prepare and compares Verify against that compares the intruder's data with itself, so every stale write is accepted and conflict detection silently never fires. The conflict_detected subtest catches it.

An adapter writes one test:

func TestConformance(t *testing.T) {
	backendconformance.Run(t, backendconformance.Suite{
		NewBackend: func(t *testing.T, seed map[string]any) (config.Backend, backendconformance.Control) {
			remote := newFakeRemote(seed) // nil seed => absent source
			return newConsulBackend(remote, "app/"), &control{remote}
		},
		Seed:     map[string]any{"server": map[string]any{"port": 9090}},
		Defines:  map[string]string{"server.port": "9090"},
		WriteKey: "server.port", WriteValue: "8080",
	})
}

It uses the standard library testing package and nothing else — deliberately no testify — so an adapter that runs it takes on no assertion-library dependency it would otherwise avoid.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(t *testing.T, s Suite)

Run executes the whole suite against s, one named subtest per contract, so a failing adapter sees exactly which one it breaks.

Every backend is checked for per-key merge and precedence with a layer of another format, tolerance of an absent source, and provenance naming the backend. A config.WritableBackend additionally has its write round-trip and — the trap the suite exists for — its refusal of a change that landed between load and commit with config.ErrConflict checked; a read-only backend instead has its layer confirmed skipped by write routing — or, when it is sensitive (a secrets backend), the routed-beneath write confirmed refused with config.ErrSensitiveLeak. A writable backend also has a set of rendering and path-addressing hazards — control bytes, a bidi character, a numeric key, and gjson path metacharacters — confirmed to either round-trip exactly or fail closed with config.ErrBackendUnsafe or config.ErrBackendParse. A config.WatchableBackend has a foreign change confirmed to reach observers.

Types

type Control

type Control interface {
	// Mutate changes the backing store as another client would: it moves the
	// version the backend's conflict check compares against, and — for a
	// watchable backend — emits the backend's change signal. It must change at
	// least one value visible in the merged config, so a reload is not coalesced
	// away as a no-op.
	Mutate(t *testing.T)

	// Reopen returns a fresh backend over the same backing store its NewBackend
	// call created, so the suite can prove a committed write actually landed
	// there rather than only in the Store's in-memory snapshot.
	Reopen(t *testing.T) config.Backend
}

Control stands in for another client of the same backing store. It is how the suite simulates a foreign change — for the conflict and watch assertions — and how it re-reads the store to prove a committed write reached it.

type Suite

type Suite struct {
	// NewBackend builds a fresh backend over a fresh backing store seeded with
	// the given nested values, plus a [Control] over that same store. A nil seed
	// means an absent source: the backend's Load must report it not there
	// (fs.ErrNotExist) rather than erroring. Each call must be independent — two
	// calls must not share a backing store — except through the [Control], whose
	// Reopen returns a backend over the store its NewBackend call created.
	NewBackend func(t *testing.T, seed map[string]any) (config.Backend, Control)

	// Seed is the nested values the backend is seeded with, and Defines is what
	// they read back through a View. Both are required, with at least one key.
	// Seed carries native types ({"server": {"port": 9090}}); Defines is the
	// string each reads back (GetString), so the suite asserts decode, merge and
	// provenance without knowing the system.
	Seed    map[string]any
	Defines map[string]string

	// WriteKey and WriteValue are a key the backend can set and the value it is
	// set to — required only when NewBackend returns a [config.WritableBackend].
	// The value must read back after the write.
	WriteKey   string
	WriteValue string

	// PinOnlyTargets declares that the backend's layers can receive a write only
	// when a change NAMES them, and are never chosen by routing on their own.
	//
	// Writable normally implies routable, and for every backend that owns its
	// own storage it does. A composed store is the exception: its layers belong
	// to the store it wraps, and letting routing choose one would mean an
	// ordinary edit of an inherited key rewriting the shared configuration it
	// was inherited from. Promotion has to be named to happen.
	//
	// With this set, the write cases pin their target rather than relying on
	// routing to find it, which is the only way such a backend can be written to
	// at all.
	PinOnlyTargets bool

	// BoundedKeySpace declares that the backend accepts writes only to keys it
	// was configured with, rather than to any key routed at it.
	//
	// Almost every backend takes arbitrary keys: a file, a Consul prefix or an
	// object store will hold whatever it is given. Some cannot. A keychain has
	// no way to enumerate itself, so an adapter over one is given an explicit
	// map of config path to keychain account, and a key outside that map has no
	// account to be written to — inventing one would put a secret somewhere no
	// read would ever look for it.
	//
	// Setting this skips the hostile-KEY cases, which invent key names the
	// backend cannot have been configured with. The hostile-VALUE cases still
	// run against WriteKey, because a bounded key space says nothing about what
	// values a backend must survive.
	//
	// Leave it false unless the backend genuinely refuses unconfigured keys. It
	// removes coverage, and a backend that merely finds a key inconvenient
	// should fail closed instead.
	BoundedKeySpace bool

	// NewUnreachable builds a backend whose connection cannot be established,
	// plus a heal func that makes it establishable — the same backend, the same
	// value, now able to connect.
	//
	// It exists because a backend that resolves its own connection has three
	// obligations no other subtest reaches: that it can still say what it is
	// before it has one, that failing to get one is an error rather than a panic,
	// and that the failure is not remembered. The last is the one that bites: a
	// backend memoising its connection with sync.OnceValues caches the first
	// error forever, so a credential chain that was briefly unavailable at
	// startup wedges the process until it restarts.
	//
	// Optional. A backend that owns no connection — a file, an in-memory store,
	// one handed an already-built client — leaves it nil and the connection
	// subtests are skipped. Supplying it is how an adapter with a zero-conf rung
	// proves that rung behaves.
	//
	// The unreachable state must be reached without a network: point the backend
	// at a fake whose dial fails, not at a real endpoint expected to be down.
	NewUnreachable func(t *testing.T) (b config.Backend, heal func())
}

Suite describes a backend and the minimal knowledge the assertions need. The capability subtests — write and watch — run only when the backend the factory returns satisfies config.WritableBackend or config.WatchableBackend, so a read-only backend runs the read contract and no more.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL