diff --git a/docs/plans/2026-04-16-supply-api-bootstrap-options-resolve-plan.md b/docs/plans/2026-04-16-supply-api-bootstrap-options-resolve-plan.md new file mode 100644 index 00000000..73b7dad3 --- /dev/null +++ b/docs/plans/2026-04-16-supply-api-bootstrap-options-resolve-plan.md @@ -0,0 +1,130 @@ +# Supply API Bootstrap Options Resolve Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 把 `supply-api/internal/app/bootstrap.go` 中剩余的参数校验和默认 rate-limit 装配收成更小的 helper,让 `BuildServer` 只保留声明式组装流程。 + +**Architecture:** 保留 `BuildServer` 现有外部接口和行为,新增 `resolveBuildServerOptions` 负责参数校验、环境解析和 server config 归一化,再新增 `resolveRateLimitConfig` 负责默认限流配置装配。`BuildServer` 最终只串联 `resolve -> buildRouteMux -> buildMiddlewareChain -> server` 四步。 + +**Tech Stack:** Go, Go test + +--- + +### Task 1: 提取 build server options resolve helper + +**Files:** +- Modify: `supply-api/internal/app/bootstrap.go` +- Modify: `supply-api/internal/app/bootstrap_test.go` + +**Step 1: Write the failing test** + +```go +func TestResolveBuildServerOptions_RequiresAuthOutsideDev(t *testing.T) { + _, err := resolveBuildServerOptions(BuildServerOptions{Env: "prod", ...}) + if err == nil { + t.Fatal("expected auth middleware requirement") + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveBuildServerOptions_RequiresAuthOutsideDev' -v` +Expected: FAIL,因为 helper 尚不存在 + +**Step 3: Write minimal implementation** + +```go +type resolvedBuildServerOptions struct { ... } +func resolveBuildServerOptions(opts BuildServerOptions) (resolvedBuildServerOptions, error) { ... } +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveBuildServerOptions_RequiresAuthOutsideDev' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/internal/app/bootstrap.go supply-api/internal/app/bootstrap_test.go +git commit -m "refactor(supply-api): extract bootstrap options resolver" +``` + +### Task 2: 提取默认 rate-limit helper + +**Files:** +- Modify: `supply-api/internal/app/bootstrap.go` +- Modify: `supply-api/internal/app/bootstrap_test.go` + +**Step 1: Write the failing test** + +```go +func TestResolveRateLimitConfig_DefaultsDisabledInDev(t *testing.T) { + cfg := resolveRateLimitConfig("dev", nil) + if cfg.Enabled { + t.Fatal("expected dev default to disable rate limit") + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveRateLimitConfig_(DefaultsDisabledInDev|DefaultsEnabledOutsideDev)' -v` +Expected: FAIL,因为 helper 尚不存在 + +**Step 3: Write minimal implementation** + +```go +func resolveRateLimitConfig(env string, cfg *middleware.RateLimitConfig) *middleware.RateLimitConfig { ... } +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveRateLimitConfig_(DefaultsDisabledInDev|DefaultsEnabledOutsideDev)' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/internal/app/bootstrap.go supply-api/internal/app/bootstrap_test.go +git commit -m "refactor(supply-api): extract bootstrap rate limit resolver" +``` + +### Task 3: 回归验证与收尾 + +**Files:** +- Modify: `supply-api/internal/app/bootstrap.go` +- Verify: `supply-api/internal/app/bootstrap_test.go` + +**Step 1: Run focused tests** + +Run: `cd "supply-api" && go test ./internal/app -run 'Test(ResolveBuildServerOptions_RequiresAuthOutsideDev|ResolveRateLimitConfig_(DefaultsDisabledInDev|DefaultsEnabledOutsideDev)|BuildServer_.*|BuildRouteMux_RegistersHealthAndSupplyRoutes|BuildMiddlewareChain_ProdRejectsQueryKey)' -v` +Expected: PASS + +**Step 2: Run package regression** + +Run: `cd "supply-api" && go test ./internal/app ./cmd/supply-api ./internal/httpapi` +Expected: PASS + +**Step 3: Run e2e tests** + +Run: `cd "supply-api" && go test -tags=e2e ./e2e` +Expected: PASS + +**Step 4: Run repo exit verification** + +Run: `bash "scripts/ci/repo_integrity_check.sh"` +Expected: PASS + +**Step 5: Check formatting** + +Run: `git diff --check` +Expected: no output + +**Step 6: Commit** + +```bash +git add docs/plans/2026-04-16-supply-api-bootstrap-options-resolve-plan.md supply-api/internal/app/bootstrap.go supply-api/internal/app/bootstrap_test.go +git commit -m "refactor(supply-api): declarify bootstrap server assembly" +``` diff --git a/supply-api/internal/app/bootstrap.go b/supply-api/internal/app/bootstrap.go index baa1742b..7b33759a 100644 --- a/supply-api/internal/app/bootstrap.go +++ b/supply-api/internal/app/bootstrap.go @@ -40,54 +40,77 @@ type middlewareChainOptions struct { RateLimitConfig *middleware.RateLimitConfig } +type resolvedBuildServerOptions struct { + Env string + ServerConfig config.ServerConfig + Logger logging.Logger + SupplyAPI *httpapi.SupplyAPI + AlertAPI *httpapi.AlertAPI + AuthMiddleware *middleware.AuthMiddleware + RateLimitConfig *middleware.RateLimitConfig + DBHealthCheck func(context.Context) error + RedisHealthCheck func(context.Context) error +} + // BuildServer 构建可复用的 HTTP server 与 handler 装配。 func BuildServer(opts BuildServerOptions) (*http.Server, error) { + resolved, err := resolveBuildServerOptions(opts) + if err != nil { + return nil, err + } + + mux := buildRouteMux(buildRouteMuxOptions{ + SupplyAPI: resolved.SupplyAPI, + AlertAPI: resolved.AlertAPI, + DBHealthCheck: resolved.DBHealthCheck, + RedisHealthCheck: resolved.RedisHealthCheck, + }) + handler := buildMiddlewareChain(middlewareChainOptions{ + Env: resolved.Env, + Logger: resolved.Logger, + AuthMiddleware: resolved.AuthMiddleware, + RateLimitConfig: resolved.RateLimitConfig, + }, mux) + + return &http.Server{ + Addr: resolved.ServerConfig.Addr, + Handler: handler, + ReadHeaderTimeout: resolved.ServerConfig.ReadTimeout, + ReadTimeout: resolved.ServerConfig.ReadTimeout, + WriteTimeout: resolved.ServerConfig.WriteTimeout, + IdleTimeout: resolved.ServerConfig.IdleTimeout, + }, nil +} + +func resolveBuildServerOptions(opts BuildServerOptions) (resolvedBuildServerOptions, error) { if opts.SupplyAPI == nil { - return nil, errors.New("supply api is required") + return resolvedBuildServerOptions{}, errors.New("supply api is required") } if opts.AlertAPI == nil { - return nil, errors.New("alert api is required") + return resolvedBuildServerOptions{}, errors.New("alert api is required") } if opts.Logger == nil { - return nil, errors.New("logger is required") + return resolvedBuildServerOptions{}, errors.New("logger is required") } env, err := ResolveEnv(opts.Env) if err != nil { - return nil, err + return resolvedBuildServerOptions{}, err } if env != "dev" && opts.AuthMiddleware == nil { - return nil, errors.New("auth middleware is required outside dev") + return resolvedBuildServerOptions{}, errors.New("auth middleware is required outside dev") } - rateLimitConfig := opts.RateLimitConfig - if rateLimitConfig == nil { - rateLimitConfig = middleware.DefaultRateLimitConfig() - rateLimitConfig.Enabled = env != "dev" - } - - mux := buildRouteMux(buildRouteMuxOptions{ + return resolvedBuildServerOptions{ + Env: env, + ServerConfig: normalizeServerConfig(opts.ServerConfig), + Logger: opts.Logger, SupplyAPI: opts.SupplyAPI, AlertAPI: opts.AlertAPI, + AuthMiddleware: opts.AuthMiddleware, + RateLimitConfig: resolveRateLimitConfig(env, opts.RateLimitConfig), DBHealthCheck: opts.DBHealthCheck, RedisHealthCheck: opts.RedisHealthCheck, - }) - handler := buildMiddlewareChain(middlewareChainOptions{ - Env: env, - Logger: opts.Logger, - AuthMiddleware: opts.AuthMiddleware, - RateLimitConfig: rateLimitConfig, - }, mux) - - serverConfig := normalizeServerConfig(opts.ServerConfig) - - return &http.Server{ - Addr: serverConfig.Addr, - Handler: handler, - ReadHeaderTimeout: serverConfig.ReadTimeout, - ReadTimeout: serverConfig.ReadTimeout, - WriteTimeout: serverConfig.WriteTimeout, - IdleTimeout: serverConfig.IdleTimeout, }, nil } @@ -110,6 +133,16 @@ func normalizeServerConfig(serverConfig config.ServerConfig) config.ServerConfig return serverConfig } +func resolveRateLimitConfig(env string, rateLimitConfig *middleware.RateLimitConfig) *middleware.RateLimitConfig { + if rateLimitConfig != nil { + return rateLimitConfig + } + + rateLimitConfig = middleware.DefaultRateLimitConfig() + rateLimitConfig.Enabled = env != "dev" + return rateLimitConfig +} + func buildRouteMux(opts buildRouteMuxOptions) *http.ServeMux { mux := http.NewServeMux() healthHandler := httpapi.NewHealthHandlerWithDefaults(opts.DBHealthCheck, opts.RedisHealthCheck) diff --git a/supply-api/internal/app/bootstrap_test.go b/supply-api/internal/app/bootstrap_test.go index 2c36add7..5cf4f21b 100644 --- a/supply-api/internal/app/bootstrap_test.go +++ b/supply-api/internal/app/bootstrap_test.go @@ -144,6 +144,43 @@ func TestBuildServer_DefaultsTimeoutsWhenUnset(t *testing.T) { } } +func TestResolveBuildServerOptions_RequiresAuthOutsideDev(t *testing.T) { + supplyAPI, alertAPI := mustBuildTestAPIs(t) + + _, err := resolveBuildServerOptions(BuildServerOptions{ + Env: "prod", + Logger: testLogger{}, + SupplyAPI: supplyAPI, + AlertAPI: alertAPI, + }) + if err == nil { + t.Fatal("expected auth middleware requirement outside dev") + } + if !strings.Contains(err.Error(), "auth middleware") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestResolveRateLimitConfig_DefaultsDisabledInDev(t *testing.T) { + cfg := resolveRateLimitConfig("dev", nil) + if cfg == nil { + t.Fatal("expected rate limit config") + } + if cfg.Enabled { + t.Fatal("expected dev default rate limit to be disabled") + } +} + +func TestResolveRateLimitConfig_DefaultsEnabledOutsideDev(t *testing.T) { + cfg := resolveRateLimitConfig("prod", nil) + if cfg == nil { + t.Fatal("expected rate limit config") + } + if !cfg.Enabled { + t.Fatal("expected non-dev default rate limit to be enabled") + } +} + func TestBuildRouteMux_RegistersHealthAndSupplyRoutes(t *testing.T) { supplyAPI, alertAPI := mustBuildTestAPIs(t)