- 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.
330 lines
10 KiB
Go
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"])
|
|
}
|