refactor(supply-api): split bootstrap http assembly

This commit is contained in:
Your Name
2026-04-16 07:11:33 +08:00
parent 39c4a11ff9
commit b9b875ac39
3 changed files with 231 additions and 21 deletions

View File

@@ -0,0 +1,129 @@
# Supply API Bootstrap HTTP Chain 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` 中的 HTTP 装配链拆清楚,让 `BuildServer` 只负责校验和组装,路由注册与中间件编排各自收口到独立 helper。
**Architecture:** 保留现有 `BuildServer` 外部接口与运行时语义,新增路由装配 helper 负责 health/supply/alert 路由注册,新增中间件链 helper 负责通用中间件和 prod/staging 鉴权链编排。先写 helper 级失败测试锁住对外可观测行为,再做最小重构并跑整组回归。
**Tech Stack:** Go, Go test
---
### Task 1: 提取路由注册 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 TestBuildRouteMux_RegistersHealthAndSupplyRoutes(t *testing.T) {
mux := buildRouteMux(buildRouteMuxOptions{...})
// /actuator/health => 200
// /api/v1/supply/accounts/verify => 405 on GET, proving route exists
}
```
**Step 2: Run test to verify it fails**
Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildRouteMux_RegistersHealthAndSupplyRoutes' -v`
Expected: FAIL因为 helper 尚不存在
**Step 3: Write minimal implementation**
```go
type buildRouteMuxOptions struct { ... }
func buildRouteMux(opts buildRouteMuxOptions) *http.ServeMux { ... }
```
**Step 4: Run test to verify it passes**
Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildRouteMux_RegistersHealthAndSupplyRoutes' -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 route mux builder"
```
### Task 2: 提取中间件链 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 TestBuildMiddlewareChain_ProdRejectsQueryKey(t *testing.T) {
handler := buildMiddlewareChain(..., http.HandlerFunc(...))
// /api/v1/supply/accounts?token=bad => 401
}
```
**Step 2: Run test to verify it fails**
Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildMiddlewareChain_ProdRejectsQueryKey' -v`
Expected: FAIL因为 helper 尚不存在
**Step 3: Write minimal implementation**
```go
type middlewareChainOptions struct { ... }
func buildMiddlewareChain(opts middlewareChainOptions, next http.Handler) http.Handler { ... }
```
**Step 4: Run test to verify it passes**
Run: `cd "supply-api" && go test ./internal/app -run 'TestBuildMiddlewareChain_ProdRejectsQueryKey' -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 middleware chain"
```
### Task 3: 回归验证与收尾
**Files:**
- Modify: `supply-api/internal/app/bootstrap.go`
- Verify: `supply-api/internal/app/bootstrap_test.go`
- Verify: `supply-api/internal/app/runtime.go`
**Step 1: Run focused tests**
Run: `cd "supply-api" && go test ./internal/app -run 'Test(BuildRouteMux_RegistersHealthAndSupplyRoutes|BuildMiddlewareChain_ProdRejectsQueryKey|BuildServer_.*)' -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-http-chain-plan.md supply-api/internal/app/bootstrap.go supply-api/internal/app/bootstrap_test.go
git commit -m "refactor(supply-api): split bootstrap http assembly"
```

View File

@@ -26,6 +26,20 @@ type BuildServerOptions struct {
RedisHealthCheck func(context.Context) error
}
type buildRouteMuxOptions struct {
SupplyAPI *httpapi.SupplyAPI
AlertAPI *httpapi.AlertAPI
DBHealthCheck func(context.Context) error
RedisHealthCheck func(context.Context) error
}
type middlewareChainOptions struct {
Env string
Logger logging.Logger
AuthMiddleware *middleware.AuthMiddleware
RateLimitConfig *middleware.RateLimitConfig
}
// BuildServer 构建可复用的 HTTP server 与 handler 装配。
func BuildServer(opts BuildServerOptions) (*http.Server, error) {
if opts.SupplyAPI == nil {
@@ -52,13 +66,18 @@ func BuildServer(opts BuildServerOptions) (*http.Server, error) {
rateLimitConfig.Enabled = env != "dev"
}
mux := http.NewServeMux()
healthHandler := httpapi.NewHealthHandlerWithDefaults(opts.DBHealthCheck, opts.RedisHealthCheck)
healthHandler.RegisterRoutes(mux)
opts.SupplyAPI.Register(mux)
opts.AlertAPI.Register(mux)
handler := buildHandler(env, mux, opts.Logger, opts.AuthMiddleware, rateLimitConfig)
mux := buildRouteMux(buildRouteMuxOptions{
SupplyAPI: opts.SupplyAPI,
AlertAPI: opts.AlertAPI,
DBHealthCheck: opts.DBHealthCheck,
RedisHealthCheck: opts.RedisHealthCheck,
})
handler := buildMiddlewareChain(middlewareChainOptions{
Env: env,
Logger: opts.Logger,
AuthMiddleware: opts.AuthMiddleware,
RateLimitConfig: rateLimitConfig,
}, mux)
serverConfig := normalizeServerConfig(opts.ServerConfig)
@@ -91,24 +110,27 @@ func normalizeServerConfig(serverConfig config.ServerConfig) config.ServerConfig
return serverConfig
}
func buildHandler(
env string,
mux *http.ServeMux,
logger logging.Logger,
authMiddleware *middleware.AuthMiddleware,
rateLimitConfig *middleware.RateLimitConfig,
) http.Handler {
var handler http.Handler = mux
func buildRouteMux(opts buildRouteMuxOptions) *http.ServeMux {
mux := http.NewServeMux()
healthHandler := httpapi.NewHealthHandlerWithDefaults(opts.DBHealthCheck, opts.RedisHealthCheck)
healthHandler.RegisterRoutes(mux)
opts.SupplyAPI.Register(mux)
opts.AlertAPI.Register(mux)
return mux
}
func buildMiddlewareChain(opts middlewareChainOptions, next http.Handler) http.Handler {
var handler http.Handler = next
handler = middleware.RequestID(handler)
handler = middleware.Recovery(handler)
handler = middleware.Logging(handler, logger)
handler = middleware.Logging(handler, opts.Logger)
handler = middleware.TracingMiddleware(handler)
if env != "dev" {
handler = middleware.NewRateLimitHandler(rateLimitConfig, handler)
handler = authMiddleware.TokenVerifyMiddleware(handler)
handler = authMiddleware.BearerExtractMiddleware(handler)
handler = authMiddleware.QueryKeyRejectMiddleware(handler)
if opts.Env != "dev" {
handler = middleware.NewRateLimitHandler(opts.RateLimitConfig, handler)
handler = opts.AuthMiddleware.TokenVerifyMiddleware(handler)
handler = opts.AuthMiddleware.BearerExtractMiddleware(handler)
handler = opts.AuthMiddleware.QueryKeyRejectMiddleware(handler)
}
return handler

View File

@@ -144,6 +144,29 @@ func TestBuildServer_DefaultsTimeoutsWhenUnset(t *testing.T) {
}
}
func TestBuildRouteMux_RegistersHealthAndSupplyRoutes(t *testing.T) {
supplyAPI, alertAPI := mustBuildTestAPIs(t)
mux := buildRouteMux(buildRouteMuxOptions{
SupplyAPI: supplyAPI,
AlertAPI: alertAPI,
})
healthReq := httptest.NewRequest(http.MethodGet, "/actuator/health", nil)
healthRec := httptest.NewRecorder()
mux.ServeHTTP(healthRec, healthReq)
if healthRec.Code != http.StatusOK {
t.Fatalf("unexpected health status: got=%d want=%d", healthRec.Code, http.StatusOK)
}
supplyReq := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts/verify", nil)
supplyRec := httptest.NewRecorder()
mux.ServeHTTP(supplyRec, supplyReq)
if supplyRec.Code != http.StatusMethodNotAllowed {
t.Fatalf("unexpected supply status: got=%d want=%d", supplyRec.Code, http.StatusMethodNotAllowed)
}
}
func mustBuildTestAPIs(t *testing.T) (*httpapi.SupplyAPI, *httpapi.AlertAPI) {
t.Helper()
@@ -205,3 +228,39 @@ func TestBuildServer_ProdBuildsAuthenticatedHandler(t *testing.T) {
t.Fatal("expected non-nil server and handler")
}
}
func TestBuildMiddlewareChain_ProdRejectsQueryKey(t *testing.T) {
authMiddleware := middleware.NewAuthMiddleware(
middleware.AuthConfig{
SecretKey: "bootstrap-test-secret-key",
Algorithm: "HS256",
Issuer: "bootstrap-test",
Enabled: true,
},
middleware.NewTokenCache(),
nil,
nil,
)
handler := buildMiddlewareChain(middlewareChainOptions{
Env: "prod",
Logger: testLogger{},
AuthMiddleware: authMiddleware,
RateLimitConfig: &middleware.RateLimitConfig{
Enabled: false,
},
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts?token=bad", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("unexpected status: got=%d want=%d", rec.Code, http.StatusUnauthorized)
}
if !strings.Contains(rec.Body.String(), "QUERY_KEY_NOT_ALLOWED") {
t.Fatalf("unexpected body: %s", rec.Body.String())
}
}