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

310 lines
9.6 KiB
Go

package helpers
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
)
func TestCrossPlatformCoverageParseDriveList_requiresAuthoritativeResult(t *testing.T) {
tests := []struct {
name string
body string
wantError string
wantItems int
}{
{name: "missing result", body: `{}`, wantError: "缺少 result"},
{name: "null result", body: `{"result":null}`, wantError: "result 为 null"},
{name: "empty result object", body: `{"result":{}}`, wantError: "缺少 items"},
{name: "null item", body: `{"result":{"items":[null]}}`, wantError: "缺少有效 type"},
{name: "empty item object", body: `{"result":{"items":[{}]}}`, wantError: "缺少有效 type"},
{name: "item missing type", body: `{"result":{"items":[{"name":"unknown"}]}}`, wantError: "缺少有效 type"},
{name: "item empty type", body: `{"result":{"items":[{"name":"unknown","type":" "}]}}`, wantError: "缺少有效 type"},
{name: "has more without token", body: `{"result":{"items":[],"hasMore":true,"nextToken":""}}`, wantError: "hasMore=true"},
{name: "empty items", body: `{"result":{"items":[]}}`},
{name: "empty result array", body: `{"result":[]}`},
{name: "explicit unsupported type remains parseable", body: `{"result":{"items":[{"name":"online.adoc","type":"document"}]}}`, wantItems: 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
items, token, err := parseDriveList(tt.body)
if tt.wantError != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantError) {
t.Fatalf("parseDriveList error = %v, want containing %q", err, tt.wantError)
}
return
}
if err != nil {
t.Fatalf("parseDriveList(%s): %v", tt.body, err)
}
if len(items) != tt.wantItems || token != "" {
t.Fatalf("parseDriveList(%s) = (%v, %q), want %d authoritative item(s) with no token", tt.body, items, token, tt.wantItems)
}
})
}
}
func TestCrossPlatformCoverageDriveRemoteTree_integrityFailuresStopFourCommandsBeforeWrites(t *testing.T) {
failures := []struct {
name string
wantError string
wantListCalls int
reply func(int) string
}{
{
name: "missing result",
wantError: "缺少 result",
wantListCalls: 1,
reply: func(int) string { return `{}` },
},
{
name: "null result",
wantError: "result 为 null",
wantListCalls: 1,
reply: func(int) string { return `{"result":null}` },
},
{
name: "empty result object",
wantError: "缺少 items",
wantListCalls: 1,
reply: func(int) string { return `{"result":{}}` },
},
{
name: "null item",
wantError: "缺少有效 type",
wantListCalls: 1,
reply: func(int) string { return `{"result":{"items":[null],"nextToken":""}}` },
},
{
name: "empty item object",
wantError: "缺少有效 type",
wantListCalls: 1,
reply: func(int) string { return `{"result":{"items":[{}],"nextToken":""}}` },
},
{
name: "item missing type",
wantError: "缺少有效 type",
wantListCalls: 1,
reply: func(int) string {
return `{"result":{"items":[{"name":"unknown"}],"nextToken":""}}`
},
},
{
name: "item empty type",
wantError: "缺少有效 type",
wantListCalls: 1,
reply: func(int) string {
return `{"result":{"items":[{"name":"unknown","type":""}],"nextToken":""}}`
},
},
{
name: "has more without token",
wantError: "hasMore=true",
wantListCalls: 1,
reply: func(int) string {
return `{"result":{"items":[],"hasMore":true,"nextToken":""}}`
},
},
{
name: "file without an ID",
wantError: "缺少文件 ID",
wantListCalls: 1,
reply: func(int) string {
return `{"result":{"items":[{"name":"remote.txt","type":"file"}],"nextToken":""}}`
},
},
{
name: "file name with an ASCII control",
wantError: "无法安全映射到本地路径",
wantListCalls: 1,
reply: func(int) string {
return `{"result":{"items":[{"name":"remote\u0000.txt","type":"file","fileId":"REMOTE"}],"nextToken":""}}`
},
},
{
name: "pagination token cycle",
wantError: "分页 token",
wantListCalls: 3,
reply: func(nth int) string {
tokens := []string{"PAGE_A", "PAGE_B", "PAGE_A"}
if nth >= len(tokens) {
return `{"result":{"items":[],"nextToken":""}}`
}
return fmt.Sprintf(`{"result":{"items":[],"nextToken":%q}}`, tokens[nth])
},
},
}
commands := []struct {
name string
args func(string) []string
prepare func(*testing.T, string)
}{
{
name: "status",
args: func(localRoot string) []string {
return []string{"status", "--local-folder", localRoot, "--remote-folder", "ROOT", "--quick"}
},
prepare: prepareRemoteTreeReadRoot,
},
{
name: "pull",
args: func(localRoot string) []string {
return []string{"pull", "--local-folder", localRoot, "--remote-folder", "ROOT"}
},
},
{
name: "push",
args: func(localRoot string) []string {
return []string{"push", "--local-folder", localRoot, "--remote-folder", "ROOT"}
},
prepare: prepareRemoteTreeWriteSource,
},
{
name: "sync",
args: func(localRoot string) []string {
return []string{"sync", "--local-folder", localRoot, "--remote-folder", "ROOT", "--quick"}
},
prepare: prepareRemoteTreeWriteSource,
},
}
for _, failure := range failures {
t.Run(failure.name, func(t *testing.T) {
for _, command := range commands {
t.Run(command.name, func(t *testing.T) {
localRoot := filepath.Join(t.TempDir(), "mirror")
if command.prepare != nil {
command.prepare(t, localRoot)
}
caller := &driveScriptCaller{reply: func(tool string, _ map[string]any, nth int) (string, error) {
if tool != "list_files" {
return "", fmt.Errorf("远端清单预检完成前调用了写工具 %s", tool)
}
return failure.reply(nth), nil
}}
getCalls, putCalls := swapRemoteTreeTransferCounters(t)
err := runDriveCmd(t, caller, command.args(localRoot)...)
if err == nil || !strings.Contains(err.Error(), failure.wantError) {
t.Fatalf("error = %v, want remote tree integrity failure containing %q", err, failure.wantError)
}
if got := len(caller.callsFor("list_files")); got != failure.wantListCalls {
t.Fatalf("list_files calls = %d, want %d", got, failure.wantListCalls)
}
for _, call := range caller.calls {
if call.toolName != "list_files" {
t.Fatalf("remote tree integrity failure must stop before MCP writes, got %s", call.toolName)
}
}
if *getCalls != 0 || *putCalls != 0 {
t.Fatalf("remote tree integrity failure must stop before transfers: GET=%d PUT=%d", *getCalls, *putCalls)
}
if command.name == "pull" {
if _, statErr := os.Stat(localRoot); !os.IsNotExist(statErr) {
t.Fatalf("pull must fail before creating its local root: %v", statErr)
}
}
})
}
})
}
}
func TestCrossPlatformCoverageDriveRemoteTree_validEmptyListsRemainSupported(t *testing.T) {
for _, listing := range []struct {
name string
body string
}{
{name: "items object", body: `{"result":{"items":[]}}`},
{name: "result array", body: `{"result":[]}`},
} {
t.Run(listing.name, func(t *testing.T) {
for _, fetcher := range []struct {
name string
fetch func(context.Context) error
}{
{
name: "status and pull walker",
fetch: func(ctx context.Context) error {
files, err := fetchRemoteDriveTree(ctx, "", "ROOT", true)
if len(files) != 0 {
return fmt.Errorf("remote files = %v, want empty", files)
}
return err
},
},
{
name: "push and sync walker",
fetch: func(ctx context.Context) error {
files, folders, err := fetchRemoteTreeForPush(ctx, "", "ROOT")
if len(files) != 0 || len(folders) != 1 || folders[""] != "ROOT" {
return fmt.Errorf("remote tree = files %v folders %v, want empty root", files, folders)
}
return err
},
},
} {
t.Run(fetcher.name, func(t *testing.T) {
caller := &driveScriptCaller{reply: func(tool string, _ map[string]any, _ int) (string, error) {
if tool != "list_files" {
t.Fatalf("unexpected tool %s", tool)
}
return listing.body, nil
}}
testseam.Swap(t, &deps, &Deps{Caller: caller, Out: &Formatter{w: io.Discard}})
testseam.Swap(t, &os.Args, []string{"dws", "drive"})
if err := fetcher.fetch(context.Background()); err != nil {
t.Fatalf("valid empty listing: %v", err)
}
if got := len(caller.callsFor("list_files")); got != 1 {
t.Fatalf("list_files calls = %d, want 1", got)
}
})
}
})
}
}
func TestCrossPlatformCoverageIsSafeRemoteSegment_rejectsASCIIControls(t *testing.T) {
for _, name := range []string{"nul\x00name", "unit\x1fseparator", "delete\x7fcharacter"} {
if isSafeRemoteSegment(name) {
t.Errorf("isSafeRemoteSegment(%q) = true, want false", name)
}
}
}
func prepareRemoteTreeReadRoot(t *testing.T, localRoot string) {
t.Helper()
if err := os.MkdirAll(localRoot, 0o755); err != nil {
t.Fatal(err)
}
}
func prepareRemoteTreeWriteSource(t *testing.T, localRoot string) {
t.Helper()
mustWrite(t, filepath.Join(localRoot, "local.txt"), "local")
}
func swapRemoteTreeTransferCounters(t *testing.T) (*int, *int) {
t.Helper()
getCalls, putCalls := 0, 0
swapPullDownloadPath(t, func(context.Context, string, map[string]string, string) error {
getCalls++
return nil
})
testseam.Swap(t, &pushPutOpenedFile, func(context.Context, string, map[string]string, *os.File, int64) error {
putCalls++
return nil
})
return &getCalls, &putCalls
}