From 58f8bae6e4c60cbf41b948b5c453feedebc87acf Mon Sep 17 00:00:00 2001 From: baiqing Date: Fri, 17 Apr 2026 19:10:02 +0800 Subject: [PATCH] test: cover in-memory Content upload paths for clipboard feature Adds unit tests for the new Content io.Reader branches introduced by the clipboard feature: - UploadDriveMediaAll with in-memory Content (drive_media_upload.go 87.5%) - UploadDriveMediaMultipart with in-memory Content (84.6%) - uploadDocMediaFile single-part and multipart with clipboard bytes (doc_media_upload.go 0% -> 88.9%) Adds TestNewRuntimeContextForAPI helper that wires Factory, context, and bot identity so package tests can invoke DoAPI without mounting the full cobra command tree. --- shortcuts/common/drive_media_upload_test.go | 92 +++++++++++++++++ shortcuts/common/testing.go | 14 +++ shortcuts/doc/doc_media_test.go | 104 ++++++++++++++++++++ 3 files changed, 210 insertions(+) diff --git a/shortcuts/common/drive_media_upload_test.go b/shortcuts/common/drive_media_upload_test.go index 1e17efa6c..4a90b7605 100644 --- a/shortcuts/common/drive_media_upload_test.go +++ b/shortcuts/common/drive_media_upload_test.go @@ -106,6 +106,98 @@ func TestUploadDriveMediaAllBuildsMultipartBody(t *testing.T) { } } +func TestUploadDriveMediaAllWithInMemoryContent(t *testing.T) { + // When Content is provided, FilePath is ignored — the in-memory reader + // is streamed directly into the multipart form. Used by the clipboard + // upload path. + runtime, reg := newDriveMediaUploadTestRuntime(t) + withDriveMediaUploadWorkingDir(t, t.TempDir()) + + uploadStub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_all", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"file_token": "file_mem_123"}, + }, + } + reg.Register(uploadStub) + + payload := []byte{0x89, 0x50, 0x4e, 0x47, 0xde, 0xad} + fileToken, err := UploadDriveMediaAll(runtime, DriveMediaUploadAllConfig{ + Content: bytes.NewReader(payload), + FileName: "clipboard.png", + FileSize: int64(len(payload)), + ParentType: "docx_image", + ParentNode: strPtr("blk_parent"), + }) + if err != nil { + t.Fatalf("UploadDriveMediaAll() error: %v", err) + } + if fileToken != "file_mem_123" { + t.Fatalf("fileToken = %q, want %q", fileToken, "file_mem_123") + } + + body := decodeCapturedDriveMediaMultipartBody(t, uploadStub) + if got := body.Fields["file_name"]; got != "clipboard.png" { + t.Fatalf("file_name = %q, want %q", got, "clipboard.png") + } + if got := body.Files["file"]; !bytes.Equal(got, payload) { + t.Fatalf("uploaded file bytes mismatch; got %v, want %v", got, payload) + } +} + +func TestUploadDriveMediaMultipartWithInMemoryContent(t *testing.T) { + // Clipboard multipart upload: Content reader replaces FilePath, and the + // server-declared block plan is honored exactly. + runtime, reg := newDriveMediaUploadTestRuntime(t) + withDriveMediaUploadWorkingDir(t, t.TempDir()) + + size := MaxDriveMediaUploadSinglePartSize + 1 + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_prepare", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "upload_id": "upload_mem_1", + "block_size": float64(4 * 1024 * 1024), + "block_num": float64(6), + }, + }, + }) + for i := 0; i < 6; i++ { + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_part", + Body: map[string]interface{}{"code": 0, "msg": "ok"}, + }) + } + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_finish", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"file_token": "file_mem_multi"}, + }, + }) + + payload := bytes.Repeat([]byte{0xAB}, int(size)) + fileToken, err := UploadDriveMediaMultipart(runtime, DriveMediaMultipartUploadConfig{ + Content: bytes.NewReader(payload), + FileName: "clipboard.png", + FileSize: size, + ParentType: "docx_image", + ParentNode: "", + }) + if err != nil { + t.Fatalf("UploadDriveMediaMultipart() error: %v", err) + } + if fileToken != "file_mem_multi" { + t.Fatalf("fileToken = %q, want %q", fileToken, "file_mem_multi") + } +} + func TestUploadDriveMediaMultipartBuildsPreparePartsAndFinish(t *testing.T) { runtime, reg := newDriveMediaUploadTestRuntime(t) withDriveMediaUploadWorkingDir(t, t.TempDir()) diff --git a/shortcuts/common/testing.go b/shortcuts/common/testing.go index 51e4bbd23..49f7b1c45 100644 --- a/shortcuts/common/testing.go +++ b/shortcuts/common/testing.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" + "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" ) @@ -37,3 +38,16 @@ func TestNewRuntimeContextWithBotInfo(cmd *cobra.Command, cfg *core.CliConfig, i }) return rctx } + +// TestNewRuntimeContextForAPI creates a RuntimeContext ready for HTTP tests: +// sets Cmd, Config, Factory, context, and bot identity so callers can invoke +// DoAPI / CallAPI directly without wiring through a cobra parent command. +func TestNewRuntimeContextForAPI(ctx context.Context, cmd *cobra.Command, cfg *core.CliConfig, f *cmdutil.Factory) *RuntimeContext { + return &RuntimeContext{ + ctx: ctx, + Cmd: cmd, + Config: cfg, + Factory: f, + resolvedAs: core.AsBot, + } +} diff --git a/shortcuts/doc/doc_media_test.go b/shortcuts/doc/doc_media_test.go index b77d4cbfb..3f75445dd 100644 --- a/shortcuts/doc/doc_media_test.go +++ b/shortcuts/doc/doc_media_test.go @@ -190,6 +190,110 @@ func TestDocMediaInsertDryRunUsesMultipartForLargeFile(t *testing.T) { } } +func TestUploadDocMediaFileWithContentUsesSinglePartUpload(t *testing.T) { + // Clipboard path: in-memory bytes (no FilePath) route through + // UploadDriveMediaAll when small enough. This also exercises the + // drive_route_token extra built from docID. + f, _, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-upload-content-app")) + uploadStub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_all", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"file_token": "file_content_123"}, + }, + } + reg.Register(uploadStub) + + runtime := common.TestNewRuntimeContextForAPI( + context.Background(), + &cobra.Command{Use: "docs +media-upload"}, + docsTestConfigWithAppID("docs-upload-content-app"), + f, + ) + + payload := []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a} // PNG magic bytes + fileToken, err := uploadDocMediaFile( + runtime, + "", // no FilePath + bytes.NewReader(payload), + "clipboard.png", + int64(len(payload)), + "docx_image", + "blk_parent", + "doxcnDocID123", + ) + if err != nil { + t.Fatalf("uploadDocMediaFile() error: %v", err) + } + if fileToken != "file_content_123" { + t.Fatalf("fileToken = %q, want %q", fileToken, "file_content_123") + } + + if !strings.Contains(string(uploadStub.CapturedBody), `drive_route_token`) { + t.Fatalf("expected drive_route_token in extra, captured body did not include it") + } +} + +func TestUploadDocMediaFileWithContentUsesMultipart(t *testing.T) { + // Clipboard path: in-memory bytes route through UploadDriveMediaMultipart + // when size exceeds the single-part threshold. + f, _, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-upload-content-multi")) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_prepare", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "upload_id": "upload_content_multi", + "block_size": float64(4 * 1024 * 1024), + "block_num": float64(6), + }, + }, + }) + for i := 0; i < 6; i++ { + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_part", + Body: map[string]interface{}{"code": 0, "msg": "ok"}, + }) + } + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/medias/upload_finish", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"file_token": "file_content_multi_done"}, + }, + }) + + runtime := common.TestNewRuntimeContextForAPI( + context.Background(), + &cobra.Command{Use: "docs +media-upload"}, + docsTestConfigWithAppID("docs-upload-content-multi"), + f, + ) + + size := common.MaxDriveMediaUploadSinglePartSize + 1 + payload := bytes.Repeat([]byte{0xAB}, int(size)) + fileToken, err := uploadDocMediaFile( + runtime, + "", + bytes.NewReader(payload), + "clipboard.png", + size, + "docx_image", + "blk_parent", + "", // no docID → no extra + ) + if err != nil { + t.Fatalf("uploadDocMediaFile() error: %v", err) + } + if fileToken != "file_content_multi_done" { + t.Fatalf("fileToken = %q, want %q", fileToken, "file_content_multi_done") + } +} + func TestDocMediaInsertExecuteResolvesWikiBeforeFileCheck(t *testing.T) { f, _, stderr, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-insert-exec-app")) reg.Register(&httpmock.Stub{