Enhance Robot API with Permission Fields and Status Management
- Added `YaoCreatedBy` and `YaoTeamID` fields to `RobotState` and `RobotResponse` for improved access control. - Updated `GetRobotStatus` to retrieve permission fields from the store and populate the robot state. - Modified `robotFields` in the store to include new Yao permission fields for better management of access control. - Enhanced OpenAPI integration by registering robot routes and ensuring proper permission checks in handlers. - Updated documentation in TODO.md to reflect the completion of permission logic and API enhancements.
This commit is contained in:
parent
df5836d8cc
commit
64b5ffd154
12 changed files with 2019 additions and 88 deletions
|
|
@ -83,6 +83,9 @@ func GetRobotStatus(ctx *types.Context, memberID string) (*RobotState, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get permission fields from store (for access control)
|
||||||
|
record, _ := robotStore.Get(context.Background(), memberID)
|
||||||
|
|
||||||
state := &RobotState{
|
state := &RobotState{
|
||||||
MemberID: robot.MemberID,
|
MemberID: robot.MemberID,
|
||||||
TeamID: robot.TeamID,
|
TeamID: robot.TeamID,
|
||||||
|
|
@ -93,6 +96,12 @@ func GetRobotStatus(ctx *types.Context, memberID string) (*RobotState, error) {
|
||||||
MaxRunning: 2, // default
|
MaxRunning: 2, // default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add permission fields if available
|
||||||
|
if record != nil {
|
||||||
|
state.YaoCreatedBy = record.YaoCreatedBy
|
||||||
|
state.YaoTeamID = record.YaoTeamID
|
||||||
|
}
|
||||||
|
|
||||||
if robot.Config != nil && robot.Config.Quota != nil {
|
if robot.Config != nil && robot.Config.Quota != nil {
|
||||||
state.MaxRunning = robot.Config.Quota.GetMax()
|
state.MaxRunning = robot.Config.Quota.GetMax()
|
||||||
}
|
}
|
||||||
|
|
@ -565,10 +574,12 @@ func recordToResponse(record *store.RobotRecord) *RobotResponse {
|
||||||
MCPServers: record.MCPServers,
|
MCPServers: record.MCPServers,
|
||||||
LanguageModel: record.LanguageModel,
|
LanguageModel: record.LanguageModel,
|
||||||
|
|
||||||
CostLimit: record.CostLimit,
|
CostLimit: record.CostLimit,
|
||||||
InvitedBy: record.InvitedBy,
|
InvitedBy: record.InvitedBy,
|
||||||
JoinedAt: record.JoinedAt,
|
JoinedAt: record.JoinedAt,
|
||||||
CreatedAt: record.CreatedAt,
|
YaoCreatedBy: record.YaoCreatedBy,
|
||||||
UpdatedAt: record.UpdatedAt,
|
YaoTeamID: record.YaoTeamID,
|
||||||
|
CreatedAt: record.CreatedAt,
|
||||||
|
UpdatedAt: record.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,18 @@ type ListResult struct {
|
||||||
|
|
||||||
// RobotState - runtime state from Status()
|
// RobotState - runtime state from Status()
|
||||||
type RobotState struct {
|
type RobotState struct {
|
||||||
MemberID string `json:"member_id"`
|
MemberID string `json:"member_id"`
|
||||||
TeamID string `json:"team_id"`
|
TeamID string `json:"team_id"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
Bio string `json:"bio,omitempty"`
|
Bio string `json:"bio,omitempty"`
|
||||||
Status types.RobotStatus `json:"status"`
|
Status types.RobotStatus `json:"status"`
|
||||||
Running int `json:"running"`
|
Running int `json:"running"`
|
||||||
MaxRunning int `json:"max_running"`
|
MaxRunning int `json:"max_running"`
|
||||||
LastRun *time.Time `json:"last_run,omitempty"`
|
LastRun *time.Time `json:"last_run,omitempty"`
|
||||||
NextRun *time.Time `json:"next_run,omitempty"`
|
NextRun *time.Time `json:"next_run,omitempty"`
|
||||||
RunningIDs []string `json:"running_ids,omitempty"`
|
RunningIDs []string `json:"running_ids,omitempty"`
|
||||||
|
YaoCreatedBy string `json:"__yao_created_by,omitempty"` // Creator user_id for permission check
|
||||||
|
YaoTeamID string `json:"__yao_team_id,omitempty"` // Team ID for permission check
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Trigger Types ====================
|
// ==================== Trigger Types ====================
|
||||||
|
|
@ -224,8 +226,10 @@ type RobotResponse struct {
|
||||||
CostLimit float64 `json:"cost_limit,omitempty"`
|
CostLimit float64 `json:"cost_limit,omitempty"`
|
||||||
|
|
||||||
// Ownership & Audit
|
// Ownership & Audit
|
||||||
InvitedBy string `json:"invited_by,omitempty"`
|
InvitedBy string `json:"invited_by,omitempty"`
|
||||||
JoinedAt *time.Time `json:"joined_at,omitempty"`
|
JoinedAt *time.Time `json:"joined_at,omitempty"`
|
||||||
|
YaoCreatedBy string `json:"__yao_created_by,omitempty"` // Creator user_id for permission check
|
||||||
|
YaoTeamID string `json:"__yao_team_id,omitempty"` // Team ID for permission check
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,12 @@ var robotFields = []interface{}{
|
||||||
// Timestamps
|
// Timestamps
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
|
||||||
|
// Yao Permission Fields (for access control)
|
||||||
|
"__yao_created_by",
|
||||||
|
"__yao_updated_by",
|
||||||
|
"__yao_team_id",
|
||||||
|
"__yao_tenant_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save creates or updates a robot member record
|
// Save creates or updates a robot member record
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/yao/openapi/agent/robot"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -26,4 +27,8 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
|
|
||||||
// Assistant Actions
|
// Assistant Actions
|
||||||
// group.POST("/assistants/:id/call", agent.HandleAssistantCall) // POST /assistants/:id/call - Execute assistant API
|
// group.POST("/assistants/:id/call", agent.HandleAssistantCall) // POST /assistants/:id/call - Execute assistant API
|
||||||
|
|
||||||
|
// Robot routes - Attach as sub-router
|
||||||
|
// Routes: GET/POST /robots, GET/PUT/DELETE /robots/:id, GET /robots/:id/status
|
||||||
|
robot.Attach(group.Group("/robots"), oauth)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,11 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟢 Phase 1: Core CRUD 🟡 [Low Risk]
|
## 🟢 Phase 1: Core CRUD ✅ [Low Risk]
|
||||||
|
|
||||||
**Goal:** Basic robot management endpoints
|
**Goal:** Basic robot management endpoints
|
||||||
**Risk:** 🟢 Low - All new code, no changes to existing logic
|
**Risk:** 🟢 Low - All new code, no changes to existing logic
|
||||||
|
**Status:** ✅ Complete
|
||||||
|
|
||||||
### 1.1 Backend Prerequisites ✅
|
### 1.1 Backend Prerequisites ✅
|
||||||
|
|
||||||
|
|
@ -72,80 +73,103 @@
|
||||||
- [x] Implement `Get<Type>` functions for map value extraction
|
- [x] Implement `Get<Type>` functions for map value extraction
|
||||||
- [x] Add tests: `utils/convert_test.go`
|
- [x] Add tests: `utils/convert_test.go`
|
||||||
|
|
||||||
### 1.2 OpenAPI Setup ⬜ (Next Step)
|
### 1.2 OpenAPI Setup ✅
|
||||||
|
|
||||||
- [ ] Create `openapi/agent/robot/` directory (sub-package under agent)
|
- [x] Create `openapi/agent/robot/` directory (sub-package under agent)
|
||||||
- [ ] Create `robot.go` - route registration with `Attach()` function
|
- [x] Create `robot.go` - route registration with `Attach()` function
|
||||||
- [ ] Register routes in `openapi/agent/agent.go` via `robot.Attach(group.Group("/robots"), oauth)`
|
- [x] Register routes in `openapi/agent/agent.go` via `robot.Attach(group.Group("/robots"), oauth)`
|
||||||
- [ ] Add OAuth guard middleware
|
- [x] Add OAuth guard middleware
|
||||||
|
|
||||||
### 1.3 OpenAPI Types ⬜
|
### 1.3 OpenAPI Types ✅
|
||||||
|
|
||||||
> Note: Core types already exist in `agent/robot/api/types.go`. OpenAPI layer needs HTTP-specific types.
|
> Note: Core types already exist in `agent/robot/api/types.go`. OpenAPI layer needs HTTP-specific types.
|
||||||
|
|
||||||
- [ ] `types.go` - HTTP request/response types
|
- [x] `types.go` - HTTP request/response types
|
||||||
- [ ] `RobotResponse` struct (with field mapping: `name` ← `member_id`, `description` ← `bio`)
|
- [x] `RobotResponse` struct (with field mapping: `name` ← `member_id`, `description` ← `bio`)
|
||||||
- [ ] `ConfigResponse` struct (and sub-types)
|
- [x] `RobotStatusResponse` struct
|
||||||
- [ ] `ListRobotsResponse` struct
|
- [x] `ListRobotsResponse` struct
|
||||||
- [ ] `CreateRobotRequest` struct (HTTP binding)
|
- [x] `CreateRobotRequest` struct (HTTP binding)
|
||||||
- [ ] `UpdateRobotRequest` struct (HTTP binding)
|
- [x] `UpdateRobotRequest` struct (HTTP binding)
|
||||||
- [ ] `NewRobotResponse()` - conversion from `api.RobotResponse`
|
- [x] `NewRobotResponse()` - conversion from `api.RobotResponse`
|
||||||
- [ ] Error response types
|
- [x] `NewRobotStatusResponse()` - conversion from `api.RobotState`
|
||||||
|
|
||||||
### 1.4 List Robots ⬜
|
### 1.4 List Robots ✅
|
||||||
|
|
||||||
- [ ] `list.go` - GET /v1/agent/robots
|
- [x] `list.go` - GET /v1/agent/robots
|
||||||
- [ ] Parse query params: `locale`, `status`, `keywords`, `page`, `pagesize`
|
- [x] Parse query params: `status`, `keywords`, `page`, `pagesize`, `team_id`
|
||||||
- [ ] Call `robot/api.ListRobots()`
|
- [x] Call `robot/api.ListRobots()`
|
||||||
- [ ] Format response with localization
|
- [x] Team constraint from auth info
|
||||||
- [ ] Test: `tests/robot/list_test.go`
|
- [x] Test: `tests/agent/robot_test.go#TestListRobots`
|
||||||
|
|
||||||
### 1.5 Get Robot ⬜
|
### 1.5 Get Robot ✅
|
||||||
|
|
||||||
- [ ] `detail.go` - GET /v1/agent/robots/:id
|
- [x] `detail.go` - GET /v1/agent/robots/:id
|
||||||
- [ ] Parse path param and `locale` query
|
- [x] Parse path param
|
||||||
- [ ] Call `robot/api.GetRobot()` and `robot/api.GetRobotStatus()`
|
- [x] Call `robot/api.GetRobotResponse()`
|
||||||
- [ ] Format response with full config
|
- [x] Team access check
|
||||||
- [ ] Team access check
|
- [x] Test: `tests/agent/robot_test.go#TestGetRobot`
|
||||||
- [ ] Test: `tests/robot/get_test.go`
|
|
||||||
|
|
||||||
### 1.6 Create Robot ⬜
|
### 1.6 Create Robot ✅
|
||||||
|
|
||||||
- [ ] POST /v1/agent/robots handler
|
- [x] POST /v1/agent/robots handler
|
||||||
- [ ] Parse HTTP request to `api.CreateRobotRequest`
|
- [x] Parse HTTP request to `CreateRobotRequest`
|
||||||
- [ ] Apply `authInfo.WithCreateScope()` for permission fields
|
- [x] Apply `AuthScope` with permission fields (CreatedBy, TeamID, TenantID)
|
||||||
- [ ] Call `robot/api.CreateRobot()`
|
- [x] Call `robot/api.CreateRobot()`
|
||||||
- [ ] Return created robot
|
- [x] Return created robot (201 Created)
|
||||||
- [ ] Test: `tests/robot/create_test.go`
|
- [x] Handle duplicate (409 Conflict)
|
||||||
|
- [x] Test: `tests/agent/robot_test.go#TestCreateRobot`
|
||||||
|
|
||||||
### 1.7 Update Robot ⬜
|
### 1.7 Update Robot ✅
|
||||||
|
|
||||||
- [ ] PUT /v1/agent/robots/:id handler
|
- [x] PUT /v1/agent/robots/:id handler
|
||||||
- [ ] Parse HTTP request to `api.UpdateRobotRequest`
|
- [x] Parse HTTP request to `UpdateRobotRequest`
|
||||||
- [ ] Ownership/permission check
|
- [x] Team permission check
|
||||||
- [ ] Apply `authInfo.WithUpdateScope()` for permission fields
|
- [x] Apply `AuthScope` with UpdatedBy
|
||||||
- [ ] Call `robot/api.UpdateRobot()`
|
- [x] Call `robot/api.UpdateRobot()`
|
||||||
- [ ] Return updated robot
|
- [x] Return updated robot
|
||||||
- [ ] Test: `tests/robot/update_test.go`
|
- [x] Test: `tests/agent/robot_test.go#TestUpdateRobot`
|
||||||
|
|
||||||
### 1.8 Delete Robot ⬜
|
### 1.8 Delete Robot ✅
|
||||||
|
|
||||||
- [ ] DELETE /v1/agent/robots/:id handler
|
- [x] DELETE /v1/agent/robots/:id handler
|
||||||
- [ ] Ownership/permission check
|
- [x] Team permission check
|
||||||
- [ ] Call `robot/api.RemoveRobot()`
|
- [x] Call `robot/api.RemoveRobot()`
|
||||||
- [ ] Return success response
|
- [x] Handle running executions (409 Conflict)
|
||||||
- [ ] Test: `tests/robot/delete_test.go`
|
- [x] Return success response
|
||||||
|
- [x] Test: `tests/agent/robot_test.go#TestDeleteRobot`
|
||||||
|
|
||||||
### 1.9 Utilities ⬜
|
### 1.9 Status Endpoint ✅
|
||||||
|
|
||||||
- [ ] `utils.go` - helper functions
|
- [x] GET /v1/agent/robots/:id/status handler
|
||||||
- [ ] `getLocale(c *gin.Context)` - extract locale from query/header
|
- [x] Call `robot/api.GetRobotStatus()`
|
||||||
- [ ] `formatTime(t *time.Time)` - format to ISO string
|
- [x] Return runtime status (running count, max, last/next run)
|
||||||
- [ ] `localizeString(value, locale)` - localization helper
|
- [x] Test: `tests/agent/robot_test.go#TestGetRobotStatus`
|
||||||
- [ ] `applyAuthScope(authInfo, req)` - apply permission fields
|
|
||||||
- [ ] `filter.go` - query filtering
|
### 1.10 Utilities ✅
|
||||||
- [ ] Parse query params to `api.ListQuery`
|
|
||||||
- [ ] Parse query params to `api.ExecutionQuery`
|
- [x] `utils.go` - helper functions
|
||||||
|
- [x] `GetLocale(c *gin.Context)` - extract locale from query/header
|
||||||
|
- [x] `ParseBoolValue()` - parse bool from string
|
||||||
|
|
||||||
|
### 1.11 Permission Logic ✅
|
||||||
|
|
||||||
|
- [x] `permission.go` - permission check functions
|
||||||
|
- [x] `CanRead()` - read permission check (creator or team member)
|
||||||
|
- [x] `CanWrite()` - write permission check (creator only)
|
||||||
|
- [x] `GetEffectiveTeamID()` - get effective team_id (user_id for personal users)
|
||||||
|
- [x] `BuildListFilter()` - build list filter based on permissions
|
||||||
|
- [x] Apply permission checks in handlers:
|
||||||
|
- [x] `GetRobot` - check `CanRead()` with `YaoTeamID` and `YaoCreatedBy`
|
||||||
|
- [x] `GetRobotStatus` - check `CanRead()`
|
||||||
|
- [x] `UpdateRobot` - check `CanWrite()`
|
||||||
|
- [x] `DeleteRobot` - check `CanWrite()`
|
||||||
|
- [x] `ListRobots` - use `BuildListFilter()` for team filtering
|
||||||
|
- [x] `CreateRobot` - auto-set `__yao_team_id` to `user_id` for personal users
|
||||||
|
- [x] Add Yao permission fields to API layer:
|
||||||
|
- [x] `api/types.go` - add `YaoCreatedBy`, `YaoTeamID` to `RobotResponse` and `RobotState`
|
||||||
|
- [x] `api/robot.go` - populate permission fields in `recordToResponse()` and `GetRobotStatus()`
|
||||||
|
- [x] `store/robot.go` - add `__yao_*` fields to `robotFields`
|
||||||
|
- [x] Permission tests in `tests/agent/robot_test.go#TestRobotPermissions`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -467,7 +491,7 @@ yao/openapi/tests/robot/
|
||||||
|
|
||||||
| Phase | Risk | Status | Description |
|
| Phase | Risk | Status | Description |
|
||||||
|-------|------|--------|-------------|
|
|-------|------|--------|-------------|
|
||||||
| 1. Core CRUD | 🟢 | 🟡 | Basic robot management (Backend ✅, OpenAPI ⬜) |
|
| 1. Core CRUD | 🟢 | ✅ | Basic robot management (Backend ✅, OpenAPI ✅) |
|
||||||
| 2. Execution | 🟢 | ⬜ | Execution listing, control, trigger/intervene |
|
| 2. Execution | 🟢 | ⬜ | Execution listing, control, trigger/intervene |
|
||||||
| 3. Results/Activities | 🟢 | ⬜ | Deliverables and activity feed |
|
| 3. Results/Activities | 🟢 | ⬜ | Deliverables and activity feed |
|
||||||
| 4. i18n | 🟢 | ⬜ | Locale parameter support |
|
| 4. i18n | 🟢 | ⬜ | Locale parameter support |
|
||||||
|
|
@ -489,7 +513,14 @@ Legend: ⬜ Not started | 🟡 In progress | ✅ Complete | 🟢 Low Risk | 🟡
|
||||||
| `api/robot_test.go` | ✅ | API tests |
|
| `api/robot_test.go` | ✅ | API tests |
|
||||||
| `utils/convert.go` | ✅ | Type conversion utilities |
|
| `utils/convert.go` | ✅ | Type conversion utilities |
|
||||||
| `utils/convert_test.go` | ✅ | Unit tests |
|
| `utils/convert_test.go` | ✅ | Unit tests |
|
||||||
| `openapi/agent/robot/` | ⬜ | HTTP handlers (next step) |
|
| `openapi/agent/robot/robot.go` | ✅ | Route registration with Attach() |
|
||||||
|
| `openapi/agent/robot/types.go` | ✅ | HTTP request/response types |
|
||||||
|
| `openapi/agent/robot/list.go` | ✅ | List robots handler with permission filter |
|
||||||
|
| `openapi/agent/robot/detail.go` | ✅ | CRUD handlers with permission checks |
|
||||||
|
| `openapi/agent/robot/permission.go` | ✅ | Permission check functions (CanRead/CanWrite) |
|
||||||
|
| `openapi/agent/robot/utils.go` | ✅ | Helper functions |
|
||||||
|
| `openapi/agent/agent.go` | ✅ | Robot routes registered |
|
||||||
|
| `openapi/tests/agent/robot_test.go` | ✅ | Integration tests + Permission tests |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -501,17 +532,18 @@ Legend: ⬜ Not started | 🟡 In progress | ✅ Complete | 🟢 Low Risk | 🟡
|
||||||
yao/openapi/agent/robot/ # This directory (sub-package under agent)
|
yao/openapi/agent/robot/ # This directory (sub-package under agent)
|
||||||
├── DESIGN.md # Design document ✅
|
├── DESIGN.md # Design document ✅
|
||||||
├── TODO.md # This file ✅
|
├── TODO.md # This file ✅
|
||||||
├── robot.go # Route registration (Attach function)
|
├── robot.go # Route registration (Attach function) ✅
|
||||||
├── types.go # All request/response types
|
├── types.go # All request/response types ✅
|
||||||
├── list.go # GET /v1/agent/robots
|
├── list.go # GET /v1/agent/robots ✅
|
||||||
├── detail.go # GET/POST/PUT/DELETE /v1/agent/robots/:id
|
├── detail.go # GET/POST/PUT/DELETE /v1/agent/robots/:id ✅
|
||||||
├── execution.go # Execution endpoints
|
├── permission.go # Permission check functions (CanRead/CanWrite) ✅
|
||||||
├── trigger.go # Trigger/Intervene SSE
|
├── utils.go # Utilities ✅
|
||||||
├── results.go # Results endpoints
|
├── execution.go # Execution endpoints (Phase 2)
|
||||||
├── activities.go # Activities endpoint
|
├── trigger.go # Trigger/Intervene SSE (Phase 2)
|
||||||
├── stream.go # Real-time streams
|
├── results.go # Results endpoints (Phase 3)
|
||||||
├── filter.go # Query filtering
|
├── activities.go # Activities endpoint (Phase 3)
|
||||||
└── utils.go # Utilities
|
├── stream.go # Real-time streams (Phase 6 - Deferred)
|
||||||
|
└── filter.go # Query filtering (optional)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parent Directory
|
### Parent Directory
|
||||||
|
|
|
||||||
425
openapi/agent/robot/detail.go
Normal file
425
openapi/agent/robot/detail.go
Normal file
|
|
@ -0,0 +1,425 @@
|
||||||
|
package robot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/kun/log"
|
||||||
|
robotapi "github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRobot retrieves a single robot by ID
|
||||||
|
// GET /v1/agent/robots/:id
|
||||||
|
func GetRobot(c *gin.Context) {
|
||||||
|
// Get authorized information
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
|
||||||
|
// Get robot ID from URL parameter
|
||||||
|
robotID := c.Param("id")
|
||||||
|
if robotID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "robot id is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create robot context
|
||||||
|
ctx := &robottypes.Context{}
|
||||||
|
|
||||||
|
// Get robot via API
|
||||||
|
robotResp, err := robotapi.GetRobotResponse(ctx, robotID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get robot %s: %v", robotID, err)
|
||||||
|
|
||||||
|
// Check for not found error
|
||||||
|
if err == robottypes.ErrRobotNotFound {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Robot not found: " + robotID,
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to get robot: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check read permission
|
||||||
|
// Permission rules:
|
||||||
|
// - No constraints: allow all
|
||||||
|
// - OwnerOnly: user must be the creator
|
||||||
|
// - TeamOnly: robot must belong to user's team
|
||||||
|
if !CanRead(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Forbidden: No permission to access this robot",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to HTTP response
|
||||||
|
resp := NewResponse(robotResp)
|
||||||
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRobotStatus retrieves the runtime status of a robot
|
||||||
|
// GET /v1/agent/robots/:id/status
|
||||||
|
func GetRobotStatus(c *gin.Context) {
|
||||||
|
// Get authorized information
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
|
||||||
|
// Get robot ID from URL parameter
|
||||||
|
robotID := c.Param("id")
|
||||||
|
if robotID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "robot id is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create robot context
|
||||||
|
ctx := &robottypes.Context{}
|
||||||
|
|
||||||
|
// Get robot status via API
|
||||||
|
status, err := robotapi.GetRobotStatus(ctx, robotID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get robot status %s: %v", robotID, err)
|
||||||
|
|
||||||
|
if err == robottypes.ErrRobotNotFound {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Robot not found: " + robotID,
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to get robot status: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check read permission
|
||||||
|
if !CanRead(c, authInfo, status.YaoTeamID, status.YaoCreatedBy) {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Forbidden: No permission to access this robot",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to HTTP response
|
||||||
|
resp := NewStatusResponse(status)
|
||||||
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRobot creates a new robot
|
||||||
|
// POST /v1/agent/robots
|
||||||
|
func CreateRobot(c *gin.Context) {
|
||||||
|
// Get authorized information
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req CreateRobotRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if req.MemberID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "member_id is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.DisplayName == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "display_name is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine effective team_id:
|
||||||
|
// - If user has a team selected (authInfo.TeamID), use it
|
||||||
|
// - Otherwise, for personal users, use user_id as team_id
|
||||||
|
effectiveTeamID := GetEffectiveTeamID(authInfo)
|
||||||
|
if req.TeamID == "" {
|
||||||
|
req.TeamID = effectiveTeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply team constraint from auth if TeamOnly
|
||||||
|
if authInfo != nil && authInfo.Constraints.TeamOnly && authInfo.TeamID != "" {
|
||||||
|
// Force team_id to auth team_id
|
||||||
|
req.TeamID = authInfo.TeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still require team_id after all fallbacks
|
||||||
|
if req.TeamID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "team_id is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to API request
|
||||||
|
apiReq := req.ToAPICreateRequest()
|
||||||
|
|
||||||
|
// Apply Yao permission fields
|
||||||
|
// Key rule: __yao_team_id = authInfo.TeamID if has team, otherwise = authInfo.UserID
|
||||||
|
if authInfo != nil {
|
||||||
|
yaoTeamID := authInfo.TeamID
|
||||||
|
if yaoTeamID == "" {
|
||||||
|
// For personal users (no team), use user_id as __yao_team_id
|
||||||
|
// This ensures the robot is scoped to the individual user
|
||||||
|
yaoTeamID = authInfo.UserID
|
||||||
|
}
|
||||||
|
apiReq.AuthScope = &robotapi.AuthScope{
|
||||||
|
CreatedBy: authInfo.UserID,
|
||||||
|
TeamID: yaoTeamID,
|
||||||
|
TenantID: authInfo.TenantID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create robot context
|
||||||
|
ctx := &robottypes.Context{}
|
||||||
|
|
||||||
|
// Call API layer
|
||||||
|
robotResp, err := robotapi.CreateRobot(ctx, apiReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create robot: %v", err)
|
||||||
|
|
||||||
|
// Check for duplicate error
|
||||||
|
if strings.Contains(err.Error(), "already exists") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusConflict, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to create robot: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to HTTP response
|
||||||
|
resp := NewResponse(robotResp)
|
||||||
|
response.RespondWithSuccess(c, response.StatusCreated, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRobot updates an existing robot
|
||||||
|
// PUT /v1/agent/robots/:id
|
||||||
|
func UpdateRobot(c *gin.Context) {
|
||||||
|
// Get authorized information
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
|
||||||
|
// Get robot ID from URL parameter
|
||||||
|
robotID := c.Param("id")
|
||||||
|
if robotID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "robot id is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req UpdateRobotRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create robot context
|
||||||
|
ctx := &robottypes.Context{}
|
||||||
|
|
||||||
|
// Check permission - first get the robot to verify ownership/team
|
||||||
|
existingRobot, err := robotapi.GetRobotResponse(ctx, robotID)
|
||||||
|
if err != nil {
|
||||||
|
if err == robottypes.ErrRobotNotFound {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Robot not found: " + robotID,
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to get robot: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check write permission (only creator can update)
|
||||||
|
if !CanWrite(c, authInfo, existingRobot.YaoTeamID, existingRobot.YaoCreatedBy) {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Forbidden: No permission to update this robot",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to API request
|
||||||
|
apiReq := req.ToAPIUpdateRequest()
|
||||||
|
|
||||||
|
// Apply Yao permission fields
|
||||||
|
if authInfo != nil {
|
||||||
|
apiReq.AuthScope = &robotapi.AuthScope{
|
||||||
|
UpdatedBy: authInfo.UserID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call API layer
|
||||||
|
robotResp, err := robotapi.UpdateRobot(ctx, robotID, apiReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to update robot %s: %v", robotID, err)
|
||||||
|
|
||||||
|
if err == robottypes.ErrRobotNotFound {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Robot not found: " + robotID,
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to update robot: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to HTTP response
|
||||||
|
resp := NewResponse(robotResp)
|
||||||
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRobot deletes a robot
|
||||||
|
// DELETE /v1/agent/robots/:id
|
||||||
|
func DeleteRobot(c *gin.Context) {
|
||||||
|
// Get authorized information
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
|
||||||
|
// Get robot ID from URL parameter
|
||||||
|
robotID := c.Param("id")
|
||||||
|
if robotID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "robot id is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create robot context
|
||||||
|
ctx := &robottypes.Context{}
|
||||||
|
|
||||||
|
// Check permission - first get the robot to verify ownership/team
|
||||||
|
existingRobot, err := robotapi.GetRobotResponse(ctx, robotID)
|
||||||
|
if err != nil {
|
||||||
|
if err == robottypes.ErrRobotNotFound {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Robot not found: " + robotID,
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to get robot: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check write permission (only creator can delete)
|
||||||
|
if !CanWrite(c, authInfo, existingRobot.YaoTeamID, existingRobot.YaoCreatedBy) {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Forbidden: No permission to delete this robot",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call API layer
|
||||||
|
err = robotapi.RemoveRobot(ctx, robotID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to delete robot %s: %v", robotID, err)
|
||||||
|
|
||||||
|
// Check for running executions
|
||||||
|
if strings.Contains(err.Error(), "running executions") {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusConflict, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == robottypes.ErrRobotNotFound {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Robot not found: " + robotID,
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to delete robot: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success with no content
|
||||||
|
response.RespondWithSuccess(c, response.StatusOK, map[string]interface{}{
|
||||||
|
"member_id": robotID,
|
||||||
|
"deleted": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
108
openapi/agent/robot/list.go
Normal file
108
openapi/agent/robot/list.go
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
package robot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/kun/log"
|
||||||
|
robotapi "github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
robottypes "github.com/yaoapp/yao/agent/robot/types"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListRobots lists robots with pagination and filtering
|
||||||
|
// GET /v1/agent/robots
|
||||||
|
func ListRobots(c *gin.Context) {
|
||||||
|
// Get authorized information
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
|
||||||
|
// Parse pagination parameters
|
||||||
|
page := 1
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
||||||
|
page = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageSize := 20
|
||||||
|
if pageSizeStr := c.Query("pagesize"); pageSizeStr != "" {
|
||||||
|
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
|
||||||
|
pageSize = ps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse filter parameters
|
||||||
|
requestedTeamID := strings.TrimSpace(c.Query("team_id"))
|
||||||
|
status := strings.TrimSpace(c.Query("status"))
|
||||||
|
keywords := strings.TrimSpace(c.Query("keywords"))
|
||||||
|
|
||||||
|
// Apply permission-based filtering
|
||||||
|
// This ensures users only see robots they have access to:
|
||||||
|
// - No constraints: use requested team_id or no filter
|
||||||
|
// - TeamOnly: force filter to user's team
|
||||||
|
// - OwnerOnly: filter by user_id (personal resources)
|
||||||
|
effectiveTeamID := BuildListFilter(c, authInfo, requestedTeamID)
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
query := &robotapi.ListQuery{
|
||||||
|
TeamID: effectiveTeamID,
|
||||||
|
Keywords: keywords,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
query.Status = robottypes.RobotStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create robot context
|
||||||
|
ctx := &robottypes.Context{}
|
||||||
|
|
||||||
|
// Call API layer
|
||||||
|
result, err := robotapi.ListRobots(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to list robots: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to list robots: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to HTTP response format
|
||||||
|
robots := make([]*Response, 0, len(result.Data))
|
||||||
|
for _, r := range result.Data {
|
||||||
|
robots = append(robots, newResponseFromRobot(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &ListResponse{
|
||||||
|
Data: robots,
|
||||||
|
Total: result.Total,
|
||||||
|
Page: result.Page,
|
||||||
|
PageSize: result.PageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newResponseFromRobot converts types.Robot to Response
|
||||||
|
func newResponseFromRobot(r *robottypes.Robot) *Response {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Response{
|
||||||
|
Name: r.MemberID, // Frontend mapping: name ← member_id
|
||||||
|
Description: r.Bio, // Frontend mapping: description ← bio
|
||||||
|
MemberID: r.MemberID,
|
||||||
|
TeamID: r.TeamID,
|
||||||
|
RobotStatus: string(r.Status),
|
||||||
|
AutonomousMode: r.AutonomousMode,
|
||||||
|
DisplayName: r.DisplayName,
|
||||||
|
Bio: r.Bio,
|
||||||
|
SystemPrompt: r.SystemPrompt,
|
||||||
|
RobotEmail: r.RobotEmail,
|
||||||
|
}
|
||||||
|
}
|
||||||
135
openapi/agent/robot/permission.go
Normal file
135
openapi/agent/robot/permission.go
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package robot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Permission check functions for robot access control
|
||||||
|
//
|
||||||
|
// Permission Rules:
|
||||||
|
// 1. No auth info or no constraints: allow all
|
||||||
|
// 2. OwnerOnly: user can only access resources they created (__yao_created_by == userID)
|
||||||
|
// 3. TeamOnly: user can access resources in their team (__yao_team_id == teamID)
|
||||||
|
// 4. For personal users (no team): __yao_team_id should be empty or equal to user_id
|
||||||
|
//
|
||||||
|
// Read vs Write:
|
||||||
|
// - Read: team members can read team resources
|
||||||
|
// - Write: only creator or team owner can write (update/delete)
|
||||||
|
|
||||||
|
// CanRead checks if the user has read permission for a robot
|
||||||
|
// Read permission is granted if:
|
||||||
|
// - No auth info (public access)
|
||||||
|
// - No constraints (admin/system)
|
||||||
|
// - User is the creator (__yao_created_by == userID)
|
||||||
|
// - TeamOnly: robot belongs to user's team (__yao_team_id == teamID)
|
||||||
|
func CanRead(c *gin.Context, authInfo *types.AuthorizedInfo, robotTeamID, robotCreatedBy string) bool {
|
||||||
|
// No auth info, allow access (handled by OAuth guard)
|
||||||
|
if authInfo == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// No constraints, allow access (admin/system user)
|
||||||
|
if !authInfo.Constraints.TeamOnly && !authInfo.Constraints.OwnerOnly {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is the creator - always allow
|
||||||
|
if robotCreatedBy != "" && robotCreatedBy == authInfo.UserID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamOnly constraint: check team membership
|
||||||
|
if authInfo.Constraints.TeamOnly && authorized.IsTeamMember(c) {
|
||||||
|
// Robot belongs to user's team
|
||||||
|
if robotTeamID != "" && robotTeamID == authInfo.TeamID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnerOnly constraint: only creator can access (already checked above)
|
||||||
|
// If we reach here with OwnerOnly, user is not the creator
|
||||||
|
if authInfo.Constraints.OwnerOnly {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanWrite checks if the user has write permission for a robot (update/delete)
|
||||||
|
// Write permission is more restrictive:
|
||||||
|
// - No auth info: deny (should not happen, OAuth guard will block)
|
||||||
|
// - No constraints: allow (admin/system)
|
||||||
|
// - User is the creator: allow
|
||||||
|
// - TeamOnly + OwnerOnly: user must be creator AND in the same team
|
||||||
|
func CanWrite(c *gin.Context, authInfo *types.AuthorizedInfo, robotTeamID, robotCreatedBy string) bool {
|
||||||
|
// No auth info, deny write access
|
||||||
|
if authInfo == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// No constraints, allow access (admin/system user)
|
||||||
|
if !authInfo.Constraints.TeamOnly && !authInfo.Constraints.OwnerOnly {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is the creator - allow write
|
||||||
|
if robotCreatedBy != "" && robotCreatedBy == authInfo.UserID {
|
||||||
|
// If TeamOnly is also set, verify team membership
|
||||||
|
if authInfo.Constraints.TeamOnly {
|
||||||
|
if robotTeamID == "" || robotTeamID == authInfo.TeamID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not the creator - deny write access
|
||||||
|
// (In the future, we could add team admin/owner check here)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEffectiveTeamID returns the effective team_id for a robot
|
||||||
|
// For personal users (no team selected), returns user_id as team_id
|
||||||
|
// For team users, returns the selected team_id
|
||||||
|
func GetEffectiveTeamID(authInfo *types.AuthorizedInfo) string {
|
||||||
|
if authInfo == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user has a team selected, use it
|
||||||
|
if authInfo.TeamID != "" {
|
||||||
|
return authInfo.TeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// For personal users, use user_id as team_id
|
||||||
|
// This ensures resources are scoped to the individual user
|
||||||
|
return authInfo.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildListFilter builds filter conditions for listing robots based on permissions
|
||||||
|
// Returns teamID filter to apply to the query
|
||||||
|
func BuildListFilter(c *gin.Context, authInfo *types.AuthorizedInfo, requestedTeamID string) string {
|
||||||
|
if authInfo == nil {
|
||||||
|
return requestedTeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// No constraints - use requested filter or no filter
|
||||||
|
if !authInfo.Constraints.TeamOnly && !authInfo.Constraints.OwnerOnly {
|
||||||
|
return requestedTeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamOnly constraint: force filter to user's team
|
||||||
|
if authInfo.Constraints.TeamOnly && authorized.IsTeamMember(c) {
|
||||||
|
return authInfo.TeamID
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnerOnly constraint: filter by user_id as team_id (personal resources)
|
||||||
|
if authInfo.Constraints.OwnerOnly {
|
||||||
|
return authInfo.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestedTeamID
|
||||||
|
}
|
||||||
25
openapi/agent/robot/robot.go
Normal file
25
openapi/agent/robot/robot.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package robot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attach attaches the robot API handlers to the router with OAuth protection
|
||||||
|
// This provides OAuth-protected endpoints for robot management
|
||||||
|
// Base path: /v1/agent/robots
|
||||||
|
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
|
|
||||||
|
// Apply OAuth guard to all routes
|
||||||
|
group.Use(oauth.Guard)
|
||||||
|
|
||||||
|
// Robot CRUD - Standard REST endpoints
|
||||||
|
group.GET("", ListRobots) // GET /robots - List robots with pagination and filtering
|
||||||
|
group.POST("", CreateRobot) // POST /robots - Create a new robot
|
||||||
|
group.GET("/:id", GetRobot) // GET /robots/:id - Get robot details
|
||||||
|
group.PUT("/:id", UpdateRobot) // PUT /robots/:id - Update robot
|
||||||
|
group.DELETE("/:id", DeleteRobot) // DELETE /robots/:id - Delete robot
|
||||||
|
|
||||||
|
// Robot Status
|
||||||
|
group.GET("/:id/status", GetRobotStatus) // GET /robots/:id/status - Get robot runtime status
|
||||||
|
}
|
||||||
255
openapi/agent/robot/types.go
Normal file
255
openapi/agent/robot/types.go
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
package robot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
robotapi "github.com/yaoapp/yao/agent/robot/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Request Types ====================
|
||||||
|
|
||||||
|
// CreateRobotRequest - HTTP request for creating a robot
|
||||||
|
type CreateRobotRequest struct {
|
||||||
|
// Required fields
|
||||||
|
MemberID string `json:"member_id" binding:"required"` // Unique robot identifier
|
||||||
|
TeamID string `json:"team_id" binding:"required"` // Team ID
|
||||||
|
|
||||||
|
// Profile
|
||||||
|
DisplayName string `json:"display_name" binding:"required"` // Display name
|
||||||
|
Bio string `json:"bio,omitempty"` // Robot description
|
||||||
|
Avatar string `json:"avatar,omitempty"` // Avatar URL
|
||||||
|
|
||||||
|
// Identity & Role
|
||||||
|
SystemPrompt string `json:"system_prompt,omitempty"` // System prompt
|
||||||
|
RoleID string `json:"role_id,omitempty"` // Role within team
|
||||||
|
ManagerID string `json:"manager_id,omitempty"` // Direct manager user_id
|
||||||
|
|
||||||
|
// Status
|
||||||
|
Status string `json:"status,omitempty"` // Member status: active | inactive | pending | suspended
|
||||||
|
RobotStatus string `json:"robot_status,omitempty"` // Robot status: idle | working | paused | error | maintenance
|
||||||
|
AutonomousMode *bool `json:"autonomous_mode,omitempty"` // Whether autonomous mode is enabled
|
||||||
|
|
||||||
|
// Communication
|
||||||
|
RobotEmail string `json:"robot_email,omitempty"` // Robot email address
|
||||||
|
AuthorizedSenders interface{} `json:"authorized_senders,omitempty"` // Email whitelist (JSON array)
|
||||||
|
EmailFilterRules interface{} `json:"email_filter_rules,omitempty"` // Email filter rules (JSON array)
|
||||||
|
|
||||||
|
// Capabilities
|
||||||
|
RobotConfig interface{} `json:"robot_config,omitempty"` // Robot config JSON
|
||||||
|
Agents interface{} `json:"agents,omitempty"` // Accessible agents (JSON array)
|
||||||
|
MCPServers interface{} `json:"mcp_servers,omitempty"` // MCP servers (JSON array)
|
||||||
|
LanguageModel string `json:"language_model,omitempty"` // Language model name
|
||||||
|
|
||||||
|
// Limits
|
||||||
|
CostLimit float64 `json:"cost_limit,omitempty"` // Monthly cost limit USD
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRobotRequest - HTTP request for updating a robot
|
||||||
|
type UpdateRobotRequest struct {
|
||||||
|
// Profile
|
||||||
|
DisplayName *string `json:"display_name,omitempty"` // Display name
|
||||||
|
Bio *string `json:"bio,omitempty"` // Robot description
|
||||||
|
Avatar *string `json:"avatar,omitempty"` // Avatar URL
|
||||||
|
|
||||||
|
// Identity & Role
|
||||||
|
SystemPrompt *string `json:"system_prompt,omitempty"` // System prompt
|
||||||
|
RoleID *string `json:"role_id,omitempty"` // Role within team
|
||||||
|
ManagerID *string `json:"manager_id,omitempty"` // Direct manager user_id
|
||||||
|
|
||||||
|
// Status
|
||||||
|
Status *string `json:"status,omitempty"` // Member status
|
||||||
|
RobotStatus *string `json:"robot_status,omitempty"` // Robot status
|
||||||
|
AutonomousMode *bool `json:"autonomous_mode,omitempty"` // Autonomous mode
|
||||||
|
|
||||||
|
// Communication
|
||||||
|
RobotEmail *string `json:"robot_email,omitempty"` // Robot email address
|
||||||
|
AuthorizedSenders interface{} `json:"authorized_senders,omitempty"` // Email whitelist
|
||||||
|
EmailFilterRules interface{} `json:"email_filter_rules,omitempty"` // Email filter rules
|
||||||
|
|
||||||
|
// Capabilities
|
||||||
|
RobotConfig interface{} `json:"robot_config,omitempty"` // Robot config JSON
|
||||||
|
Agents interface{} `json:"agents,omitempty"` // Accessible agents
|
||||||
|
MCPServers interface{} `json:"mcp_servers,omitempty"` // MCP servers
|
||||||
|
LanguageModel *string `json:"language_model,omitempty"` // Language model name
|
||||||
|
|
||||||
|
// Limits
|
||||||
|
CostLimit *float64 `json:"cost_limit,omitempty"` // Monthly cost limit USD
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Response Types ====================
|
||||||
|
|
||||||
|
// Response - HTTP response for a robot
|
||||||
|
// Maps to frontend expectations: name ← member_id, description ← bio
|
||||||
|
type Response struct {
|
||||||
|
// Basic (mapped for frontend)
|
||||||
|
ID int64 `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"` // Frontend name ← member_id
|
||||||
|
Description string `json:"description"` // Frontend description ← bio
|
||||||
|
|
||||||
|
// Original fields
|
||||||
|
MemberID string `json:"member_id"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RobotStatus string `json:"robot_status"`
|
||||||
|
AutonomousMode bool `json:"autonomous_mode"`
|
||||||
|
|
||||||
|
// Profile
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Bio string `json:"bio,omitempty"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
|
||||||
|
// Identity & Role
|
||||||
|
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||||
|
RoleID string `json:"role_id,omitempty"`
|
||||||
|
ManagerID string `json:"manager_id,omitempty"`
|
||||||
|
|
||||||
|
// Communication
|
||||||
|
RobotEmail string `json:"robot_email,omitempty"`
|
||||||
|
AuthorizedSenders interface{} `json:"authorized_senders,omitempty"`
|
||||||
|
EmailFilterRules interface{} `json:"email_filter_rules,omitempty"`
|
||||||
|
|
||||||
|
// Capabilities
|
||||||
|
RobotConfig interface{} `json:"robot_config,omitempty"`
|
||||||
|
Agents interface{} `json:"agents,omitempty"`
|
||||||
|
MCPServers interface{} `json:"mcp_servers,omitempty"`
|
||||||
|
LanguageModel string `json:"language_model,omitempty"`
|
||||||
|
|
||||||
|
// Limits
|
||||||
|
CostLimit float64 `json:"cost_limit,omitempty"`
|
||||||
|
|
||||||
|
// Ownership & Audit
|
||||||
|
InvitedBy string `json:"invited_by,omitempty"`
|
||||||
|
JoinedAt *time.Time `json:"joined_at,omitempty"`
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusResponse - runtime status response
|
||||||
|
type StatusResponse struct {
|
||||||
|
MemberID string `json:"member_id"`
|
||||||
|
TeamID string `json:"team_id"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Bio string `json:"bio,omitempty"`
|
||||||
|
Status string `json:"status"` // Robot runtime status
|
||||||
|
Running int `json:"running"` // Current running executions
|
||||||
|
MaxRunning int `json:"max_running"` // Maximum concurrent executions
|
||||||
|
LastRun *time.Time `json:"last_run,omitempty"`
|
||||||
|
NextRun *time.Time `json:"next_run,omitempty"`
|
||||||
|
RunningIDs []string `json:"running_ids,omitempty"` // IDs of running executions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResponse - paginated list response
|
||||||
|
type ListResponse struct {
|
||||||
|
Data []*Response `json:"data"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pagesize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Conversion Functions ====================
|
||||||
|
|
||||||
|
// NewResponse creates a Response from api.RobotResponse
|
||||||
|
func NewResponse(r *robotapi.RobotResponse) *Response {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Response{
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.MemberID, // Frontend mapping: name ← member_id
|
||||||
|
Description: r.Bio, // Frontend mapping: description ← bio
|
||||||
|
MemberID: r.MemberID,
|
||||||
|
TeamID: r.TeamID,
|
||||||
|
Status: r.Status,
|
||||||
|
RobotStatus: r.RobotStatus,
|
||||||
|
AutonomousMode: r.AutonomousMode,
|
||||||
|
DisplayName: r.DisplayName,
|
||||||
|
Bio: r.Bio,
|
||||||
|
Avatar: r.Avatar,
|
||||||
|
SystemPrompt: r.SystemPrompt,
|
||||||
|
RoleID: r.RoleID,
|
||||||
|
ManagerID: r.ManagerID,
|
||||||
|
RobotEmail: r.RobotEmail,
|
||||||
|
AuthorizedSenders: r.AuthorizedSenders,
|
||||||
|
EmailFilterRules: r.EmailFilterRules,
|
||||||
|
RobotConfig: r.RobotConfig,
|
||||||
|
Agents: r.Agents,
|
||||||
|
MCPServers: r.MCPServers,
|
||||||
|
LanguageModel: r.LanguageModel,
|
||||||
|
CostLimit: r.CostLimit,
|
||||||
|
InvitedBy: r.InvitedBy,
|
||||||
|
JoinedAt: r.JoinedAt,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToAPICreateRequest converts HTTP request to api.CreateRobotRequest
|
||||||
|
func (r *CreateRobotRequest) ToAPICreateRequest() *robotapi.CreateRobotRequest {
|
||||||
|
return &robotapi.CreateRobotRequest{
|
||||||
|
MemberID: r.MemberID,
|
||||||
|
TeamID: r.TeamID,
|
||||||
|
DisplayName: r.DisplayName,
|
||||||
|
Bio: r.Bio,
|
||||||
|
Avatar: r.Avatar,
|
||||||
|
SystemPrompt: r.SystemPrompt,
|
||||||
|
RoleID: r.RoleID,
|
||||||
|
ManagerID: r.ManagerID,
|
||||||
|
Status: r.Status,
|
||||||
|
RobotStatus: r.RobotStatus,
|
||||||
|
AutonomousMode: r.AutonomousMode,
|
||||||
|
RobotEmail: r.RobotEmail,
|
||||||
|
AuthorizedSenders: r.AuthorizedSenders,
|
||||||
|
EmailFilterRules: r.EmailFilterRules,
|
||||||
|
RobotConfig: r.RobotConfig,
|
||||||
|
Agents: r.Agents,
|
||||||
|
MCPServers: r.MCPServers,
|
||||||
|
LanguageModel: r.LanguageModel,
|
||||||
|
CostLimit: r.CostLimit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToAPIUpdateRequest converts HTTP request to api.UpdateRobotRequest
|
||||||
|
func (r *UpdateRobotRequest) ToAPIUpdateRequest() *robotapi.UpdateRobotRequest {
|
||||||
|
return &robotapi.UpdateRobotRequest{
|
||||||
|
DisplayName: r.DisplayName,
|
||||||
|
Bio: r.Bio,
|
||||||
|
Avatar: r.Avatar,
|
||||||
|
SystemPrompt: r.SystemPrompt,
|
||||||
|
RoleID: r.RoleID,
|
||||||
|
ManagerID: r.ManagerID,
|
||||||
|
Status: r.Status,
|
||||||
|
RobotStatus: r.RobotStatus,
|
||||||
|
AutonomousMode: r.AutonomousMode,
|
||||||
|
RobotEmail: r.RobotEmail,
|
||||||
|
AuthorizedSenders: r.AuthorizedSenders,
|
||||||
|
EmailFilterRules: r.EmailFilterRules,
|
||||||
|
RobotConfig: r.RobotConfig,
|
||||||
|
Agents: r.Agents,
|
||||||
|
MCPServers: r.MCPServers,
|
||||||
|
LanguageModel: r.LanguageModel,
|
||||||
|
CostLimit: r.CostLimit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatusResponse creates a StatusResponse from api.RobotState
|
||||||
|
func NewStatusResponse(s *robotapi.RobotState) *StatusResponse {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StatusResponse{
|
||||||
|
MemberID: s.MemberID,
|
||||||
|
TeamID: s.TeamID,
|
||||||
|
DisplayName: s.DisplayName,
|
||||||
|
Bio: s.Bio,
|
||||||
|
Status: string(s.Status),
|
||||||
|
Running: s.Running,
|
||||||
|
MaxRunning: s.MaxRunning,
|
||||||
|
LastRun: s.LastRun,
|
||||||
|
NextRun: s.NextRun,
|
||||||
|
RunningIDs: s.RunningIDs,
|
||||||
|
}
|
||||||
|
}
|
||||||
43
openapi/agent/robot/utils.go
Normal file
43
openapi/agent/robot/utils.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package robot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLocale extracts locale from request
|
||||||
|
// Priority: query param > Accept-Language header > default
|
||||||
|
func GetLocale(c *gin.Context) string {
|
||||||
|
// Check query param first
|
||||||
|
if locale := c.Query("locale"); locale != "" {
|
||||||
|
return strings.ToLower(strings.TrimSpace(locale))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Accept-Language header
|
||||||
|
if acceptLang := c.GetHeader("Accept-Language"); acceptLang != "" {
|
||||||
|
// Parse first language from header (e.g., "en-US,en;q=0.9" -> "en-us")
|
||||||
|
parts := strings.Split(acceptLang, ",")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
lang := strings.Split(parts[0], ";")[0]
|
||||||
|
return strings.ToLower(strings.TrimSpace(lang))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default locale
|
||||||
|
return "en-us"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBoolValue parses various string formats into a boolean pointer
|
||||||
|
func ParseBoolValue(value string) *bool {
|
||||||
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
|
switch value {
|
||||||
|
case "1", "true", "yes", "on":
|
||||||
|
v := true
|
||||||
|
return &v
|
||||||
|
case "0", "false", "no", "off":
|
||||||
|
v := false
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
882
openapi/tests/agent/robot_test.go
Normal file
882
openapi/tests/agent/robot_test.go
Normal file
|
|
@ -0,0 +1,882 @@
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
|
||||||
|
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("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("CreateRobotMissingRequiredFields", func(t *testing.T) {
|
||||||
|
// Missing member_id
|
||||||
|
createData := map[string]interface{}{
|
||||||
|
"team_id": "test_team_001",
|
||||||
|
"display_name": "Test 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.StatusBadRequest, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue