Files
yatm/internal/executor/fileops/library_test.go
T
Samuel Cui 4fef653b71 Prepare YATM v1.0.0-alpha.2
Unify file queries and Scan execution, retain indexed Identical results,
improve Job and Preview workflows, and verify release packaging.
2026-10-05 19:08:49 +08:00

149 lines
7.6 KiB
Go

package fileops
import (
"context"
"io/fs"
"path/filepath"
"testing"
"github.com/samuelncui/yatm/entity"
"github.com/samuelncui/yatm/internal/library"
"github.com/stretchr/testify/require"
)
func libraryRef(id int64) *entity.FileOperationRef {
return &entity.FileOperationRef{Target: &entity.FileOperationRef_FileId{FileId: id}}
}
func TestLibraryOperationsPreserveOrganizationBoundaries(t *testing.T) {
f := setup(t)
ctx := context.Background()
f.write(t, "disk.txt", "unchanged")
file := f.original(t, "disk.txt")
version := &library.FileVersion{FileID: file.ID, Signature: []byte("saved")}
require.NoError(t, f.libDB.Create(version).Error)
// Nested logical paths and returned identities use the shared request/stream without disk changes.
mkdir := f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MKDIR, Destination: libraryRef(0), Name: "archive/nested"}, nil)
require.EqualValues(t, 2, mkdir.summary().SucceededCount)
dirID := mkdir.updates[2].Entry.GetFileId()
require.NotZero(t, dirID)
move := f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MOVE, Sources: []*entity.FileOperationRef{libraryRef(file.ID)}, Destination: libraryRef(dirID), Name: "renamed.txt"}, nil)
require.EqualValues(t, 1, move.summary().SucceededCount)
stored, err := f.exe.Lib().GetByPath(ctx, 0, "archive/nested/renamed.txt")
require.NoError(t, err)
require.Equal(t, file.ID, stored.ID)
require.Equal(t, file.Note, stored.Note)
require.FileExists(t, filepath.Join(f.root, "disk.txt"))
// Library removal uses Trash and preserves the original, saved version and metadata.
f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_REMOVE, Sources: []*entity.FileOperationRef{libraryRef(file.ID), libraryRef(file.ID)}}, nil)
parents, err := f.exe.Lib().ListParents(ctx, file.ID)
require.NoError(t, err)
require.EqualValues(t, library.TrashFileID, parents[0].ID)
original, err := f.exe.Lib().GetFileLocation(ctx, file.ID)
require.NoError(t, err)
require.Equal(t, "disk.txt", original.Path)
require.NoError(t, f.libDB.First(new(library.FileVersion), version.ID).Error)
}
func TestLibraryMoveNestedNameAndDescendantRollback(t *testing.T) {
f := setup(t)
ctx := context.Background()
file := &library.File{Name: "old.txt", Kind: entity.FileKind_FILE_KIND_REGULAR}
require.NoError(t, f.exe.Lib().SaveFile(ctx, file))
stream := f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MOVE, Sources: []*entity.FileOperationRef{libraryRef(file.ID)}, Destination: libraryRef(0), Name: "archive/new.txt"}, nil)
require.EqualValues(t, 2, stream.summary().SucceededCount)
stored, err := f.exe.Lib().GetByPath(ctx, 0, "archive/new.txt")
require.NoError(t, err)
require.Equal(t, file.ID, stored.ID)
// A failed nested self-move rolls back intermediate directories and reports the affected item.
dir, err := f.exe.Lib().MkdirAll(ctx, 0, "parent", fs.ModePerm)
require.NoError(t, err)
stream = f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MOVE, Sources: []*entity.FileOperationRef{libraryRef(dir.ID)}, Destination: libraryRef(0), Name: "parent/new/renamed"}, nil)
require.EqualValues(t, 1, stream.summary().FailedCount)
uncommitted, err := f.exe.Lib().GetByPath(ctx, 0, "parent/new")
require.NoError(t, err)
require.Nil(t, uncommitted)
}
func TestLibraryMkdirRetainsExistingPathRules(t *testing.T) {
f := setup(t)
// MkdirAll owns logical path validation, including trailing separators and current-directory lookup.
stream := f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MKDIR,
Destination: libraryRef(0), Name: "nested/folder/"}, nil)
require.EqualValues(t, 2, stream.summary().SucceededCount)
id := stream.updates[2].Entry.GetFileId()
require.NotZero(t, id)
stream = f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MKDIR,
Destination: libraryRef(id), Name: "."}, nil)
require.EqualValues(t, 1, stream.summary().SucceededCount)
require.Equal(t, id, stream.updates[1].Entry.GetFileId())
}
func TestLibraryMovePreservesMetadataEditedAfterPreparation(t *testing.T) {
f := setup(t)
ctx := context.Background()
file := &library.File{Name: "old.txt", Kind: entity.FileKind_FILE_KIND_REGULAR, Note: "old note"}
require.NoError(t, f.exe.Lib().SaveFile(ctx, file))
// Streaming can delay execution after preparation; moving must not save an old Note snapshot.
stream := f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MOVE,
Sources: []*entity.FileOperationRef{libraryRef(file.ID)}, Destination: libraryRef(0), Name: "new.txt"}, func() {
require.NoError(t, f.libDB.Model(library.ModelFile).Where("id = ?", file.ID).Update("note", "edited while preparing").Error)
})
require.EqualValues(t, 1, stream.summary().SucceededCount)
stored, err := f.exe.Lib().GetFile(ctx, file.ID)
require.NoError(t, err)
require.Equal(t, "new.txt", stored.Name)
require.Equal(t, "edited while preparing", stored.Note)
}
func TestLibraryOverlappingSelectionAndDirectoryMerge(t *testing.T) {
f := setup(t)
ctx := context.Background()
source, err := f.exe.Lib().MkdirAll(ctx, 0, "from/folder", fs.ModePerm)
require.NoError(t, err)
child, err := f.exe.Lib().MkdirAll(ctx, source.ID, "child", fs.ModePerm)
require.NoError(t, err)
target, err := f.exe.Lib().MkdirAll(ctx, 0, "to", fs.ModePerm)
require.NoError(t, err)
existing, err := f.exe.Lib().MkdirAll(ctx, target.ID, "folder", fs.ModePerm)
require.NoError(t, err)
// The selected parent owns its descendants; Library's existing directory merge remains intact.
stream := f.execute(t, &entity.FileOperationSpec{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MOVE,
Sources: []*entity.FileOperationRef{libraryRef(child.ID), libraryRef(source.ID)}, Destination: libraryRef(target.ID)}, nil)
require.EqualValues(t, 2, stream.summary().SucceededCount)
require.Equal(t, existing.ID, stream.updates[2].Entry.GetFileId())
stored, err := f.exe.Lib().GetFile(ctx, child.ID)
require.NoError(t, err)
require.Equal(t, existing.ID, stored.ParentID)
}
func TestSharedOperationRejectsInvalidSelectionsBeforeMutation(t *testing.T) {
f := setup(t)
f.write(t, "physical.txt", "must remain")
file := f.original(t, "physical.txt")
for _, spec := range []*entity.FileOperationSpec{
nil,
{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_REMOVE, Sources: []*entity.FileOperationRef{nil}},
{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_REMOVE, Sources: []*entity.FileOperationRef{{}}},
{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_REMOVE, Sources: []*entity.FileOperationRef{libraryRef(file.ID), f.ref(t, "physical.txt")}},
{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_REMOVE, Sources: []*entity.FileOperationRef{libraryRef(file.ID), libraryRef(999999)}},
{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_REMOVE, Sources: []*entity.FileOperationRef{libraryRef(0)}},
{Kind: entity.FileOperationKind(2), Sources: []*entity.FileOperationRef{libraryRef(file.ID)}, Destination: libraryRef(0)},
{Kind: entity.FileOperationKind_FILE_OPERATION_KIND_MOVE, Sources: []*entity.FileOperationRef{libraryRef(file.ID)}, Destination: libraryRef(file.ID)},
} {
stream := &operationStream{ctx: context.Background()}
err := (&service{exe: f.exe}).executeRequest(&operationRequest{Spec: spec}, stream)
require.Error(t, err)
require.Empty(t, stream.updates)
stored, err := f.exe.Lib().GetFile(context.Background(), file.ID)
require.NoError(t, err)
require.Equal(t, file.ParentID, stored.ParentID)
require.FileExists(t, filepath.Join(f.root, "physical.txt"))
}
}