Files
larksuite-cli/shortcuts/drive/drive_status_test.go
evandance 99e314fe0b feat(errs): typed envelope contract for auth-domain errors (#1135)
Every failure on the authentication, authorization, and configuration
path now surfaces as a typed structured error instead of an ad-hoc
envelope. Users and scripts that consume CLI output get:

  - a fixed nine-category taxonomy on the wire, each mapped to a
    stable shell exit code (authentication/authorization/config = 3,
    network = 4, internal = 5, policy = 6, confirmation = 10)
  - identity-aware detail fields (missing_scopes, requested_scopes,
    granted_scopes, console_url, log_id, retryable, hint) carried
    uniformly on the envelope
  - a single canonical policy envelope at exit 6; the legacy
    auth_error carve-out is retired
  - per-subtype canonical message + hint that preserves Lark's
    diagnostic phrasing and routes recovery to the right actor:
    app developer (app_scope_not_applied), user (missing_scope,
    token_scope_insufficient, user_unauthorized), or tenant admin
    (app_unavailable, app_disabled)
  - wrong app credentials classify as config/invalid_client whether
    surfaced by the Open API endpoint (99991543) or the tenant
    access-token mint endpoint (10003 / 10014), instead of
    collapsing to a transport error or api/unknown
  - local shortcut scope preflight emits the same
    authorization/missing_scope envelope (identity + deterministic
    missing-scope set) used by the post-call permission path, so AI
    consumers read the same structured shape from precheck and from
    server-returned permission denial
  - streaming download/upload failures keep the same network subtype
    split (timeout / TLS / DNS / transport) as the non-stream path
    instead of collapsing every cause to a generic transport failure
  - console_url is carried only on the bot-perspective
    app_scope_not_applied envelope (where the recovery action is
    "developer applies the scope at the developer console"); the
    user-perspective missing_scope envelope drops the field, since
    the only actionable user recovery is `lark-cli auth login --scope`
    and pointing an end user at a console they cannot modify is
    misleading
  - bind workflows (Hermes / OpenClaw / lark-channel) flatten dynamic
    Type tags to wire 'config' with the original module name kept
    as a metric label

All 10 typed errors are cause-bearing, nil-safe on .Error() and
.Unwrap(), and defensively clone slice setter inputs. Four lint
rules (CheckNilSafeError / CheckBuilderImmutable / CheckUnwrapSymmetry
/ CheckBuildAPIErrorArms) lock these invariants on migrated paths.
2026-05-30 19:08:41 +08:00

851 lines
28 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package drive
import (
"context"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
)
// driveStatusScopedTokenResolver returns a token with caller-controlled scopes
// so tests can deterministically exercise the shortcut scope preflight.
type driveStatusScopedTokenResolver struct {
scopes string
}
// ResolveToken satisfies credential.TokenProvider for scope-preflight tests.
func (r *driveStatusScopedTokenResolver) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.TokenResult, error) {
return &credential.TokenResult{Token: "test-token", Scopes: r.scopes}, nil
}
// TestDriveStatusCategorizesByHash exercises the four-bucket classification
// against a real walk of the temp dir and a mocked Drive listing.
func TestDriveStatusCategorizesByHash(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
// Local layout:
// local/a.txt — also on remote with different content → modified
// local/b.txt — only local → new_local
// local/sub/c.txt — also on remote with same content → unchanged
// Remote-only:
// d.txt → new_remote
if err := os.MkdirAll("local/sub", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile("local/a.txt", []byte("aaa"), 0o644); err != nil {
t.Fatalf("WriteFile a.txt: %v", err)
}
if err := os.WriteFile("local/b.txt", []byte("bbb"), 0o644); err != nil {
t.Fatalf("WriteFile b.txt: %v", err)
}
if err := os.WriteFile("local/sub/c.txt", []byte("ccc"), 0o644); err != nil {
t.Fatalf("WriteFile sub/c.txt: %v", err)
}
// Root folder list — order matters: stubs match in registration order.
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_a", "name": "a.txt", "type": "file"},
map[string]interface{}{"token": "tok_sub", "name": "sub", "type": "folder"},
map[string]interface{}{"token": "tok_d", "name": "d.txt", "type": "file"},
// noise: an online doc and a shortcut should be ignored
map[string]interface{}{"token": "tok_doc", "name": "ignored.docx", "type": "docx"},
map[string]interface{}{"token": "tok_sc", "name": "ignored.lnk", "type": "shortcut"},
},
"has_more": false,
},
},
})
// Subfolder list
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=tok_sub",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_c", "name": "c.txt", "type": "file"},
},
"has_more": false,
},
},
})
// Download a.txt: remote content differs from local "aaa" → modified.
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/tok_a/download",
Status: 200,
Body: []byte("AAA"),
Headers: http.Header{"Content-Type": []string{"application/octet-stream"}},
})
// Download c.txt: remote content matches local "ccc" → unchanged.
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/tok_c/download",
Status: 200,
Body: []byte("ccc"),
Headers: http.Header{"Content-Type": []string{"application/octet-stream"}},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--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, `"detection": "exact"`) {
t.Fatalf("output missing detection=exact\noutput: %s", out)
}
checks := []struct {
bucket string
path string
token string
}{
{"new_local", "b.txt", ""},
{"new_remote", "d.txt", "tok_d"},
{"modified", "a.txt", "tok_a"},
{"unchanged", "sub/c.txt", "tok_c"},
}
for _, c := range checks {
if !strings.Contains(out, `"`+c.bucket+`":`) {
t.Errorf("output missing bucket %q\noutput: %s", c.bucket, out)
}
if !strings.Contains(out, `"rel_path": "`+c.path+`"`) {
t.Errorf("output missing rel_path %q (expected in %s)\noutput: %s", c.path, c.bucket, out)
}
if c.token != "" && !strings.Contains(out, `"file_token": "`+c.token+`"`) {
t.Errorf("output missing file_token %q (expected in %s)\noutput: %s", c.token, c.bucket, out)
}
}
if strings.Contains(out, "ignored.docx") || strings.Contains(out, "ignored.lnk") {
t.Errorf("output should skip docx/shortcut entries\noutput: %s", out)
}
reg.Verify(t)
}
func TestDriveStatusQuickCategorizesByModifiedTimeWithoutDownloads(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local/sub", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile("local/a.txt", []byte("local-a"), 0o644); err != nil {
t.Fatalf("WriteFile a.txt: %v", err)
}
if err := os.WriteFile("local/b.txt", []byte("local-b"), 0o644); err != nil {
t.Fatalf("WriteFile b.txt: %v", err)
}
if err := os.WriteFile("local/sub/c.txt", []byte("local-c"), 0o644); err != nil {
t.Fatalf("WriteFile sub/c.txt: %v", err)
}
matchTime := time.Unix(1715594880, 0)
changedTime := time.Unix(1715594940, 0)
if err := os.Chtimes("local/a.txt", matchTime, matchTime); err != nil {
t.Fatalf("Chtimes a.txt: %v", err)
}
if err := os.Chtimes("local/sub/c.txt", changedTime, changedTime); err != nil {
t.Fatalf("Chtimes sub/c.txt: %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_a", "name": "a.txt", "type": "file", "modified_time": "1715594880"},
map[string]interface{}{"token": "tok_sub", "name": "sub", "type": "folder"},
map[string]interface{}{"token": "tok_d", "name": "d.txt", "type": "file", "modified_time": "1715595000"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=tok_sub",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_c", "name": "c.txt", "type": "file", "modified_time": "1715594880"},
},
"has_more": false,
},
},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "local",
"--folder-token", "folder_root",
"--quick",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"detection": "quick"`) {
t.Fatalf("output missing detection=quick\noutput: %s", out)
}
checks := []struct {
bucket string
path string
token string
}{
{"new_local", "b.txt", ""},
{"new_remote", "d.txt", "tok_d"},
{"modified", "sub/c.txt", "tok_c"},
{"unchanged", "a.txt", "tok_a"},
}
for _, c := range checks {
if !strings.Contains(out, `"`+c.bucket+`":`) {
t.Errorf("output missing bucket %q\noutput: %s", c.bucket, out)
}
if !strings.Contains(out, `"rel_path": "`+c.path+`"`) {
t.Errorf("output missing rel_path %q (expected in %s)\noutput: %s", c.path, c.bucket, out)
}
if c.token != "" && !strings.Contains(out, `"file_token": "`+c.token+`"`) {
t.Errorf("output missing file_token %q (expected in %s)\noutput: %s", c.token, c.bucket, out)
}
}
reg.Verify(t)
}
// TestDriveStatusQuickMarksUntrustedTimestampAsModified locks in the
// conservative fallback for malformed remote modified_time values.
func TestDriveStatusQuickMarksUntrustedTimestampAsModified(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("local/a.txt", []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile a.txt: %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_a", "name": "a.txt", "type": "file", "modified_time": "not-a-timestamp"},
},
"has_more": false,
},
},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "local",
"--folder-token", "folder_root",
"--quick",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
out := stdout.String()
if !strings.Contains(out, `"detection": "quick"`) {
t.Fatalf("output missing detection=quick\noutput: %s", out)
}
if !strings.Contains(out, `"modified":`) || !strings.Contains(out, `"rel_path": "a.txt"`) {
t.Fatalf("invalid remote modified_time must fall back to modified\noutput: %s", out)
}
reg.Verify(t)
}
// TestDriveStatusExactRejectsMissingDownloadScope proves that exact mode keeps
// requiring drive:file:download even after quick mode made download optional.
func TestDriveStatusExactRejectsMissingDownloadScope(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, driveTestConfig())
f.Credential = credential.NewCredentialProvider(nil, nil, &driveStatusScopedTokenResolver{scopes: "drive:drive.metadata:readonly"}, nil)
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile("local/a.txt", []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile a.txt: %v", err)
}
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected missing_scope error for exact mode without drive:file:download")
}
var permErr *errs.PermissionError
if !errors.As(err, &permErr) {
t.Fatalf("expected *errs.PermissionError, got %T", err)
}
if permErr.Subtype != errs.SubtypeMissingScope {
t.Fatalf("Subtype = %q, want %q", permErr.Subtype, errs.SubtypeMissingScope)
}
if !strings.Contains(err.Error(), "missing required scope(s): drive:file:download") {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(permErr.Hint, "auth login --scope") {
t.Fatalf("missing scope hint not found: %q", permErr.Hint)
}
foundScope := false
for _, s := range permErr.MissingScopes {
if s == "drive:file:download" {
foundScope = true
break
}
}
if !foundScope {
t.Fatalf("MissingScopes must include drive:file:download, got %v", permErr.MissingScopes)
}
}
// TestDriveStatusQuickAcceptsMissingDownloadScope ensures quick mode is not
// blocked on the exact-mode download scope precheck.
func TestDriveStatusQuickAcceptsMissingDownloadScope(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
f.Credential = credential.NewCredentialProvider(nil, nil, &driveStatusScopedTokenResolver{scopes: "drive:drive.metadata:readonly"}, nil)
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll("local", 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile("local/a.txt", []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile a.txt: %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_a", "name": "a.txt", "type": "file", "modified_time": "not-a-timestamp"},
},
"has_more": false,
},
},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "local",
"--folder-token", "folder_root",
"--quick",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("quick mode should not require drive:file:download: %v\nstdout: %s", err, stdout.String())
}
if !strings.Contains(stdout.String(), `"detection": "quick"`) {
t.Fatalf("output missing detection=quick\noutput: %s", stdout.String())
}
reg.Verify(t)
}
// TestDriveStatusShouldTreatAsUnchangedQuick exercises the tiny quick helper
// directly so Codecov also sees coverage on the helper body itself.
func TestDriveStatusShouldTreatAsUnchangedQuick(t *testing.T) {
t.Run("matching timestamp returns true", func(t *testing.T) {
if !driveStatusShouldTreatAsUnchangedQuick("1715594880", time.Unix(1715594880, 500)) {
t.Fatal("expected matching second-resolution timestamps to be unchanged")
}
})
t.Run("different timestamp returns false", func(t *testing.T) {
if driveStatusShouldTreatAsUnchangedQuick("1715594881", time.Unix(1715594880, 0)) {
t.Fatal("expected different timestamps to be treated as modified")
}
})
t.Run("invalid timestamp returns false", func(t *testing.T) {
if driveStatusShouldTreatAsUnchangedQuick("not-a-timestamp", time.Unix(1715594880, 0)) {
t.Fatal("expected invalid timestamp to be treated as modified")
}
})
}
// TestDriveStatusPaginatesRemoteListing pins multi-page handling end-to-end
// AND the dual-field tolerance of common.PaginationMeta. Page 1 surfaces
// `next_page_token` (Drive's historical name); page 2 surfaces `page_token`
// (what the shared helper also accepts). If the shortcut had hard-coded
// either field name, one of the two pages' files would be silently dropped
// from the comparison and would land in the wrong bucket. Stub order is
// significant: httpmock matches in registration order, and both stubs key on
// the GET .../files URL — they pop in turn, so page 1's response (with the
// continuation token) must be registered before page 2's terminator.
func TestDriveStatusPaginatesRemoteListing(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)
}
// Page 1: returns one file plus a continuation token via
// next_page_token (the field Drive currently emits).
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_p1", "name": "page1.txt", "type": "file"},
},
"has_more": true,
"next_page_token": "cursor-page-2",
},
},
})
// Page 2: returns the second file with has_more=false. This stub uses
// page_token (the alternate spelling) to lock in that the shared
// PaginationMeta helper accepts BOTH field names.
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_p2", "name": "page2.txt", "type": "file"},
},
"has_more": false,
"page_token": "",
},
},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--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()
// Both pages contributed to new_remote (local is empty).
for _, want := range []string{
`"rel_path": "page1.txt"`,
`"file_token": "tok_p1"`,
`"rel_path": "page2.txt"`,
`"file_token": "tok_p2"`,
} {
if !strings.Contains(out, want) {
t.Errorf("output missing %q (a page must have been silently dropped)\noutput: %s", want, out)
}
}
reg.Verify(t)
}
func TestDriveStatusFailsOnRemoteFileFolderConflict(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)
}
registerRemoteListing(reg, "folder_root", []map[string]interface{}{
{"token": duplicateRemoteFileIDFirst, "name": "dup", "type": "file", "size": 5, "created_time": "1", "modified_time": "1"},
{"token": duplicateRemoteFolderID, "name": "dup", "type": "folder", "created_time": "2", "modified_time": "2"},
})
registerRemoteListing(reg, duplicateRemoteFolderID, []map[string]interface{}{
{"token": "nested-file-token", "name": "child.txt", "type": "file", "size": 1, "created_time": "3", "modified_time": "3"},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "local",
"--folder-token", "folder_root",
"--as", "bot",
}, f, stdout)
assertDuplicateRemotePathError(t, err, "dup", duplicateRemoteFileIDFirst, duplicateRemoteFolderID)
if stdout.Len() != 0 {
t.Fatalf("stdout should be empty on duplicate_remote_path, got: %s", stdout.String())
}
reg.Verify(t)
}
func TestDriveStatusRejectsMissingLocalDir(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "does-not-exist",
"--folder-token", "folder_root",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected validation error for missing local dir, got nil")
}
}
func TestDriveStatusRejectsLocalFile(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.WriteFile("not-a-dir.txt", []byte("x"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "not-a-dir.txt",
"--folder-token", "folder_root",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected validation error when --local-dir is a file, got nil")
}
if !strings.Contains(err.Error(), "not a directory") {
t.Fatalf("unexpected error message: %v", err)
}
}
func TestDriveStatusRejectsAbsoluteLocalDir(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--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")
}
}
// TestDriveStatusRejectsEmptyFolderToken covers the Validate-stage required
// check that runs before ResourceName: an empty --folder-token must surface
// a structured FlagError referencing the flag name.
func TestDriveStatusRejectsEmptyFolderToken(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, DriveStatus, []string{
"+status",
"--local-dir", "local",
"--folder-token", "",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected validation error for empty --folder-token, got nil")
}
if !strings.Contains(err.Error(), "--folder-token") {
t.Fatalf("error must reference --folder-token, got: %v", err)
}
}
// TestDriveStatusDoesNotEscapeViaSymlinkParentRef is the regression for the
// "link/.." escape: filepath.Clean string-shrinks "link/.." to ".", so a
// raw walk on the user-supplied input can land on the kernel-resolved
// path through link's target's parent — outside cwd. The fix is to walk
// SafeInputPath's canonical absolute root instead of the raw input.
//
// Setup: an "escape" sibling directory contains a sentinel file; cwd
// contains a "link" symlink pointing into that escape directory.
// Calling +status with --local-dir "link/.." must not surface the
// sentinel — the walk must stay inside cwd.
func TestDriveStatusDoesNotEscapeViaSymlinkParentRef(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
// Sentinel lives outside cwd; the agent must never see it.
escapeDir := t.TempDir()
if err := os.WriteFile(filepath.Join(escapeDir, "secret.txt"), []byte("S3CRET"), 0o644); err != nil {
t.Fatalf("WriteFile secret: %v", err)
}
// cwd has a symlink that points into the sentinel's parent.
cwdDir := t.TempDir()
withDriveWorkingDir(t, cwdDir)
if err := os.Symlink(escapeDir, filepath.Join(cwdDir, "link")); err != nil {
t.Fatalf("Symlink: %v", err)
}
// A normal file inside cwd just to make the walk non-trivial.
if err := os.WriteFile(filepath.Join(cwdDir, "ok.txt"), []byte("ok"), 0o644); err != nil {
t.Fatalf("WriteFile ok: %v", err)
}
// Empty remote folder so any path that surfaces in the output
// must have come from the local walk.
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,
},
},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--local-dir", "link/..",
"--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, "secret.txt") || strings.Contains(out, "S3CRET") {
t.Fatalf("walk escaped via link/..: secret.txt leaked into output\noutput:\n%s", out)
}
// ok.txt is in cwd and must classify as new_local (no remote stub for it).
if !strings.Contains(out, `"rel_path": "ok.txt"`) {
t.Fatalf("expected ok.txt in new_local, got:\n%s", out)
}
}
// TestDriveStatusSkipsSymlinkInsideRoot pins down WalkDir's default policy
// for symlinks discovered as child entries: they are reported with a
// non-regular file mode and the callback skips them, so a symlink inside
// the validated root pointing into an out-of-tree directory cannot leak
// the target's contents.
func TestDriveStatusSkipsSymlinkInsideRoot(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
// Sentinel sits outside cwd; a child symlink inside the walked root
// points there. If the walker followed child symlinks (it must not),
// the sentinel's name would surface in new_local.
escapeDir := t.TempDir()
if err := os.WriteFile(filepath.Join(escapeDir, "secret.txt"), []byte("S3CRET"), 0o644); err != nil {
t.Fatalf("WriteFile secret: %v", err)
}
cwdDir := t.TempDir()
withDriveWorkingDir(t, cwdDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "ok.txt"), []byte("ok"), 0o644); err != nil {
t.Fatalf("WriteFile ok: %v", err)
}
// Child-of-root symlink that resolves out of the validated subtree.
if err := os.Symlink(escapeDir, filepath.Join("local", "sub", "escape")); err != nil {
t.Fatalf("Symlink: %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,
},
},
})
err := mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--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, "secret.txt") || strings.Contains(out, "S3CRET") {
t.Fatalf("walk followed child symlink and leaked sentinel:\n%s", out)
}
if !strings.Contains(out, `"rel_path": "ok.txt"`) {
t.Fatalf("expected ok.txt in new_local; got:\n%s", out)
}
}
// TestDriveStatusSurvivesCircularSymlinkInsideRoot makes sure WalkDir
// terminates even when a child symlink points back at one of its
// ancestors. WalkDir's default policy already declines to follow child
// symlinks; this test pins that contract for our caller.
func TestDriveStatusSurvivesCircularSymlinkInsideRoot(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
cwdDir := t.TempDir()
withDriveWorkingDir(t, cwdDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "sub", "real.txt"), []byte("real"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
// loop symlink: cwd/local/sub/loop -> cwd/local (an ancestor).
loopTarget, err := filepath.Abs(filepath.Join("local"))
if err != nil {
t.Fatalf("Abs: %v", err)
}
if err := os.Symlink(loopTarget, filepath.Join("local", "sub", "loop")); err != nil {
t.Fatalf("Symlink: %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,
},
},
})
// If WalkDir followed the loop, this test would never finish; the
// test runner's per-test timeout would surface that as a failure.
err = mountAndRunDrive(t, DriveStatus, []string{
"+status",
"--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(), `"rel_path": "sub/real.txt"`) {
t.Fatalf("expected sub/real.txt in new_local; got:\n%s", stdout.String())
}
}
// TestDriveStatusRejectsMalformedFolderToken covers the ResourceName format
// guard: a token with control characters (newline) must be rejected before
// any API call is made.
func TestDriveStatusRejectsMalformedFolderToken(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, DriveStatus, []string{
"+status",
"--local-dir", "local",
"--folder-token", "tok\nwithnewline",
"--as", "bot",
}, f, nil)
if err == nil {
t.Fatal("expected validation error for malformed --folder-token, got nil")
}
if !strings.Contains(err.Error(), "--folder-token") {
t.Fatalf("error must reference --folder-token, got: %v", err)
}
}
func TestWalkLocalForStatusMissingRootReturnsInternalError(t *testing.T) {
missingRoot := filepath.Join(t.TempDir(), "does-not-exist")
_, err := walkLocalForStatus(missingRoot, t.TempDir())
if err == nil {
t.Fatal("expected walkLocalForStatus() to fail for missing root")
}
var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected structured ExitError, got %T", err)
}
if exitErr.Detail == nil || exitErr.Detail.Type != "io" {
t.Fatalf("expected io error detail, got %#v", exitErr.Detail)
}
if !strings.Contains(err.Error(), "walk") {
t.Fatalf("expected walk-related error, got: %v", err)
}
}
func TestHashLocalForStatusWrapsOpenError(t *testing.T) {
config := driveTestConfig()
f, _, _, _ := cmdutil.TestFactory(t, config)
runtime := common.TestNewRuntimeContext(&cobra.Command{Use: "drive"}, config)
runtime.Factory = f
_, err := hashLocalForStatus(runtime, "missing.txt")
if err == nil {
t.Fatal("expected hashLocalForStatus() to fail for missing file")
}
if !strings.Contains(err.Error(), "missing.txt") {
t.Fatalf("expected error to mention the missing file, got: %v", err)
}
}