- Introduced a new `Type` field in the `ActivityQuery` struct to allow filtering activities by type (e.g., execution.started, execution.completed, execution.failed, execution.cancelled). - Updated the `ListActivities` method in `ExecutionStore` to handle the new type filter, mapping it to corresponding execution statuses. - Enhanced unit tests in `execution_test.go` to validate filtering by activity type, including tests for valid and invalid type scenarios. - Modified OpenAPI definitions and related types to support the new type filter in the activities endpoint, improving API usability and flexibility.
494 lines
16 KiB
Go
494 lines
16 KiB
Go
package openapi_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/yao/openapi"
|
|
"github.com/yaoapp/yao/openapi/tests/testutils"
|
|
)
|
|
|
|
// TestListResults tests the results listing endpoint
|
|
// GET /v1/agent/robots/:id/results
|
|
func TestListResults(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping results tests in short mode")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Results 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")
|
|
|
|
// Create a test robot
|
|
robotID := fmt.Sprintf("test_results_list_%d", time.Now().UnixNano())
|
|
createRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Results List Robot")
|
|
defer deleteRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID)
|
|
|
|
t.Run("ListResultsSuccess", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// Verify pagination fields exist
|
|
assert.Contains(t, response, "data")
|
|
assert.Contains(t, response, "page")
|
|
assert.Contains(t, response, "pagesize")
|
|
assert.Contains(t, response, "total")
|
|
|
|
// Verify result items structure (if any results exist)
|
|
data, ok := response["data"].([]interface{})
|
|
if ok && len(data) > 0 {
|
|
result := data[0].(map[string]interface{})
|
|
// Basic fields should exist
|
|
assert.Contains(t, result, "id")
|
|
assert.Contains(t, result, "member_id")
|
|
assert.Contains(t, result, "trigger_type")
|
|
assert.Contains(t, result, "status")
|
|
assert.Contains(t, result, "name")
|
|
assert.Contains(t, result, "summary")
|
|
assert.Contains(t, result, "has_attachments")
|
|
t.Logf("Result response fields: id=%v, name=%v, summary=%v",
|
|
result["id"], result["name"], result["summary"])
|
|
}
|
|
})
|
|
|
|
t.Run("ListResultsWithPagination", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results?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("ListResultsWithTriggerTypeFilter", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results?trigger_type=clock", 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.Contains(t, response, "data")
|
|
})
|
|
|
|
t.Run("ListResultsWithKeywordFilter", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results?keyword=test", 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.Contains(t, response, "data")
|
|
})
|
|
|
|
t.Run("ListResultsRobotNotFound", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/non_existent_robot/results", 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)
|
|
})
|
|
|
|
t.Run("ListResultsUnauthorized", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results", 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)
|
|
})
|
|
}
|
|
|
|
// TestGetResult tests the result detail endpoint
|
|
// GET /v1/agent/robots/:id/results/:result_id
|
|
func TestGetResult(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping result tests in short mode")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Result 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
|
|
robotID := fmt.Sprintf("test_result_get_%d", time.Now().UnixNano())
|
|
createRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Result Get Robot")
|
|
defer deleteRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID)
|
|
|
|
t.Run("GetResultNotFound", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results/non_existent_result", 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)
|
|
})
|
|
|
|
t.Run("GetResultRobotNotFound", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/non_existent_robot/results/some_result", 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)
|
|
})
|
|
|
|
t.Run("GetResultUnauthorized", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results/some_result", 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)
|
|
})
|
|
}
|
|
|
|
// TestListActivities tests the activities listing endpoint
|
|
// GET /v1/agent/robots/activities
|
|
func TestListActivities(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping activities tests in short mode")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Activities 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("ListActivitiesSuccess", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities", 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 data field exists
|
|
assert.Contains(t, response, "data")
|
|
|
|
// Verify activity items structure (if any activities exist)
|
|
data, ok := response["data"].([]interface{})
|
|
if ok && len(data) > 0 {
|
|
activity := data[0].(map[string]interface{})
|
|
// Basic fields should exist
|
|
assert.Contains(t, activity, "type")
|
|
assert.Contains(t, activity, "robot_id")
|
|
assert.Contains(t, activity, "execution_id")
|
|
assert.Contains(t, activity, "message")
|
|
assert.Contains(t, activity, "timestamp")
|
|
t.Logf("Activity response fields: type=%v, robot_id=%v, message=%v",
|
|
activity["type"], activity["robot_id"], activity["message"])
|
|
}
|
|
})
|
|
|
|
t.Run("ListActivitiesWithLimit", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities?limit=10", 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.Contains(t, response, "data")
|
|
})
|
|
|
|
t.Run("ListActivitiesWithTypeFilter", func(t *testing.T) {
|
|
// Test filtering by type: execution.completed
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities?type=execution.completed", 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.Contains(t, response, "data")
|
|
|
|
// Verify all returned activities are of the specified type
|
|
data, ok := response["data"].([]interface{})
|
|
if ok && len(data) > 0 {
|
|
for _, item := range data {
|
|
activity := item.(map[string]interface{})
|
|
assert.Equal(t, "execution.completed", activity["type"])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ListActivitiesWithTypeStarted", func(t *testing.T) {
|
|
// Test filtering by type: execution.started
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities?type=execution.started", 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.Contains(t, response, "data")
|
|
|
|
// Verify all returned activities are of the specified type
|
|
data, ok := response["data"].([]interface{})
|
|
if ok && len(data) > 0 {
|
|
for _, item := range data {
|
|
activity := item.(map[string]interface{})
|
|
assert.Equal(t, "execution.started", activity["type"])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ListActivitiesWithInvalidType", func(t *testing.T) {
|
|
// Test with an invalid/unknown type - should return empty data (not error)
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities?type=invalid.type", 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.Contains(t, response, "data")
|
|
// Invalid type should return empty array
|
|
data, ok := response["data"].([]interface{})
|
|
assert.True(t, ok)
|
|
assert.Empty(t, data)
|
|
})
|
|
|
|
t.Run("ListActivitiesWithSince", func(t *testing.T) {
|
|
// Use a timestamp in the past - URL encode properly
|
|
since := time.Now().Add(-24 * time.Hour).UTC().Format(time.RFC3339)
|
|
// URL encode the since parameter (+ becomes %2B, : stays)
|
|
encodedSince := url.QueryEscape(since)
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities?since="+encodedSince, 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.Contains(t, response, "data")
|
|
})
|
|
|
|
t.Run("ListActivitiesWithInvalidSince", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities?since=invalid_date", 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()
|
|
|
|
// Should return 400 Bad Request for invalid date format
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ListActivitiesUnauthorized", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/activities", 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)
|
|
})
|
|
}
|
|
|
|
// TestResultsPermissions tests result permission inheritance from robot
|
|
func TestResultsPermissions(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping results permission tests in short mode")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client
|
|
client := testutils.RegisterTestClient(t, "Results Permission Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
|
|
// Create User 1
|
|
token1 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
user1ID := token1.UserID
|
|
|
|
// Create User 2
|
|
token2 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// User 1 creates a robot
|
|
robotID := fmt.Sprintf("test_results_perm_%d", time.Now().UnixNano())
|
|
createRobotWithTeam(t, serverURL, baseURL, token1.AccessToken, robotID, "Results Permission Test Robot", user1ID)
|
|
defer deleteRobot(t, serverURL, baseURL, token1.AccessToken, robotID)
|
|
|
|
t.Run("OwnerCanListResults", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results", nil)
|
|
require.NoError(t, err)
|
|
|
|
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.StatusOK, resp.StatusCode)
|
|
t.Logf("Owner (User 1) successfully listed results for their robot")
|
|
})
|
|
|
|
t.Run("OtherUserResultsAccess", func(t *testing.T) {
|
|
// User 2 attempts to list results for User 1's robot
|
|
// With system:root scope this might succeed
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/results", nil)
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+token2.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
t.Logf("User 2 results list attempt status: %d (with system:root scope)", resp.StatusCode)
|
|
})
|
|
}
|