- Added concurrent fetching of runtime status for each robot in the ListRobots function, improving response efficiency. - Updated the Response struct to include new fields for runtime status: Running, MaxRunning, LastRun, and NextRun, optimizing dashboard display. - Implemented unit tests to verify the inclusion of runtime status fields in the ListRobots response, ensuring accurate data representation for users.
1032 lines
36 KiB
Go
1032 lines
36 KiB
Go
package openapi_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/yao/openapi"
|
|
"github.com/yaoapp/yao/openapi/tests/testutils"
|
|
)
|
|
|
|
// TestListRobots tests the robot listing endpoint
|
|
func TestListRobots(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Robot List Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
t.Run("ListRobotsSuccess", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// Verify pagination fields exist
|
|
assert.Contains(t, response, "data")
|
|
assert.Contains(t, response, "page")
|
|
assert.Contains(t, response, "pagesize")
|
|
assert.Contains(t, response, "total")
|
|
|
|
// Verify runtime status fields are included in robot items
|
|
if data, ok := response["data"].([]interface{}); ok && len(data) > 0 {
|
|
robot := data[0].(map[string]interface{})
|
|
// Runtime status fields should be present (added for dashboard optimization)
|
|
assert.Contains(t, robot, "running", "Robot should include running count")
|
|
assert.Contains(t, robot, "max_running", "Robot should include max_running")
|
|
// last_run and next_run are optional (omitempty)
|
|
}
|
|
})
|
|
|
|
t.Run("ListRobotsWithPagination", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?page=1&pagesize=5", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, float64(1), response["page"])
|
|
assert.Equal(t, float64(5), response["pagesize"])
|
|
})
|
|
|
|
t.Run("ListRobotsWithAutonomousModeFilter", func(t *testing.T) {
|
|
// Test with autonomous_mode=true
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?autonomous_mode=true", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// Verify response structure
|
|
assert.Contains(t, response, "data")
|
|
assert.Contains(t, response, "total")
|
|
|
|
// If there are robots, verify they are all autonomous
|
|
if data, ok := response["data"].([]interface{}); ok && len(data) > 0 {
|
|
for _, item := range data {
|
|
if robot, ok := item.(map[string]interface{}); ok {
|
|
assert.True(t, robot["autonomous_mode"].(bool), "All robots should have autonomous_mode=true")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ListRobotsWithAutonomousModeFalse", func(t *testing.T) {
|
|
// Test with autonomous_mode=false
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?autonomous_mode=false", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// Verify response structure
|
|
assert.Contains(t, response, "data")
|
|
assert.Contains(t, response, "total")
|
|
|
|
// If there are robots, verify they are all on-demand (not autonomous)
|
|
if data, ok := response["data"].([]interface{}); ok && len(data) > 0 {
|
|
for _, item := range data {
|
|
if robot, ok := item.(map[string]interface{}); ok {
|
|
assert.False(t, robot["autonomous_mode"].(bool), "All robots should have autonomous_mode=false")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ListRobotsIncludesRuntimeStatus", func(t *testing.T) {
|
|
// Verify that list response includes runtime status fields for dashboard optimization
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// Verify we have robots to test
|
|
data, ok := response["data"].([]interface{})
|
|
require.True(t, ok, "Response should have data array")
|
|
|
|
if len(data) > 0 {
|
|
robot := data[0].(map[string]interface{})
|
|
|
|
// Runtime status fields (running is always present, defaults to 0)
|
|
_, hasRunning := robot["running"]
|
|
assert.True(t, hasRunning, "Robot should include 'running' field for dashboard")
|
|
|
|
// max_running should be present (with omitempty, only if > 0)
|
|
// The field is returned by GetRobotStatus, so it should be there
|
|
if maxRunning, ok := robot["max_running"]; ok {
|
|
assert.GreaterOrEqual(t, maxRunning.(float64), float64(0), "max_running should be >= 0")
|
|
}
|
|
|
|
// robot_status should reflect runtime status
|
|
robotStatus, hasStatus := robot["robot_status"]
|
|
assert.True(t, hasStatus, "Robot should include 'robot_status' field")
|
|
if hasStatus {
|
|
validStatuses := []string{"idle", "working", "paused", "error", "maintenance"}
|
|
assert.Contains(t, validStatuses, robotStatus.(string), "robot_status should be a valid status")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ListRobotsUnauthorized", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots", nil)
|
|
require.NoError(t, err)
|
|
|
|
// No Authorization header
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestCreateRobot tests the robot creation endpoint
|
|
func TestCreateRobot(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Robot Create Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Track created robots for cleanup
|
|
var createdRobotIDs []string
|
|
defer func() {
|
|
// Cleanup created robots
|
|
for _, robotID := range createdRobotIDs {
|
|
req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
http.DefaultClient.Do(req)
|
|
}
|
|
}()
|
|
|
|
t.Run("CreateRobotSuccess", func(t *testing.T) {
|
|
robotID := fmt.Sprintf("test_robot_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_001",
|
|
"display_name": "Test Robot",
|
|
"bio": "A test robot for API testing",
|
|
"robot_email": "test@robot.local",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, robotID, response["member_id"])
|
|
assert.Equal(t, "Test Robot", response["display_name"])
|
|
assert.Equal(t, "A test robot for API testing", response["bio"])
|
|
|
|
// Track for cleanup
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
})
|
|
|
|
t.Run("CreateRobotMissingDisplayName", func(t *testing.T) {
|
|
// Missing display_name (the only required field)
|
|
createData := map[string]interface{}{
|
|
"team_id": "test_team_001",
|
|
// display_name is missing
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("CreateRobotAutoGenerateMemberID", func(t *testing.T) {
|
|
// member_id is optional - should be auto-generated
|
|
createData := map[string]interface{}{
|
|
"team_id": "test_team_001",
|
|
"display_name": "Auto ID Robot",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// Verify member_id was auto-generated
|
|
memberID, ok := response["member_id"].(string)
|
|
assert.True(t, ok, "member_id should be a string")
|
|
assert.NotEmpty(t, memberID, "member_id should be auto-generated")
|
|
assert.Len(t, memberID, 12, "auto-generated member_id should be 12 digits")
|
|
t.Logf("Auto-generated member_id: %s", memberID)
|
|
|
|
// Cleanup
|
|
createdRobotIDs = append(createdRobotIDs, memberID)
|
|
})
|
|
|
|
t.Run("CreateRobotDuplicate", func(t *testing.T) {
|
|
robotID := fmt.Sprintf("test_robot_dup_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_001",
|
|
"display_name": "Test Robot Duplicate",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
|
|
// First create
|
|
req1, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
req1.Header.Set("Content-Type", "application/json")
|
|
req1.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
resp1, err := http.DefaultClient.Do(req1)
|
|
require.NoError(t, err)
|
|
resp1.Body.Close()
|
|
assert.Equal(t, http.StatusCreated, resp1.StatusCode)
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
|
|
// Second create with same ID should fail
|
|
body2, _ := json.Marshal(createData)
|
|
req2, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body2))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
req2.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
resp2, err := http.DefaultClient.Do(req2)
|
|
require.NoError(t, err)
|
|
defer resp2.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusConflict, resp2.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestGetRobot tests the robot get endpoint
|
|
func TestGetRobot(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Robot Get Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a test robot first
|
|
robotID := fmt.Sprintf("test_robot_get_%d", time.Now().UnixNano())
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_001",
|
|
"display_name": "Test Robot Get",
|
|
"bio": "A robot for get test",
|
|
}
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
createResp, err := http.DefaultClient.Do(createReq)
|
|
require.NoError(t, err)
|
|
createResp.Body.Close()
|
|
|
|
// Cleanup
|
|
defer func() {
|
|
req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
http.DefaultClient.Do(req)
|
|
}()
|
|
|
|
t.Run("GetRobotSuccess", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, robotID, response["member_id"])
|
|
assert.Equal(t, "Test Robot Get", response["display_name"])
|
|
})
|
|
|
|
t.Run("GetRobotNotFound", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/non_existent_robot", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestUpdateRobot tests the robot update endpoint
|
|
func TestUpdateRobot(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Robot Update Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a test robot first
|
|
robotID := fmt.Sprintf("test_robot_update_%d", time.Now().UnixNano())
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_001",
|
|
"display_name": "Test Robot Update",
|
|
"bio": "Original bio",
|
|
}
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
createResp, err := http.DefaultClient.Do(createReq)
|
|
require.NoError(t, err)
|
|
createResp.Body.Close()
|
|
|
|
// Cleanup
|
|
defer func() {
|
|
req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
http.DefaultClient.Do(req)
|
|
}()
|
|
|
|
t.Run("UpdateRobotSuccess", func(t *testing.T) {
|
|
updateData := map[string]interface{}{
|
|
"display_name": "Updated Robot Name",
|
|
"bio": "Updated bio",
|
|
}
|
|
|
|
body, _ := json.Marshal(updateData)
|
|
req, err := http.NewRequest("PUT", serverURL+baseURL+"/agent/robots/"+robotID, bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "Updated Robot Name", response["display_name"])
|
|
assert.Equal(t, "Updated bio", response["bio"])
|
|
})
|
|
|
|
t.Run("UpdateRobotNotFound", func(t *testing.T) {
|
|
updateData := map[string]interface{}{
|
|
"display_name": "Updated Name",
|
|
}
|
|
|
|
body, _ := json.Marshal(updateData)
|
|
req, err := http.NewRequest("PUT", serverURL+baseURL+"/agent/robots/non_existent_robot", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestDeleteRobot tests the robot delete endpoint
|
|
func TestDeleteRobot(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Robot Delete Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
t.Run("DeleteRobotSuccess", func(t *testing.T) {
|
|
// Create a test robot first
|
|
robotID := fmt.Sprintf("test_robot_delete_%d", time.Now().UnixNano())
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_001",
|
|
"display_name": "Test Robot Delete",
|
|
}
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
createResp, err := http.DefaultClient.Do(createReq)
|
|
require.NoError(t, err)
|
|
createResp.Body.Close()
|
|
|
|
// Delete the robot
|
|
req, err := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, true, response["deleted"])
|
|
assert.Equal(t, robotID, response["member_id"])
|
|
|
|
// Verify it's deleted by trying to get it
|
|
getReq, _ := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
getResp, err := http.DefaultClient.Do(getReq)
|
|
require.NoError(t, err)
|
|
defer getResp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusNotFound, getResp.StatusCode)
|
|
})
|
|
|
|
t.Run("DeleteRobotNotFound", func(t *testing.T) {
|
|
req, err := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/non_existent_robot", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestGetRobotStatus tests the robot status endpoint
|
|
func TestGetRobotStatus(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Robot Status Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a test robot first
|
|
robotID := fmt.Sprintf("test_robot_status_%d", time.Now().UnixNano())
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_001",
|
|
"display_name": "Test Robot Status",
|
|
"autonomous_mode": true,
|
|
}
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
createResp, err := http.DefaultClient.Do(createReq)
|
|
require.NoError(t, err)
|
|
createResp.Body.Close()
|
|
|
|
// Cleanup
|
|
defer func() {
|
|
req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
http.DefaultClient.Do(req)
|
|
}()
|
|
|
|
t.Run("GetRobotStatusSuccess", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/status", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, robotID, response["member_id"])
|
|
assert.Contains(t, response, "status")
|
|
assert.Contains(t, response, "running")
|
|
assert.Contains(t, response, "max_running")
|
|
})
|
|
|
|
t.Run("GetRobotStatusNotFound", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/non_existent_robot/status", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestRobotPermissions tests robot permission scenarios
|
|
// Tests personal user vs team user access control
|
|
func TestRobotPermissions(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
// Get base URL from server config
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client
|
|
client := testutils.RegisterTestClient(t, "Robot Permission Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
|
|
// Create User 1 (Personal user - no team)
|
|
token1 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
user1ID := token1.UserID
|
|
|
|
// Create User 2 (Different user)
|
|
token2 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
user2ID := token2.UserID
|
|
|
|
t.Logf("Test users created: User1=%s, User2=%s", user1ID, user2ID)
|
|
|
|
// Track created robots for cleanup
|
|
var createdRobotIDs []string
|
|
defer func() {
|
|
for _, robotID := range createdRobotIDs {
|
|
req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
http.DefaultClient.Do(req)
|
|
}
|
|
}()
|
|
|
|
t.Run("PersonalUserCreateRobot", func(t *testing.T) {
|
|
// Personal user creates a robot with their user_id as team_id
|
|
// This simulates a personal user (no team) creating their own robot
|
|
robotID := fmt.Sprintf("test_personal_robot_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": user1ID, // Personal user: team_id = user_id
|
|
"display_name": "Personal Robot",
|
|
"bio": "A robot created by a personal user",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, robotID, response["member_id"])
|
|
assert.Equal(t, user1ID, response["team_id"])
|
|
t.Logf("Personal robot created: %s (team_id: %s)", robotID, user1ID)
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
})
|
|
|
|
t.Run("PersonalUserCanAccessOwnRobot", func(t *testing.T) {
|
|
// User 1 creates a robot
|
|
robotID := fmt.Sprintf("test_own_robot_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": user1ID, // Personal user: team_id = user_id
|
|
"display_name": "User 1 Robot",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
createResp, _ := http.DefaultClient.Do(createReq)
|
|
createResp.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
|
|
// User 1 can access their own robot
|
|
getReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
require.NoError(t, err)
|
|
getReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
getResp, err := http.DefaultClient.Do(getReq)
|
|
require.NoError(t, err)
|
|
defer getResp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, getResp.StatusCode)
|
|
t.Logf("User 1 successfully accessed their own robot: %s", robotID)
|
|
})
|
|
|
|
t.Run("PersonalUserCanUpdateOwnRobot", func(t *testing.T) {
|
|
// User 1 creates a robot
|
|
robotID := fmt.Sprintf("test_update_robot_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": user1ID,
|
|
"display_name": "Original Name",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
createResp, _ := http.DefaultClient.Do(createReq)
|
|
createResp.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
|
|
// User 1 can update their own robot
|
|
updateData := map[string]interface{}{
|
|
"display_name": "Updated Name",
|
|
}
|
|
|
|
updateBody, _ := json.Marshal(updateData)
|
|
updateReq, err := http.NewRequest("PUT", serverURL+baseURL+"/agent/robots/"+robotID, bytes.NewBuffer(updateBody))
|
|
require.NoError(t, err)
|
|
updateReq.Header.Set("Content-Type", "application/json")
|
|
updateReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
updateResp, err := http.DefaultClient.Do(updateReq)
|
|
require.NoError(t, err)
|
|
defer updateResp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, updateResp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
json.NewDecoder(updateResp.Body).Decode(&response)
|
|
assert.Equal(t, "Updated Name", response["display_name"])
|
|
t.Logf("User 1 successfully updated their own robot")
|
|
})
|
|
|
|
t.Run("PersonalUserCanDeleteOwnRobot", func(t *testing.T) {
|
|
// User 1 creates a robot
|
|
robotID := fmt.Sprintf("test_delete_robot_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": user1ID,
|
|
"display_name": "Robot to Delete",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
createResp, _ := http.DefaultClient.Do(createReq)
|
|
createResp.Body.Close()
|
|
|
|
// User 1 can delete their own robot
|
|
deleteReq, err := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
require.NoError(t, err)
|
|
deleteReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
deleteResp, err := http.DefaultClient.Do(deleteReq)
|
|
require.NoError(t, err)
|
|
defer deleteResp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, deleteResp.StatusCode)
|
|
t.Logf("User 1 successfully deleted their own robot")
|
|
})
|
|
|
|
t.Run("TeamRobotAccess", func(t *testing.T) {
|
|
// Create a robot with a shared team_id
|
|
sharedTeamID := fmt.Sprintf("team_%d", time.Now().UnixNano())
|
|
robotID := fmt.Sprintf("test_team_robot_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": sharedTeamID,
|
|
"display_name": "Team Robot",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
createResp, _ := http.DefaultClient.Do(createReq)
|
|
createResp.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
|
|
// Creator can access the team robot
|
|
getReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
require.NoError(t, err)
|
|
getReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
getResp, err := http.DefaultClient.Do(getReq)
|
|
require.NoError(t, err)
|
|
defer getResp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, getResp.StatusCode)
|
|
t.Logf("Creator successfully accessed team robot: %s (team: %s)", robotID, sharedTeamID)
|
|
})
|
|
|
|
t.Run("VerifyYaoPermissionFieldsSet", func(t *testing.T) {
|
|
// Create a robot and verify __yao_created_by and __yao_team_id are set
|
|
robotID := fmt.Sprintf("test_perm_fields_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": user1ID, // Personal user: team_id = user_id
|
|
"display_name": "Permission Fields Test Robot",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
createResp, err := http.DefaultClient.Do(createReq)
|
|
require.NoError(t, err)
|
|
defer createResp.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
|
|
assert.Equal(t, http.StatusCreated, createResp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
json.NewDecoder(createResp.Body).Decode(&response)
|
|
|
|
// The response should contain the robot data
|
|
// Note: __yao_created_by and __yao_team_id might not be in the public response
|
|
// but they should be set in the database
|
|
assert.Equal(t, robotID, response["member_id"])
|
|
t.Logf("Robot created with permission fields (user_id: %s)", user1ID)
|
|
})
|
|
|
|
t.Run("DifferentUserCannotUpdateRobot", func(t *testing.T) {
|
|
// User 1 creates a robot
|
|
robotID := fmt.Sprintf("test_cross_update_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": user1ID,
|
|
"display_name": "User 1 Private Robot",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
createResp, _ := http.DefaultClient.Do(createReq)
|
|
createResp.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
|
|
// User 2 attempts to update User 1's robot - should be denied
|
|
// Note: With system:root scope, this might still succeed due to admin privileges
|
|
// In production, user2 would not have system:root
|
|
updateData := map[string]interface{}{
|
|
"display_name": "Unauthorized Update",
|
|
}
|
|
|
|
updateBody, _ := json.Marshal(updateData)
|
|
updateReq, err := http.NewRequest("PUT", serverURL+baseURL+"/agent/robots/"+robotID, bytes.NewBuffer(updateBody))
|
|
require.NoError(t, err)
|
|
updateReq.Header.Set("Content-Type", "application/json")
|
|
updateReq.Header.Set("Authorization", "Bearer "+token2.AccessToken)
|
|
|
|
updateResp, err := http.DefaultClient.Do(updateReq)
|
|
require.NoError(t, err)
|
|
defer updateResp.Body.Close()
|
|
|
|
// With system:root scope (no constraints), user2 can still update
|
|
// This test documents the current behavior with admin privileges
|
|
t.Logf("User 2 update attempt status: %d (with system:root scope)", updateResp.StatusCode)
|
|
})
|
|
|
|
t.Run("DifferentUserCannotDeleteRobot", func(t *testing.T) {
|
|
// User 1 creates a robot
|
|
robotID := fmt.Sprintf("test_cross_delete_%d", time.Now().UnixNano())
|
|
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": user1ID,
|
|
"display_name": "User 1 Robot for Delete Test",
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
createReq, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
createResp, _ := http.DefaultClient.Do(createReq)
|
|
createResp.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robotID)
|
|
|
|
// User 2 attempts to delete User 1's robot
|
|
// Note: With system:root scope, this might still succeed due to admin privileges
|
|
deleteReq, err := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
require.NoError(t, err)
|
|
deleteReq.Header.Set("Authorization", "Bearer "+token2.AccessToken)
|
|
|
|
deleteResp, err := http.DefaultClient.Do(deleteReq)
|
|
require.NoError(t, err)
|
|
defer deleteResp.Body.Close()
|
|
|
|
// With system:root scope (no constraints), user2 can still delete
|
|
// This test documents the current behavior with admin privileges
|
|
t.Logf("User 2 delete attempt status: %d (with system:root scope)", deleteResp.StatusCode)
|
|
})
|
|
|
|
t.Run("ListRobotsWithTeamFilter", func(t *testing.T) {
|
|
// Create robots for both users
|
|
robot1ID := fmt.Sprintf("test_list_user1_%d", time.Now().UnixNano())
|
|
robot2ID := fmt.Sprintf("test_list_user2_%d", time.Now().UnixNano())
|
|
|
|
// User 1 creates their robot
|
|
create1 := map[string]interface{}{
|
|
"member_id": robot1ID,
|
|
"team_id": user1ID,
|
|
"display_name": "User 1 List Robot",
|
|
}
|
|
body1, _ := json.Marshal(create1)
|
|
req1, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body1))
|
|
req1.Header.Set("Content-Type", "application/json")
|
|
req1.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
resp1, _ := http.DefaultClient.Do(req1)
|
|
resp1.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robot1ID)
|
|
|
|
// User 2 creates their robot
|
|
create2 := map[string]interface{}{
|
|
"member_id": robot2ID,
|
|
"team_id": user2ID,
|
|
"display_name": "User 2 List Robot",
|
|
}
|
|
body2, _ := json.Marshal(create2)
|
|
req2, _ := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body2))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
req2.Header.Set("Authorization", "Bearer "+token2.AccessToken)
|
|
resp2, _ := http.DefaultClient.Do(req2)
|
|
resp2.Body.Close()
|
|
createdRobotIDs = append(createdRobotIDs, robot2ID)
|
|
|
|
// User 1 lists robots with their team_id filter
|
|
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?team_id="+user1ID, nil)
|
|
require.NoError(t, err)
|
|
listReq.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
listResp, err := http.DefaultClient.Do(listReq)
|
|
require.NoError(t, err)
|
|
defer listResp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, listResp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
json.NewDecoder(listResp.Body).Decode(&response)
|
|
|
|
data := response["data"].([]interface{})
|
|
t.Logf("User 1 sees %d robots with team_id=%s filter", len(data), user1ID)
|
|
})
|
|
}
|