Files
larksuite-cli/shortcuts/drive/drive_push_test.go
fangshuyu-768 5778adfefa fix(drive): preserve parent token on nested overwrite (#908)
* fix(drive): preserve parent token on nested overwrite

Ensure drive +push overwrite requests for nested files keep parent_node aligned with the actual remote parent folder and report parent resolution failures explicitly.

* test(drive): cover nested overwrite push workflow

Add a live drive +push workflow case for overwriting a nested remote file so the PR parent-token fix is exercised against the real backend and verified to converge via +status.
2026-05-15 18:32:58 +08:00

1638 lines
54 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package drive
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
)
// countingOpenProvider wraps a fileio.Provider and counts FileIO.Open
// calls. Used by the multipart test to pin the single-shared-fd
// optimization: a regression to reopen-per-block would push the
// counter above 1.
type countingOpenProvider struct {
inner fileio.Provider
opens *atomic.Int32
}
func (p *countingOpenProvider) Name() string { return "counting-open" }
func (p *countingOpenProvider) ResolveFileIO(ctx context.Context) fileio.FileIO {
return &countingOpenFileIO{inner: p.inner.ResolveFileIO(ctx), opens: p.opens}
}
type countingOpenFileIO struct {
inner fileio.FileIO
opens *atomic.Int32
}
func (c *countingOpenFileIO) Open(name string) (fileio.File, error) {
c.opens.Add(1)
return c.inner.Open(name)
}
func (c *countingOpenFileIO) Stat(name string) (fileio.FileInfo, error) {
return c.inner.Stat(name)
}
func (c *countingOpenFileIO) ResolvePath(p string) (string, error) { return c.inner.ResolvePath(p) }
func (c *countingOpenFileIO) Save(p string, opts fileio.SaveOptions, body io.Reader) (fileio.SaveResult, error) {
return c.inner.Save(p, opts, body)
}
// TestDrivePushUploadsAndCreatesParents verifies the happy path: a local
// tree with a top-level file and a subdirectory is mirrored onto a Drive
// folder, with create_folder called once for the missing sub-folder.
func TestDrivePushUploadsAndCreatesParents(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "a.txt"), []byte("AAA"), 0o644); err != nil {
t.Fatalf("WriteFile a: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "sub", "b.txt"), []byte("BBB"), 0o644); err != nil {
t.Fatalf("WriteFile b: %v", err)
}
// Empty remote: no files, no folders.
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
},
})
// Upload a.txt into the root folder.
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_a"},
},
})
// create_folder for "sub", returns a fresh token.
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/create_folder",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"token": "fld_sub"},
},
})
// Upload b.txt into the freshly created sub-folder.
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_b"},
},
})
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 2`) {
t.Errorf("expected uploaded=2, got: %s", out)
}
if !strings.Contains(out, `"failed": 0`) {
t.Errorf("expected failed=0, got: %s", out)
}
if !strings.Contains(out, `"deleted_remote": 0`) {
t.Errorf("expected deleted_remote=0 (flag not set), got: %s", out)
}
}
// TestDrivePushOverwritesWhenIfExistsOverwrite verifies that a local file
// whose rel_path already maps to a type=file on Drive is sent through
// upload_all with the existing file_token in the form body, and that the
// returned version is propagated to items[].version.
func TestDrivePushOverwritesWhenIfExistsOverwrite(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "keep.txt"), []byte("local-new"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep_old", "name": "keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"file_token": "tok_keep_new",
"version": "v42",
},
},
}
reg.Register(uploadStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
// Default --if-exists is now "skip"; opt into overwrite explicitly
// to exercise the file_token-on-upload_all path this test pins.
"--if-exists", "overwrite",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 1`) {
t.Errorf("expected uploaded=1, got: %s", out)
}
if !strings.Contains(out, `"action": "overwritten"`) {
t.Errorf("expected action=overwritten, got: %s", out)
}
if !strings.Contains(out, `"version": "v42"`) {
t.Errorf("expected version propagated, got: %s", out)
}
// The overwrite request must carry the existing file_token in the
// form body so the backend knows to bump the existing file's version
// rather than create a sibling. Decode the captured multipart body
// using the existing decoder helper.
body := decodeDriveMultipartBody(t, uploadStub)
if got := body.Fields["file_token"]; got != "tok_keep_old" {
t.Fatalf("upload_all form file_token = %q, want tok_keep_old", got)
}
if got := body.Fields["file_name"]; got != "keep.txt" {
t.Fatalf("upload_all form file_name = %q, want keep.txt", got)
}
}
// TestDrivePushDefaultsToSkipForExistingRemote pins the new default. Until
// the upload_all overwrite/version protocol field is fully rolled out, the
// default --if-exists is "skip" so a first-time push against a non-empty
// folder never trips on the missing-version branch by surprise. A test
// that registers no upload_all stub would fail with "unmatched stub" if
// the default were still "overwrite".
func TestDrivePushDefaultsToSkipForExistingRemote(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep", "name": "keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
// Intentionally NO upload_all stub: with the new default, the file
// is silently skipped — any upload_all call would trip the registry's
// strict no-stub-or-die contract.
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"skipped": 1`) {
t.Errorf("expected default behavior to skip existing remote files, got: %s", out)
}
if !strings.Contains(out, `"uploaded": 0`) {
t.Errorf("expected uploaded=0 under default --if-exists=skip, got: %s", out)
}
if !strings.Contains(out, `"failed": 0`) {
t.Errorf("expected failed=0, got: %s", out)
}
}
// TestDrivePushSkipsWhenIfExistsSkip verifies --if-exists=skip leaves Drive
// content untouched and counts the file under summary.skipped without
// hitting upload_all.
func TestDrivePushSkipsWhenIfExistsSkip(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep", "name": "keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "skip",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"skipped": 1`) {
t.Errorf("expected skipped=1, got: %s", out)
}
if !strings.Contains(out, `"uploaded": 0`) {
t.Errorf("expected uploaded=0 with --if-exists=skip, got: %s", out)
}
// reg.Verify is implicit via the registry's no-stub-or-die contract:
// if --if-exists=skip had triggered an upload_all anyway, the request
// would 404 against the registry and the run would have errored above.
}
// TestDrivePushSkipsWhenIfExistsSmartAndRemoteIsUpToDate verifies the smart
// fast path for local → Drive mirrors: when the remote copy is already at
// least as new as the local file, +push skips the upload.
func TestDrivePushSkipsWhenIfExistsSmartAndRemoteIsUpToDate(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
localPath := filepath.Join("local", "keep.txt")
if err := os.WriteFile(localPath, []byte("hello"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
localMTime := time.Unix(100, 500*int64(time.Millisecond))
if err := os.Chtimes(localPath, localMTime, localMTime); err != nil {
t.Fatalf("Chtimes: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep", "name": "keep.txt", "type": "file", "size": 5, "modified_time": "200"},
},
"has_more": false,
},
},
})
// Intentionally NO upload_all stub: smart mode should skip the transfer.
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "smart",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"skipped": 1`) {
t.Errorf("expected skipped=1, got: %s", out)
}
if !strings.Contains(out, `"uploaded": 0`) {
t.Errorf("expected uploaded=0, got: %s", out)
}
}
// TestDrivePushOverwritesWhenIfExistsSmartAndLocalIsNewer verifies the smart
// path still uploads when the local file is newer than the remote one.
func TestDrivePushOverwritesWhenIfExistsSmartAndLocalIsNewer(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
localPath := filepath.Join("local", "keep.txt")
if err := os.WriteFile(localPath, []byte("hello"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
localMTime := time.Unix(200, 500*int64(time.Millisecond))
if err := os.Chtimes(localPath, localMTime, localMTime); err != nil {
t.Fatalf("Chtimes: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep_old", "name": "keep.txt", "type": "file", "size": 5, "modified_time": "100"},
},
"has_more": false,
},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_keep_new", "version": "v43"},
},
}
reg.Register(uploadStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "smart",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 1`) {
t.Errorf("expected uploaded=1, got: %s", out)
}
if !strings.Contains(out, `"action": "overwritten"`) {
t.Errorf("expected overwritten action, got: %s", out)
}
body := decodeDriveMultipartBody(t, uploadStub)
if got := body.Fields["file_token"]; got != "tok_keep_old" {
t.Fatalf("upload_all form file_token = %q, want tok_keep_old", got)
}
}
func TestDrivePushShouldSkipSmartFallsBackWhenMetadataCannotBeTrusted(t *testing.T) {
t.Parallel()
localFile := drivePushLocalFile{
Size: 5,
ModTime: time.Unix(100, 500*int64(time.Millisecond)),
}
for _, tt := range []struct {
name string
remoteFile driveRemoteEntry
}{
{
name: "invalid remote timestamp",
remoteFile: driveRemoteEntry{ModifiedTime: "not-a-time"},
},
} {
t.Run(tt.name, func(t *testing.T) {
if got := drivePushShouldSkipSmart(localFile, tt.remoteFile); got {
t.Fatalf("drivePushShouldSkipSmart() = true, want false for %s", tt.name)
}
})
}
}
func TestDrivePushSkipsWhenSmartIgnoresRemoteSize(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
localPath := filepath.Join("local", "keep.txt")
if err := os.WriteFile(localPath, []byte("hello"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
localMTime := time.Unix(100, 500*int64(time.Millisecond))
if err := os.Chtimes(localPath, localMTime, localMTime); err != nil {
t.Fatalf("Chtimes: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep", "name": "keep.txt", "type": "file", "size": 999, "modified_time": "200"},
},
"has_more": false,
},
},
})
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "smart",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"skipped": 1`) {
t.Errorf("expected skipped=1, got: %s", out)
}
if !strings.Contains(out, `"uploaded": 0`) {
t.Errorf("expected uploaded=0, got: %s", out)
}
}
// TestDrivePushDeleteRemoteRequiresYes locks in the upfront safety guard:
// --delete-remote without --yes must be refused before any list / upload
// happens, so a stray flag never silently deletes anything.
func TestDrivePushDeleteRemoteRequiresYes(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--delete-remote",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected validation error for --delete-remote without --yes, got nil")
}
if !strings.Contains(err.Error(), "--delete-remote") || !strings.Contains(err.Error(), "--yes") {
t.Fatalf("error must mention --delete-remote and --yes, got: %v", err)
}
}
// TestDrivePushDeleteRemoteSkipsOnlineDocs is the load-bearing safety
// regression: with --delete-remote --yes, the command must only DELETE
// type=file Drive entries that have no local counterpart. Online docs
// (docx/sheet/bitable/...), shortcuts and folders that share a rel_path
// with a missing local file must be left alone.
func TestDrivePushDeleteRemoteSkipsOnlineDocs(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
// One local file that already exists remotely and matches by rel_path.
if err := os.WriteFile(filepath.Join("local", "kept.txt"), []byte("kept"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
// Remote contains:
// - kept.txt: a type=file with a local counterpart -> overwritten
// - orphan.txt: a type=file without local counterpart -> deleted_remote
// - notes.docx: an online doc -> must be left alone (not deleted)
// - link: a shortcut -> must be left alone
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_kept", "name": "kept.txt", "type": "file"},
map[string]interface{}{"token": "tok_orphan", "name": "orphan.txt", "type": "file"},
map[string]interface{}{"token": "tok_doc", "name": "notes.docx", "type": "docx"},
map[string]interface{}{"token": "tok_link", "name": "link", "type": "shortcut"},
},
"has_more": false,
},
},
})
// Overwrite kept.txt.
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"file_token": "tok_kept",
"version": "v2",
},
},
})
deleteStub := &httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/drive/v1/files/tok_orphan",
Body: map[string]interface{}{"code": 0, "msg": "ok"},
}
reg.Register(deleteStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
// This test exercises the overwrite path against kept.txt; opt
// into overwrite explicitly now that the default is "skip".
"--if-exists", "overwrite",
"--delete-remote",
"--yes",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 1`) {
t.Errorf("expected uploaded=1 (overwrite of kept.txt), got: %s", out)
}
if !strings.Contains(out, `"deleted_remote": 1`) {
t.Errorf("expected deleted_remote=1 (just orphan.txt), got: %s", out)
}
if strings.Contains(out, "notes.docx") {
t.Errorf("notes.docx must not appear in items (online docs are protected): %s", out)
}
// Bare-substring check (matches the notes.docx assertion above):
// drivePushItem serializes the field as "file_token", so a check
// against `"token": "tok_link"` would never fire — silent test
// failure to catch a regression. A bare substring is strictly
// stronger: it catches leaks into file_token, log lines, anywhere.
if strings.Contains(out, "tok_link") {
t.Errorf("shortcut tok_link must not appear in items: %s", out)
}
// Registry.RoundTrip sets CapturedHeaders unconditionally on a match,
// so a non-nil value proves the DELETE for tok_orphan actually went
// through. If the test reached this point with no DELETE issued the
// registry would already have errored on the first uncovered request.
if deleteStub.CapturedHeaders == nil {
t.Fatal("DELETE for tok_orphan was never issued; --delete-remote did not run")
}
}
func TestDrivePushNewestOverwritesChosenDuplicateAndDeletesSibling(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "dup.txt"), []byte("LOCAL"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
registerDuplicateRemoteFiles(reg)
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"file_token": "dup-new-token",
"version": "v99",
},
},
}
reg.Register(uploadStub)
deleteStub := &httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/drive/v1/files/" + duplicateRemoteFileIDFirst,
Body: map[string]interface{}{"code": 0, "msg": "ok"},
}
reg.Register(deleteStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "overwrite",
"--on-duplicate-remote", "newest",
"--delete-remote",
"--yes",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
body := decodeDriveMultipartBody(t, uploadStub)
if got := body.Fields["file_token"]; got != duplicateRemoteFileIDSecond {
t.Fatalf("upload_all form file_token = %q, want %q", got, duplicateRemoteFileIDSecond)
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 1`) {
t.Fatalf("expected uploaded=1, got: %s", out)
}
if !strings.Contains(out, `"deleted_remote": 1`) {
t.Fatalf("expected deleted_remote=1, got: %s", out)
}
assertPushItemAction(t, stdout.Bytes(), "dup.txt", "deleted_remote", duplicateRemoteFileIDFirst)
if deleteStub.CapturedHeaders == nil {
t.Fatal("DELETE for the unchosen duplicate sibling was never issued")
}
reg.Verify(t)
}
func TestDrivePushDeleteRemoteDeletesEntireDuplicateGroupWithoutLocalCounterpart(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
registerDuplicateRemoteFiles(reg)
deleteFirst := &httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/drive/v1/files/" + duplicateRemoteFileIDFirst,
Body: map[string]interface{}{"code": 0, "msg": "ok"},
}
deleteSecond := &httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/drive/v1/files/" + duplicateRemoteFileIDSecond,
Body: map[string]interface{}{"code": 0, "msg": "ok"},
}
reg.Register(deleteFirst)
reg.Register(deleteSecond)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "skip",
"--on-duplicate-remote", "newest",
"--delete-remote",
"--yes",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 0`) {
t.Fatalf("expected uploaded=0, got: %s", out)
}
if !strings.Contains(out, `"deleted_remote": 2`) {
t.Fatalf("expected deleted_remote=2, got: %s", out)
}
assertPushItemAction(t, stdout.Bytes(), "dup.txt", "deleted_remote", duplicateRemoteFileIDFirst)
assertPushItemAction(t, stdout.Bytes(), "dup.txt", "deleted_remote", duplicateRemoteFileIDSecond)
if deleteFirst.CapturedHeaders == nil || deleteSecond.CapturedHeaders == nil {
t.Fatal("expected both duplicate remote DELETE requests to be issued")
}
reg.Verify(t)
}
// TestDrivePushRejectsAbsoluteLocalDir confirms SafeLocalFlagPath surfaces
// the proper flag name in the error message.
func TestDrivePushRejectsAbsoluteLocalDir(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "/etc",
"--folder-token", "folder_root",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected validation error for absolute --local-dir, got nil")
}
if !strings.Contains(err.Error(), "--local-dir") {
t.Fatalf("error must reference --local-dir, got: %v", err)
}
}
// TestDrivePushRejectsBadIfExistsEnum verifies the framework's enum guard.
func TestDrivePushRejectsBadIfExistsEnum(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "fail-and-die",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected enum validation error, got nil")
}
if !strings.Contains(err.Error(), "if-exists") {
t.Fatalf("error must reference --if-exists, got: %v", err)
}
}
// TestDrivePushOverwriteWithoutVersionFails surfaces the protocol contract:
// when the upload_all response for an overwrite call lacks both `version`
// and `data_version`, the shortcut must report a structured api_error
// rather than silently report success. Important during the transitional
// period where the deployed backend may not yet expose the field.
func TestDrivePushOverwriteWithoutVersionFails(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep", "name": "keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
// upload_all returns a NEW file_token but no version. Using a distinct
// value (tok_keep_new vs the entry's tok_keep) is what makes the test
// able to distinguish "items[].file_token comes from the upload_all
// response" from "fall back to entry.FileToken" — if the assertion
// stubbed the same token as the entry, a regression to the fallback
// branch would silently still pass.
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_keep_new"},
},
})
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
// Default is "skip" now; this test specifically exercises the
// overwrite-failure branch, so opt into overwrite explicitly.
"--if-exists", "overwrite",
"--as", "bot",
}, f, stdout)
// Item-level failures bump the exit code via output.ErrBare(ExitAPI),
// preserving the structured items[] envelope on stdout. Older behavior
// was to silently return nil; the assertion below pins the new contract.
if err == nil {
t.Fatalf("expected non-zero exit on item-level failure, got nil\nstdout: %s", stdout.String())
}
var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected *output.ExitError, got %T: %v", err, err)
}
if exitErr.Code != output.ExitAPI {
t.Errorf("expected ExitAPI (%d), got code=%d", output.ExitAPI, exitErr.Code)
}
if exitErr.Detail != nil {
t.Errorf("ErrBare should carry no Detail (the items[] envelope already covered the per-item error), got: %#v", exitErr.Detail)
}
out := stdout.String()
// summary.failed should reflect the missing version; summary.uploaded
// should not pretend the overwrite succeeded.
if !strings.Contains(out, `"failed": 1`) {
t.Errorf("expected failed=1, got: %s", out)
}
if !strings.Contains(out, "no version") {
t.Errorf("expected error about missing version in items[].error, got: %s", out)
}
// Pin the token-stability contract: the failed item must surface the
// token returned by upload_all (tok_keep_new), NOT the fallback
// entry.FileToken (tok_keep). Without this, a regression that always
// uses entry.FileToken on failure would slip through.
if !strings.Contains(out, `"file_token": "tok_keep_new"`) {
t.Errorf("expected failed item to surface upload_all's returned file_token (tok_keep_new), got: %s", out)
}
}
// TestDrivePushOverwritePartialSuccessSurfacesReturnedToken pins the
// partial-success contract for upload_all: when the backend returns a
// non-zero `code` AND a non-empty `data.file_token`, the bytes have
// already landed under that returned token. The shortcut must surface
// THAT token in items[].file_token (not silently fall back to
// entry.FileToken via the empty-string sentinel), so callers always
// have a usable handle to whatever state Drive ended up in.
func TestDrivePushOverwritePartialSuccessSurfacesReturnedToken(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep_old", "name": "keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
// upload_all returns a partial-success: non-zero code + a brand-new
// file_token. The shortcut must not drop that token.
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 1234, "msg": "partial-success simulated",
"data": map[string]interface{}{"file_token": "tok_keep_partial"},
},
})
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "overwrite",
"--as", "bot",
}, f, stdout)
if err == nil {
t.Fatalf("expected non-zero exit on item-level failure, got nil\nstdout: %s", stdout.String())
}
var exitErr *output.ExitError
if !errors.As(err, &exitErr) || exitErr.Code != output.ExitAPI {
t.Fatalf("expected ExitAPI from output.ExitError, got %T %v", err, err)
}
out := stdout.String()
if !strings.Contains(out, `"failed": 1`) {
t.Errorf("expected failed=1, got: %s", out)
}
// The freshly returned token must be the one in items[].file_token,
// not the stale entry.FileToken (tok_keep_old).
if !strings.Contains(out, `"file_token": "tok_keep_partial"`) {
t.Errorf("expected items[].file_token to surface upload_all's returned token (tok_keep_partial), got: %s", out)
}
if strings.Contains(out, `"file_token": "tok_keep_old"`) {
t.Errorf("must NOT fall back to entry.FileToken when upload_all returned a token; got: %s", out)
}
}
// TestDrivePushSkipsDeleteAfterUploadFailure pins the half-sync safety
// guard: when any upload / overwrite / folder step fails, the
// --delete-remote phase must be skipped entirely. Otherwise a partial
// upload would proceed to delete remote orphans, leaving the tenant in
// a worse state than where it started (files missing locally and now
// also gone from Drive).
//
// Test setup mirrors the missing-version overwrite failure: one local
// file with a same-name remote that overwrites with no version field,
// plus one orphan remote file that --delete-remote --yes would
// otherwise delete. With the fix in place the orphan must NOT be
// reached.
func TestDrivePushSkipsDeleteAfterUploadFailure(t *testing.T) {
f, stdout, stderrBuf, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep", "name": "keep.txt", "type": "file"},
map[string]interface{}{"token": "tok_orphan", "name": "orphan.txt", "type": "file"},
},
"has_more": false,
},
},
})
// Overwrite returns no version → fails.
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_keep"},
},
})
// Crucially: do NOT register a DELETE stub for tok_orphan. If the
// delete phase were still triggered after the upload failure, the
// registry's no-stub-or-die contract would surface an unmatched
// request and the test would catch the regression.
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "overwrite",
"--delete-remote",
"--yes",
"--as", "bot",
}, f, stdout)
if err == nil {
t.Fatalf("expected non-zero exit on overwrite failure, got nil\nstdout: %s", stdout.String())
}
var exitErr *output.ExitError
if !errors.As(err, &exitErr) || exitErr.Code != output.ExitAPI {
t.Fatalf("expected ExitAPI ExitError, got %v", err)
}
out := stdout.String()
if !strings.Contains(out, `"failed": 1`) {
t.Errorf("expected failed=1 (overwrite missing-version), got: %s", out)
}
if !strings.Contains(out, `"deleted_remote": 0`) {
t.Errorf("expected deleted_remote=0 (delete phase must skip after upload failure), got: %s", out)
}
if strings.Contains(out, "tok_orphan") {
t.Errorf("orphan file token must not appear in items (delete phase skipped), got: %s", out)
}
// Operator-facing hint that cleanup was skipped lives on stderr.
if !strings.Contains(stderrBuf.String(), "Skipping --delete-remote") {
t.Errorf("expected stderr hint about skipped delete phase, got: %s", stderrBuf.String())
}
}
// TestDrivePushExitsZeroOnCleanRun pins the inverse: a successful run
// with no failures must NOT bump the exit code. Without this the
// ErrBare-on-failure path could regress to "always non-zero" silently.
func TestDrivePushExitsZeroOnCleanRun(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "a.txt"), []byte("AAA"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_a"},
},
})
if err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout); err != nil {
t.Fatalf("expected nil error on clean run, got: %v\nstdout: %s", err, stdout.String())
}
if !strings.Contains(stdout.String(), `"failed": 0`) {
t.Errorf("expected failed=0, got: %s", stdout.String())
}
}
// TestDrivePushUploadsSiblingWhenRemoteSameNameIsNativeDoc pins the
// behavior when a local regular file shares its rel_path with a Lark
// native cloud document on Drive (sheet/docx/bitable/...).
//
// The contract:
// - The native doc is NOT a candidate for overwrite — the remoteFiles
// view collapsed off listRemoteFolder only keeps type=file entries,
// so the local "report" is treated as new and the upload_all body
// must NOT carry the sheet's token in the file_token form field
// (that would mean overwriting the sheet's bytes, not creating a
// sibling).
// - The native doc must NOT be deleted by --delete-remote --yes either.
// The orphan check iterates remoteFiles only; a sheet at the same
// rel_path as a missing local file would otherwise look orphaned.
//
// Both invariants together: native cloud docs are never reached by either
// the upload or the delete path, regardless of whether a same-named
// local file exists.
func TestDrivePushUploadsSiblingWhenRemoteSameNameIsNativeDoc(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "report"), []byte("local-csv-bytes"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
// Remote contains:
// - "report" as a Lark native sheet (type=sheet, tok_sheet) — must be
// left strictly alone
// - "minutes" as a native docx (type=docx, tok_docx) — no
// local counterpart, --delete-remote must skip it
// No type=file entries at all — every local rel_path is a "new upload"
// from the remoteFiles view's perspective (listRemoteFolder returns the
// sheet/docx in the unified entry map but they're filtered out when
// collapsing to the type=file view).
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_sheet", "name": "report", "type": "sheet"},
map[string]interface{}{"token": "tok_docx", "name": "minutes", "type": "docx"},
},
"has_more": false,
},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_report_file"},
},
}
reg.Register(uploadStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--delete-remote",
"--yes",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 1`) {
t.Errorf("expected uploaded=1 (sibling create, not overwrite), got: %s", out)
}
// Native docs are never iterated for delete; --delete-remote must be 0.
if !strings.Contains(out, `"deleted_remote": 0`) {
t.Errorf("native docs must not be deleted, got: %s", out)
}
// Items must show the upload as a fresh "uploaded", not "overwritten",
// and must point at the new token, never at the sheet's token.
if !strings.Contains(out, `"action": "uploaded"`) {
t.Errorf("expected action=uploaded for sibling create, got: %s", out)
}
if !strings.Contains(out, `"file_token": "tok_report_file"`) {
t.Errorf("expected new file_token in items, got: %s", out)
}
if strings.Contains(out, "tok_sheet") {
t.Errorf("sheet token must not appear anywhere in output, got: %s", out)
}
if strings.Contains(out, "tok_docx") {
t.Errorf("docx token must not appear anywhere in output, got: %s", out)
}
// The upload_all multipart body must NOT carry file_token. Carrying
// the sheet's token would mean asking the backend to overwrite the
// sheet's bytes with our local file's bytes — the exact bug this
// test guards against. file_name is "report" (no extension, matching
// the basename) and parent_node is the root folder.
body := decodeDriveMultipartBody(t, uploadStub)
if got, present := body.Fields["file_token"]; present {
t.Fatalf("upload_all body must NOT include file_token (would overwrite the sheet); got %q", got)
}
if got := body.Fields["file_name"]; got != "report" {
t.Fatalf("upload_all file_name = %q, want \"report\"", got)
}
if got := body.Fields["parent_node"]; got != "folder_root" {
t.Fatalf("upload_all parent_node = %q, want folder_root", got)
}
}
// TestDrivePushReusesExistingRemoteFolder verifies that when a remote
// folder already exists at the target rel_path, the upload uses its
// pre-existing token instead of calling create_folder. Together with the
// "creates parents" test this pins the folderCache contract end-to-end.
func TestDrivePushReusesExistingRemoteFolder(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "sub", "b.txt"), []byte("BBB"), 0o644); err != nil {
t.Fatalf("WriteFile b: %v", err)
}
// Root contains the folder "sub" already; recurse returns no files.
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "fld_existing_sub", "name": "sub", "type": "folder"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=fld_existing_sub",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_b"},
},
}
reg.Register(uploadStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
if !strings.Contains(stdout.String(), `"uploaded": 1`) {
t.Errorf("expected uploaded=1, got: %s", stdout.String())
}
// Upload must have been routed under the pre-existing sub-folder
// rather than re-creating it; the form's parent_node should be the
// remote folder token discovered in the listing.
body := decodeDriveMultipartBody(t, uploadStub)
if got := body.Fields["parent_node"]; got != "fld_existing_sub" {
t.Fatalf("upload parent_node = %q, want fld_existing_sub (folderCache miss?)", got)
}
}
// TestDrivePushOverwriteNestedFileUsesParentFolderToken verifies that
// overwriting an existing nested remote file keeps parent_node aligned with
// the file's actual parent folder instead of the root folder token.
func TestDrivePushOverwriteNestedFileUsesParentFolderToken(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "sub", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "fld_existing_sub", "name": "sub", "type": "folder"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=fld_existing_sub",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep_nested", "name": "keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"file_token": "tok_keep_nested",
"version": "v2",
},
},
}
reg.Register(uploadStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "overwrite",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
body := decodeDriveMultipartBody(t, uploadStub)
if got := body.Fields["file_token"]; got != "tok_keep_nested" {
t.Fatalf("upload_all file_token = %q, want tok_keep_nested", got)
}
if got := body.Fields["parent_node"]; got != "fld_existing_sub" {
t.Fatalf("upload_all parent_node = %q, want fld_existing_sub", got)
}
}
func TestDrivePushOverwriteNestedFileReportsParentEnsureFailure(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "sub", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep_nested", "name": "sub/keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/create_folder",
Body: map[string]interface{}{
"code": 9999,
"msg": "create parent failed",
},
})
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "overwrite",
"--as", "bot",
}, f, stdout)
if err == nil {
t.Fatalf("expected parent ensure failure\nstdout: %s", stdout.String())
}
if !strings.Contains(stdout.String(), `"action": "failed"`) || !strings.Contains(stdout.String(), "create parent failed") {
t.Fatalf("expected failed item with create_folder error, got: %s", stdout.String())
}
}
// TestDrivePushMirrorsEmptyDirectories confirms the gap codex review
// flagged: a local directory with no files inside must still surface on
// Drive as a created sub-folder, not be silently dropped because the
// upload loop only iterates regular files.
func TestDrivePushMirrorsEmptyDirectories(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll(filepath.Join("local", "empty"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
// Empty remote.
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
},
})
// create_folder for "empty" — must be issued even though no file
// will land underneath it.
createStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/create_folder",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"token": "fld_empty"},
},
}
reg.Register(createStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"action": "folder_created"`) {
t.Errorf("expected folder_created action for empty dir, got: %s", out)
}
if !strings.Contains(out, `"rel_path": "empty"`) {
t.Errorf("expected rel_path=empty in items, got: %s", out)
}
// Empty dirs do NOT bump summary.uploaded — that field is for files.
if !strings.Contains(out, `"uploaded": 0`) {
t.Errorf("uploaded should remain 0 for an empty-dir-only push, got: %s", out)
}
if createStub.CapturedHeaders == nil {
t.Fatal("create_folder for empty/ was never issued")
}
}
// TestDrivePushUploadsLargeFileViaMultipart locks in the 3-step
// upload_prepare/upload_part/upload_finish flow used for files larger
// than common.MaxDriveMediaUploadSinglePartSize. The single shared file
// handle (opened once outside the part loop, reused via SectionReader)
// is implicitly exercised: the test asserts upload_part is invoked
// twice in a row before upload_finish, and would deadlock or surface a
// fileio open error if the helper still re-opened per chunk and the
// httpmock registry didn't have enough Open-permission stubs.
//
// The local file is created with Truncate to size+1 so it crosses the
// single-part threshold by one byte, exercising the "remaining < block
// size" tail path on the second chunk.
//
// Use a distinct AppID to avoid SDK token cache collisions with other
// tests (mirrors the existing TestDriveUploadLargeFileUsesMultipart
// pattern).
func TestDrivePushUploadsLargeFileViaMultipart(t *testing.T) {
pushTestConfig := &core.CliConfig{
AppID: "drive-push-multipart-test", AppSecret: "test-secret", Brand: core.BrandFeishu,
}
f, stdout, _, reg := cmdutil.TestFactory(t, pushTestConfig)
// Wrap the default FileIO provider to count Open calls. The
// shared-fd refactor opens the local file exactly once and feeds
// each block through io.NewSectionReader at distinct offsets; a
// regression back to "open per block" would bump this counter to
// block_num (2 in this test).
opens := &atomic.Int32{}
f.FileIOProvider = &countingOpenProvider{inner: f.FileIOProvider, opens: opens}
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
largePath := filepath.Join("local", "large.bin")
fh, err := os.Create(largePath)
if err != nil {
t.Fatalf("Create: %v", err)
}
if err := fh.Truncate(common.MaxDriveMediaUploadSinglePartSize + 1); err != nil {
t.Fatalf("Truncate: %v", err)
}
if err := fh.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// Empty remote — large.bin is a fresh upload, not an overwrite.
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"files": []interface{}{}, "has_more": false},
},
})
prepareStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_prepare",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"upload_id": "upid-large",
"block_size": float64(common.MaxDriveMediaUploadSinglePartSize),
"block_num": float64(2),
},
},
}
reg.Register(prepareStub)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_part",
Body: map[string]interface{}{"code": 0, "msg": "ok"},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_part",
Body: map[string]interface{}{"code": 0, "msg": "ok"},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_finish",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{"file_token": "tok_large"},
},
})
err = mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"uploaded": 1`) {
t.Errorf("expected uploaded=1, got: %s", out)
}
if !strings.Contains(out, `"file_token": "tok_large"`) {
t.Errorf("expected file_token=tok_large in items, got: %s", out)
}
// upload_prepare's body must NOT include file_token (this was a
// fresh upload, not an overwrite). Decoding the body proves both
// that the request was issued and that the conditional file_token
// branch in drivePushUploadMultipart was skipped correctly.
body := decodeCapturedJSONBody(t, prepareStub)
if _, present := body["file_token"]; present {
t.Fatalf("upload_prepare body must omit file_token for fresh uploads, got: %#v", body)
}
if got := body["parent_type"]; got != driveUploadParentTypeExplorer {
t.Fatalf("parent_type = %#v, want %q", got, driveUploadParentTypeExplorer)
}
if got := body["parent_node"]; got != "folder_root" {
t.Fatalf("parent_node = %#v, want folder_root", got)
}
// One Open across the entire multipart upload — the load-bearing
// pin for the shared-fd refactor. block_num=2 above means a
// reopen-per-block regression would land at 2.
if got := opens.Load(); got != 1 {
t.Fatalf("FileIO.Open invocations = %d, want exactly 1 (single shared fd across all blocks)", got)
}
}
// TestDrivePushHelpersRelPath pins the path utilities since the upload
// loop relies on slash-only rel_paths even on Windows.
func TestDrivePushHelpersRelPath(t *testing.T) {
t.Parallel()
if got := drivePushParentRel("a.txt"); got != "" {
t.Fatalf("parent of root file should be empty, got %q", got)
}
if got := drivePushParentRel("a/b/c.txt"); got != "a/b" {
t.Fatalf("parent of a/b/c.txt = %q, want a/b", got)
}
if parent, name := drivePushSplitRel("a/b/c"); parent != "a/b" || name != "c" {
t.Fatalf("split a/b/c = (%q,%q), want (a/b,c)", parent, name)
}
if parent, name := drivePushSplitRel("solo"); parent != "" || name != "solo" {
t.Fatalf("split solo = (%q,%q), want (\"\",solo)", parent, name)
}
if got := joinRelDrive("", "x"); got != "x" {
t.Fatalf("joinRelDrive(\"\", x) = %q, want x", got)
}
if got := joinRelDrive("a", "x"); got != "a/x" {
t.Fatalf("joinRelDrive(a, x) = %q, want a/x", got)
}
}