Files
dingtalk-workspace-cli/internal/helpers/drive_pull_coverage_test.go
T

260 lines
11 KiB
Go

package helpers
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
)
// ──────────────────────────────────────────────────────────
// runDrivePull — 端到端:远端树 → 本地落盘
// ──────────────────────────────────────────────────────────
// pullListingCaller 返回固定的单层远端清单,download_file 给出预签名链接。
func pullListingCaller(items string) *driveScriptCaller {
return &driveScriptCaller{reply: func(tool string, _ map[string]any, nth int) (string, error) {
switch tool {
case "list_files":
if nth > 0 {
return `{"result":{"items":[],"nextToken":""}}`, nil
}
return `{"result":{"items":[` + items + `],"nextToken":""}}`, nil
case "download_file":
return `{"result":{"downloadUrl":"https://oss.example.com/get","headers":{}}}`, nil
}
return `{"result":{},"success":true}`, nil
}}
}
// 端到端 pull:远端两个文件都下载到本地,并自动创建缺失的本地根目录。
func TestCrossPlatformCoverageDrivePull_endToEndDownloadsAll(t *testing.T) {
root := filepath.Join(t.TempDir(), "created-by-pull")
caller := pullListingCaller(
`{"name":"a.txt","type":"file","fileId":"A","modifyTime":1000},` +
`{"name":"b.txt","type":"file","fileId":"B","modifyTime":2000}`)
swapPullDownloadPath(t, func(_ context.Context, _ string, _ map[string]string, dest string) error {
return os.WriteFile(dest, []byte("remote"), 0o644)
})
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, name := range []string{"a.txt", "b.txt"} {
if b, err := os.ReadFile(filepath.Join(root, name)); err != nil || string(b) != "remote" {
t.Errorf("%s not written: %v / %q", name, err, string(b))
}
}
}
// --if-exists skip:本地已存在则不下载。
func TestCrossPlatformCoverageDrivePull_ifExistsSkipKeepsLocal(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, "a.txt"), "local-original")
caller := pullListingCaller(`{"name":"a.txt","type":"file","fileId":"A","modifyTime":9000}`)
swapPullDownloadPath(t, func(context.Context, string, map[string]string, string) error {
t.Error("skip must not download")
return nil
})
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT", "--if-exists", "skip"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if b, _ := os.ReadFile(filepath.Join(root, "a.txt")); string(b) != "local-original" {
t.Errorf("local file changed: %q", string(b))
}
}
// --if-exists smart:本地 mtime 已 ≥ 远端 → 跳过;远端更新 → 下载。
func TestCrossPlatformCoverageDrivePull_ifExistsSmart(t *testing.T) {
t.Run("local newer skips", func(t *testing.T) {
root := t.TempDir()
p := filepath.Join(root, "a.txt")
mustWrite(t, p, "local")
local := readFileMTimeMillis(t, p)
caller := pullListingCaller(`{"name":"a.txt","type":"file","fileId":"A","modifyTime":` + strconv.FormatInt(local-5000, 10) + `}`)
swapPullDownloadPath(t, func(context.Context, string, map[string]string, string) error {
t.Error("smart must not download when local is newer")
return nil
})
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT", "--if-exists", "smart"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if b, _ := os.ReadFile(p); string(b) != "local" {
t.Errorf("local file changed: %q", string(b))
}
})
t.Run("remote newer downloads", func(t *testing.T) {
root := t.TempDir()
p := filepath.Join(root, "a.txt")
mustWrite(t, p, "local")
local := readFileMTimeMillis(t, p)
caller := pullListingCaller(`{"name":"a.txt","type":"file","fileId":"A","modifyTime":` + strconv.FormatInt(local+60000, 10) + `}`)
swapPullDownloadPath(t, func(_ context.Context, _ string, _ map[string]string, dest string) error {
return os.WriteFile(dest, []byte("fresh"), 0o644)
})
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT", "--if-exists", "smart"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if b, _ := os.ReadFile(p); string(b) != "fresh" {
t.Errorf("expected download, got %q", string(b))
}
})
}
// --if-exists 非法值直接拒绝。
func TestCrossPlatformCoverageDrivePull_invalidIfExistsRejected(t *testing.T) {
caller := pullListingCaller("")
err := runDriveCmd(t, caller, "pull", "--local-folder", t.TempDir(), "--remote-folder", "ROOT", "--if-exists", "bogus")
if err == nil || !strings.Contains(err.Error(), "--if-exists") {
t.Fatalf("expected --if-exists rejection, got %v", err)
}
}
// space-id 透传到 download_file。
func TestCrossPlatformCoverageDrivePull_passesSpaceIDToDownload(t *testing.T) {
root := t.TempDir()
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("x"), 0o644)
})
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT", "--space-id", "SP9"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := caller.callsFor("download_file")[0].args["spaceId"]; got != "SP9" {
t.Errorf("download_file spaceId = %v, want SP9", got)
}
}
// 下载失败 → 该项记 failed,命令以 partial_failure 非零退出。
func TestCrossPlatformCoverageDrivePull_downloadFailureIsPartialFailure(t *testing.T) {
root := t.TempDir()
caller := pullListingCaller(`{"name":"a.txt","type":"file","fileId":"A","modifyTime":1}`)
swapPullDownloadPath(t, func(context.Context, string, map[string]string, string) error { return errTestDownload })
var out strings.Builder
testseam.Swap(t, &deps, &Deps{Caller: caller, Out: &Formatter{w: &out}})
testseam.Swap(t, &os.Args, []string{"dws", "drive", "pull"})
cmd := findDriveSubcommand(t, "pull")
mustSetFlags(t, cmd, map[string]string{"local-folder": root, "remote-folder": "ROOT"})
err := runDrivePull(cmd, nil)
var pf *drivePartialFailure
if !errors.As(err, &pf) {
t.Fatalf("expected drivePartialFailure, got %T %v", err, err)
}
if pf.failed != 1 || pf.RawStderr() != "drive pull: 1 file(s) failed" {
t.Errorf("partial failure = %#v, stderr = %q", pf, pf.RawStderr())
}
stdout := out.String()
if !strings.Contains(stdout, `"failed": 1`) || !strings.Contains(stdout, `"rel_path": "a.txt"`) {
t.Errorf("stdout must retain the structured partial result: %s", stdout)
}
}
// download_file 未返回下载链接 → failed(不是 panic,也不落盘)。
func TestCrossPlatformCoverageDrivePull_missingDownloadURLFails(t *testing.T) {
root := t.TempDir()
caller := &driveScriptCaller{reply: func(tool string, _ map[string]any, nth int) (string, error) {
if tool == "list_files" {
if nth > 0 {
return `{"result":{"items":[],"nextToken":""}}`, nil
}
return `{"result":{"items":[{"name":"a.txt","type":"file","fileId":"A"}],"nextToken":""}}`, nil
}
return `{"result":{"fileId":"A"},"success":true}`, nil // 无 downloadUrl
}}
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT"); err == nil {
t.Fatal("expected failure when download_file returns no URL")
}
if _, err := os.Stat(filepath.Join(root, "a.txt")); !os.IsNotExist(err) {
t.Error("nothing must be written when the download URL is missing")
}
}
// download_file 的 MCP 调用本身失败 → failed。
func TestCrossPlatformCoverageDrivePull_downloadToolErrorFails(t *testing.T) {
root := t.TempDir()
caller := &driveScriptCaller{reply: func(tool string, _ map[string]any, nth int) (string, error) {
if tool == "list_files" {
if nth > 0 {
return `{"result":{"items":[],"nextToken":""}}`, nil
}
return `{"result":{"items":[{"name":"a.txt","type":"file","fileId":"A"}],"nextToken":""}}`, nil
}
return "", errTestDownload
}}
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT"); err == nil {
t.Fatal("expected failure when download_file errors")
}
}
// 远端名称逃逸出本地根 → 记 failed,不落盘(resolveLocalTarget 二次确认)。
func TestCrossPlatformCoverageDrivePull_escapingRelPathIsFailed(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)
}
caller := &driveScriptCaller{reply: func(tool string, args map[string]any, _ int) (string, error) {
if tool != "list_files" {
return `{"result":{"downloadUrl":"https://oss.example.com/get"}}`, nil
}
switch args["parentId"] {
case "ROOT":
return `{"result":{"items":[{"name":"sub","type":"folder","fileId":"SUB"}],"nextToken":""}}`, nil
case "SUB":
return `{"result":{"items":[{"name":"leaked.txt","type":"file","fileId":"L"}],"nextToken":""}}`, nil
}
return `{"result":{"items":[],"nextToken":""}}`, nil
}}
swapPullDownloadPath(t, func(_ context.Context, _ string, _ map[string]string, dest string) error {
return os.WriteFile(dest, []byte("leak"), 0o644)
})
if err := runDriveCmd(t, caller, "pull", "--local-folder", root, "--remote-folder", "ROOT"); err == nil {
t.Fatal("expected symlink escape to be reported as failure")
}
if _, err := os.Stat(filepath.Join(outside, "leaked.txt")); !os.IsNotExist(err) {
t.Error("file escaped the local root")
}
}
// 目标父目录不可创建(同名常规文件占位)→ failed,不 panic。
func TestCrossPlatformCoveragePullOneFile_mkdirFailureIsFailed(t *testing.T) {
root := t.TempDir()
// "sub" 是普通文件,MkdirAll("sub") 必然失败。
mustWrite(t, filepath.Join(root, "sub"), "occupied")
testseam.Swap(t, &deps, &Deps{Caller: pullListingCaller(""), Out: &Formatter{w: io.Discard}})
testseam.Swap(t, &os.Args, []string{"dws", "drive"})
rf := &remoteFile{RelPath: "sub/x.txt", FileID: "X"}
action, err := pullOneFile(context.Background(), "", rf, filepath.Join(root, "sub", "x.txt"), ifExistsOverwrite)
if action != pullActionFailed || err == nil {
t.Fatalf("action=%q err=%v, want failed + error", action, err)
}
}
// 探测目录不存在时 isCaseInsensitiveFS 回退平台默认,不 panic。
func TestCrossPlatformCoverageIsCaseInsensitiveFS_missingDirFallsBackToPlatformDefault(t *testing.T) {
missing := filepath.Join(t.TempDir(), "absent")
// 只断言不 panic 且返回布尔(具体值取决于平台默认)。
_ = isCaseInsensitiveFS(missing)
}