package documents import ( "bytes" "errors" "fmt" "os" "testing" "time" "github.com/glebarez/sqlite" "github.com/stretchr/testify/require" "gorm.io/gorm" "senlinai-agent/backend/internal/models" ) func TestDocumentTreeRevisionMoveDeleteAndShareLifecycle(t *testing.T) { service, database, user, project := newDocumentTestService(t) folder, err := service.CreateFolder(user.ID, project.Identity, CreateInput{Name: "产品"}) require.NoError(t, err) child, err := service.CreateFolder(user.ID, project.Identity, CreateInput{Name: "方案", ParentIdentity: folder.Identity}) require.NoError(t, err) document, err := service.CreateMarkdown(user.ID, project.Identity, CreateInput{ Name: "说明.md", ParentIdentity: child.Identity, Markdown: "# 第一版", }) require.NoError(t, err) loaded, err := service.Get(user.ID, project.Identity, document.Identity) require.NoError(t, err) require.Equal(t, "# 第一版", loaded.Content.Markdown) revision := loaded.Document.Revision nextMarkdown := "# 第二版" updated, err := service.Update(user.ID, project.Identity, document.Identity, UpdateInput{ Markdown: &nextMarkdown, Revision: &revision, }) require.NoError(t, err) require.Equal(t, revision+1, updated.Document.Revision) stale := "# 陈旧覆盖" _, err = service.Update(user.ID, project.Identity, document.Identity, UpdateInput{ Markdown: &stale, Revision: &revision, }) require.ErrorIs(t, err, ErrRevisionConflict) parent := child.Identity _, err = service.Update(user.ID, project.Identity, folder.Identity, UpdateInput{ParentIdentity: &parent}) require.ErrorIs(t, err, ErrInvalidParent) share, token, err := service.CreateShare(user.ID, project.Identity, document.Identity, 24*time.Hour) require.NoError(t, err) require.NotEmpty(t, token) require.NotEqual(t, token, share.TokenHash) shared, err := service.ResolveShare(token) require.NoError(t, err) require.Equal(t, document.Identity, shared.Document.Identity) require.NoError(t, service.RevokeShare(user.ID, project.Identity, document.Identity, share.Identity)) _, err = service.ResolveShare(token) require.ErrorIs(t, err, ErrShareNotFound) require.NoError(t, service.Delete(user.ID, project.Identity, folder.Identity)) var visible int64 require.NoError(t, database.Model(&models.SaDocumentTree{}).Where("project_id = ?", project.ID).Count(&visible).Error) require.Zero(t, visible) var retained int64 require.NoError(t, database.Unscoped().Model(&models.SaDocumentTree{}).Where("project_id = ?", project.ID).Count(&retained).Error) require.Equal(t, int64(3), retained) } func TestDocumentUploadUsesOpaqueStorageAndMarkdownNamesAreDeduplicated(t *testing.T) { service, _, user, project := newDocumentTestService(t) first, err := service.CreateMarkdown(user.ID, project.Identity, CreateInput{Name: "记录.md", Markdown: "first"}) require.NoError(t, err) second, err := service.CreateMarkdown(user.ID, project.Identity, CreateInput{Name: "记录.md", Markdown: "second"}) require.NoError(t, err) require.Equal(t, "记录.md", first.Name) require.Equal(t, "记录 (1).md", second.Name) content := bytes.Repeat([]byte("pdf-content"), 128) stored, err := service.Store(project.Identity, "../../合同.pdf", bytes.NewReader(content)) require.NoError(t, err) require.Equal(t, "合同.pdf", stored.OriginalName) require.NotContains(t, stored.RelativePath, "合同.pdf") require.FileExists(t, stored.AbsolutePath) uploaded, err := service.CreateUploaded(user.ID, project.Identity, "", stored) require.NoError(t, err) require.Equal(t, ".pdf", uploaded.Extension) require.Equal(t, int64(len(content)), uploaded.Size) file, err := os.Open(stored.AbsolutePath) require.NoError(t, err) defer file.Close() } func TestDocumentExportProducesDownloadableFormats(t *testing.T) { service, _, user, project := newDocumentTestService(t) document, err := service.CreateMarkdown(user.ID, project.Identity, CreateInput{Name: "导出.md", Markdown: "# 标题\n\n正文"}) require.NoError(t, err) loaded, err := service.Get(user.ID, project.Identity, document.Identity) require.NoError(t, err) for _, format := range []string{"md", "docx", "pdf"} { data, name, contentType, err := service.Export(*loaded, format) require.NoError(t, err) require.NotEmpty(t, data) require.Equal(t, "导出."+format, name) require.NotEmpty(t, contentType) } } func newDocumentTestService(t *testing.T) (*Service, *gorm.DB, models.SaUser, models.SaProject) { t.Helper() database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{TranslateError: true}) require.NoError(t, err) require.NoError(t, models.AutoMigrate(database)) user := models.SaUser{Email: t.Name() + "@example.com", DisplayName: "Owner", PasswordHash: "hash"} require.NoError(t, database.Create(&user).Error) project := models.SaProject{OwnerID: user.ID, Name: "Documents", Identifier: "DOCS-" + fmt.Sprint(time.Now().UnixNano())} require.NoError(t, database.Create(&project).Error) service := NewService(database, t.TempDir()) service.now = func() time.Time { return time.Date(2026, 7, 24, 0, 0, 0, 0, time.UTC) } t.Cleanup(func() { require.False(t, errors.Is(database.Error, gorm.ErrInvalidDB)) }) return service, database, user, project }