yao/openapi/tests/setting/cloud_test.go
Max bba43c369a feat(setting): implement cloud settings endpoints and owner verification
- Added new endpoints for managing cloud settings under /setting/cloud, including GET, PUT, and POST methods.
- Introduced owner verification logic to ensure only team owners can modify settings, utilizing caching for efficiency.
- Enhanced data structures for cloud settings responses, including CloudRegion and CloudPageData types.
- Refactored existing functions to integrate new owner verification and error handling mechanisms.
2026-04-28 21:20:43 +08:00

330 lines
10 KiB
Go

package setting_test
import (
"bytes"
"encoding/json"
"net/http"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/openapi/oauth"
"github.com/yaoapp/yao/openapi/tests/testutils"
"github.com/yaoapp/yao/setting"
)
func initSettingRegistry(t *testing.T) {
t.Helper()
if setting.Global == nil {
if err := setting.Init(); err != nil {
t.Fatalf("setting.Init: %v", err)
}
}
}
func obtainToken(t *testing.T, serverURL string) string {
t.Helper()
client := testutils.RegisterTestClient(t, "Cloud Test", []string{"https://localhost/callback"})
t.Cleanup(func() { testutils.CleanupTestClient(t, client.ClientID) })
token := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
return token.AccessToken
}
// obtainRestrictedToken creates a token with specific scope (no system:root).
// Used to test ACL permission denial.
func obtainRestrictedToken(t *testing.T, serverURL, scope string) string {
t.Helper()
client := testutils.RegisterTestClient(t, "Cloud Restricted", []string{"https://localhost/callback"})
t.Cleanup(func() { testutils.CleanupTestClient(t, client.ClientID) })
oauthService := oauth.OAuth
if oauthService == nil {
t.Fatal("Global OAuth service not initialized")
}
token := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
subject, err := oauthService.Subject(client.ClientID, token.UserID)
if err != nil {
t.Fatalf("Failed to create subject: %v", err)
}
accessToken, err := oauthService.MakeAccessToken(client.ClientID, scope, subject, 3600)
if err != nil {
t.Fatalf("Failed to create access token: %v", err)
}
return accessToken
}
// ----------- Functional tests (system:root token) -----------
func TestCloudGet(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/cloud", nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if !assert.NoError(t, err) || !assert.NotNil(t, resp) {
return
}
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&body)
assert.NoError(t, err)
assert.Contains(t, body, "regions")
assert.Contains(t, body, "region")
assert.Contains(t, body, "api_url")
assert.Contains(t, body, "api_key")
assert.Contains(t, body, "status")
regions, ok := body["regions"].([]interface{})
assert.True(t, ok)
assert.GreaterOrEqual(t, len(regions), 4)
assert.Equal(t, "unconfigured", body["status"])
assert.Equal(t, "", body["api_key"])
}
func TestCloudGetUnauthenticated(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
req, err := http.NewRequest("GET", serverURL+baseURL()+"/setting/cloud", nil)
assert.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
func TestCloudUpdate(t *testing.T) {
apiKey := os.Getenv("CLOUD_TEST_API_KEY")
if apiKey == "" {
t.Skip("CLOUD_TEST_API_KEY not set, skipping cloud update test (key validation required)")
}
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
payload := map[string]interface{}{
"region": "us",
"api_url": "https://api-us.yao.run",
"api_key": apiKey,
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/cloud", bytes.NewReader(raw))
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var body map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&body)
assert.NoError(t, err)
assert.Equal(t, "us", body["region"])
assert.Equal(t, "https://api-us.yao.run", body["api_url"])
assert.Equal(t, "connected", body["status"])
maskedKey, _ := body["api_key"].(string)
assert.True(t, strings.Contains(maskedKey, "..."), "masked key should use prefix...suffix format")
// GET should also return masked key and connected status
req2, _ := http.NewRequest("GET", serverURL+baseURL()+"/setting/cloud", nil)
req2.Header.Set("Authorization", "Bearer "+token)
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
var body2 map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&body2)
assert.Equal(t, "us", body2["region"])
assert.Equal(t, "connected", body2["status"])
}
func TestCloudUpdateInvalidKey(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
payload := map[string]interface{}{
"region": "us",
"api_url": "https://api-us.yao.run",
"api_key": "sk-invalid-key-that-should-fail",
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/cloud", bytes.NewReader(raw))
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "invalid API key should be rejected")
}
func TestCloudUpdateInvalidRegion(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
payload := map[string]interface{}{
"region": "mars",
"api_url": "https://api-mars.yao.run",
"api_key": "sk-test",
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/cloud", bytes.NewReader(raw))
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
func TestCloudTest(t *testing.T) {
apiKey := os.Getenv("CLOUD_TEST_API_KEY")
if apiKey == "" {
t.Skip("CLOUD_TEST_API_KEY not set, skipping cloud connection test")
}
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
// Save config first (key is validated during save)
payload := map[string]interface{}{
"region": "us",
"api_url": "https://api-us.yao.run",
"api_key": apiKey,
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/cloud", bytes.NewReader(raw))
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Test connection with explicit api_url and api_key
testPayload := map[string]interface{}{
"api_url": "https://api-us.yao.run",
"api_key": apiKey,
}
testRaw, _ := json.Marshal(testPayload)
req2, err := http.NewRequest("POST", serverURL+baseURL()+"/setting/cloud/test", bytes.NewReader(testRaw))
assert.NoError(t, err)
req2.Header.Set("Authorization", "Bearer "+token)
req2.Header.Set("Content-Type", "application/json")
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode)
var body map[string]interface{}
json.NewDecoder(resp2.Body).Decode(&body)
assert.Equal(t, true, body["success"])
assert.NotEmpty(t, body["message"])
}
// ----------- ACL permission tests -----------
func TestCloudACL_ReadOnlyScopeCannotWrite(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
// Token with read-only scope (no system:root, only setting:cloud:read:all)
readToken := obtainRestrictedToken(t, serverURL, "setting:cloud:read:all")
// GET should work
req, _ := http.NewRequest("GET", serverURL+baseURL()+"/setting/cloud", nil)
req.Header.Set("Authorization", "Bearer "+readToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "read-only scope should allow GET")
// PUT should be denied
payload := map[string]interface{}{
"region": "cn",
"api_url": "https://api.yaoagents.cn",
"api_key": "sk-test",
}
raw, _ := json.Marshal(payload)
req2, _ := http.NewRequest("PUT", serverURL+baseURL()+"/setting/cloud", bytes.NewReader(raw))
req2.Header.Set("Authorization", "Bearer "+readToken)
req2.Header.Set("Content-Type", "application/json")
resp2, err := http.DefaultClient.Do(req2)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusForbidden, resp2.StatusCode, "read-only scope should deny PUT")
}
func TestCloudACL_NoScopeCannotAccess(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
// Token with irrelevant scope (no setting scopes at all)
noSettingToken := obtainRestrictedToken(t, serverURL, "kb:collections:read:all")
req, _ := http.NewRequest("GET", serverURL+baseURL()+"/setting/cloud", nil)
req.Header.Set("Authorization", "Bearer "+noSettingToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusForbidden, resp.StatusCode, "token without setting scope should be denied")
}
func TestCloudUpdateRegionOnly(t *testing.T) {
serverURL := testutils.Prepare(t)
defer testutils.Clean()
initSettingRegistry(t)
token := obtainToken(t, serverURL)
payload := map[string]interface{}{
"region": "cn",
"api_url": "https://api.yaoagents.cn",
}
raw, _ := json.Marshal(payload)
req, err := http.NewRequest("PUT", serverURL+baseURL()+"/setting/cloud", bytes.NewReader(raw))
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "update without api_key should succeed (no validation needed)")
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
assert.Equal(t, "cn", body["region"])
assert.Equal(t, "https://api.yaoagents.cn", body["api_url"])
}