mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
364 lines
13 KiB
Go
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)
|
|
}
|
|
}
|