mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 12:54:47 +08:00
## 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>
266 lines
7.3 KiB
Go
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 } |