feat(documents): rebuild project document workbench
This commit is contained in:
126
backend/internal/logic/documents/service_test.go
Normal file
126
backend/internal/logic/documents/service_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
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.SaDocument{}).Where("project_id = ?", project.ID).Count(&visible).Error)
|
||||
require.Zero(t, visible)
|
||||
var retained int64
|
||||
require.NoError(t, database.Unscoped().Model(&models.SaDocument{}).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
|
||||
}
|
||||
Reference in New Issue
Block a user