Files
dingtalk-workspace-cli/internal/helpers/drive_sync_edges_test.go

296 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
"github.com/spf13/cobra"
)
var errTestRead = errors.New("simulated read failure")
// errReader 让 bufio 读取返回非 EOF 错误,覆盖 ask 的读取失败分支。
type errReader struct{}
func (errReader) Read([]byte) (int, error) { return 0, errTestRead }
// findDriveSubcommand 取出 drive 下的指定子命令,便于直接调用其 RunE。
func findDriveSubcommand(t *testing.T, name string) *cobra.Command {
t.Helper()
for _, c := range newDriveCommand().Commands() {
if c.Name() == name {
return c
}
}
t.Fatalf("drive subcommand %q not found", name)
return nil
}
// mustSetFlags 设置一组 flag,任何失败都直接终止用例。
func mustSetFlags(t *testing.T, cmd *cobra.Command, kv map[string]string) {
t.Helper()
for k, v := range kv {
if err := cmd.Flags().Set(k, v); err != nil {
t.Fatalf("set --%s=%s: %v", k, v, err)
}
}
}
// ──────────────────────────────────────────────────────────
// --if-exists 显式空值回落默认
// ──────────────────────────────────────────────────────────
func TestCrossPlatformCoverageDrivePull_emptyIfExistsDefaultsToSkip(t *testing.T) {
root := t.TempDir()
p := filepath.Join(root, "a.txt")
mustWrite(t, p, "local-old")
caller := pullListingCaller(`{"name":"a.txt","type":"file","fileId":"A","modifyTime":1}`)
swapPullDownloadPath(t, func(_ context.Context, _ string, _ map[string]string, dest string) error {
return os.WriteFile(dest, []byte("remote-new"), 0o644)
})
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT", "--if-exists", ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if b, _ := os.ReadFile(p); string(b) != "local-old" {
t.Errorf("empty --if-exists must default to skip and keep the local file, got %q", string(b))
}
}
func TestCrossPlatformCoverageDrivePush_emptyIfExistsDefaultsToSkip(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.txt"), "local")
withNoopPut(t)
caller := pushOKCaller(map[string]string{
"ROOT": `{"result":{"items":[{"name":"a.txt","type":"file","fileId":"A","modifyTime":1}],"nextToken":""}}`,
})
if err := runDriveCmd(t, caller, "push", "--local-folder", dir, "--remote-folder", "ROOT", "--if-exists", ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := caller.callsFor("get_upload_info"); len(got) != 0 {
t.Errorf("empty --if-exists must behave as skip, got %v", got)
}
}
// ──────────────────────────────────────────────────────────
// pullOneFile 的落盘失败分支
// ──────────────────────────────────────────────────────────
// 目标目录不可写 → 创建临时文件失败。
func TestCrossPlatformCoveragePullOneFile_tempFileCreationFailure(t *testing.T) {
root := t.TempDir()
testseam.Swap(t, &pullCreateTemp, func(*os.Root) (*os.File, string, error) {
return nil, "", errors.New("create temp boom")
})
testseam.Swap(t, &deps, &Deps{Caller: pullListingCaller(""), Out: &Formatter{w: io.Discard}})
testseam.Swap(t, &os.Args, []string{"dws", "drive"})
action, err := pullOneFile(context.Background(), "", &remoteFile{RelPath: "a.txt", FileID: "A"},
filepath.Join(root, "a.txt"), ifExistsOverwrite)
if action != pullActionFailed || err == nil {
t.Fatalf("action=%q err=%v, want failed + error", action, err)
}
}
// 目标路径是目录 → 最后的原子 rename 失败。
func TestCrossPlatformCoveragePullOneFile_renameOntoDirectoryFails(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "a.txt")
if err := os.MkdirAll(target, 0o755); err != nil { // 目标被目录占位
t.Fatal(err)
}
testseam.Swap(t, &deps, &Deps{Caller: pullListingCaller(""), Out: &Formatter{w: io.Discard}})
testseam.Swap(t, &os.Args, []string{"dws", "drive"})
swapPullDownloadPath(t, func(_ context.Context, _ string, _ map[string]string, dest string) error {
return os.WriteFile(dest, []byte("payload"), 0o644)
})
action, err := pullOneFile(context.Background(), "", &remoteFile{RelPath: "a.txt", FileID: "A"},
target, ifExistsOverwrite)
if action != pullActionFailed || err == nil {
t.Fatalf("action=%q err=%v, want failed + error", action, err)
}
}
// ──────────────────────────────────────────────────────────
// PrintJSON 失败分支
// ──────────────────────────────────────────────────────────
func TestCrossPlatformCoverageDrivePull_printFailurePropagates(t *testing.T) {
dir := t.TempDir()
testseam.Swap(t, &deps, &Deps{Caller: pullListingCaller(""), Out: &Formatter{w: failingWriter{}}})
testseam.Swap(t, &os.Args, []string{"dws", "drive", "pull"})
cmd := findDriveSubcommand(t, "pull")
mustSetFlags(t, cmd, map[string]string{"local-folder": dir, "remote-folder": "ROOT"})
if err := runDrivePull(cmd, nil); err == nil {
t.Fatal("expected the PrintJSON writer failure to propagate")
}
}
func TestCrossPlatformCoverageDrivePush_printFailurePropagates(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.txt"), "x")
withNoopPut(t)
testseam.Swap(t, &deps, &Deps{Caller: pushOKCaller(nil), Out: &Formatter{w: failingWriter{}}})
testseam.Swap(t, &os.Args, []string{"dws", "drive", "push"})
cmd := findDriveSubcommand(t, "push")
mustSetFlags(t, cmd, map[string]string{"local-folder": dir, "remote-folder": "ROOT"})
if err := runDrivePush(cmd, nil); err == nil {
t.Fatal("expected the PrintJSON writer failure to propagate")
}
}
func TestCrossPlatformCoverageDriveSync_printFailurePropagates(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.txt"), "x")
testseam.Swap(t, &pushPutOpenedFile, func(context.Context, string, map[string]string, *os.File, int64) error { return nil })
testseam.Swap(t, &deps, &Deps{Caller: syncCaller(nil), Out: &Formatter{w: failingWriter{}}})
testseam.Swap(t, &os.Args, []string{"dws", "drive", "sync"})
cmd := findDriveSubcommand(t, "sync")
mustSetFlags(t, cmd, map[string]string{"local-folder": dir, "remote-folder": "ROOT", "quick": "true"})
if err := runDriveSync(cmd, nil); err == nil {
t.Fatal("expected the PrintJSON writer failure to propagate")
}
}
// ──────────────────────────────────────────────────────────
// walkLocalForPush / walkLocalTree 的 WalkDir 错误回调
// ──────────────────────────────────────────────────────────
func TestCrossPlatformCoverageWalkLocal_unreadableSubdirectoryErrors(t *testing.T) {
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
t.Skip("unreadable dir needs POSIX perms and a non-root user")
}
dir := t.TempDir()
sub := filepath.Join(dir, "locked")
if err := os.MkdirAll(sub, 0o755); err != nil {
t.Fatal(err)
}
mustWrite(t, filepath.Join(sub, "inner.txt"), "x")
if err := os.Chmod(sub, 0o000); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chmod(sub, 0o755) })
if _, _, err := walkLocalForPush(dir); err == nil {
t.Error("walkLocalForPush must surface the WalkDir error")
}
if _, err := walkLocalTree(dir); err == nil {
t.Error("walkLocalTree must surface the WalkDir error")
}
}
// ──────────────────────────────────────────────────────────
// verifyNoSymlinkEscape 的剩余分支
// ──────────────────────────────────────────────────────────
// 目标位于 absDir 之外 → 走到根目录之上即放行(不是逃逸判定范围)。
func TestCrossPlatformCoverageVerifyNoSymlinkEscape_targetOutsideRootIsIgnored(t *testing.T) {
root := t.TempDir()
outside := filepath.Join(t.TempDir(), "elsewhere", "x.txt")
if err := verifyNoSymlinkEscape(root, outside); err != nil {
t.Errorf("target above the root must be ignored, got %v", err)
}
}
// 已存在路径含符号链接环 → EvalSymlinks 失败要如实上报。
func TestCrossPlatformCoverageVerifyNoSymlinkEscape_symlinkLoopErrors(t *testing.T) {
root := t.TempDir()
loop := filepath.Join(root, "loop")
if err := os.Symlink(loop, loop); err != nil {
t.Skipf("self-symlink unsupported: %v", err)
}
err := verifyNoSymlinkEscape(root, filepath.Join(loop, "child.txt"))
if err == nil || !strings.Contains(err.Error(), "解析路径") {
t.Fatalf("expected EvalSymlinks failure, got %v", err)
}
}
// ──────────────────────────────────────────────────────────
// reserveSyncKeepBothTarget 的失败分支
// ──────────────────────────────────────────────────────────
// 候选目标逃逸出本地根(父目录是指向外部的软链)→ 直接失败。
func TestCrossPlatformCoverageReserveSyncKeepBothTarget_escapeFails(t *testing.T) {
root := t.TempDir()
outside := t.TempDir()
if err := os.Symlink(outside, filepath.Join(root, "sub")); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
if _, _, err := reserveSyncKeepBothTarget(root, "sub/f.txt", "FID12345678", map[string]bool{}); err == nil {
t.Fatal("expected escaping keep-both target to fail")
}
}
// 候选硬链接创建返回非 EEXIST 错误时直接上报。
func TestCrossPlatformCoverageReserveSyncKeepBothTarget_unwritableDirFails(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "f.txt"), "local")
testseam.Swap(t, &syncRootLink, func(*os.Root, string, string) error {
return errors.New("link boom")
})
if _, _, err := reserveSyncKeepBothTarget(root, "f.txt", "FID12345678", map[string]bool{}); err == nil {
t.Fatal("expected unwritable directory to fail")
}
}
// occupied 中已知占用的首选候选被跳过,改用下一个后缀。
func TestCrossPlatformCoverageReserveSyncKeepBothTarget_skipsKnownOccupied(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "f.txt"), "local")
first := syncKeepBothCandidate("f.txt", "FID12345678", 0)
occupied := map[string]bool{first: true}
got, _, err := reserveSyncKeepBothTarget(root, "f.txt", "FID12345678", occupied)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == first {
t.Errorf("known-occupied candidate %q must be skipped", first)
}
}
// ──────────────────────────────────────────────────────────
// ask 读取失败(非 EOF)
// ──────────────────────────────────────────────────────────
func TestCrossPlatformCoverageDriveSyncAskConflict_readErrorPropagates(t *testing.T) {
testseam.Swap(t, &syncAskStdin, io.Reader(errReader{}))
if _, err := driveSyncAskConflict("f.txt"); err == nil {
t.Fatal("expected read failure to propagate")
}
}
// ──────────────────────────────────────────────────────────
// sync:本地根不可创建
// ──────────────────────────────────────────────────────────
// --local-folder 指向一个常规文件 → MkdirAll 失败。
func TestCrossPlatformCoverageDriveSync_localRootIsFileFails(t *testing.T) {
dir := t.TempDir()
asFile := filepath.Join(dir, "not-a-dir")
mustWrite(t, asFile, "x")
if err := runDriveCmd(t, syncCaller(nil), "sync", "--local-folder", asFile, "--remote-folder", "ROOT", "--quick"); err == nil {
t.Fatal("expected a regular-file local root to fail")
}
}