Files
ragflow/internal/service/agent_test.go
Jack 6143205b37 feat: implement GET /api/v1/agents/<agent_id>/versions/<version_id> API (#15640)
## Summary

Implement the `GET /api/v1/agents/<agent_id>/versions/<version_id>`
endpoint in Go, returning full version details including DSL.

Depends on #15629 which introduced the version list endpoint and
`UserCanvasVersionDAO` infrastructure.

### Changes

- **Modified**: `internal/handler/agent.go` — Added `GetAgentVersion`
handler with auth check and ownership verification
- **Modified**: `internal/router/router.go` — Registered `GET
/:agent_id/versions/:version_id` route
- **New/Modified tests**: Service and handler tests for the version
detail endpoint

### Testing

```
=== RUN   TestGetVersion_Success       --- PASS
=== RUN   TestGetVersion_WrongCanvas   --- PASS
=== RUN   TestGetVersion_NotFound      --- PASS
=== RUN   TestGetAgentVersionHandler_Success      --- PASS
=== RUN   TestGetAgentVersionHandler_VersionNotFound --- PASS
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:13:26 +08:00

348 lines
9.2 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package service
import (
"testing"
"time"
"ragflow/internal/dao"
"ragflow/internal/entity"
)
// TestListVersions_Success verifies that ListVersions returns all versions
// for a canvas, ordered by update_time DESC.
func TestListVersions_Success(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
// Migrate tables needed for agent versions
if err := testDB.AutoMigrate(
&entity.User{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.UserTenant{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
now := time.Now()
// Insert canvas owner
testDB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "owner@test.com"})
// Insert canvas
testDB.Create(&entity.UserCanvas{
ID: "canvas-1",
UserID: "user-1",
Title: sptr("Test Agent"),
})
// Insert 3 versions with staggered timestamps
testDB.Create(&entity.UserCanvasVersion{
ID: "v1",
UserCanvasID: "canvas-1",
Title: sptr("v1_oldest"),
BaseModel: entity.BaseModel{
CreateTime: ptr(now.Add(-2 * time.Hour).UnixMilli()),
UpdateTime: ptr(now.Add(-2 * time.Hour).UnixMilli()),
},
})
testDB.Create(&entity.UserCanvasVersion{
ID: "v2",
UserCanvasID: "canvas-1",
Title: sptr("v2_middle"),
BaseModel: entity.BaseModel{
CreateTime: ptr(now.Add(-1 * time.Hour).UnixMilli()),
UpdateTime: ptr(now.Add(-1 * time.Hour).UnixMilli()),
},
})
testDB.Create(&entity.UserCanvasVersion{
ID: "v3",
UserCanvasID: "canvas-1",
Title: sptr("v3_newest"),
BaseModel: entity.BaseModel{
CreateTime: ptr(now.UnixMilli()),
UpdateTime: ptr(now.UnixMilli()),
},
})
svc := NewAgentService()
versions, err := svc.ListVersions("canvas-1")
if err != nil {
t.Fatalf("ListVersions failed: %v", err)
}
if len(versions) != 3 {
t.Fatalf("expected 3 versions, got %d", len(versions))
}
// Verify DESC order
if *versions[0].Title != "v3_newest" {
t.Errorf("expected v3_newest first, got %s", *versions[0].Title)
}
if *versions[1].Title != "v2_middle" {
t.Errorf("expected v2_middle second, got %s", *versions[1].Title)
}
if *versions[2].Title != "v1_oldest" {
t.Errorf("expected v1_oldest last, got %s", *versions[2].Title)
}
}
// TestListVersions_Empty verifies that ListVersions returns an empty slice
// when no versions exist.
func TestListVersions_Empty(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
// Insert canvas with no versions
testDB.Create(&entity.UserCanvas{
ID: "canvas-empty",
UserID: "user-1",
Title: sptr("Empty Agent"),
})
svc := NewAgentService()
versions, err := svc.ListVersions("canvas-empty")
if err != nil {
t.Fatalf("ListVersions failed: %v", err)
}
if len(versions) != 0 {
t.Errorf("expected 0 versions, got %d", len(versions))
}
}
// TestCheckCanvasAccess_Owner verifies that the canvas owner gets access.
func TestCheckCanvasAccess_Owner(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.User{},
&entity.UserCanvas{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
testDB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "a@b.com"})
testDB.Create(&entity.UserCanvas{ID: "c-1", UserID: "user-1", Title: sptr("My Agent")})
svc := NewAgentService()
ok, err := svc.CheckCanvasAccess("user-1", "c-1")
if err != nil {
t.Fatalf("CheckCanvasAccess failed: %v", err)
}
if !ok {
t.Error("expected owner to have access")
}
}
// TestCheckCanvasAccess_NotOwner verifies that a tenant member can access
// a team-level canvas.
func TestCheckCanvasAccess_NotOwner(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.User{},
&entity.UserCanvas{},
&entity.UserTenant{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
testDB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "a@b.com"})
testDB.Create(&entity.User{ID: "user-2", Nickname: "member", Email: "c@d.com"})
// user-2 is a member of user-1's tenant (status "1" = active)
testDB.Create(&entity.UserTenant{ID: "ut-1", UserID: "user-2", TenantID: "user-1", Role: "member", Status: sptr("1")})
// Canvas has team-level permission
testDB.Create(&entity.UserCanvas{ID: "c-1", UserID: "user-1", Permission: "team", Title: sptr("Team Agent")})
svc := NewAgentService()
ok, err := svc.CheckCanvasAccess("user-2", "c-1")
if err != nil {
t.Fatalf("CheckCanvasAccess failed: %v", err)
}
if !ok {
t.Error("expected tenant member to have access to team canvas")
}
}
// TestCheckCanvasAccess_PrivateCanvas_Denied verifies that a tenant member
// cannot access a private (default "me") canvas.
func TestCheckCanvasAccess_PrivateCanvas_Denied(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.User{},
&entity.UserCanvas{},
&entity.UserTenant{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
testDB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "a@b.com"})
testDB.Create(&entity.User{ID: "user-2", Nickname: "member", Email: "c@d.com"})
// user-2 is a tenant member (status "1" = active)
testDB.Create(&entity.UserTenant{ID: "ut-1", UserID: "user-2", TenantID: "user-1", Role: "member", Status: sptr("1")})
// Canvas has default "me" permission (private)
testDB.Create(&entity.UserCanvas{ID: "c-1", UserID: "user-1", Title: sptr("Private Agent")})
svc := NewAgentService()
ok, err := svc.CheckCanvasAccess("user-2", "c-1")
if err != nil {
t.Fatalf("CheckCanvasAccess failed: %v", err)
}
if ok {
t.Error("expected tenant member to be denied access to private canvas")
}
}
// TestCheckCanvasAccess_NotFound verifies behavior for non-existent canvas.
func TestCheckCanvasAccess_NotFound(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.User{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
testDB.Create(&entity.User{ID: "user-1", Nickname: "tester", Email: "a@b.com"})
svc := NewAgentService()
_, err := svc.CheckCanvasAccess("user-1", "non-existent")
if err == nil {
t.Error("expected error for non-existent canvas")
}
}
// TestGetVersion_Success verifies getting a specific version by ID.
func TestGetVersion_Success(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.UserCanvasVersion{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
testDB.Create(&entity.UserCanvasVersion{
ID: "v1",
UserCanvasID: "canvas-1",
Title: sptr("version-1"),
DSL: entity.JSONMap{"model": "gpt-4"},
})
svc := NewAgentService()
v, err := svc.GetVersion("canvas-1", "v1")
if err != nil {
t.Fatalf("GetVersion failed: %v", err)
}
if *v.Title != "version-1" {
t.Errorf("expected title 'version-1', got %s", *v.Title)
}
}
// TestGetVersion_WrongCanvas verifies version belonging to another canvas returns error.
func TestGetVersion_WrongCanvas(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.UserCanvasVersion{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
testDB.Create(&entity.UserCanvasVersion{
ID: "v1",
UserCanvasID: "canvas-1",
Title: sptr("version-1"),
})
svc := NewAgentService()
_, err := svc.GetVersion("canvas-other", "v1")
if err == nil {
t.Error("expected error for version belonging to another canvas")
}
}
// TestGetVersion_NotFound verifies error for non-existent version.
func TestGetVersion_NotFound(t *testing.T) {
testDB := setupServiceTestDB(t)
t.Helper()
if err := testDB.AutoMigrate(
&entity.UserCanvasVersion{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
orig := dao.DB
dao.DB = testDB
t.Cleanup(func() { dao.DB = orig })
svc := NewAgentService()
_, err := svc.GetVersion("canvas-1", "non-existent")
if err == nil {
t.Error("expected error for non-existent version")
}
}
// ptr returns a pointer to the given int64.
func ptr(v int64) *int64 { return &v }