refactor(supply-api): slim runtime constructor prelude
This commit is contained in:
@@ -81,6 +81,16 @@ type runtimeFactory struct {
|
||||
newRedisCache func(cfg config.RedisConfig) (*cache.RedisCache, error)
|
||||
}
|
||||
|
||||
type runtimeBuildInputs struct {
|
||||
env string
|
||||
cfg *config.Config
|
||||
logger logging.Logger
|
||||
initCtx context.Context
|
||||
now func() time.Time
|
||||
isProd bool
|
||||
tuning runtimeTuning
|
||||
}
|
||||
|
||||
type runtimeStoreBundle struct {
|
||||
accountStore domain.AccountStore
|
||||
packageStore domain.PackageStore
|
||||
@@ -129,23 +139,42 @@ func BuildRuntime(opts RuntimeOptions) (*Runtime, error) {
|
||||
}
|
||||
|
||||
func buildRuntimeWithFactory(opts RuntimeOptions, factory runtimeFactory) (*Runtime, error) {
|
||||
if opts.Config == nil {
|
||||
return nil, errors.New("config is required")
|
||||
}
|
||||
if opts.Logger == nil {
|
||||
return nil, errors.New("logger is required")
|
||||
inputs, err := resolveRuntimeBuildInputs(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if factory.newDB == nil {
|
||||
factory.newDB = repository.NewDB
|
||||
resources, err := initializeRuntimeExternalResources(inputs, factory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if factory.newRedisCache == nil {
|
||||
factory.newRedisCache = cache.NewRedisCache
|
||||
|
||||
storeBundle := buildStoreBundle(resources.db, inputs.logger)
|
||||
securityBundle := buildSecurityBundle(inputs.env, inputs.cfg, inputs.logger, storeBundle.auditStore, resources.redisCache, storeBundle.tokenStatusRepo)
|
||||
apiBundle, err := buildAPIBundle(inputs.env, inputs.cfg, inputs.now, inputs.tuning, inputs.logger, inputs.isProd, storeBundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startupViews := buildRuntimeStartupViews(inputs.env, inputs.logger, inputs.cfg.Server, inputs.tuning, securityBundle, apiBundle)
|
||||
|
||||
return &Runtime{
|
||||
resources: resources,
|
||||
startupViews: startupViews,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveRuntimeBuildInputs(opts RuntimeOptions) (runtimeBuildInputs, error) {
|
||||
if opts.Config == nil {
|
||||
return runtimeBuildInputs{}, errors.New("config is required")
|
||||
}
|
||||
if opts.Logger == nil {
|
||||
return runtimeBuildInputs{}, errors.New("logger is required")
|
||||
}
|
||||
|
||||
env, err := ResolveEnv(opts.Env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return runtimeBuildInputs{}, err
|
||||
}
|
||||
now := opts.Now
|
||||
if now == nil {
|
||||
@@ -156,62 +185,88 @@ func buildRuntimeWithFactory(opts RuntimeOptions, factory runtimeFactory) (*Runt
|
||||
initCtx = context.Background()
|
||||
}
|
||||
|
||||
isProd := env == "prod"
|
||||
tuning := defaultRuntimeTuning()
|
||||
return runtimeBuildInputs{
|
||||
env: env,
|
||||
cfg: opts.Config,
|
||||
logger: opts.Logger,
|
||||
initCtx: initCtx,
|
||||
now: now,
|
||||
isProd: env == "prod",
|
||||
tuning: defaultRuntimeTuning(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
db, err := factory.newDB(initCtx, opts.Config.Database)
|
||||
func withDefaultRuntimeFactory(factory runtimeFactory) runtimeFactory {
|
||||
if factory.newDB == nil {
|
||||
factory.newDB = repository.NewDB
|
||||
}
|
||||
if factory.newRedisCache == nil {
|
||||
factory.newRedisCache = cache.NewRedisCache
|
||||
}
|
||||
return factory
|
||||
}
|
||||
|
||||
func initializeRuntimeExternalResources(inputs runtimeBuildInputs, factory runtimeFactory) (runtimeExternalResources, error) {
|
||||
factory = withDefaultRuntimeFactory(factory)
|
||||
|
||||
db, err := factory.newDB(inputs.initCtx, inputs.cfg.Database)
|
||||
if err != nil {
|
||||
if isProd {
|
||||
return nil, fmt.Errorf("database unavailable: %w", err)
|
||||
if inputs.isProd {
|
||||
return runtimeExternalResources{}, fmt.Errorf("database unavailable: %w", err)
|
||||
}
|
||||
warnf(opts.Logger, "failed to connect to database: %v (using in-memory store)", err)
|
||||
warnf(inputs.logger, "failed to connect to database: %v (using in-memory store)", err)
|
||||
db = nil
|
||||
} else if db != nil {
|
||||
infof(opts.Logger, "connected to database at %s:%d", opts.Config.Database.Host, opts.Config.Database.Port)
|
||||
infof(inputs.logger, "connected to database at %s:%d", inputs.cfg.Database.Host, inputs.cfg.Database.Port)
|
||||
}
|
||||
|
||||
redisCache, err := factory.newRedisCache(opts.Config.Redis)
|
||||
redisCache, err := factory.newRedisCache(inputs.cfg.Redis)
|
||||
if err != nil {
|
||||
if isProd {
|
||||
warnf(opts.Logger, "redis unavailable at startup: %v", err)
|
||||
if inputs.isProd {
|
||||
warnf(inputs.logger, "redis unavailable at startup: %v", err)
|
||||
} else {
|
||||
warnf(opts.Logger, "failed to connect to redis: %v (caching disabled)", err)
|
||||
warnf(inputs.logger, "failed to connect to redis: %v (caching disabled)", err)
|
||||
}
|
||||
redisCache = nil
|
||||
} else if redisCache != nil {
|
||||
infof(opts.Logger, "connected to redis at %s:%d", opts.Config.Redis.Host, opts.Config.Redis.Port)
|
||||
infof(inputs.logger, "connected to redis at %s:%d", inputs.cfg.Redis.Host, inputs.cfg.Redis.Port)
|
||||
}
|
||||
|
||||
storeBundle := buildStoreBundle(db, opts.Logger)
|
||||
securityBundle := buildSecurityBundle(env, opts.Config, opts.Logger, storeBundle.auditStore, redisCache, storeBundle.tokenStatusRepo)
|
||||
apiBundle, err := buildAPIBundle(env, opts.Config, now, tuning, opts.Logger, isProd, storeBundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildRuntimeResources(db, redisCache), nil
|
||||
}
|
||||
|
||||
return &Runtime{
|
||||
resources: runtimeExternalResources{
|
||||
db: db,
|
||||
redisCache: redisCache,
|
||||
func buildRuntimeResources(db *repository.DB, redisCache *cache.RedisCache) runtimeExternalResources {
|
||||
return runtimeExternalResources{
|
||||
db: db,
|
||||
redisCache: redisCache,
|
||||
}
|
||||
}
|
||||
|
||||
func buildRuntimeStartupViews(
|
||||
env string,
|
||||
logger logging.Logger,
|
||||
serverConfig config.ServerConfig,
|
||||
tuning runtimeTuning,
|
||||
securityBundle runtimeSecurityBundle,
|
||||
apiBundle runtimeAPIBundle,
|
||||
) runtimeStartupViews {
|
||||
return runtimeStartupViews{
|
||||
http: runtimeHTTPStartupView{
|
||||
env: env,
|
||||
logger: logger,
|
||||
serverConfig: normalizeServerConfig(serverConfig),
|
||||
supplyAPI: apiBundle.supplyAPI,
|
||||
alertAPI: apiBundle.alertAPI,
|
||||
authMiddleware: securityBundle.authMiddleware,
|
||||
rateLimitConfig: apiBundle.rateLimitConfig,
|
||||
},
|
||||
startupViews: runtimeStartupViews{
|
||||
http: runtimeHTTPStartupView{
|
||||
env: env,
|
||||
logger: opts.Logger,
|
||||
serverConfig: normalizeServerConfig(opts.Config.Server),
|
||||
supplyAPI: apiBundle.supplyAPI,
|
||||
alertAPI: apiBundle.alertAPI,
|
||||
authMiddleware: securityBundle.authMiddleware,
|
||||
rateLimitConfig: apiBundle.rateLimitConfig,
|
||||
},
|
||||
background: runtimeBackgroundStartupView{
|
||||
env: env,
|
||||
logger: opts.Logger,
|
||||
tuning: tuning,
|
||||
revocationSubscriber: securityBundle.revocationSubscriber,
|
||||
},
|
||||
background: runtimeBackgroundStartupView{
|
||||
env: env,
|
||||
logger: logger,
|
||||
tuning: tuning,
|
||||
revocationSubscriber: securityBundle.revocationSubscriber,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildStoreBundle(db *repository.DB, logger logging.Logger) runtimeStoreBundle {
|
||||
|
||||
@@ -198,6 +198,95 @@ func TestResolveEnv_RejectsUnsupportedValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRuntimeBuildInputs_NormalizesDefaults(t *testing.T) {
|
||||
inputs, err := resolveRuntimeBuildInputs(RuntimeOptions{
|
||||
Config: testRuntimeConfig(),
|
||||
Logger: testLogger{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected input normalization to succeed, got %v", err)
|
||||
}
|
||||
if inputs.cfg == nil {
|
||||
t.Fatal("expected config to be preserved")
|
||||
}
|
||||
if inputs.logger == nil {
|
||||
t.Fatal("expected logger to be preserved")
|
||||
}
|
||||
if inputs.env != "dev" {
|
||||
t.Fatalf("unexpected env: %s", inputs.env)
|
||||
}
|
||||
if inputs.isProd {
|
||||
t.Fatal("expected default env to be non-prod")
|
||||
}
|
||||
if inputs.now == nil {
|
||||
t.Fatal("expected now func to be defaulted")
|
||||
}
|
||||
if inputs.initCtx == nil {
|
||||
t.Fatal("expected init context to be defaulted")
|
||||
}
|
||||
if inputs.tuning.outboxStreamName != "supply:outbox:stream" {
|
||||
t.Fatalf("unexpected outbox stream: %s", inputs.tuning.outboxStreamName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeRuntimeExternalResources_ProdRejectsDatabaseFailure(t *testing.T) {
|
||||
_, err := initializeRuntimeExternalResources(runtimeBuildInputs{
|
||||
env: "prod",
|
||||
cfg: testRuntimeConfig(),
|
||||
logger: testLogger{},
|
||||
initCtx: context.Background(),
|
||||
isProd: true,
|
||||
tuning: defaultRuntimeTuning(),
|
||||
now: time.Now,
|
||||
}, runtimeFactory{
|
||||
newDB: func(context.Context, config.DatabaseConfig) (*repository.DB, error) {
|
||||
return nil, errors.New("db down")
|
||||
},
|
||||
newRedisCache: func(config.RedisConfig) (*cache.RedisCache, error) {
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected prod runtime resources to reject database outage")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "database unavailable") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeRuntimeExternalResources_DevFallsBackToNilResources(t *testing.T) {
|
||||
logger := &captureLogger{}
|
||||
|
||||
resources, err := initializeRuntimeExternalResources(runtimeBuildInputs{
|
||||
env: "dev",
|
||||
cfg: testRuntimeConfig(),
|
||||
logger: logger,
|
||||
initCtx: context.Background(),
|
||||
isProd: false,
|
||||
tuning: defaultRuntimeTuning(),
|
||||
now: time.Now,
|
||||
}, runtimeFactory{
|
||||
newDB: func(context.Context, config.DatabaseConfig) (*repository.DB, error) {
|
||||
return nil, errors.New("db down")
|
||||
},
|
||||
newRedisCache: func(config.RedisConfig) (*cache.RedisCache, error) {
|
||||
return nil, errors.New("redis down")
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected dev runtime resources to fall back, got %v", err)
|
||||
}
|
||||
if resources.db != nil {
|
||||
t.Fatal("expected nil db after dev fallback")
|
||||
}
|
||||
if resources.redisCache != nil {
|
||||
t.Fatal("expected nil redis cache after dev fallback")
|
||||
}
|
||||
if len(logger.warnMessages) == 0 {
|
||||
t.Fatal("expected warning logs during dev resource fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRuntime_RejectsUnsupportedEnv(t *testing.T) {
|
||||
_, err := buildRuntimeWithFactory(RuntimeOptions{
|
||||
Env: "qa",
|
||||
@@ -355,6 +444,74 @@ func TestBuildRuntime_DevFallbackLogsWarnings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRuntimeResources_GroupsExternalDependencies(t *testing.T) {
|
||||
db := &repository.DB{}
|
||||
redisCache := &cache.RedisCache{}
|
||||
|
||||
resources := buildRuntimeResources(db, redisCache)
|
||||
if resources.db != db {
|
||||
t.Fatal("expected db resource to be preserved")
|
||||
}
|
||||
if resources.redisCache != redisCache {
|
||||
t.Fatal("expected redis cache resource to be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRuntimeStartupViews_GroupsHTTPAndBackgroundDependencies(t *testing.T) {
|
||||
supplyAPI, alertAPI := mustBuildTestAPIs(t)
|
||||
logger := testLogger{}
|
||||
authMiddleware := &middleware.AuthMiddleware{}
|
||||
rateLimitConfig := &middleware.RateLimitConfig{Enabled: true}
|
||||
tuning := defaultRuntimeTuning()
|
||||
subscriber := stubRevocationSubscriber{}
|
||||
|
||||
views := buildRuntimeStartupViews(
|
||||
"staging",
|
||||
logger,
|
||||
config.ServerConfig{},
|
||||
tuning,
|
||||
runtimeSecurityBundle{
|
||||
authMiddleware: authMiddleware,
|
||||
revocationSubscriber: subscriber,
|
||||
},
|
||||
runtimeAPIBundle{
|
||||
supplyAPI: supplyAPI,
|
||||
alertAPI: alertAPI,
|
||||
rateLimitConfig: rateLimitConfig,
|
||||
},
|
||||
)
|
||||
if views.http.env != "staging" {
|
||||
t.Fatalf("unexpected http env: %s", views.http.env)
|
||||
}
|
||||
if views.background.env != "staging" {
|
||||
t.Fatalf("unexpected background env: %s", views.background.env)
|
||||
}
|
||||
if views.http.serverConfig.Addr != ":18082" {
|
||||
t.Fatalf("unexpected default addr: %s", views.http.serverConfig.Addr)
|
||||
}
|
||||
if views.http.serverConfig.ShutdownTimeout != 5*time.Second {
|
||||
t.Fatalf("unexpected default shutdown timeout: %s", views.http.serverConfig.ShutdownTimeout)
|
||||
}
|
||||
if views.http.supplyAPI != supplyAPI {
|
||||
t.Fatal("expected supply api to be preserved")
|
||||
}
|
||||
if views.http.alertAPI != alertAPI {
|
||||
t.Fatal("expected alert api to be preserved")
|
||||
}
|
||||
if views.http.authMiddleware != authMiddleware {
|
||||
t.Fatal("expected auth middleware to be preserved")
|
||||
}
|
||||
if views.http.rateLimitConfig != rateLimitConfig {
|
||||
t.Fatal("expected rate limit config to be preserved")
|
||||
}
|
||||
if views.background.revocationSubscriber != subscriber {
|
||||
t.Fatal("expected revocation subscriber to be preserved")
|
||||
}
|
||||
if views.background.tuning.outboxStreamName != tuning.outboxStreamName {
|
||||
t.Fatalf("unexpected background outbox stream: %s", views.background.tuning.outboxStreamName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRuntime_GroupsResourcesAndStartupViews(t *testing.T) {
|
||||
runtime, err := buildRuntimeWithFactory(RuntimeOptions{
|
||||
Env: "dev",
|
||||
|
||||
Reference in New Issue
Block a user