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:
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.
Related¶
- Enable OS-keychain storage
- Test the keychain on a headless host — exercising the real go-keyring path (vs the in-process fake here)
- Implement a custom backend