Skip to content

Test credential flows

Code that reads a credential — a resolver, a setup flow, an API client constructor — needs a backend to test against. You do not want those tests touching the real OS keychain (non-deterministic, platform-specific, needs a session bus). The credtest subpackage provides an in-process Backend for exactly this.

credtest.Install — the common case

Call Install at the top of a test. It registers a fresh in-memory backend as the process-wide backend and restores the stub on cleanup:

import (
    "gitlab.com/phpboyscout/go/credentials"
    "gitlab.com/phpboyscout/go/credentials/credtest"
)

func TestResolverReadsStoredToken(t *testing.T) {
    credtest.Install(t) // fresh MemoryBackend now active; stub restored on cleanup

    require.NoError(t, credentials.Store(t.Context(), "mytool", "github.auth", "ghp_test"))

    // drive the code under test — it resolves through credentials.Retrieve
    token, err := myResolver.Resolve(t.Context())
    require.NoError(t, err)
    assert.Equal(t, "ghp_test", token)
}

Install returns the backend, so you can seed entries directly without going through credentials.Store:

b := credtest.Install(t)
require.NoError(t, b.Store(t.Context(), "mytool", "api", "seeded"))

Under this backend, KeychainAvailable reports true and Probe succeeds, so you also exercise the keychain-mode branches of your setup flow.

Install is not parallel-safe

Install swaps the process-wide backend and restores it via t.Cleanup. Because the registry is global, a test that calls Install must not call t.Parallel() — a concurrent test would observe the wrong backend. Keep Install in serial tests, and do not share an installed backend across parallel subtests.

MemoryBackend directly — no global state

When you want a backend instance without touching the global registry (e.g. to test a component that takes a Backend by injection), construct a MemoryBackend directly. Its zero value is usable and it is safe for concurrent use, so tests that use only injected backends can run in parallel:

func TestClientUsesInjectedBackend(t *testing.T) {
    t.Parallel()

    var b credtest.MemoryBackend
    require.NoError(t, b.Store(t.Context(), "svc", "acct", "secret"))

    client := NewClient(WithBackend(&b)) // your component takes a credentials.Backend
    // ... assert client behaviour
}

MemoryBackend mirrors the real contract exactly — Retrieve on an unknown key returns ErrCredentialNotFound, Delete is idempotent, Store overwrites — so a test passing against it behaves the same against a real keychain.

Testing the "no backend" path

To test how your code behaves when no keychain is compiled in, register nothing — the default stub returns ErrCredentialUnsupported:

func TestFallsThroughWhenUnsupported(t *testing.T) {
    // no credtest.Install — the stub backend is active
    _, err := credentials.Retrieve(t.Context(), "svc", "acct")
    require.ErrorIs(t, err, credentials.ErrCredentialUnsupported)
}

Why a test helper ships in the module

credtest is a normal (non-_test) package so it can be imported by your test files. It performs zero IPC — everything lives in an in-process map — so linking it into a binary that must prove (via SBOM) it does no keychain I/O is safe. It never pulls in go-keyring.