Files
larksuite-cli/shortcuts/markdown/markdown_test.go
fangshuyu-768 816927f8b8 fix: surface auto-grant failures via stderr and JSON hint (#1015)
When a resource is created with bot identity, the CLI attempts to
auto-grant full_access to the current user. If the user open_id is
missing or the grant API call fails, the result was only written to
the JSON permission_grant field and easily overlooked.

Changes:
- Add stderr warnings when auto-grant is skipped or fails
- Add 'hint' field to permission_grant JSON output with failure reason
  and actionable next step (e.g. auth login, check scope, retry)
- Add end-to-end skipped/failed tests across all affected shortcuts
  (doc, drive, sheets, slides, wiki, markdown, base)

Closes #963
2026-05-21 18:17:24 +08:00

1704 lines
48 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package markdown
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"io/fs"
"mime"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
"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/shortcuts/common"
)
func markdownTestConfig() *core.CliConfig {
return &core.CliConfig{
AppID: "markdown-test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
}
}
func markdownPermissionTestConfig(userOpenID string) *core.CliConfig {
return &core.CliConfig{
AppID: "markdown-perm-test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
UserOpenId: userOpenID,
}
}
func mountAndRunMarkdown(t *testing.T, s common.Shortcut, args []string, f *cmdutil.Factory, stdout *bytes.Buffer) error {
t.Helper()
parent := &cobra.Command{Use: "markdown"}
s.Mount(parent, f)
parent.SetArgs(args)
parent.SilenceErrors = true
parent.SilenceUsage = true
if stdout != nil {
stdout.Reset()
}
return parent.Execute()
}
func withMarkdownWorkingDir(t *testing.T, dir string) {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd() error: %v", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("Chdir(%q) error: %v", dir, err)
}
t.Cleanup(func() {
if err := os.Chdir(cwd); err != nil {
t.Fatalf("restore cwd error: %v", err)
}
})
}
type capturedMultipartBody struct {
Fields map[string]string
Files map[string][]byte
}
func decodeCapturedMultipartBody(t *testing.T, stub *httpmock.Stub) capturedMultipartBody {
t.Helper()
contentType := stub.CapturedHeaders.Get("Content-Type")
mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil {
t.Fatalf("parse multipart content type: %v", err)
}
if mediaType != "multipart/form-data" {
t.Fatalf("content type = %q, want multipart/form-data", mediaType)
}
reader := multipart.NewReader(bytes.NewReader(stub.CapturedBody), params["boundary"])
body := capturedMultipartBody{
Fields: map[string]string{},
Files: map[string][]byte{},
}
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("read multipart part: %v", err)
}
data, err := io.ReadAll(part)
if err != nil {
t.Fatalf("read multipart data: %v", err)
}
if part.FileName() != "" {
body.Files[part.FormName()] = data
continue
}
body.Fields[part.FormName()] = string(data)
}
return body
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req)
}
type errReadCloser struct{ err error }
func (r *errReadCloser) Read(_ []byte) (int, error) { return 0, r.err }
func (r *errReadCloser) Close() error { return nil }
type staticFileIOProvider struct {
fileIO fileio.FileIO
}
func (p *staticFileIOProvider) Name() string { return "static" }
func (p *staticFileIOProvider) ResolveFileIO(context.Context) fileio.FileIO {
return p.fileIO
}
type failingSaveFileIO struct {
base fileio.FileIO
err error
}
func (f *failingSaveFileIO) Open(name string) (fileio.File, error) {
return f.base.Open(name)
}
func (f *failingSaveFileIO) Stat(name string) (fileio.FileInfo, error) {
return f.base.Stat(name)
}
func (f *failingSaveFileIO) ResolvePath(path string) (string, error) {
return f.base.ResolvePath(path)
}
func (f *failingSaveFileIO) Save(string, fileio.SaveOptions, io.Reader) (fileio.SaveResult, error) {
return nil, &fileio.WriteError{Err: f.err}
}
type stubFileInfo struct {
size int64
}
func (i stubFileInfo) Size() int64 { return i.size }
func (i stubFileInfo) IsDir() bool { return false }
func (i stubFileInfo) Mode() fs.FileMode { return 0 }
type statOnlyFileIO struct {
base fileio.FileIO
size int64
openErr error
}
func (f *statOnlyFileIO) Open(string) (fileio.File, error) {
return nil, f.openErr
}
func (f *statOnlyFileIO) Stat(string) (fileio.FileInfo, error) {
return stubFileInfo{size: f.size}, nil
}
func (f *statOnlyFileIO) ResolvePath(path string) (string, error) {
return f.base.ResolvePath(path)
}
func (f *statOnlyFileIO) Save(path string, opts fileio.SaveOptions, body io.Reader) (fileio.SaveResult, error) {
return f.base.Save(path, opts, body)
}
func TestShortcutsIncludesExpectedCommands(t *testing.T) {
t.Parallel()
got := Shortcuts()
want := []string{"+create", "+diff", "+fetch", "+patch", "+overwrite"}
if len(got) != len(want) {
t.Fatalf("len(Shortcuts()) = %d, want %d", len(got), len(want))
}
seen := make(map[string]bool, len(got))
for _, shortcut := range got {
if seen[shortcut.Command] {
t.Fatalf("duplicate shortcut command: %s", shortcut.Command)
}
seen[shortcut.Command] = true
}
for _, command := range want {
if !seen[command] {
t.Fatalf("missing shortcut command %q in Shortcuts()", command)
}
}
}
func TestMarkdownCreateRequiresNameWithContent(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--content", "# hello",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "--name is required when using --content") {
t.Fatalf("expected name validation error, got %v", err)
}
}
func TestMarkdownCreateRejectsNonMarkdownFile(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("note.txt", []byte("hello"), 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "note.txt",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "--file must end with .md") {
t.Fatalf("expected .md validation error, got %v", err)
}
}
func TestMarkdownCreateValidationBranches(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
tests := []struct {
name string
args []string
want string
}{
{
name: "content and file are mutually exclusive",
args: []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--file", "note.md",
},
want: "--content and --file are mutually exclusive",
},
{
name: "exactly one source is required",
args: []string{
"+create",
"--name", "README.md",
},
want: "specify exactly one of --content or --file",
},
{
name: "folder token cannot be empty",
args: []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--folder-token=",
},
want: "--folder-token cannot be empty",
},
{
name: "wiki token cannot be empty",
args: []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--wiki-token=",
},
want: "--wiki-token cannot be empty",
},
{
name: "folder and wiki tokens are mutually exclusive",
args: []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--folder-token", "fld_target",
"--wiki-token", "wikcn_target",
},
want: "--folder-token and --wiki-token are mutually exclusive",
},
{
name: "folder token must be valid",
args: []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--folder-token", "../bad",
},
want: "--folder-token",
},
{
name: "wiki token must be valid",
args: []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--wiki-token", "../bad",
},
want: "--wiki-token",
},
{
name: "content mode still validates markdown file name",
args: []string{
"+create",
"--name", "README.txt",
"--content", "# hello",
},
want: "--name must end with .md",
},
{
name: "file flag cannot be empty",
args: []string{
"+create",
"--file=",
},
want: "--file cannot be empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := mountAndRunMarkdown(t, MarkdownCreate, tt.args, f, stdout)
if err == nil || !strings.Contains(err.Error(), tt.want) {
t.Fatalf("expected error containing %q, got %v", tt.want, err)
}
})
}
}
func TestMarkdownCreateRejectsEmptyInlineContent(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "empty.md",
"--content", "",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "empty markdown content is not supported") {
t.Fatalf("expected empty content validation error, got %v", err)
}
}
func TestMarkdownCreateRejectsEmptyContentFromFileInput(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("empty.md", []byte{}, 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "empty.md",
"--content", "@./empty.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "empty markdown content is not supported") {
t.Fatalf("expected empty content validation error, got %v", err)
}
}
func TestMarkdownCreateRejectsEmptyLocalFile(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("empty.md", []byte{}, 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "empty.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "empty markdown content is not supported") {
t.Fatalf("expected empty content validation error, got %v", err)
}
}
func TestMarkdownCreateDryRunWithInlineContent(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--dry-run",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "/open-apis/drive/v1/files/upload_all") {
t.Fatalf("dry-run missing upload_all: %s", out)
}
if !strings.Contains(out, "/open-apis/drive/v1/metas/batch_query") || !strings.Contains(out, `"with_url": true`) {
t.Fatalf("dry-run missing metadata URL lookup: %s", out)
}
if !strings.Contains(out, "markdown content") {
t.Fatalf("dry-run missing content marker: %s", out)
}
}
func TestMarkdownCreateDryRunWithWikiToken(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "README.md",
"--content", "# hello",
"--wiki-token", "wikcn_markdown_dryrun_target",
"--dry-run",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, `"parent_type": "wiki"`) {
t.Fatalf("dry-run missing wiki parent_type: %s", out)
}
if !strings.Contains(out, `"parent_node": "wikcn_markdown_dryrun_target"`) {
t.Fatalf("dry-run missing wiki parent_node: %s", out)
}
if !strings.Contains(out, "/open-apis/drive/v1/metas/batch_query") || !strings.Contains(out, `"with_url": true`) {
t.Fatalf("dry-run missing metadata URL lookup: %s", out)
}
}
func TestMarkdownCreateDryRunReportsSourceFileError(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "missing.md",
"--dry-run",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected dry-run error: %v", err)
}
if !strings.Contains(stdout.String(), `"error"`) || !strings.Contains(stdout.String(), "cannot read file") {
t.Fatalf("dry-run output missing file error: %s", stdout.String())
}
}
func TestMarkdownCreateDryRunWithFileUsesStatOnly(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
f.FileIOProvider = &staticFileIOProvider{
fileIO: &statOnlyFileIO{
base: fileio.GetProvider().ResolveFileIO(context.Background()),
size: markdownSinglePartSizeLimit + 1,
openErr: errors.New("open should not be called in dry-run"),
},
}
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "large.md",
"--dry-run",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected dry-run error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "/open-apis/drive/v1/files/upload_prepare") {
t.Fatalf("dry-run missing multipart prepare step: %s", out)
}
if !strings.Contains(out, "/open-apis/drive/v1/metas/batch_query") || !strings.Contains(out, `"with_url": true`) {
t.Fatalf("dry-run missing metadata URL lookup: %s", out)
}
if strings.Contains(out, "open should not be called in dry-run") {
t.Fatalf("dry-run unexpectedly tried to open the source file: %s", out)
}
}
func TestMarkdownCreateSuccessUploadAll(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_create",
"version": "1001",
},
},
}
reg.Register(uploadStub)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "box_md_create", "doc_type": "file", "url": "https://tenant.example.com/file/box_md_create"},
},
},
},
})
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "README.md",
"--content", "# hello\n",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeCapturedMultipartBody(t, uploadStub)
if got := body.Fields["file_name"]; got != "README.md" {
t.Fatalf("file_name = %q, want README.md", got)
}
if got := body.Fields["parent_type"]; got != "explorer" {
t.Fatalf("parent_type = %q, want explorer", got)
}
if got := body.Fields["parent_node"]; got != "" {
t.Fatalf("parent_node = %q, want empty root folder", got)
}
if _, exists := body.Fields["file_token"]; exists {
t.Fatalf("did not expect file_token on create upload_all body")
}
if got := string(body.Files["file"]); got != "# hello\n" {
t.Fatalf("uploaded file content = %q", got)
}
if !strings.Contains(stdout.String(), `"file_token": "box_md_create"`) {
t.Fatalf("stdout missing file_token: %s", stdout.String())
}
if !strings.Contains(stdout.String(), `"file_name": "README.md"`) {
t.Fatalf("stdout missing file_name: %s", stdout.String())
}
if !strings.Contains(stdout.String(), `"url": "https://tenant.example.com/file/box_md_create"`) {
t.Fatalf("stdout missing url: %s", stdout.String())
}
}
func TestMarkdownCreateSuccessUploadAllToWikiReturnsMetaURL(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_create_wiki",
"version": "1002",
},
},
}
reg.Register(uploadStub)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "box_md_create_wiki", "doc_type": "file", "url": "https://tenant.example.com/file/box_md_create_wiki"},
},
},
},
})
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "README.md",
"--content", "# hello\n",
"--wiki-token", "wikcn_markdown_create_target",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeCapturedMultipartBody(t, uploadStub)
if got := body.Fields["parent_type"]; got != markdownUploadParentTypeWiki {
t.Fatalf("parent_type = %q, want %q", got, markdownUploadParentTypeWiki)
}
if got := body.Fields["parent_node"]; got != "wikcn_markdown_create_target" {
t.Fatalf("parent_node = %q, want %q", got, "wikcn_markdown_create_target")
}
if !strings.Contains(stdout.String(), `"url": "https://tenant.example.com/file/box_md_create_wiki"`) {
t.Fatalf("stdout missing metadata url for wiki-hosted markdown file: %s", stdout.String())
}
}
func TestMarkdownCreatePrettyOutputIncludesPermissionGrant(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_create_pretty",
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "box_md_create_pretty", "doc_type": "file", "url": "https://tenant.example.com/file/box_md_create_pretty"},
},
},
},
})
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "README.md",
"--content", "# hello\n",
"--as", "bot",
"--format", "pretty",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "file_token: box_md_create_pretty") {
t.Fatalf("pretty output missing file_token: %s", out)
}
if !strings.Contains(out, "url: https://tenant.example.com/file/box_md_create_pretty") {
t.Fatalf("pretty output missing url: %s", out)
}
if !strings.Contains(out, "permission_grant.status: skipped") {
t.Fatalf("pretty output missing permission_grant.status: %s", out)
}
if !strings.Contains(out, "permission_grant.perm: full_access") {
t.Fatalf("pretty output missing permission_grant.perm: %s", out)
}
}
func TestMarkdownCreateBotAutoGrantSkippedNoUser(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownPermissionTestConfig(""))
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_skipped",
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "box_md_skipped", "doc_type": "file", "url": "https://example.feishu.cn/file/box_md_skipped"},
},
},
},
})
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "README.md",
"--content", "# hello\n",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var envelope struct {
Data map[string]interface{} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("unmarshal: %v", err)
}
grant, _ := envelope.Data["permission_grant"].(map[string]interface{})
if grant["status"] != common.PermissionGrantSkipped {
t.Fatalf("permission_grant.status = %#v, want %q", grant["status"], common.PermissionGrantSkipped)
}
if hint, ok := grant["hint"].(string); !ok || !strings.Contains(hint, "auth login") {
t.Fatalf("hint = %#v, want string containing 'auth login'", grant["hint"])
}
}
func TestMarkdownCreateBotAutoGrantFailed(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownPermissionTestConfig("ou_current_user"))
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_grant_fail",
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "box_md_grant_fail", "doc_type": "file", "url": "https://example.feishu.cn/file/box_md_grant_fail"},
},
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/permissions/box_md_grant_fail/members",
Body: map[string]interface{}{
"code": 230001,
"msg": "no permission",
},
})
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--name", "README.md",
"--content", "# hello\n",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var envelope struct {
Data map[string]interface{} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("unmarshal: %v", err)
}
grant, _ := envelope.Data["permission_grant"].(map[string]interface{})
if grant["status"] != common.PermissionGrantFailed {
t.Fatalf("permission_grant.status = %#v, want %q", grant["status"], common.PermissionGrantFailed)
}
if hint, ok := grant["hint"].(string); !ok || !strings.Contains(hint, "Retry later") {
t.Fatalf("hint = %#v, want string containing 'Retry later'", grant["hint"])
}
}
func TestMarkdownCreateMissingFileReturnsReadError(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "missing.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "cannot read file") {
t.Fatalf("expected cannot read file error, got %v", err)
}
}
func TestMarkdownCreateMultipartUploadSuccess(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_prepare",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"upload_id": "upload_markdown_ok",
"block_size": float64(markdownSinglePartSizeLimit),
"block_num": float64(2),
},
},
})
uploadPartStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_part",
Reusable: true,
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{},
},
}
reg.Register(uploadPartStub)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_finish",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_multipart",
"version": "1004",
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "box_md_multipart", "doc_type": "file", "url": "https://tenant.example.com/file/box_md_multipart"},
},
},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
fh, err := os.Create("large.md")
if err != nil {
t.Fatalf("Create() error: %v", err)
}
if err := fh.Truncate(markdownSinglePartSizeLimit + 1); err != nil {
fh.Close()
t.Fatalf("Truncate() error: %v", err)
}
if err := fh.Close(); err != nil {
t.Fatalf("Close() error: %v", err)
}
err = mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "large.md",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(uploadPartStub.CapturedBodies) != 2 {
t.Fatalf("upload_part call count = %d, want 2", len(uploadPartStub.CapturedBodies))
}
if !strings.Contains(stdout.String(), `"file_token": "box_md_multipart"`) {
t.Fatalf("stdout missing multipart file_token: %s", stdout.String())
}
if !strings.Contains(stdout.String(), `"url": "https://tenant.example.com/file/box_md_multipart"`) {
t.Fatalf("stdout missing multipart metadata url: %s", stdout.String())
}
}
func TestMarkdownCreateMultipartUploadToWikiUsesWikiParent(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
prepareStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_prepare",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"upload_id": "upload_markdown_wiki_ok",
"block_size": float64(markdownSinglePartSizeLimit),
"block_num": float64(2),
},
},
}
reg.Register(prepareStub)
uploadPartStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_part",
Reusable: true,
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{},
},
}
reg.Register(uploadPartStub)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_finish",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_multipart_wiki",
"version": "1005",
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"doc_token": "box_md_multipart_wiki", "doc_type": "file", "url": "https://tenant.example.com/file/box_md_multipart_wiki"},
},
},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
fh, err := os.Create("large.md")
if err != nil {
t.Fatalf("Create() error: %v", err)
}
if err := fh.Truncate(markdownSinglePartSizeLimit + 1); err != nil {
fh.Close()
t.Fatalf("Truncate() error: %v", err)
}
if err := fh.Close(); err != nil {
t.Fatalf("Close() error: %v", err)
}
err = mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "large.md",
"--wiki-token", "wikcn_markdown_multipart_target",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var body map[string]interface{}
if err := json.Unmarshal(prepareStub.CapturedBody, &body); err != nil {
t.Fatalf("decode upload_prepare body: %v\nraw=%s", err, string(prepareStub.CapturedBody))
}
if got := body["parent_type"]; got != markdownUploadParentTypeWiki {
t.Fatalf("parent_type = %#v, want %q", got, markdownUploadParentTypeWiki)
}
if got := body["parent_node"]; got != "wikcn_markdown_multipart_target" {
t.Fatalf("parent_node = %#v, want %q", got, "wikcn_markdown_multipart_target")
}
if !strings.Contains(stdout.String(), `"url": "https://tenant.example.com/file/box_md_multipart_wiki"`) {
t.Fatalf("stdout missing metadata url for wiki-hosted multipart markdown file: %s", stdout.String())
}
}
func TestMarkdownCreateFailsWhenMultipartPlanIsTooSmall(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_prepare",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"upload_id": "upload_markdown_bad_plan",
"block_size": float64(markdownSinglePartSizeLimit),
"block_num": float64(1),
},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
fh, err := os.Create("large.md")
if err != nil {
t.Fatalf("Create() error: %v", err)
}
if err := fh.Truncate(markdownSinglePartSizeLimit + 1); err != nil {
fh.Close()
t.Fatalf("Truncate() error: %v", err)
}
if err := fh.Close(); err != nil {
t.Fatalf("Close() error: %v", err)
}
err = mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "large.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "inconsistent chunk plan") {
t.Fatalf("expected inconsistent chunk plan error, got %v", err)
}
}
func TestMarkdownCreateFailsWhenMultipartPlanIsTooLarge(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_prepare",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"upload_id": "upload_markdown_bad_plan_large",
"block_size": float64(markdownSinglePartSizeLimit),
"block_num": float64(3),
},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
fh, err := os.Create("large.md")
if err != nil {
t.Fatalf("Create() error: %v", err)
}
if err := fh.Truncate(markdownSinglePartSizeLimit + 1); err != nil {
fh.Close()
t.Fatalf("Truncate() error: %v", err)
}
if err := fh.Close(); err != nil {
t.Fatalf("Close() error: %v", err)
}
err = mountAndRunMarkdown(t, MarkdownCreate, []string{
"+create",
"--file", "large.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "inconsistent chunk plan") {
t.Fatalf("expected inconsistent chunk plan error, got %v", err)
}
}
func TestUploadMarkdownMultipartPartsRejectsOversizedBlockSize(t *testing.T) {
maxBufferSize := int64(^uint(0) >> 1)
if maxBufferSize == int64(^uint64(0)>>1) {
t.Skip("oversized block_size guard is only reachable on platforms where int is narrower than int64")
}
err := uploadMarkdownMultipartParts(nil, bytes.NewReader([]byte("x")), 1, markdownMultipartSession{
UploadID: "upload_markdown_bad_block_size",
BlockSize: maxBufferSize + 1,
BlockNum: 1,
})
if err == nil || !strings.Contains(err.Error(), "invalid block_size returned") {
t.Fatalf("expected invalid block_size error, got %v", err)
}
}
func TestMarkdownOverwriteUploadAllIncludesFileTokenAndVersion(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"title": "README.md"},
},
},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_existing",
"version": "7633658129540910621",
},
},
}
reg.Register(uploadStub)
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--content", "# updated\n",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeCapturedMultipartBody(t, uploadStub)
if got := body.Fields["file_token"]; got != "box_md_existing" {
t.Fatalf("file_token = %q, want box_md_existing", got)
}
if got := body.Fields["file_name"]; got != "README.md" {
t.Fatalf("file_name = %q, want README.md", got)
}
if got := string(body.Files["file"]); got != "# updated\n" {
t.Fatalf("uploaded file content = %q", got)
}
if !strings.Contains(stdout.String(), `"version": "7633658129540910621"`) {
t.Fatalf("stdout missing version: %s", stdout.String())
}
}
func TestMarkdownOverwriteUsesExplicitNameWhenProvided(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_existing",
"version": "7633658129540910622",
},
},
}
reg.Register(uploadStub)
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--name", "Renamed.md",
"--content", "# updated\n",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeCapturedMultipartBody(t, uploadStub)
if got := body.Fields["file_name"]; got != "Renamed.md" {
t.Fatalf("file_name = %q, want Renamed.md", got)
}
if !strings.Contains(stdout.String(), `"file_name": "Renamed.md"`) {
t.Fatalf("stdout missing overridden file_name: %s", stdout.String())
}
}
func TestMarkdownOverwriteUsesLocalFileNameByDefault(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_existing",
"version": "7633658129540910623",
},
},
}
reg.Register(uploadStub)
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("local-name.md", []byte("# local\n"), 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--file", "local-name.md",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeCapturedMultipartBody(t, uploadStub)
if got := body.Fields["file_name"]; got != "local-name.md" {
t.Fatalf("file_name = %q, want local-name.md", got)
}
}
func TestMarkdownOverwriteFailsWithoutVersion(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{
{"title": "README.md"},
},
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_existing",
},
},
})
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--content", "# updated\n",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "overwrite failed: no version returned") {
t.Fatalf("expected version error, got %v", err)
}
}
func TestMarkdownOverwriteFallsBackToFileTokenNameWhenMetadataMissing(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/metas/batch_query",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"metas": []map[string]interface{}{},
},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_existing",
"version": "7633658129540910624",
},
},
}
reg.Register(uploadStub)
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--content", "# updated\n",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
body := decodeCapturedMultipartBody(t, uploadStub)
if got := body.Fields["file_name"]; got != "box_md_existing.md" {
t.Fatalf("file_name = %q, want box_md_existing.md", got)
}
}
func TestMarkdownOverwriteDryRunWithInlineContent(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--content", "# updated\n",
"--dry-run",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "/open-apis/drive/v1/metas/batch_query") {
t.Fatalf("dry-run missing metas lookup: %s", out)
}
if !strings.Contains(out, "/open-apis/drive/v1/files/upload_all") {
t.Fatalf("dry-run missing upload_all: %s", out)
}
if !strings.Contains(out, `"file_token":"box_md_existing"`) && !strings.Contains(out, `"file_token": "box_md_existing"`) {
t.Fatalf("dry-run missing file_token: %s", out)
}
}
func TestMarkdownOverwriteDryRunReportsSourceFileError(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--file", "missing.md",
"--dry-run",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected dry-run error: %v", err)
}
if !strings.Contains(stdout.String(), `"error"`) || !strings.Contains(stdout.String(), "cannot read file") {
t.Fatalf("dry-run output missing file error: %s", stdout.String())
}
}
func TestMarkdownOverwriteRejectsEmptyInlineContent(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--content", "",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "empty markdown content is not supported") {
t.Fatalf("expected empty content validation error, got %v", err)
}
}
func TestMarkdownOverwriteRejectsEmptyLocalFile(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("empty.md", []byte{}, 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--file", "empty.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "empty markdown content is not supported") {
t.Fatalf("expected empty content validation error, got %v", err)
}
}
func TestMarkdownOverwriteMetadataLookupFailure(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--content", "# updated\n",
}, f, stdout)
if err == nil {
t.Fatal("expected metadata lookup failure")
}
}
func TestMarkdownOverwriteMissingFileReturnsReadError(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--file", "missing.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "cannot read file") {
t.Fatalf("expected cannot read file error, got %v", err)
}
}
func TestMarkdownOverwritePrettyOutputUsesDataVersionFallback(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"file_token": "box_md_existing",
"data_version": "7633658129540910625",
},
},
})
err := mountAndRunMarkdown(t, MarkdownOverwrite, []string{
"+overwrite",
"--file-token", "box_md_existing",
"--name", "README.md",
"--content", "# updated\n",
"--format", "pretty",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "file_name: README.md") {
t.Fatalf("pretty output missing file_name: %s", out)
}
if !strings.Contains(out, "version: 7633658129540910625") {
t.Fatalf("pretty output missing fallback version: %s", out)
}
}
func TestMarkdownFetchReturnsContent(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(stdout.String(), `"file_name": "README.md"`) {
t.Fatalf("stdout missing file_name: %s", stdout.String())
}
if !strings.Contains(stdout.String(), `"content": "# hello\n"`) {
t.Fatalf("stdout missing content: %s", stdout.String())
}
}
func TestMarkdownFetchDownloadNetworkError(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "download failed") {
t.Fatalf("expected download failed error, got %v", err)
}
}
func TestMarkdownFetchReadFailure(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
f.HttpClient = func() (*http.Client, error) {
return &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Header: http.Header{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
Body: &errReadCloser{err: errors.New("stream broke")},
}, nil
}),
}, nil
}
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "download failed") {
t.Fatalf("expected read failure error, got %v", err)
}
}
func TestMarkdownFetchPrettyReturnsContent(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--format", "pretty",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "# hello\n") {
t.Fatalf("pretty output missing content: %s", out)
}
}
func TestMarkdownFetchSavesFile(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--output", "copy.md",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := os.ReadFile("copy.md")
if err != nil {
t.Fatalf("ReadFile() error: %v", err)
}
if string(data) != "# hello\n" {
t.Fatalf("saved content = %q", string(data))
}
var envelope struct {
Data map[string]interface{} `json:"data"`
}
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
t.Fatalf("unmarshal stdout: %v", err)
}
if got := common.GetString(envelope.Data, "saved_path"); !strings.HasSuffix(got, "copy.md") {
t.Fatalf("saved_path = %q, want suffix copy.md", got)
}
}
func TestMarkdownFetchRejectsExistingFileWithoutOverwrite(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("copy.md", []byte("existing"), 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--output", "copy.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "output file already exists") {
t.Fatalf("expected output exists error, got %v", err)
}
}
func TestMarkdownFetchOverwritesExistingFileWhenRequested(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.WriteFile("copy.md", []byte("existing"), 0o644); err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--output", "copy.md",
"--overwrite",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := os.ReadFile("copy.md")
if err != nil {
t.Fatalf("ReadFile() error: %v", err)
}
if string(data) != "# hello\n" {
t.Fatalf("saved content = %q", string(data))
}
}
func TestMarkdownFetchSavesUsingRemoteNameWhenOutputIsExistingDirectory(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
if err := os.MkdirAll("downloads", 0o755); err != nil {
t.Fatalf("MkdirAll() error: %v", err)
}
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--output", "downloads",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := os.ReadFile(filepath.Join("downloads", "README.md"))
if err != nil {
t.Fatalf("ReadFile() error: %v", err)
}
if string(data) != "# hello\n" {
t.Fatalf("saved content = %q", string(data))
}
}
func TestMarkdownFetchSavesUsingRemoteNameWhenOutputUsesDirectorySyntax(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--output", "downloads/",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
data, err := os.ReadFile(filepath.Join("downloads", "README.md"))
if err != nil {
t.Fatalf("ReadFile() error: %v", err)
}
if string(data) != "# hello\n" {
t.Fatalf("saved content = %q", string(data))
}
}
func TestMarkdownFetchPrettySavesFile(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
tmpDir := t.TempDir()
withMarkdownWorkingDir(t, tmpDir)
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--output", "copy.md",
"--format", "pretty",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
if !strings.Contains(out, "saved_path:") {
t.Fatalf("pretty output missing saved_path: %s", out)
}
if !strings.Contains(out, "file_name: README.md") {
t.Fatalf("pretty output missing file_name: %s", out)
}
}
func TestMarkdownFetchSaveFailure(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/drive/v1/files/box_md_fetch/download",
Status: 200,
RawBody: []byte("# hello\n"),
Headers: map[string][]string{
"Content-Type": {"text/plain"},
"Content-Disposition": {`attachment; filename="README.md"`},
},
})
f.FileIOProvider = &staticFileIOProvider{
fileIO: &failingSaveFileIO{
base: fileio.GetProvider().ResolveFileIO(context.Background()),
err: errors.New("disk full"),
},
}
err := mountAndRunMarkdown(t, MarkdownFetch, []string{
"+fetch",
"--file-token", "box_md_fetch",
"--output", "copy.md",
}, f, stdout)
if err == nil || !strings.Contains(err.Error(), "cannot create file") {
t.Fatalf("expected save failure error, got %v", err)
}
}