package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/volcano-tts/tts-api/setting" ) // okHandler 是被保护的假 handler。 func okHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) } // TestRequireAdmin_EmptyCredentialDenies 凭证未配置时必须拒绝,不能放行。 // // 这是 v0.3.0 阶段 1 的核心修复:v0.3.0 之前 len(keys)==0 直接放行, // 导致"没配 key"的部署上管理接口完全裸奔,也让后续给 /metrics、/health // 套 RequireAdmin 的加固形同虚设。 func TestRequireAdmin_EmptyCredentialDenies(t *testing.T) { t.Cleanup(func() { setting.SetAdminKeys(nil, "") }) setting.SetAdminKeys(nil, "") h := RequireAdmin(okHandler()) // 完全不带 Authorization w := httptest.NewRecorder() h.ServeHTTP(w, httptest.NewRequest("GET", "/api/admin/overview", nil)) if w.Code != http.StatusUnauthorized { t.Errorf("无凭证 + 未配置 admin credential: code=%d, want 401", w.Code) } // 带任意 Bearer token 也必须拒绝(列表为空,没有合法 token 可言) w2 := httptest.NewRecorder() r2 := httptest.NewRequest("GET", "/api/admin/overview", nil) r2.Header.Set("Authorization", "Bearer anything") h.ServeHTTP(w2, r2) if w2.Code != http.StatusUnauthorized { t.Errorf("任意 token + 未配置 admin credential: code=%d, want 401", w2.Code) } } // TestRequireAdmin_ConfiguredCredentialEnforced 配了凭证后:命中放行、错误拒绝。 func TestRequireAdmin_ConfiguredCredentialEnforced(t *testing.T) { t.Cleanup(func() { setting.SetAdminKeys(nil, "") }) setting.SetAdminKeys([]string{"admin-secret"}, "admin_key") h := RequireAdmin(okHandler()) cases := []struct { name string authHeader string wantStatus int }{ {"正确凭证", "Bearer admin-secret", http.StatusOK}, {"错误凭证", "Bearer wrong-secret", http.StatusUnauthorized}, {"缺少 Bearer 前缀", "admin-secret", http.StatusUnauthorized}, {"空 Authorization", "", http.StatusUnauthorized}, {"仅空白 token", "Bearer ", http.StatusUnauthorized}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/api/admin/overview", nil) if c.authHeader != "" { r.Header.Set("Authorization", c.authHeader) } h.ServeHTTP(w, r) if w.Code != c.wantStatus { t.Errorf("code=%d, want %d", w.Code, c.wantStatus) } }) } } // TestRequireAdmin_OptionsAlwaysAllowed OPTIONS 预检必须放行(浏览器预检不带 Authorization)。 func TestRequireAdmin_OptionsAlwaysAllowed(t *testing.T) { t.Cleanup(func() { setting.SetAdminKeys(nil, "") }) setting.SetAdminKeys([]string{"admin-secret"}, "admin_key") w := httptest.NewRecorder() h := RequireAdmin(okHandler()) h.ServeHTTP(w, httptest.NewRequest("OPTIONS", "/api/admin/overview", nil)) if w.Code != http.StatusOK { t.Errorf("OPTIONS: code=%d, want 200", w.Code) } } // TestAdminAndBusinessCredentialIsolation 权限隔离: // 配置了独立 admin_key 后,业务凭证(auth_key)不得访问管理接口; // 未配置 admin_key 时回退,业务凭证仍可管理(向后兼容)。 func TestAdminAndBusinessCredentialIsolation(t *testing.T) { t.Cleanup(func() { setting.SetAdminKeys(nil, "") setting.SetAuthAPIKeys(nil) }) const businessKey = "business-key" const adminKey = "admin-key" // 业务侧凭证固定为 businessKey(模拟 /v1/audio/speech 用的 key) setting.SetAuthAPIKeys([]string{businessKey}) // --- 场景 A:配置了独立 admin_key(隔离生效) --- setting.SetAdminKeys([]string{adminKey}, "admin_key") h := RequireAdmin(okHandler()) wBiz := httptest.NewRecorder() rBiz := httptest.NewRequest("GET", "/api/admin/overview", nil) rBiz.Header.Set("Authorization", "Bearer "+businessKey) h.ServeHTTP(wBiz, rBiz) if wBiz.Code != http.StatusUnauthorized { t.Errorf("隔离生效时,业务 key 访问管理接口: code=%d, want 401", wBiz.Code) } wAdmin := httptest.NewRecorder() rAdmin := httptest.NewRequest("GET", "/api/admin/overview", nil) rAdmin.Header.Set("Authorization", "Bearer "+adminKey) h.ServeHTTP(wAdmin, rAdmin) if wAdmin.Code != http.StatusOK { t.Errorf("隔离生效时,admin_key 访问管理接口: code=%d, want 200", wAdmin.Code) } // --- 场景 B:未配置 admin_key,回退用业务凭证(向后兼容) --- setting.SetAdminKeys([]string{businessKey}, "auth_key") wFallback := httptest.NewRecorder() rFallback := httptest.NewRequest("GET", "/api/admin/overview", nil) rFallback.Header.Set("Authorization", "Bearer "+businessKey) h.ServeHTTP(wFallback, rFallback) if wFallback.Code != http.StatusOK { t.Errorf("回退模式下,业务 key 应可管理: code=%d, want 200", wFallback.Code) } }