Documentation
¶
Overview ¶
Package configcoretest provides testutil builders and fakes for the configcore cell. It exposes constructors for the configwrite and configsubscribe slices without requiring Docker or real infrastructure, enabling fast, docker-free unit and journey-criterion tests.
Import scope ¶
This package must only be imported from test code: *_test.go files or other test-infrastructure packages (paths containing a "testutil" segment or a segment ending in "test"). Production binaries must never import it. Enforcement: archtest CELLTEST-IMPORT-SCOPE-01 (tools/archtest/celltest_import_scope_test.go) auto-discovers this package via the cells/{X}/{X}test$ naming pattern.
Relationship to production wiring ¶
Production wiring for configcore lives in cells/configcore/cell.go (Init, AfterStart, etc.) and is assembled in cmd/corebundle. This package replicates the same wiring decisions in a test-friendly way:
- Real service implementations (configwrite.NewService, configsubscribe.NewService).
- An in-memory repository (backed by the mem package — always zero-latency, no external dependencies).
- kernel/outbox.DemoCellTxManager() as a pass-through TxRunner.
- kernel/outbox/outboxtest.NewRecorder() as the event emitter.
Relationship to internal/testutil ¶
cells/configcore/internal/testutil provides low-level raw doubles (fake repositories, stub ports) intended for in-package unit tests within the configcore subtree. Import it only from test files inside cells/configcore/.
configcoretest (this package) provides higher-level Service builders for cross-package tests and Journey-criterion tests outside cells/configcore. It wires full service graphs so callers interact with the same service API that production code uses — no knowledge of internal types or ports required.
Rule of thumb: if your test file lives inside cells/configcore/ and needs a raw double, use internal/testutil. If your test lives outside cells/configcore/ (e.g. tests/integration/, journeys/, or a sibling cell), use configcoretest.
Usage ¶
svc, repo, rec := configcoretest.BuildWriteService(t)
entry, err := svc.Create(ctx, configwrite.CreateInput{Key: "k", Value: "v"})
require.NoError(t, err)
require.Len(t, rec.Entries(), 1)
snap, _ := repo.Snapshot(ctx) // direct repo handle for state assertions
Clock single-source contract ¶
BuildWriteService always constructs the repository internally using the configured clock (WithWriteClock, defaults to clock.Real()). There is no option to inject a pre-built repository: the service and repository must share one clock so that Create-stamped CreatedAt/UpdatedAt and Update-stamped UpdatedAt come from the same time source.
Debug logging ¶
By default builders discard all logs. To surface slog output during debugging, inject a real handler:
svc, repo, rec := configcoretest.BuildWriteService(t,
configcoretest.WithWriteLogger(slog.New(slog.NewTextHandler(os.Stderr, nil))),
)
_ = repo
_ = rec
Index ¶
Constants ¶
const TestTenant tenant.TenantID = testTenantStr
TestTenant is the TenantID value that CtxWithTenant injects and that Seed / Snapshot operate under.
Variables ¶
This section is empty.
Functions ¶
func BuildSubscribeService ¶
func BuildSubscribeService(t *testing.T, opts ...BuildSubscribeOption) *configsubscribe.Service
BuildSubscribeService constructs a configsubscribe.Service wired with default settings suitable for unit tests.
No Recorder is returned because configsubscribe is a pure event consumer: it drives a local version-tracking cache but never emits outbox events of its own. Use svc.Cache() to assert cache state after calling HandleEntryUpserted or HandleEntryDeleted.
Defaults:
- logger: slog.New(slog.DiscardHandler)
- clock: clock.Real()
Types ¶
type BuildSubscribeOption ¶
type BuildSubscribeOption func(*buildSubscribeConfig)
BuildSubscribeOption configures BuildSubscribeService.
func WithSubscribeClock ¶
func WithSubscribeClock(clk clock.Clock) BuildSubscribeOption
WithSubscribeClock injects a custom clock. Required by configsubscribe.NewService; defaults to clock.Real().
func WithSubscribeLogger ¶
func WithSubscribeLogger(l *slog.Logger) BuildSubscribeOption
WithSubscribeLogger injects a custom logger. Defaults to slog.DiscardHandler.
type BuildWriteOption ¶
type BuildWriteOption func(*buildWriteConfig)
BuildWriteOption configures BuildWriteService.
func WithWriteClock ¶
func WithWriteClock(clk clock.Clock) BuildWriteOption
WithWriteClock injects a custom clock. The same clock instance is wired into both the FakeConfigRepository and the configwrite.Service, so Create-stamped CreatedAt/UpdatedAt and Update-stamped UpdatedAt come from a single source. Use clockmock.New(t) for time-sensitive assertions.
func WithWriteLogger ¶
func WithWriteLogger(l *slog.Logger) BuildWriteOption
WithWriteLogger injects a custom logger. Defaults to slog.DiscardHandler.
type FakeConfigRepository ¶
type FakeConfigRepository struct {
*mem.ConfigRepository
}
FakeConfigRepository wraps an in-memory ConfigRepository and adds Seed and Snapshot helpers for test scenario setup and assertion.
FakeConfigRepository implements ports.ConfigRepository via the embedded *mem.ConfigRepository. Tests obtain a wired instance from BuildWriteService (which returns the repo as its second value); direct construction via NewFakeConfigRepository is for repo-only tests that do not need a service.
Spy methods (CallsOf/Reset) are deferred until a test needs them; add via Seed + Snapshot if you need state assertions without a Recorder.
func BuildWriteService ¶
func BuildWriteService(t *testing.T, opts ...BuildWriteOption) (*configwrite.Service, *FakeConfigRepository, *outboxtest.Recorder)
BuildWriteService constructs a configwrite.Service wired with a fresh in-memory repository and an outboxtest.Recorder as the event emitter. Both the repository and the Recorder are returned so the test can Seed/Snapshot state and assert on captured outbox entries.
The repository is always constructed internally with the configured clock — callers cannot inject a pre-built repository, which removes the clock-fork foot-gun where the repo's clock (Update path) would diverge from the service's clock (Create path).
Defaults:
- tx: outbox.DemoCellTxManager()
- emitter: outboxtest.NewRecorder()
- logger: slog.New(slog.DiscardHandler)
- clock: clock.Real()
func NewFakeConfigRepository ¶
func NewFakeConfigRepository(clk clock.Clock) *FakeConfigRepository
NewFakeConfigRepository creates an empty FakeConfigRepository backed by the in-memory implementation. clk must be non-nil; pass clock.Real() or clockmock.New(...) in tests.
func (*FakeConfigRepository) Seed ¶
func (r *FakeConfigRepository) Seed(ctx context.Context, entry SeededEntry) error
Seed inserts a single entry into the repository directly, bypassing service-layer validation. Use this to pre-populate the repository before a test scenario.
If entry.Version is 0 it is treated as 1.
Seed does NOT support upsert; calling Seed twice with the same Key returns ErrConfigDuplicate. Construct a fresh repo via NewFakeConfigRepository between scenarios.
Tenant scoping: Seed uses TestTenant so that service tests that inject CtxWithTenant(ctx) see the same rows. Tests that need cross-tenant isolation should call the underlying repo methods directly with explicit TenantID values.
func (*FakeConfigRepository) Snapshot ¶
func (r *FakeConfigRepository) Snapshot(ctx context.Context) ([]SeededEntry, error)
Snapshot returns a point-in-time copy of all config entries currently stored under TestTenant, sorted by key for deterministic ordering in assertions. It does not modify the repository.
Snapshot returns up to 500 entries (query.MaxPageSize). Tests seeding more entries than this limit will receive a truncated result — the returned slice will contain exactly 500 entries in that case.
WARNING: entries with Sensitive=true contain the plaintext Value as stored in the in-memory backend. Tests should not log entry.Value when Sensitive=true.
type SeededEntry ¶
type SeededEntry struct {
Key string
Value string
Sensitive bool
Version int // optional; defaults to 1 on Seed when zero
}
SeededEntry is a configcoretest-local view of a config entry suitable for public test code outside the cells/configcore subtree. It mirrors the fields callers need without leaking internal/domain types.
Version defaults to 1 when passed to Seed and Version == 0.