160 lines
6.2 KiB
Go
160 lines
6.2 KiB
Go
package tasks
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"senlinai-agent/backend/internal/models"
|
|
)
|
|
|
|
func TestUpdateLocksTaskBeforeProjectValidation(t *testing.T) {
|
|
database, owner, project, task := newTaskLockFixture(t)
|
|
queries := captureTaskQueryOrder(t, database)
|
|
|
|
_, err := NewService(database).Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title})
|
|
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, *queries)
|
|
require.Equal(t, "task:locked-in-tx", (*queries)[0])
|
|
}
|
|
|
|
func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) {
|
|
database, _, project, task := newTaskLockFixture(t)
|
|
document := models.SaDocumentTree{ProjectID: project.ID, CreatedBy: task.CreatedBy, Kind: models.DocumentKindFile, Name: "Context.md", NormalizedName: "context.md", Extension: ".md", Revision: 1}
|
|
require.NoError(t, database.Create(&document).Error)
|
|
queries := captureTaskQueryOrder(t, database)
|
|
|
|
err := NewService(database).ShareObject(task.ID, "document", document.ID)
|
|
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, *queries)
|
|
require.Equal(t, "task:locked-in-tx", (*queries)[0])
|
|
}
|
|
|
|
func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) {
|
|
database := newTestDB(t)
|
|
assigneeID := uint(2)
|
|
task := models.SaTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"}
|
|
document := models.SaDocumentTree{ProjectID: 1, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "合同背景.md", NormalizedName: "合同背景.md", Extension: ".md", Revision: 1}
|
|
require.NoError(t, database.Create(&task).Error)
|
|
require.NoError(t, database.Create(&document).Error)
|
|
service := NewService()
|
|
|
|
before, err := service.VisibleLinkedObjects(task.ID, assigneeID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, before)
|
|
|
|
require.NoError(t, service.ShareObject(task.ID, "document", document.ID))
|
|
after, err := service.VisibleLinkedObjects(task.ID, assigneeID)
|
|
require.NoError(t, err)
|
|
require.Len(t, after, 1)
|
|
require.Equal(t, "document", after[0].ObjectType)
|
|
require.Equal(t, document.Identity, after[0].ObjectID)
|
|
}
|
|
|
|
func TestShareObjectRejectsUnsupportedType(t *testing.T) {
|
|
newTestDB(t)
|
|
service := NewService()
|
|
|
|
err := service.ShareObject(1, "ai_session", 9)
|
|
|
|
require.ErrorContains(t, err, "unsupported shared object type")
|
|
}
|
|
|
|
func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) {
|
|
database := newTestDB(t)
|
|
task := models.SaTask{ProjectID: 1, CreatedBy: 1, Title: "Review"}
|
|
document := models.SaDocumentTree{ProjectID: 2, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Other project.md", NormalizedName: "other project.md", Extension: ".md", Revision: 1}
|
|
require.NoError(t, database.Create(&task).Error)
|
|
require.NoError(t, database.Create(&document).Error)
|
|
service := NewService()
|
|
|
|
err := service.ShareObject(task.ID, "document", document.ID)
|
|
|
|
require.ErrorContains(t, err, "shared object not found in task project")
|
|
}
|
|
|
|
func TestAssignResolvesIdentityAndTaskDTOReflectsReassignment(t *testing.T) {
|
|
database := newTestDB(t)
|
|
owner := models.SaUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
|
first := models.SaUser{Email: "first@example.com", DisplayName: "First", PasswordHash: "hash"}
|
|
second := models.SaUser{Email: "second@example.com", DisplayName: "Second", PasswordHash: "hash"}
|
|
require.NoError(t, database.Create(&owner).Error)
|
|
require.NoError(t, database.Create(&first).Error)
|
|
require.NoError(t, database.Create(&second).Error)
|
|
project := models.SaProject{OwnerID: owner.ID, Name: "Alpha", Identifier: "ALPHA"}
|
|
require.NoError(t, database.Create(&project).Error)
|
|
task := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "安排评审", Status: "open"}
|
|
require.NoError(t, database.Create(&task).Error)
|
|
service := NewService(database)
|
|
|
|
require.NoError(t, service.Assign(task.ID, first.Identity))
|
|
assigned, err := service.Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title})
|
|
require.NoError(t, err)
|
|
require.Equal(t, first.Identity, *assigned.AssigneeID)
|
|
|
|
require.NoError(t, service.Assign(task.ID, second.Identity))
|
|
reassigned, err := service.Update(owner.ID, project.Identity, task.Identity, UpdateTaskInput{Title: task.Title})
|
|
require.NoError(t, err)
|
|
require.Equal(t, second.Identity, *reassigned.AssigneeID)
|
|
|
|
var event models.SaProjectEvent
|
|
require.NoError(t, database.Where("project_id = ? AND entity_type = ? AND entity_id = ?", project.ID, "task", task.ID).Order("id desc").First(&event).Error)
|
|
require.Equal(t, "task_assigned", event.EventType)
|
|
}
|
|
|
|
func newTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, models.AutoMigrate(database))
|
|
models.DBService = database
|
|
return database
|
|
}
|
|
|
|
func newTaskLockFixture(t *testing.T) (*gorm.DB, models.SaUser, models.SaProject, models.SaTask) {
|
|
t.Helper()
|
|
database := newTestDB(t)
|
|
owner := models.SaUser{Email: "lock-owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}
|
|
require.NoError(t, database.Create(&owner).Error)
|
|
project := models.SaProject{OwnerID: owner.ID, Name: "Lock", Identifier: "LOCK"}
|
|
require.NoError(t, database.Create(&project).Error)
|
|
task := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "Lock me", Status: "open"}
|
|
require.NoError(t, database.Create(&task).Error)
|
|
return database, owner, project, task
|
|
}
|
|
|
|
func captureTaskQueryOrder(t *testing.T, database *gorm.DB) *[]string {
|
|
t.Helper()
|
|
var mutex sync.Mutex
|
|
queries := make([]string, 0, 4)
|
|
callbackName := "test:capture_task_lock_" + t.Name()
|
|
require.NoError(t, database.Callback().Query().Before("gorm:query").Register(callbackName, func(tx *gorm.DB) {
|
|
table := tx.Statement.Table
|
|
if tx.Statement.Schema != nil {
|
|
table = tx.Statement.Schema.Table
|
|
}
|
|
if table != (models.SaTask{}).TableName() && table != (models.SaProject{}).TableName() {
|
|
return
|
|
}
|
|
entry := "project"
|
|
if table == (models.SaTask{}).TableName() {
|
|
entry = "task"
|
|
if _, ok := tx.Statement.Clauses["FOR"]; ok {
|
|
entry += ":locked"
|
|
if _, ok := tx.Statement.ConnPool.(gorm.TxCommitter); ok {
|
|
entry += "-in-tx"
|
|
}
|
|
}
|
|
}
|
|
mutex.Lock()
|
|
queries = append(queries, entry)
|
|
mutex.Unlock()
|
|
}))
|
|
return &queries
|
|
}
|