Files
flvx/go-backend/internal/http/handler/config_access_test.go
T

364 lines
13 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"go-backend/internal/auth"
"go-backend/internal/http/middleware"
"go-backend/internal/http/response"
"go-backend/internal/store/repo"
)
func TestPublicConfigGetAllowsBrandKeys(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
seedConfigValue(t, r, "app_name", "FLVX Brand")
seedConfigValue(t, r, "app_logo", "logo-data")
seedConfigValue(t, r, "app_favicon", "favicon-data")
seedConfigValue(t, r, "app_bg_image", "bg-data")
seedConfigValue(t, r, "app_bg_image_light", "light-bg-data")
seedConfigValue(t, r, "app_bg_image_dark", "dark-bg-data")
seedConfigValue(t, r, "cloudflare_site_key", "site-key")
req := httptest.NewRequest(http.MethodPost, "/api/v1/public/config/get", bytes.NewBufferString(`{"name":"app_name"}`))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCode(t, resp, 0)
for name, want := range map[string]string{
"app_bg_image_light": "light-bg-data",
"app_bg_image_dark": "dark-bg-data",
} {
req := httptest.NewRequest(http.MethodPost, "/api/v1/public/config/get", bytes.NewBufferString(`{"name":"`+name+`"}`))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerConfigValue(t, resp, name, want)
}
}
func TestPublicBrandConfigFallsBackWithoutCommercialLicense(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
seedConfigValue(t, r, "app_name", "Paid Brand")
seedConfigValue(t, r, "app_logo", "logo-data")
seedConfigValue(t, r, "app_favicon", "favicon-data")
seedConfigValue(t, r, "hide_footer_brand", "true")
seedConfigValue(t, r, "is_commercial", "false")
for name, want := range map[string]string{
"app_name": "FLVX",
"app_logo": "",
"app_favicon": "",
"hide_footer_brand": "false",
} {
req := httptest.NewRequest(http.MethodPost, "/api/v1/public/config/get", bytes.NewBufferString(`{"name":"`+name+`"}`))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerConfigValue(t, resp, name, want)
}
}
func TestPublicBrandConfigUsesSavedValuesWithCommercialLicense(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
seedConfigValue(t, r, "app_name", "Paid Brand")
seedConfigValue(t, r, "is_commercial", "true")
req := httptest.NewRequest(http.MethodPost, "/api/v1/public/config/get", bytes.NewBufferString(`{"name":"app_name"}`))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerConfigValue(t, resp, "app_name", "Paid Brand")
}
func TestPublicConfigGetRejectsSensitiveKeys(t *testing.T) {
router, _ := setupConfigAccessTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/api/v1/public/config/get", bytes.NewBufferString(`{"name":"jwt_secret"}`))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCodeMsg(t, resp, 403, "禁止访问敏感配置")
}
func TestConfigGetAllowsPublicCloudflareSiteKeyWithoutAuthForCachedLoginPage(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
seedConfigValue(t, r, "cloudflare_site_key", "site-key")
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"cloudflare_site_key"}`))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerConfigValue(t, resp, "cloudflare_site_key", "site-key")
}
func TestConfigGetRejectsSensitiveKeysWithoutAuth(t *testing.T) {
router, _ := setupConfigAccessTestRouter(t)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"jwt_secret"}`))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCodeMsg(t, resp, 403, "禁止访问敏感配置")
}
func TestConfigGetAllowsSensitiveKeysForAdmin(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
seedConfigValue(t, r, "jwt_secret", "jwt-secret")
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"jwt_secret"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerConfigValue(t, resp, "jwt_secret", "jwt-secret")
}
func TestConfigGetNeverReturnsLicenseCredentials(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
seedConfigValue(t, r, "license_key", "license-secret")
seedConfigValue(t, r, "machine_fingerprint", "fingerprint-secret")
for _, name := range []string{"license_key", "license_machine_id", "machine_fingerprint"} {
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"`+name+`"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCodeMsg(t, resp, 403, "禁止访问系统授权凭据")
}
}
func TestConfigListNeverReturnsLicenseCredentials(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
seedConfigValue(t, r, "license_key", "license-secret")
seedConfigValue(t, r, "license_machine_id", "machine-id")
seedConfigValue(t, r, "machine_fingerprint", "fingerprint-secret")
seedConfigValue(t, r, "is_commercial", "true")
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/list", nil)
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
var out struct {
Code int `json:"code"`
Data map[string]string `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode response: %v", err)
}
if out.Code != 0 || out.Data["is_commercial"] != "true" {
t.Fatalf("unexpected config response: %+v", out)
}
if _, ok := out.Data["license_key"]; ok {
t.Fatal("license_key must not be returned")
}
if _, ok := out.Data["license_machine_id"]; ok {
t.Fatal("license_machine_id must not be returned")
}
if _, ok := out.Data["machine_fingerprint"]; ok {
t.Fatal("machine_fingerprint must not be returned")
}
}
func TestConfigUpdateAllowsSensitiveKeysForAdmin(t *testing.T) {
router, _ := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update", bytes.NewBufferString(`{"jwt_secret":"rotated-secret"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCode(t, resp, 0)
}
func TestConfigUpdateSingleAllowsSensitiveKeysForAdmin(t *testing.T) {
router, _ := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update-single", bytes.NewBufferString(`{"name":"jwt_secret","value":"rotated-secret"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCode(t, resp, 0)
}
func TestConfigUpdateAllowsCloudflareSecretKeyWrite(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update", bytes.NewBufferString(`{"cloudflare_secret_key":"turnstile-secret"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCode(t, resp, 0)
cfg, err := r.GetConfigByName("cloudflare_secret_key")
if err != nil {
t.Fatalf("get config: %v", err)
}
if cfg == nil || cfg.Value != "turnstile-secret" {
t.Fatalf("expected cloudflare_secret_key to be updated, got %#v", cfg)
}
}
func TestConfigUpdateSingleAllowsCloudflareSecretKeyWrite(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update-single", bytes.NewBufferString(`{"name":"cloudflare_secret_key","value":"turnstile-secret"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCode(t, resp, 0)
cfg, err := r.GetConfigByName("cloudflare_secret_key")
if err != nil {
t.Fatalf("get config: %v", err)
}
if cfg == nil || cfg.Value != "turnstile-secret" {
t.Fatalf("expected cloudflare_secret_key to be updated, got %#v", cfg)
}
}
func TestConfigUpdateRejectsLicenseKeyWrite(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update", bytes.NewBufferString(`{"license_key":"license-secret"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCodeMsg(t, resp, -1, "该配置由系统管理")
if cfg, err := r.GetConfigByName("license_key"); err != nil || cfg != nil {
t.Fatalf("license_key should not be written, got %#v err=%v", cfg, err)
}
}
func TestConfigUpdateSingleRejectsLicenseKeyWrite(t *testing.T) {
router, r := setupConfigAccessTestRouter(t)
adminToken := mustGenerateConfigAccessToken(t, 1, "admin_user", 0)
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update-single", bytes.NewBufferString(`{"name":"license_key","value":"license-secret"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", adminToken)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
assertHandlerCodeMsg(t, resp, -1, "该配置由系统管理")
if cfg, err := r.GetConfigByName("license_key"); err != nil || cfg != nil {
t.Fatalf("license_key should not be written, got %#v err=%v", cfg, err)
}
}
func setupConfigAccessTestRouter(t *testing.T) (http.Handler, *repo.Repository) {
t.Helper()
r, err := repo.Open(t.TempDir() + "/config-access.db")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() {
_ = r.Close()
})
h := New(r, "unit-test-secret")
mux := http.NewServeMux()
h.Register(mux)
wrapped := middleware.Recover(mux)
wrapped = middleware.JWT(middleware.AuthOptions{JWTSecret: "unit-test-secret", GetUserAuthState: h.GetUserAuthState})(wrapped)
wrapped = middleware.RequestLog(wrapped)
wrapped = middleware.CORS(wrapped)
return wrapped, r
}
func seedConfigValue(t *testing.T, r *repo.Repository, name, value string) {
t.Helper()
if err := r.DB().Exec(`INSERT INTO vite_config(name, value, time) VALUES(?, ?, 0) ON CONFLICT(name) DO UPDATE SET value = excluded.value, time = excluded.time`, name, value).Error; err != nil {
t.Fatalf("seed config %s: %v", name, err)
}
}
func mustGenerateConfigAccessToken(t *testing.T, userID int64, username string, roleID int) string {
t.Helper()
token, err := auth.GenerateToken(userID, username, roleID, "unit-test-secret")
if err != nil {
t.Fatalf("generate token: %v", err)
}
return token
}
func assertHandlerCode(t *testing.T, rec *httptest.ResponseRecorder, expected int) {
t.Helper()
var out response.R
if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
t.Fatalf("decode response: %v", err)
}
if out.Code != expected {
t.Fatalf("expected code %d, got %d", expected, out.Code)
}
}
func assertHandlerCodeMsg(t *testing.T, rec *httptest.ResponseRecorder, expectedCode int, expectedMsg string) {
t.Helper()
var out response.R
if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
t.Fatalf("decode response: %v", err)
}
if out.Code != expectedCode || out.Msg != expectedMsg {
t.Fatalf("expected (%d,%q), got (%d,%q)", expectedCode, expectedMsg, out.Code, out.Msg)
}
}
func assertHandlerConfigValue(t *testing.T, rec *httptest.ResponseRecorder, expectedName, expectedValue string) {
t.Helper()
var out struct {
Code int `json:"code"`
Data struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"data"`
}
if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
t.Fatalf("decode response: %v", err)
}
if out.Code != 0 {
t.Fatalf("expected code 0, got %d", out.Code)
}
if out.Data.Name != expectedName || out.Data.Value != expectedValue {
t.Fatalf("expected config (%q,%q), got (%q,%q)", expectedName, expectedValue, out.Data.Name, out.Data.Value)
}
}