Files
ragflow/internal/service/agent_test.go
Jack 02d163a177 feat: implement GET /api/v1/agents/<agent_id>/versions API (#15629)
## Summary

Implement the `GET /api/v1/agents/<agent_id>/versions` endpoint in Go,
listing all version snapshots for an agent canvas in descending update
time order.

### Changes

- **New**: `internal/dao/user_canvas_version.go` —
`UserCanvasVersionDAO` with `ListByCanvasID` (ordered by update_time
DESC) and `GetByID`
- **Modified**: `internal/service/agent.go` — Added `CheckCanvasAccess`,
`ListVersions`, `GetVersion` methods
- **Modified**: `internal/handler/agent.go` — Added `ListAgentVersions`
handler with auth check
- **Modified**: `internal/router/router.go` — Registered `GET
/:agent_id/versions` route
- **New**: `internal/service/agent_test.go` — 5 service-level tests
(SQLite in-memory DB, zero mock)
- **Modified**: `internal/handler/agent_test.go` — 3 handler-level tests
(real DB, pre-authenticated context)

### Testing

All 8 tests pass with zero mocking (in-memory SQLite replaces MySQL):

```
=== RUN   TestListVersions_Success         --- PASS
=== RUN   TestListVersions_Empty           --- PASS
=== RUN   TestCheckCanvasAccess_Owner      --- PASS
=== RUN   TestCheckCanvasAccess_NotOwner   --- PASS
=== RUN   TestCheckCanvasAccess_NotFound   --- PASS
=== RUN   TestListAgentVersionsHandler_Success      --- PASS
=== RUN   TestListAgentVersionsHandler_NoPermission --- PASS
=== RUN   TestListAgentVersionsHandler_CanvasNotFound --- PASS
```

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

---------

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

266 lines
7.3 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")
}
}
// ptr returns a pointer to the given int64.
func ptr(v int64) *int64 { return &v }