refactor(supply-api): split bootstrap http assembly
This commit is contained in:
129
docs/plans/2026-04-16-supply-api-bootstrap-http-chain-plan.md
Normal file
129
docs/plans/2026-04-16-supply-api-bootstrap-http-chain-plan.md
Normal 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"
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user