testhelper

package
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package testhelper ships the fakes and assertions consumers of github.com/AtomiCloud/diene.go-auth-engine would otherwise rebuild in every test suite.

The auth engine is seam-heavy by design — a verification-key source, an identity provider, a token store, a deferred-login store, per-backend onboarding surfaces — and every one of them has to be faked before a consumer can test a single guarded handler. Rebuilding that scaffolding per repository is how subtly different fakes (one that forgets token expiry, one whose store is not atomic) end up proving different contracts in different services.

FakeIDP is the centrepiece: it signs REAL RS256 tokens with a generated key pair and publishes them through a real [VerificationKeys] implementation, so a consumer's validation path is exercised end to end without a network, a container, or a live tenant. Its scriptable failure hooks then let a consumer prove the unhappy paths — expired, wrong issuer, unknown key — that are otherwise unreachable in a test.

The assertion helpers depend only on the minimal TestingT interface, never on the concrete testing type, so they stay framework-free and are themselves black-box testable with a recording double.

Index

Examples

Constants

View Source
const (
	// DefaultIssuer is the fake IdP's issuer.
	DefaultIssuer = "https://api.lithium.alcohol.mew.cluster.atomi.cloud"
	// DefaultAudience is the fake IdP's access-token audience.
	DefaultAudience = "alcohol-zinc"
	// DefaultKeyID is the fake IdP's signing key id.
	DefaultKeyID = "fake-idp-key-1"
	// DefaultSubject is the fake IdP's default token subject.
	DefaultSubject = "user-1"
	// DefaultKeyBits is the generated signing-key size, matching what a real
	// tenant publishes.
	DefaultKeyBits = 2048
)

Fake IdP defaults. They are realistic rather than minimal so a consumer's fixtures look like the real thing: the issuer follows the per-platform identity host convention, and the audience is a resource-tree style backend name.

View Source
const HeaderKeyID = "kid"

HeaderKeyID is the JOSE header naming the signing key.

Variables

View Source
var ErrFakeProvider = errors.New("fake identity provider failure")

ErrFakeProvider is the error the fake raises when a test enqueues a failure without supplying one of its own.

View Source
var ErrLandscapeUnreachable = errors.New("fake pinger cannot reach landscape")

ErrLandscapeUnreachable is the error a fake pinger reports for a landscape a test marked unreachable without supplying an error of its own.

Functions

func AssertAuthProblem

func AssertAuthProblem(t TestingT, err error, id string) problem.Problem

AssertAuthProblem fails t unless err carries an auth-engine problem with the expected id.

Matching on the ID rather than the full type URI is deliberate: the URI embeds the consumer's own landscape, platform, service, and module, so a test that spelled it out would break the moment it ran in a different landscape.

Example
package main

import (
	"fmt"

	"github.com/AtomiCloud/diene.go-auth-engine/lib/authengine"
	"github.com/AtomiCloud/diene.go-auth-engine/testhelper"
)

func main() {
	idp, _ := testhelper.NewFakeIDP(testhelper.FakeIDPOptions{})
	guard, _ := idp.Guard()

	owner := authengine.NewClaimMapper().Map(authengine.Claims{authengine.ClaimSubject: "user-1"})
	other := "user-2"

	// Matching on the id keeps a test portable: the full type URI embeds the
	// consumer's own landscape, platform, service, and module.
	envelope, failure := testhelper.CheckAuthProblem(
		guard.Sub(owner, &other), authengine.ProblemOwnershipDenied,
	)
	fmt.Println(envelope.Status, failure == nil)
}
Output:
403 true

func AssertNoAuthProblem

func AssertNoAuthProblem(t TestingT, err error)

AssertNoAuthProblem fails t when err is non-nil, rendering the carried problem so the failure message names the auth decision that went wrong rather than just "unexpected error".

func AssertOwnershipDenied

func AssertOwnershipDenied(t TestingT, err error) problem.Problem

AssertOwnershipDenied fails t unless err is the ownership-denied problem, which is the single most repeated assertion in a consumer's guard tests.

func AssertPhase

func AssertPhase(t TestingT, states map[string]onboard.State, backend string, want onboard.Phase)

AssertPhase fails t unless the backend's onboarding state reached want.

func CheckAuthProblem

func CheckAuthProblem(err error, id string) (problem.Problem, error)

CheckAuthProblem recovers an auth-engine problem from a (T, error) result and verifies its id, returning a descriptive error rather than failing a test.

It is the half a consumer composes into its own assertions, and the half the meta tier drives directly.

func CheckPhase

func CheckPhase(states map[string]onboard.State, backend string, want onboard.Phase) error

CheckPhase verifies one backend's onboarding phase, returning a descriptive error rather than failing a test. A backend absent from the round is reported as such rather than compared against the zero phase, because "never ran" and "ran and failed" are different bugs.

func FixedNow

func FixedNow() time.Time

FixedNow returns the deterministic instant the fakes issue tokens at.

It is a real, plausible date rather than the Unix epoch so a token's claims read sensibly in a failure message, and it is fixed so a suite that crosses an expiry boundary does so by advancing the clock rather than by sleeping.

func ProblemID

func ProblemID(envelope problem.Problem) string

ProblemID returns the trailing id segment of a problem type URI, so a caller can match a problem without spelling out the consumer-specific URI.

func SampleErrorPortal

func SampleErrorPortal() problem.ErrorPortal

SampleErrorPortal returns a realistic, valid error portal for tests, so fixtures never hand-format a problem type URI.

Types

type ClaimCall

type ClaimCall struct {
	// Subject is the identity-provider user the claim was written on.
	Subject string
	// Name is the claim name.
	Name string
	// Value is the claim value.
	Value any
}

ClaimCall records one claim write-back.

type FakeBackend

type FakeBackend struct {
	// contains filtered or unexported fields
}

FakeBackend is a scriptable onboard.Backend.

Onboarding is a five-step sequence whose interesting cases are all failures at step three, four, or five, so this fake exists to make each of them reachable: a row that already exists (the first-sign-in race), a create that fails, a probe that fails. It records the registrations it received so a test can prove the raw tokens really travelled as data.

func NewFakeBackend

func NewFakeBackend(options FakeBackendOptions) *FakeBackend

NewFakeBackend creates a fake onboarding backend.

func (*FakeBackend) Configured

func (b *FakeBackend) Configured(_ context.Context, token authengine.AccessToken) (bool, error)

Configured implements onboard.Configurable. The fake always advertises the capability so a consumer can drive both settled outcomes from one type; a backend with no second step simply does not implement the interface.

func (*FakeBackend) Create

func (b *FakeBackend) Create(
	_ context.Context,
	token authengine.AccessToken,
	registration onboard.Registration,
) error

Create implements onboard.Backend with create-or-ok semantics: registering an already-registered caller succeeds rather than conflicting.

func (*FakeBackend) EnqueueConfiguredError

func (b *FakeBackend) EnqueueConfiguredError(err error)

EnqueueConfiguredError makes the next Configured call fail with err.

func (*FakeBackend) EnqueueCreateError

func (b *FakeBackend) EnqueueCreateError(err error)

EnqueueCreateError makes the next Create call fail with err.

func (*FakeBackend) EnqueueExistsError

func (b *FakeBackend) EnqueueExistsError(err error)

EnqueueExistsError makes the next Exists call fail with err.

func (*FakeBackend) Exists

func (b *FakeBackend) Exists(_ context.Context, token authengine.AccessToken) (bool, error)

Exists implements onboard.Backend.

func (*FakeBackend) Name

func (b *FakeBackend) Name() string

Name implements onboard.Backend.

func (*FakeBackend) Registrations

func (b *FakeBackend) Registrations() []onboard.Registration

Registrations returns the registrations the backend received.

func (*FakeBackend) Tokens

func (b *FakeBackend) Tokens() []authengine.AccessToken

Tokens returns the access tokens the backend was called with, so a test can prove each backend was called with its OWN per-resource token.

type FakeBackendOptions

type FakeBackendOptions struct {
	// Name is the backend's resource-tree name.
	Name string
	// Exists starts the backend with a user row already present, which is the
	// concurrent-first-sign-in race.
	Exists bool
	// NeedsConfiguration makes the backend report its app-specific onboarding step
	// as outstanding, so a round settles in onboard.PhaseNeedsOnboarding.
	NeedsConfiguration bool
}

FakeBackendOptions configures a FakeBackend.

type FakeIDP

type FakeIDP struct {
	// contains filtered or unexported fields
}

FakeIDP is an in-memory identity provider that signs real RS256 tokens.

It is not a mock of the JWT library: the tokens it mints are genuinely signed and genuinely verified, so a consumer's validator, key resolution, algorithm restriction, and claim mapping all run for real. What is faked is only the network and the tenant.

func NewFakeIDP

func NewFakeIDP(options FakeIDPOptions) (*FakeIDP, error)

NewFakeIDP creates a fake identity provider with a freshly generated signing key.

The key is 2048-bit because that is what a real tenant publishes; generating it per instance keeps suites independent, and it costs a few milliseconds once.

Example
package main

import (
	"context"
	"fmt"

	"github.com/AtomiCloud/diene.go-auth-engine/lib/authengine"
	"github.com/AtomiCloud/diene.go-auth-engine/testhelper"
)

func main() {
	idp, _ := testhelper.NewFakeIDP(testhelper.FakeIDPOptions{})
	validator, _ := idp.Validator()

	// The tokens are really signed and really verified: only the network and the
	// tenant are faked.
	token, _ := idp.MintAccessToken(testhelper.TokenRequest{Subject: "user-1", Roles: []string{"admin"}})
	principal, err := validator.Validate(context.Background(), token)
	fmt.Println(principal.Subject, principal.HasRole("admin"), err == nil)

	// Advancing the clock reaches the expiry branch without a sleep.
	idp.Advance(authengine.AccessTokenLifetime + authengine.DefaultClockSkew + 1)
	_, err = validator.Validate(context.Background(), token)
	fmt.Println(err != nil)
}
Output:
user-1 true true
true

func (*FakeIDP) Advance

func (f *FakeIDP) Advance(by time.Duration)

Advance moves the fake clock forward, which is how a test crosses a token's expiry boundary without sleeping.

func (*FakeIDP) Audience

func (f *FakeIDP) Audience() string

Audience returns the access-token audience this fake mints for.

func (*FakeIDP) Clock

func (f *FakeIDP) Clock() *mocks.InMemorySystem

Clock returns the fake's injectable clock seam.

func (*FakeIDP) Guard

func (f *FakeIDP) Guard() (authengine.Guard, error)

Guard returns an ownership guard sharing this fake's problem factory.

func (*FakeIDP) Issuer

func (f *FakeIDP) Issuer() string

Issuer returns the issuer this fake mints for.

func (*FakeIDP) Keys

Keys returns a authengine.VerificationKeys publishing this fake's public key.

func (*FakeIDP) MintAccessToken

func (f *FakeIDP) MintAccessToken(request TokenRequest) (string, error)

MintAccessToken signs an access token from request.

func (*FakeIDP) MintIDToken

func (f *FakeIDP) MintIDToken(request TokenRequest) (string, error)

MintIDToken signs an ID token from request.

An ID token is minted for the CLIENT rather than for a resource server, so its audience is deliberately different from the access token's — which is exactly the asymmetry a consumer's ID-token validation has to tolerate.

func (*FakeIDP) Problems

func (f *FakeIDP) Problems() *authengine.Problems

Problems returns the problem factory bound to this fake's error portal, so a consumer's own engine components share one portal with the fake.

func (*FakeIDP) Validator

func (f *FakeIDP) Validator() (authengine.Validator, error)

Validator returns a validator wired to this fake: its issuer, its audience, its key, and its clock.

type FakeIDPOptions

type FakeIDPOptions struct {
	// Issuer overrides [DefaultIssuer].
	Issuer string
	// Audience overrides [DefaultAudience].
	Audience string
	// KeyID overrides [DefaultKeyID].
	KeyID string
	// Now fixes the fake clock. Zero uses a fixed, deterministic instant.
	Now time.Time
	// Portal overrides the error portal problems are attributed to.
	Portal *problem.ErrorPortal
	// ExtraProblems registers a consumer's own problem types on the fake's factory,
	// so a suite asserts its domain problems and the engine.s through one registry.
	ExtraProblems []problem.Type
	// KeyBits overrides the generated signing-key size. Zero uses 2048, which is
	// what a real tenant publishes; a large suite may trade strength for speed.
	KeyBits int
}

FakeIDPOptions configures a FakeIDP. Every field is optional.

type FakePinger

type FakePinger struct {
	// contains filtered or unexported fields
}

FakePinger is a scriptable onboard.Pinger returning fixed latencies per landscape name.

func NewFakePinger

func NewFakePinger() *FakePinger

NewFakePinger creates a pinger with no known landscapes.

func (*FakePinger) Calls

func (p *FakePinger) Calls() []string

Calls returns the landscape names that were pinged.

func (*FakePinger) Ping

func (p *FakePinger) Ping(_ context.Context, landscape onboard.Landscape) (time.Duration, error)

Ping implements onboard.Pinger. A landscape with no configured latency and no configured error is treated as unreachable, so a test never accidentally picks a region it forgot to script.

func (*FakePinger) SetError

func (p *FakePinger) SetError(landscape string, err error)

SetError makes landscape fail to answer, which is how a test proves an unreachable region is skipped rather than chosen.

func (*FakePinger) SetLatency

func (p *FakePinger) SetLatency(landscape string, milliseconds int)

SetLatency makes landscape answer with the given latency in milliseconds.

type FakeProvider

type FakeProvider struct {
	// contains filtered or unexported fields
}

FakeProvider is a scriptable in-memory authengine.Provider.

It records every call and lets a test enqueue the next outcome per operation, which is what makes the awkward cases reachable: a refresh that fails once and then succeeds, a claim write-back that errors after the user row was created, a provider that returns a token with no rotation. Recording the calls is what proves the single-flight cache made exactly ONE mint under concurrency.

func NewFakeProvider

func NewFakeProvider(options FakeProviderOptions) *FakeProvider

NewFakeProvider creates a fake provider.

func (*FakeProvider) ClaimCalls

func (p *FakeProvider) ClaimCalls() []ClaimCall

ClaimCalls returns the recorded claim write-backs.

func (*FakeProvider) ClientCredentials

ClientCredentials implements authengine.Provider.

func (*FakeProvider) ClientCredentialsCalls

func (p *FakeProvider) ClientCredentialsCalls() []authengine.ClientCredentialsRequest

ClientCredentialsCalls returns the recorded machine-to-machine requests.

func (*FakeProvider) EnqueueClaimError

func (p *FakeProvider) EnqueueClaimError(err error)

EnqueueClaimError makes the next SetClaim call fail with err.

func (*FakeProvider) EnqueueClientCredentialsError

func (p *FakeProvider) EnqueueClientCredentialsError(err error)

EnqueueClientCredentialsError makes the next ClientCredentials call fail with err.

func (*FakeProvider) EnqueueOneTimeTokenError

func (p *FakeProvider) EnqueueOneTimeTokenError(err error)

EnqueueOneTimeTokenError makes the next MintOneTimeToken call fail with err.

func (*FakeProvider) EnqueueRefreshError

func (p *FakeProvider) EnqueueRefreshError(err error)

EnqueueRefreshError makes the next Refresh call fail with err.

func (*FakeProvider) EnqueueResourceTokenError

func (p *FakeProvider) EnqueueResourceTokenError(err error)

EnqueueResourceTokenError makes the next ResourceToken call fail with err.

func (*FakeProvider) MintOneTimeToken

MintOneTimeToken implements authengine.Provider.

func (*FakeProvider) Minted

func (p *FakeProvider) Minted() int

Minted returns how many tokens the provider has minted, which is the assertion a single-flight test makes.

func (*FakeProvider) OneTimeTokenCalls

func (p *FakeProvider) OneTimeTokenCalls() []authengine.OneTimeTokenRequest

OneTimeTokenCalls returns the recorded one-time-token requests.

func (*FakeProvider) Refresh

func (p *FakeProvider) Refresh(_ context.Context, refreshToken string) (authengine.Session, error)

Refresh implements authengine.Provider, rotating the refresh token unless the fake was built without rotation.

func (*FakeProvider) RefreshCalls

func (p *FakeProvider) RefreshCalls() []string

RefreshCalls returns the refresh tokens presented to Refresh.

func (*FakeProvider) ResourceToken

ResourceToken implements authengine.Provider.

func (*FakeProvider) ResourceTokenCalls

func (p *FakeProvider) ResourceTokenCalls() []authengine.ResourceTokenRequest

ResourceTokenCalls returns the recorded per-resource token requests.

func (*FakeProvider) SetClaim

func (p *FakeProvider) SetClaim(_ context.Context, subject string, name string, value any) error

SetClaim implements authengine.Provider.

type FakeProviderOptions

type FakeProviderOptions struct {
	// Now fixes the instant minted tokens are issued at. Zero uses [FixedNow].
	Now time.Time
	// NoRotation makes Refresh return no replacement refresh token, so a consumer
	// can prove it copes with a provider that does not rotate.
	NoRotation bool
}

FakeProviderOptions configures a FakeProvider.

type FakeRefresher

type FakeRefresher struct {
	// contains filtered or unexported fields
}

FakeRefresher is a scriptable onboard.TokenRefresher.

The queue models claim propagation: a test enqueues the principal each refresh should observe, so it can prove that a claim which never appears is reported as a stalled onboarding rather than as success.

func NewFakeRefresher

func NewFakeRefresher(principal authengine.Principal) *FakeRefresher

NewFakeRefresher creates a refresher that returns principal by default.

func (*FakeRefresher) Calls

func (r *FakeRefresher) Calls() []string

Calls returns the subjects Refresh was called for.

func (*FakeRefresher) Enqueue

func (r *FakeRefresher) Enqueue(principal authengine.Principal)

Enqueue makes the next Refresh call return principal.

func (*FakeRefresher) EnqueueError

func (r *FakeRefresher) EnqueueError(err error)

EnqueueError makes the next Refresh call fail with err.

func (*FakeRefresher) Refresh

func (r *FakeRefresher) Refresh(_ context.Context, subject string) (authengine.Principal, error)

Refresh implements onboard.TokenRefresher.

type MemoryDeferredStore

type MemoryDeferredStore struct {
	// contains filtered or unexported fields
}

MemoryDeferredStore is an in-memory deferred.Store with a genuinely atomic consume.

The atomicity is the whole point of the fake. A test that consumes concurrently must see exactly one first-redemption, and a store that read-then-wrote would let two callers both believe they won — which is the bug a replay-rejection test exists to catch.

func NewMemoryDeferredStore

func NewMemoryDeferredStore() *MemoryDeferredStore

NewMemoryDeferredStore creates an empty deferred-login store.

func (*MemoryDeferredStore) Consume

func (s *MemoryDeferredStore) Consume(_ context.Context, digest string) (deferred.Record, bool, error)

Consume implements deferred.Store, returning the record as it was before the call and marking it consumed under one lock.

func (*MemoryDeferredStore) Digests

func (s *MemoryDeferredStore) Digests() []string

Digests returns the stored nonce digests in sorted order.

func (*MemoryDeferredStore) EnqueueError

func (s *MemoryDeferredStore) EnqueueError(err error)

EnqueueError makes the next store operation fail with err.

func (*MemoryDeferredStore) Put

Put implements deferred.Store.

type MemoryRefreshStore

type MemoryRefreshStore struct {
	// contains filtered or unexported fields
}

MemoryRefreshStore is an in-memory authengine.RefreshStore.

func NewMemoryRefreshStore

func NewMemoryRefreshStore() *MemoryRefreshStore

NewMemoryRefreshStore creates an empty refresh store.

func (*MemoryRefreshStore) EnqueueError

func (s *MemoryRefreshStore) EnqueueError(err error)

EnqueueError makes the next store operation fail with err.

func (*MemoryRefreshStore) Read

func (s *MemoryRefreshStore) Read(
	_ context.Context,
	fingerprint string,
) (authengine.RefreshRecord, bool, error)

Read implements authengine.RefreshStore.

func (*MemoryRefreshStore) Records

Records returns the stored records keyed by fingerprint.

func (*MemoryRefreshStore) Write

Write implements authengine.RefreshStore.

type MemoryTokenStore

type MemoryTokenStore struct {
	// contains filtered or unexported fields
}

MemoryTokenStore is an in-memory authengine.TokenStore.

It is the fake a single-process consumer can also ship to production behind the same seam, so the contract it satisfies is the real one — including that a miss is (zero, false, nil) rather than an error, which is the distinction a cache built on an error-on-miss store gets subtly wrong.

func NewMemoryTokenStore

func NewMemoryTokenStore() *MemoryTokenStore

NewMemoryTokenStore creates an empty token store.

func (*MemoryTokenStore) Delete

func (s *MemoryTokenStore) Delete(_ context.Context, key string) error

Delete implements authengine.TokenStore.

func (*MemoryTokenStore) EnqueueError

func (s *MemoryTokenStore) EnqueueError(err error)

EnqueueError makes the next store operation fail with err.

func (*MemoryTokenStore) Get

Get implements authengine.TokenStore.

func (*MemoryTokenStore) Keys

func (s *MemoryTokenStore) Keys() []string

Keys returns the cached keys in sorted order.

func (*MemoryTokenStore) Set

Set implements authengine.TokenStore. The TTL is recorded rather than enforced: expiry is the cache's own decision, made off the injected clock, and a fake that expired entries on a real timer would make that untestable.

type TestingT

type TestingT interface {
	// Helper marks the caller as a test helper.
	Helper()
	// Fatalf reports a fatal failure.
	Fatalf(format string, args ...any)
}

TestingT is the minimal subset of *testing.T the assertion helpers use.

Depending on the interface rather than the concrete type keeps the helpers framework-free and — more importantly — lets them be black-box tested with a recording double, which is how the meta tier proves an assertion fails on known-bad input instead of merely passing on known-good.

type TokenRequest

type TokenRequest struct {
	// Subject overrides [DefaultSubject].
	Subject string
	// Username sets the username claim; blank omits it.
	Username string
	// Email sets the email claim; blank omits it.
	Email string
	// EmailUnverified emits email_verified=false instead of true.
	EmailUnverified bool
	// Roles sets the roles claim.
	Roles []string
	// Scopes sets the space-delimited scope claim.
	Scopes []string
	// HomeLandscape sets the home-landscape claim; blank omits it.
	HomeLandscape string
	// Registered names the backends whose registration claim is emitted as true.
	Registered []string
	// Claims are extra claims merged in last, so a test can emit anything.
	Claims map[string]any
	// Issuer overrides the fake IdP's issuer, to mint an untrusted token.
	Issuer string
	// Audience overrides the fake IdP's audience, to mint a mis-audienced token.
	Audience string
	// KeyID overrides the signing key id, to mint a token with an unknown key.
	KeyID string
	// ExpiresAt overrides the expiry, to mint an expired token.
	ExpiresAt time.Time
	// OmitExpiry omits the exp claim entirely.
	OmitExpiry bool
	// OmitKeyID omits the kid header entirely, which is how a consumer proves its
	// key resolver refuses to guess a key rather than picking the only one it has.
	OmitKeyID bool
}

TokenRequest describes a token the fake IdP should mint.

The deliberately wrong-looking fields — Issuer, Audience, KeyID, ExpiresAt — are how a consumer reaches the failure branches of its own validation code. A test that can only mint valid tokens cannot prove that an expired one is rejected.

Jump to

Keyboard shortcuts

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