Documentation
¶
Overview ¶
Package accesscoretest provides public test-infrastructure helpers for the accesscore cell.
Purpose ¶
Go's internal-package barrier prevents tests/integration/ and other external packages from importing cells/accesscore/internal/ports (the real UserRepository / RoleRepository / ConfigGetter interfaces) or cells/accesscore/internal/domain (the User / Role aggregates). This package lives inside the cells/accesscore subtree, so it can import those internal packages — but its public API exposes only value-type DTOs and builder helpers. External callers (tests/integration/*, journey suites) never need to import any cells/accesscore/internal/* package.
Public API surface (no internal leak) ¶
- AccessFixture (sealed; constructor NewAccessFixture)
- SeededUser / SeededRole — value-type seed inputs
- SeededUserView / SeededRoleView — value-type read outputs
- UserStatus + UserStatusActive / Suspended / Locked
- AccessFixture.SeedUser / SeedRole / SeedAssignment
- AccessFixture.GetUser / GetRole / UserRoles / TxRunner
- CredentialInvalidator (opaque wrapper) + NewCredentialInvalidator + WithInvalidatorFixture (fixture-only collapse); the wrapper hides *internal/credentialinvalidate.Invalidator from public signatures so no external caller can name the internal type
- BuildIdentityManageService + With* options
- BuildConfigReceiveService + With* options
- FakeConfigGetter + four typed stub constructors: PresentStub / SensitiveStub / NotFoundStub / ErrorStub
Continued absence of internal-type leaks is gated by external_smoke_test.go, which uses a foreign _test package and would stop compiling the moment any public API forces callers to import cells/accesscore/internal/*.
Relationship to cell.go composition root ¶
cells/accesscore/cell.go is the production composition root: it wires real PG adapters, real JWT issuers, and real TxManagers. This package is the test-composition root: it wires in-memory stores and stubs so tests remain docker-free and fast.
Why AccessFixture aggregates via mem.Bundle + holds session/refresh ¶
PR #595 review caught a test helper in cmd/corebundle that wired UserRepository + RoleRepository + SetupLock from different mem stores. Because the effective-admin invariant (S4.0) requires atomic cross-repo operations, silently using two independent stores breaks the invariant without any compile-time or runtime signal.
cells/accesscore/mem.NewBundle is the sealed funnel that vends all four store-paired primitives (UserRepository, RoleRepository, SetupLock, TxRunner) from the same underlying mem.Store. AccessFixture wraps a single Bundle, then adds singleton session.MemStore and refresh.Store instances of its own. Round-2 review of PR #845 added this extension: without the singletons, NewCredentialInvalidator's default fell back to brand-new session/refresh stores from internal/testutil, silently isolating it from any sessionlogin.Service a caller might later inject as TokenIssuer — the same mis-pairing failure PR #595 set out to prevent. NewCredentialInvalidator now only accepts a fixture (WithInvalidatorFixture), making the non-paired wiring path unexpressible.
Debug logger swap ¶
All builders default to slog.New(slog.DiscardHandler). To see verbose output during local debugging, swap in a text handler:
svc, fix, rec := accesscoretest.BuildIdentityManageService(t,
accesscoretest.WithIdentityLogger(
slog.New(slog.NewTextHandler(os.Stderr, nil)),
),
)
End-to-end example: BuildIdentityManageService ¶
func ExampleBuildIdentityManageService() {
svc, fix, _ := accesscoretest.BuildIdentityManageService(nil) // nil only in pkg example
_ = svc
_ = fix
}
Import scope ¶
This package is test-infrastructure: it must NOT be imported by production Go files. The CELLTEST-IMPORT-SCOPE-01 archtest enforces this automatically for all cells/{X}/{X}test packages.
Tests may import this package freely. The complementary guarantee — that accesscoretest itself does not re-export internal types and therefore does not force its consumers to import cells/accesscore/internal/* — is gated by external_smoke_test.go (Hard funnel: Go internal barrier upstream, compile-time check downstream).
Deferred: spy methods ¶
AccessFixture does not expose CallsOf / Snapshot spy methods. The current design converges on AccessFixture as an aggregate; spy methods will be added when a journey criterion actually needs them.
ref: PR #595 (store-pairing precedent) ref: PR #845 round-2 review (session/refresh pairing + internal-leak closure)
Index ¶
- Variables
- func BuildConfigReceiveService(t *testing.T, opts ...BuildConfigReceiveOption) *configreceive.Service
- func MinCostPasswordHasherOption() accesscore.Option
- type AccessFixture
- func (f *AccessFixture) GetRole(ctx context.Context, id string) (SeededRoleView, error)
- func (f *AccessFixture) GetUser(ctx context.Context, id string) (SeededUserView, error)
- func (f *AccessFixture) SeedAssignment(ctx context.Context, userID, roleID string) error
- func (f *AccessFixture) SeedRole(ctx context.Context, r SeededRole) error
- func (f *AccessFixture) SeedUser(ctx context.Context, u SeededUser) error
- func (f *AccessFixture) TxRunner() persistence.CellTxManager
- func (f *AccessFixture) UserRoles(ctx context.Context, userID string) ([]SeededRoleView, error)
- type BuildConfigReceiveOption
- type BuildIdentityManageOption
- func WithIdentityClock(clk clock.Clock) BuildIdentityManageOption
- func WithIdentityFixture(f *AccessFixture) BuildIdentityManageOption
- func WithIdentityInvalidator(inv *CredentialInvalidator) BuildIdentityManageOption
- func WithIdentityLogger(l *slog.Logger) BuildIdentityManageOption
- func WithIdentityTokenIssuer(ti identitymanage.TokenIssuer) BuildIdentityManageOption
- type ConfigGetterStub
- type CredentialInvalidator
- type CredentialInvalidatorOption
- type FakeConfigGetter
- type SeededRole
- type SeededRoleView
- type SeededUser
- type SeededUserView
- type UserStatus
Constants ¶
This section is empty.
Variables ¶
var DefaultFixtureTenantID = func() tenant.TenantID { t, err := tenant.ParseTenantID("00000000-0000-0000-0000-000000000001") if err != nil { panic(panicregister.Approved("accesscoretest-fixture-tenant", errcode.Assertion("accesscoretest: invalid DefaultFixtureTenantID: %v", err))) } return t }()
DefaultFixtureTenantID is the canonical test tenant UUID used by AccessFixture for all tenant-scoped repo operations. Tests that need multi-tenant isolation should use the TenantID field on fixture calls directly.
Functions ¶
func BuildConfigReceiveService ¶
func BuildConfigReceiveService(t *testing.T, opts ...BuildConfigReceiveOption) *configreceive.Service
BuildConfigReceiveService constructs a ready-to-use *configreceive.Service.
Default wiring:
- ConfigGetter: NewFakeConfigGetter(nil) — all keys return ErrConfigRepoNotFound
- Logger: slog.New(slog.DiscardHandler)
- ConfigEventCollector: not set — configreceive.NewService falls back to its own noop collector. Inject via WithConfigReceiveCollector to assert metric attribution.
func MinCostPasswordHasherOption ¶
func MinCostPasswordHasherOption() accesscore.Option
MinCostPasswordHasherOption returns an accesscore.Option that wires a low-cost (bcrypt.MinCost) password hasher so test harnesses that build the accesscore cell get fast seedAdmin / login instead of the ~1.5s/hash production cost. Apply it alongside the cell's other options.
This is the sanctioned bridge for external test packages (e.g. the tests/integration harnesses) that cannot import cells/accesscore/internal/credential directly under Go's internal-package rule. It returns the fully public accesscore.Option rather than the internal credential.Hasher, so the public surface carries no internal type — pinned by external_smoke_test.go's typed gate. BCRYPT-COST-FUNNEL-01 rule A2 permits credential.NewTestHasher here because accesscoretest is test-support imported only by tests.
Types ¶
type AccessFixture ¶
type AccessFixture struct {
// TenantID scopes all repo operations. Defaults to DefaultFixtureTenantID.
TenantID tenant.TenantID
// contains filtered or unexported fields
}
AccessFixture is the canonical in-memory test aggregate for accesscore. It wraps a single mem.Bundle so the four store-paired primitives (UserRepository, RoleRepository, SetupLock, TxRunner) all originate from the same underlying mem.Store — preventing the store-pairing footgun that caused PR #595.
In addition, the fixture holds singleton session.MemStore and refresh.Store instances so that test-built services (e.g. via BuildIdentityManageService) share session/refresh state with the credential invalidator. Round-2 review of PR #845 added this extension: the invalidator used to build its own independent session/refresh stores via internal/testutil, silently isolating it from any sessionlogin.Service that callers might later inject as TokenIssuer.
All tenant-scoped repo operations use DefaultFixtureTenantID unless the caller passes a specific tenant (#1337 PR-2).
Use NewAccessFixture to construct; never embed the zero value.
func BuildIdentityManageService ¶
func BuildIdentityManageService( t *testing.T, opts ...BuildIdentityManageOption, ) (*identitymanage.Service, *AccessFixture, *outboxtest.Recorder)
BuildIdentityManageService constructs a ready-to-use *identitymanage.Service along with the AccessFixture and outboxtest.Recorder it is wired to.
Default wiring:
- AccessFixture: fresh NewAccessFixture(t, clock.Real())
- invalidator: NewCredentialInvalidator(t, WithInvalidatorFixture(fixture)) — shares the fixture's UserRepository + session.MemStore + refresh.Store so any sessionlogin.Service injected via WithIdentityTokenIssuer sees the same session/refresh state the invalidator revokes against
- TxManager: fixture.TxRunner() (store-paired, full atomic semantics)
- Emitter: outboxtest.NewRecorder()
- Clock: clock.Real()
- Logger: slog.New(slog.DiscardHandler)
- TokenIssuer: failingTokenIssuer (t.Fatal on IssueForUser; replace via WithIdentityTokenIssuer for ChangePassword paths)
- roleRepo: fixture.RoleRepository (required positional param) — wires the at-least-one-effective-admin guard
The returned fixture is the same one used internally — seed via fixture.SeedUser / SeedRole / SeedAssignment, then assert via fixture.GetUser / GetRole / UserRoles.
func NewAccessFixture ¶
func NewAccessFixture(t *testing.T, clk clock.Clock) *AccessFixture
NewAccessFixture constructs an AccessFixture backed by mem.NewBundle(clk). session/refresh stores use the canonical test protocol that internal/testutil.RealSessionRepo / RealRefreshStore use so behavior matches existing in-package unit tests.
func (*AccessFixture) GetRole ¶
func (f *AccessFixture) GetRole(ctx context.Context, id string) (SeededRoleView, error)
GetRole returns the seeded role as a SeededRoleView.
func (*AccessFixture) GetUser ¶
func (f *AccessFixture) GetUser(ctx context.Context, id string) (SeededUserView, error)
GetUser returns the seeded user as a SeededUserView. External tests use this to assert that service operations updated user state without needing to import internal/domain.
func (*AccessFixture) SeedAssignment ¶
func (f *AccessFixture) SeedAssignment(ctx context.Context, userID, roleID string) error
SeedAssignment assigns the role to the user via RoleRepository.AssignToUser. Returns an error if either the user or role does not exist in the store.
func (*AccessFixture) SeedRole ¶
func (f *AccessFixture) SeedRole(ctx context.Context, r SeededRole) error
SeedRole persists role directly via the bundle-paired RoleRepository. AccessFixture does not expose the raw RoleRepository.
func (*AccessFixture) SeedUser ¶
func (f *AccessFixture) SeedUser(ctx context.Context, u SeededUser) error
SeedUser persists u via domain.NewUser → UserRepository.Create. The resulting user is Active with AuthzEpoch=1; tests that need non-Active state must drive a service operation (Lock / Suspend / Delete) after seeding so the credential-event funnel runs.
func (*AccessFixture) TxRunner ¶
func (f *AccessFixture) TxRunner() persistence.CellTxManager
TxRunner returns the bundle-paired store-bound CellTxManager. Use this when wiring services that require a real atomic TxManager (e.g. identitymanage Create → lock-scoped read-modify-write).
func (*AccessFixture) UserRoles ¶
func (f *AccessFixture) UserRoles(ctx context.Context, userID string) ([]SeededRoleView, error)
UserRoles returns the roles currently assigned to the given user.
type BuildConfigReceiveOption ¶
type BuildConfigReceiveOption func(*buildReceiveConfig)
BuildConfigReceiveOption configures BuildConfigReceiveService.
func WithConfigReceiveCollector ¶
func WithConfigReceiveCollector(c obmetrics.ConfigEventCollector) BuildConfigReceiveOption
WithConfigReceiveCollector injects a config event collector so tests can observe ack / stale / permanent_error metric attribution downstream of HandleEntryUpserted / HandleEntryDeleted.
func WithConfigReceiveConfigGetter ¶
func WithConfigReceiveConfigGetter(g *FakeConfigGetter) BuildConfigReceiveOption
WithConfigReceiveConfigGetter injects a FakeConfigGetter into the configreceive service so that GetEntry calls can be observed and stubbed in tests.
func WithConfigReceiveLogger ¶
func WithConfigReceiveLogger(l *slog.Logger) BuildConfigReceiveOption
WithConfigReceiveLogger injects a logger. Defaults to slog.New(slog.DiscardHandler). See WithIdentityLogger for stderr-swap example.
type BuildIdentityManageOption ¶
type BuildIdentityManageOption func(*buildIdentityConfig)
BuildIdentityManageOption configures BuildIdentityManageService.
func WithIdentityClock ¶
func WithIdentityClock(clk clock.Clock) BuildIdentityManageOption
WithIdentityClock injects a clock (e.g. a test clock) into the identity service and its underlying invalidator. Defaults to clock.Real().
func WithIdentityFixture ¶
func WithIdentityFixture(f *AccessFixture) BuildIdentityManageOption
WithIdentityFixture injects a pre-constructed (and possibly pre-seeded) AccessFixture into the builder. When not set, the builder creates a fresh one backed by clock.Real(). Use this option when you need to inspect or pre-seed state before calling BuildIdentityManageService.
func WithIdentityInvalidator ¶
func WithIdentityInvalidator(inv *CredentialInvalidator) BuildIdentityManageOption
WithIdentityInvalidator overrides the credential invalidator. By default BuildIdentityManageService constructs an invalidator wired to the fixture (so user/session/refresh state stays paired). Use this escape hatch only when a test needs to substitute a custom invalidator built against the same fixture — passing nil keeps the default.
The parameter is the public opaque *CredentialInvalidator returned by NewCredentialInvalidator; the unwrap to internal/credentialinvalidate happens once inside BuildIdentityManageService, keeping the internal type unreachable from external test packages.
func WithIdentityLogger ¶
func WithIdentityLogger(l *slog.Logger) BuildIdentityManageOption
WithIdentityLogger injects a logger. Defaults to slog.New(slog.DiscardHandler). To see verbose output during local debugging, swap in:
WithIdentityLogger(slog.New(slog.NewTextHandler(os.Stderr, nil)))
func WithIdentityTokenIssuer ¶
func WithIdentityTokenIssuer(ti identitymanage.TokenIssuer) BuildIdentityManageOption
WithIdentityTokenIssuer injects a custom TokenIssuer. When not provided the default failingTokenIssuer is wired, which fails the test on IssueForUser call. Tests exercising ChangePassword must inject a real sessionlogin.Service. See cells/accesscore/slices/sessionlogin.Service which implements TokenIssuer.
type ConfigGetterStub ¶
type ConfigGetterStub struct {
// contains filtered or unexported fields
}
ConfigGetterStub is the sealed value type accepted by NewFakeConfigGetter. The fields are unexported; the only way to construct one is via the four typed constructors below. This makes self-contradictory states like "Sensitive=true with plaintext Value" unrepresentable in the type system — selecting the wrong stub semantic is selecting the wrong constructor name, surfaced at compile time.
AI Hard form: "typed function choice" — one API per semantic, no flag fields decoded at runtime.
func ErrorStub ¶
func ErrorStub(err error) ConfigGetterStub
ErrorStub returns a stub that surfaces the supplied error from GetEntry. Use for transient or permanent error coverage; the error is returned to the caller verbatim (no wrapping).
func NotFoundStub ¶
func NotFoundStub() ConfigGetterStub
NotFoundStub returns a stub representing a key that is explicitly absent; GetEntry returns ErrConfigRepoNotFound with CategoryDomain. Use this when stubbing stale-event paths (configcore reports the key was upserted but the subsequent fetch races a delete and finds nothing).
func PresentStub ¶
func PresentStub(key, value string, version int) ConfigGetterStub
PresentStub returns a stub for a non-sensitive config key whose value is returned to callers via GetEntry. Use this for the common "key exists with plaintext value" path.
func SensitiveStub ¶
func SensitiveStub(key string, version int) ConfigGetterStub
SensitiveStub returns a stub mirroring what configcore's internal HTTP getter returns for keys flagged Sensitive=true: Value is the redacted placeholder "******". This matches the production redaction contract documented in cells/accesscore/internal/ports/configport.go and lets tests assert the "reload triggers but plaintext never revealed" path without hard-coding the redaction sentinel in every test.
type CredentialInvalidator ¶
type CredentialInvalidator struct {
// contains filtered or unexported fields
}
CredentialInvalidator is the public opaque handle returned by NewCredentialInvalidator. It wraps the unexported *cells/accesscore/internal/credentialinvalidate.Invalidator so external _test packages can hold and pass the value without importing the internal/ package — which Go's internal-package rule forbids.
The struct intentionally has no exported fields and no exported methods: it is a sealed handle, not an API surface. The single legal touchpoint of the underlying internal type lives in this package (where the internal import is permitted), keeping the testutil → internal coupling unidirectional and unbypassable from outside cells/accesscore/.
Hard funnel form (sibling of Hard 范本 §"single sanctioned holder"):
- upstream Hard — Go's internal-package rule makes *credentialinvalidate.Invalidator unnameable in any package not rooted at cells/accesscore/. External callers therefore cannot bypass this wrapper by declaring the raw type.
- downstream Hard — unwrap field is unexported (.inv), so no external code can read or rewrap it; the only consumers are NewCredentialInvalidator (constructor) and BuildIdentityManageService (which threads the fixture-paired invalidator into identitymanage.NewService).
func NewCredentialInvalidator ¶
func NewCredentialInvalidator(t *testing.T, opts ...CredentialInvalidatorOption) *CredentialInvalidator
NewCredentialInvalidator constructs an opaque *CredentialInvalidator wired to the user/session/refresh stores held by the supplied AccessFixture. The fixture is required: omitting WithInvalidatorFixture fails the test immediately so the invalidator can never silently run against an isolated store.
The returned *CredentialInvalidator is the only handle external test packages may hold. Pass it to WithIdentityInvalidator when building services; the internal unwrap to *credentialinvalidate.Invalidator happens inside this package.
type CredentialInvalidatorOption ¶
type CredentialInvalidatorOption func(*invalidatorConfig)
CredentialInvalidatorOption configures NewCredentialInvalidator.
The only public option is WithInvalidatorFixture. Round-2 review of PR #845 (worktree 650) collapsed the previous WithInvalidatorUsers / WithInvalidatorSessions / WithInvalidatorRefresh trio because they let callers wire independent stores into the invalidator while a real sessionlogin.Service held its own state — the exact mis-pairing failure mode PR #595 was supposed to prevent. The fixture is now the sole source of the (UserRepository, session.Store, refresh.Store) triple; any other wiring path is unexpressible in the type system.
func WithInvalidatorFixture ¶
func WithInvalidatorFixture(f *AccessFixture) CredentialInvalidatorOption
WithInvalidatorFixture is the required option for NewCredentialInvalidator. Passing nil is a no-op (the option function is idempotent); the final nil check happens inside NewCredentialInvalidator and fails the test.
Usage:
fix := accesscoretest.NewAccessFixture(t, clock.Real())
inv := accesscoretest.NewCredentialInvalidator(t,
accesscoretest.WithInvalidatorFixture(fix),
)
type FakeConfigGetter ¶
type FakeConfigGetter struct {
// contains filtered or unexported fields
}
FakeConfigGetter is a stub implementation of ports.ConfigGetter for use in tests. It is concurrency-safe. Unknown keys (not in the stubs map) return ErrConfigRepoNotFound by convention so test authors do not need to explicitly stub every key they do not care about.
Usage:
g := accesscoretest.NewFakeConfigGetter(map[string]accesscoretest.ConfigGetterStub{
"jwt.ttl": accesscoretest.PresentStub("jwt.ttl", "3600", 1),
"kms.key": accesscoretest.SensitiveStub("kms.key", 4),
"stale.key": accesscoretest.NotFoundStub(),
"broken": accesscoretest.ErrorStub(myTransientErr),
})
func NewFakeConfigGetter ¶
func NewFakeConfigGetter(stubs map[string]ConfigGetterStub) *FakeConfigGetter
NewFakeConfigGetter constructs a FakeConfigGetter with a deep copy of the supplied stubs map. Subsequent mutations of the caller's map (or of any ConfigGetterStub value re-used as a map alias) do not affect the fake. A nil or empty stubs map means all keys return ErrConfigRepoNotFound.
func (*FakeConfigGetter) Calls ¶
func (g *FakeConfigGetter) Calls() []string
Calls returns the ordered list of keys passed to GetEntry since the last Reset. Returns a copy; mutating the slice does not affect the recorder.
func (*FakeConfigGetter) GetEntry ¶
func (g *FakeConfigGetter) GetEntry(ctx context.Context, t tenant.TenantID, key string) (ports.ConfigEntry, error)
GetEntry implements ports.ConfigGetter. It honors ctx.Err() (matching the production HTTP getter's cancellation semantic), records the call (key and tenant), and returns the stubbed response for the given key. Unknown keys and NotFoundStub entries return ErrConfigRepoNotFound with CategoryDomain.
func (*FakeConfigGetter) Reset ¶
func (g *FakeConfigGetter) Reset()
Reset clears the call log. Stubs are not modified.
func (*FakeConfigGetter) TenantCalls ¶
func (g *FakeConfigGetter) TenantCalls() []tenant.TenantID
TenantCalls returns the ordered list of tenant.TenantID values passed to GetEntry since the last Reset. The i-th element corresponds to the i-th entry in Calls(). Returns a copy.
type SeededRole ¶
SeededRole is the value-type input for AccessFixture.SeedRole. Permissions are not modeled here — current journeys do not seed them; add a SeededPermission type when needed.
type SeededRoleView ¶
SeededRoleView is the value-type output returned by AccessFixture.GetRole / AccessFixture.UserRoles.
type SeededUser ¶
type SeededUser struct {
ID string
Username string
Email string
PasswordHash string // bcrypt-formatted; tests can use "$2a$04$dummy"
}
SeededUser is the value-type input for AccessFixture.SeedUser. It mirrors the public field set of internal/domain.User that test authors need at seed time — all four fields are required.
Active status and AuthzEpoch=1 are implied (domain.NewUser defaults). Non-Active state must come from a service operation invoked after seeding.
type SeededUserView ¶
type SeededUserView struct {
ID string
Username string
Email string
PasswordHash string
Status UserStatus
AuthzEpoch int64
CreatedAt time.Time
UpdatedAt time.Time
}
SeededUserView is the value-type output returned by AccessFixture.GetUser. It surfaces the seed fields plus rehydrated authz/lifecycle state so test assertions don't need to import internal/domain.
type UserStatus ¶
type UserStatus string
UserStatus is the public mirror of domain.UserStatus used by SeededUserView so external test packages do not need to import internal/domain to read user state. SeedUser always produces Active users; tests that need Locked/Suspended state must drive a service operation (Lock / Suspend) after seeding so the credential-event funnel runs correctly.
const ( UserStatusActive UserStatus = "active" UserStatusSuspended UserStatus = "suspended" UserStatusLocked UserStatus = "locked" )