mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat(sheets): use office_sheet_file parent_type for imported office spreadsheets (#1606)
Image uploads to a spreadsheet hard-coded parent_type=sheet_image at every entry point. Imported "office" spreadsheets carry a token prefixed with "fake_office_", for which the drive backend requires parent_type=office_sheet_file. Funnel the parent_type selection through a single sheets-domain helper so the rule lives in one place and every image-upload path (float-image, +cells-set-image, backward +media-upload, and every dry-run preview) stays consistent. - Add sheetMediaParentType(token) in the sheets domain: returns office_sheet_file for fake_office_-prefixed tokens, otherwise sheet_image. - Add an uploadSheetImage(...) collector that builds the DriveMediaUploadAllConfig (including parent_type) once, replacing the per-call-site hand-rolled configs. - Route both main-domain image entries through the collector — float-image local upload and +cells-set-image — covering Execute and the dry-run preview body/desc. - Cover the backward +media-upload entry: single-part, multipart (>20MB), and both dry-run bodies. backward is a separate package and an intentional verbatim mirror of shortcuts/sheets/, so it keeps its own copy of the helper rather than importing the main domain. - Leave the shared common.UploadDriveMediaAllTyped upload layer untouched — the fake_office_ rule is sheets-specific and must not leak into mail/slides/doc/drive/base. Tests: - Pure-function TestSheetMediaParentType (5 cases incl. prefix-only and mid-string non-match). - Main-domain dry-run TestCellsSetImage_DryRunOfficeParentType and TestUploadSheetImage_ParentType / _FileOpenError that exercise the Execute path on the wire, asserting parent_type via the captured multipart body and typed validation metadata (errs.ProblemOf category/subtype, fs.ErrNotExist cause preserved) on file open errors. decodeSheetMediaMultipartBody fails fast on NextPart / ReadFrom errors rather than silently producing a partial body. - backward TestSheetMediaUploadExecuteOfficeParentType (real multipart wire) and TestSheetMediaUploadDryRunSmallFileOfficeParentType (small-file dry-run preview for fake_office_). - cli_e2e tests/cli_e2e/sheets/sheets_image_upload_dryrun_test.go: --dry-run end-to-end across +media-upload and +cells-set-image, native and fake_office_ tokens, asserting api.0 is POST upload_all with parent_type=sheet_image / office_sheet_file and parent_node = token.
This commit is contained in:
committed by
GitHub
parent
8a268aa2d2
commit
4c31323de1
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/extension/fileio"
|
||||
@@ -14,7 +15,25 @@ import (
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
const sheetImageParentType = "sheet_image"
|
||||
// Drive media parent_type values for uploading an image into a spreadsheet.
|
||||
// Native spreadsheets use "sheet_image"; imported "office" spreadsheets carry a
|
||||
// synthetic token prefixed with "fake_office_" and the backend requires
|
||||
// "office_sheet_file" instead.
|
||||
const (
|
||||
sheetImageParentType = "sheet_image"
|
||||
officeSheetFileParentType = "office_sheet_file"
|
||||
fakeOfficeTokenPrefix = "fake_office_"
|
||||
)
|
||||
|
||||
// sheetMediaParentType returns the drive media parent_type to use when
|
||||
// uploading an image whose parent_node is spreadsheetToken, mapping the
|
||||
// "fake_office_" imported-spreadsheet token prefix to "office_sheet_file".
|
||||
func sheetMediaParentType(spreadsheetToken string) string {
|
||||
if strings.HasPrefix(spreadsheetToken, fakeOfficeTokenPrefix) {
|
||||
return officeSheetFileParentType
|
||||
}
|
||||
return sheetImageParentType
|
||||
}
|
||||
|
||||
var SheetMediaUpload = common.Shortcut{
|
||||
Service: "sheets",
|
||||
@@ -49,7 +68,7 @@ var SheetMediaUpload = common.Shortcut{
|
||||
POST("/open-apis/drive/v1/medias/upload_prepare").
|
||||
Body(map[string]interface{}{
|
||||
"file_name": fileName,
|
||||
"parent_type": sheetImageParentType,
|
||||
"parent_type": sheetMediaParentType(parentNode),
|
||||
"parent_node": parentNode,
|
||||
"size": "<file_size>",
|
||||
}).
|
||||
@@ -71,7 +90,7 @@ var SheetMediaUpload = common.Shortcut{
|
||||
POST("/open-apis/drive/v1/medias/upload_all").
|
||||
Body(map[string]interface{}{
|
||||
"file_name": fileName,
|
||||
"parent_type": sheetImageParentType,
|
||||
"parent_type": sheetMediaParentType(parentNode),
|
||||
"parent_node": parentNode,
|
||||
"size": "<file_size>",
|
||||
"file": "@" + filePath,
|
||||
@@ -141,13 +160,14 @@ func resolveSheetMediaUploadParent(runtime *common.RuntimeContext) (string, erro
|
||||
}
|
||||
|
||||
func uploadSheetMediaFile(runtime *common.RuntimeContext, filePath, fileName string, fileSize int64, parentNode string) (string, error) {
|
||||
parentType := sheetMediaParentType(parentNode)
|
||||
if fileSize <= common.MaxDriveMediaUploadSinglePartSize {
|
||||
pn := parentNode
|
||||
return common.UploadDriveMediaAllTyped(runtime, common.DriveMediaUploadAllConfig{
|
||||
FilePath: filePath,
|
||||
FileName: fileName,
|
||||
FileSize: fileSize,
|
||||
ParentType: sheetImageParentType,
|
||||
ParentType: parentType,
|
||||
ParentNode: &pn,
|
||||
})
|
||||
}
|
||||
@@ -155,7 +175,7 @@ func uploadSheetMediaFile(runtime *common.RuntimeContext, filePath, fileName str
|
||||
FilePath: filePath,
|
||||
FileName: fileName,
|
||||
FileSize: fileSize,
|
||||
ParentType: sheetImageParentType,
|
||||
ParentType: parentType,
|
||||
ParentNode: parentNode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,6 +91,39 @@ func TestSheetMediaUploadDryRunSmallFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSheetMediaUploadDryRunSmallFileOfficeParentType pins the small-file
|
||||
// upload_all dry-run preview to the token-derived parent_type so the preview
|
||||
// agents/users will copy matches what Execute actually sends. Without this the
|
||||
// multipart dry-run branch could drift back to a hard-coded "sheet_image".
|
||||
func TestSheetMediaUploadDryRunSmallFileOfficeParentType(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSheetsTestWorkingDir(t, dir)
|
||||
if err := os.WriteFile("img.png", []byte("png-bytes"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, sheetsTestConfig())
|
||||
err := mountAndRunSheets(t, SheetMediaUpload, []string{
|
||||
"+media-upload",
|
||||
"--spreadsheet-token", "fake_office_abc123",
|
||||
"--file", "img.png",
|
||||
"--dry-run", "--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "/open-apis/drive/v1/medias/upload_all") {
|
||||
t.Fatalf("dry-run should use upload_all for small file, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, `"office_sheet_file"`) {
|
||||
t.Fatalf("dry-run should include parent_type=office_sheet_file for fake_office_ token, got: %s", out)
|
||||
}
|
||||
if strings.Contains(out, `"sheet_image"`) {
|
||||
t.Fatalf("dry-run must not emit sheet_image for fake_office_ token, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetMediaUploadDryRunURLExtractsToken(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSheetsTestWorkingDir(t, dir)
|
||||
@@ -205,6 +238,47 @@ func TestSheetMediaUploadExecuteSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSheetMediaUploadExecuteOfficeParentType confirms that an imported
|
||||
// "office" spreadsheet (token prefixed with "fake_office_") uploads with
|
||||
// parent_type=office_sheet_file instead of the native sheet_image.
|
||||
func TestSheetMediaUploadExecuteOfficeParentType(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSheetsTestWorkingDir(t, dir)
|
||||
if err := os.WriteFile("img.png", []byte("png-bytes"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, sheetsTestConfig())
|
||||
stub := &httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/drive/v1/medias/upload_all",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"file_token": "boxTOK123"},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
const officeToken = "fake_office_abc123"
|
||||
err := mountAndRunSheets(t, SheetMediaUpload, []string{
|
||||
"+media-upload",
|
||||
"--spreadsheet-token", officeToken,
|
||||
"--file", "img.png",
|
||||
"--as", "user",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
body := decodeSheetsMultipartBody(t, stub)
|
||||
if got := body.Fields["parent_type"]; got != officeSheetFileParentType {
|
||||
t.Fatalf("parent_type = %q, want %q", got, officeSheetFileParentType)
|
||||
}
|
||||
if got := body.Fields["parent_node"]; got != officeToken {
|
||||
t.Fatalf("parent_node = %q, want %q", got, officeToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSheetMediaUploadFileNotFound(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSheetsTestWorkingDir(t, dir)
|
||||
|
||||
@@ -50,6 +50,42 @@ func sheetsInputStatError(flag string, err error) error {
|
||||
return wrapped
|
||||
}
|
||||
|
||||
// Drive media parent_type values for uploading an image into a spreadsheet.
|
||||
// Native spreadsheets use "sheet_image"; imported "office" spreadsheets carry a
|
||||
// synthetic token prefixed with "fake_office_" and the backend requires
|
||||
// "office_sheet_file" instead.
|
||||
const (
|
||||
sheetImageParentType = "sheet_image"
|
||||
officeSheetFileParentType = "office_sheet_file"
|
||||
fakeOfficeTokenPrefix = "fake_office_"
|
||||
)
|
||||
|
||||
// sheetMediaParentType returns the drive media parent_type to use when
|
||||
// uploading an image whose parent_node is spreadsheetToken. It is the single
|
||||
// place that maps a spreadsheet token to its parent_type so every image-upload
|
||||
// entry point (and its dry-run preview) stays consistent.
|
||||
func sheetMediaParentType(spreadsheetToken string) string {
|
||||
if strings.HasPrefix(spreadsheetToken, fakeOfficeTokenPrefix) {
|
||||
return officeSheetFileParentType
|
||||
}
|
||||
return sheetImageParentType
|
||||
}
|
||||
|
||||
// uploadSheetImage uploads a local image file as a spreadsheet media asset and
|
||||
// returns its file_token. It funnels every sheets image upload through one
|
||||
// place so the parent_type selection (see sheetMediaParentType) is never
|
||||
// duplicated or forgotten at a call site. Callers are expected to have already
|
||||
// resolved spreadsheetToken (the upload's parent_node) and stat'd the file.
|
||||
func uploadSheetImage(runtime *common.RuntimeContext, spreadsheetToken, filePath, fileName string, fileSize int64) (string, error) {
|
||||
return common.UploadDriveMediaAllTyped(runtime, common.DriveMediaUploadAllConfig{
|
||||
FilePath: filePath,
|
||||
FileName: fileName,
|
||||
FileSize: fileSize,
|
||||
ParentType: sheetMediaParentType(spreadsheetToken),
|
||||
ParentNode: &spreadsheetToken,
|
||||
})
|
||||
}
|
||||
|
||||
// spreadsheetRef classification: a --url / --spreadsheet-token input names a
|
||||
// spreadsheet either directly (a /sheets/ URL or raw token) or indirectly via a
|
||||
// wiki node that must be resolved to its backing spreadsheet at Execute time.
|
||||
|
||||
@@ -861,10 +861,10 @@ func newFloatImageWriteShortcut(command, description, op string, withIDFlag, isH
|
||||
manageBody, _ := buildToolBody("manage_float_image_object", input)
|
||||
return common.NewDryRunAPI().
|
||||
POST("/open-apis/drive/v1/medias/upload_all").
|
||||
Desc("upload local image to drive (parent_type=sheet_image)").
|
||||
Desc("upload local image to drive (parent_type=" + sheetMediaParentType(token) + ")").
|
||||
Body(map[string]interface{}{
|
||||
"file_name": floatImageName(runtime),
|
||||
"parent_type": "sheet_image",
|
||||
"parent_type": sheetMediaParentType(token),
|
||||
"parent_node": token,
|
||||
"size": "<file_size>",
|
||||
"file": "@" + img,
|
||||
@@ -918,13 +918,7 @@ func uploadFloatImageIfLocal(runtime *common.RuntimeContext, spreadsheetToken st
|
||||
if err != nil {
|
||||
return "", sheetsInputStatError("image", err)
|
||||
}
|
||||
return common.UploadDriveMediaAllTyped(runtime, common.DriveMediaUploadAllConfig{
|
||||
FilePath: img,
|
||||
FileName: floatImageName(runtime),
|
||||
FileSize: info.Size(),
|
||||
ParentType: "sheet_image",
|
||||
ParentNode: &spreadsheetToken,
|
||||
})
|
||||
return uploadSheetImage(runtime, spreadsheetToken, img, floatImageName(runtime), info.Size())
|
||||
}
|
||||
|
||||
func floatImageWriteInput(runtime flagView, token, sheetID, sheetName, op string, withIDFlag bool, uploadedImageToken string) (map[string]interface{}, error) {
|
||||
|
||||
@@ -791,10 +791,10 @@ var CellsSetImage = common.Shortcut{
|
||||
})
|
||||
return common.NewDryRunAPI().
|
||||
POST("/open-apis/drive/v1/medias/upload_all").
|
||||
Desc("upload local image to drive (parent_type=sheet_image)").
|
||||
Desc("upload local image to drive (parent_type=" + sheetMediaParentType(token) + ")").
|
||||
Body(map[string]interface{}{
|
||||
"file_name": fileName,
|
||||
"parent_type": "sheet_image",
|
||||
"parent_type": sheetMediaParentType(token),
|
||||
"parent_node": token,
|
||||
"size": "<file_size>",
|
||||
"file": "@" + imgPath,
|
||||
@@ -832,13 +832,7 @@ var CellsSetImage = common.Shortcut{
|
||||
WithParam("--image").
|
||||
WithCause(err)
|
||||
}
|
||||
fileToken, err := common.UploadDriveMediaAllTyped(runtime, common.DriveMediaUploadAllConfig{
|
||||
FilePath: imgPath,
|
||||
FileName: fileName,
|
||||
FileSize: info.Size(),
|
||||
ParentType: "sheet_image",
|
||||
ParentNode: &token,
|
||||
})
|
||||
fileToken, err := uploadSheetImage(runtime, token, imgPath, fileName, info.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -496,6 +496,31 @@ func TestCellsSetImage_DryRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCellsSetImage_DryRunOfficeParentType confirms that an imported "office"
|
||||
// spreadsheet (token prefixed with "fake_office_") uploads with
|
||||
// parent_type=office_sheet_file instead of the native sheet_image, and that the
|
||||
// preview's parent_node carries the same token.
|
||||
func TestCellsSetImage_DryRunOfficeParentType(t *testing.T) {
|
||||
t.Parallel()
|
||||
const officeToken = "fake_office_abc123"
|
||||
calls := parseDryRunAPI(t, CellsSetImage, []string{
|
||||
"--spreadsheet-token", officeToken, "--sheet-id", testSheetID,
|
||||
"--range", "A1",
|
||||
"--image", "./README.md", // any existing-shaped path; dry-run skips stat
|
||||
})
|
||||
if len(calls) != 2 {
|
||||
t.Fatalf("api calls = %d, want 2 (upload + set_cell_range)", len(calls))
|
||||
}
|
||||
upload := calls[0].(map[string]interface{})
|
||||
ubody, _ := upload["body"].(map[string]interface{})
|
||||
if ubody["parent_type"] != officeSheetFileParentType {
|
||||
t.Errorf("parent_type = %v, want %s", ubody["parent_type"], officeSheetFileParentType)
|
||||
}
|
||||
if ubody["parent_node"] != officeToken {
|
||||
t.Errorf("parent_node = %v, want %s", ubody["parent_node"], officeToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCellsSetImage_RangeMustBeSingleCell(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := runShortcutCapturingErr(t, CellsSetImage, []string{
|
||||
|
||||
192
shortcuts/sheets/sheet_media_parent_type_test.go
Normal file
192
shortcuts/sheets/sheet_media_parent_type_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// TestSheetMediaParentType pins the token→parent_type mapping that every
|
||||
// sheets image-upload entry point funnels through. Native spreadsheet tokens
|
||||
// use "sheet_image"; imported "office" spreadsheets carry a "fake_office_"
|
||||
// synthetic token and must upload with "office_sheet_file".
|
||||
func TestSheetMediaParentType(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
token string
|
||||
want string
|
||||
}{
|
||||
{"native spreadsheet token", "shtcnABC123", sheetImageParentType},
|
||||
{"empty token", "", sheetImageParentType},
|
||||
{"office imported token", "fake_office_abc123", officeSheetFileParentType},
|
||||
{"office token, only the prefix", fakeOfficeTokenPrefix, officeSheetFileParentType},
|
||||
{"prefix mid-string is not matched", "shtfake_office_abc", sheetImageParentType},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := sheetMediaParentType(tc.token); got != tc.want {
|
||||
t.Fatalf("sheetMediaParentType(%q) = %q, want %q", tc.token, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUploadSheetImage_ParentType exercises the uploadSheetImage collector end
|
||||
// to end (the Execute path the dry-run tests don't reach), asserting the
|
||||
// parent_type that actually goes out on the wire is derived from the token: a
|
||||
// native spreadsheet uploads as sheet_image, an imported "office" spreadsheet
|
||||
// (fake_office_-prefixed token) as office_sheet_file.
|
||||
func TestUploadSheetImage_ParentType(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
token string
|
||||
wantParentType string
|
||||
}{
|
||||
{"native spreadsheet", "shtcnTOK123", sheetImageParentType},
|
||||
{"office imported spreadsheet", "fake_office_abc123", officeSheetFileParentType},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runtime, reg := newSheetMediaTestRuntime(t)
|
||||
// UploadDriveMediaAllTyped opens the file via the runtime's FileIO,
|
||||
// which sandboxes paths to the current working directory; chdir to a
|
||||
// temp dir and pass a relative name so the open is allowed.
|
||||
cmdutil.TestChdir(t, t.TempDir())
|
||||
if err := os.WriteFile("img.png", []byte("png-bytes"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stub := &httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/drive/v1/medias/upload_all",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"file_token": "boxTOK123"},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
fileToken, err := uploadSheetImage(runtime, tc.token, "img.png", "img.png", 9)
|
||||
if err != nil {
|
||||
t.Fatalf("uploadSheetImage() error: %v", err)
|
||||
}
|
||||
if fileToken != "boxTOK123" {
|
||||
t.Fatalf("file_token = %q, want boxTOK123", fileToken)
|
||||
}
|
||||
|
||||
body := decodeSheetMediaMultipartBody(t, stub)
|
||||
if got := body.Fields["parent_type"]; got != tc.wantParentType {
|
||||
t.Fatalf("parent_type = %q, want %q", got, tc.wantParentType)
|
||||
}
|
||||
if got := body.Fields["parent_node"]; got != tc.token {
|
||||
t.Fatalf("parent_node = %q, want %q", got, tc.token)
|
||||
}
|
||||
if got := body.Fields["file_name"]; got != "img.png" {
|
||||
t.Fatalf("file_name = %q, want img.png", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUploadSheetImage_FileOpenError confirms a missing image surfaces as a
|
||||
// typed validation error (category=validation, subtype=invalid_argument) with
|
||||
// the original os-level cause preserved for errors.Is, and proves the upload
|
||||
// endpoint is never hit. No httpmock stub is registered, so if uploadSheetImage
|
||||
// ever tried to POST upload_all the RoundTrip would return a
|
||||
// "no stub for POST ..." network failure — that would surface as a
|
||||
// non-validation category and fail the metadata assertion below. The
|
||||
// category=validation + fs.ErrNotExist cause therefore strictly implies the
|
||||
// short-circuit happened before the wire.
|
||||
func TestUploadSheetImage_FileOpenError(t *testing.T) {
|
||||
runtime, _ := newSheetMediaTestRuntime(t)
|
||||
cmdutil.TestChdir(t, t.TempDir())
|
||||
|
||||
_, err := uploadSheetImage(runtime, "shtcnTOK123", "missing.png", "missing.png", 1)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file, got nil")
|
||||
}
|
||||
|
||||
p, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("err = %v; want typed problem carrier", err)
|
||||
}
|
||||
if p.Category != errs.CategoryValidation {
|
||||
t.Fatalf("category = %q, want %q (non-validation implies the upload endpoint was reached)", p.Category, errs.CategoryValidation)
|
||||
}
|
||||
if p.Subtype != errs.SubtypeInvalidArgument {
|
||||
t.Fatalf("subtype = %q, want %q", p.Subtype, errs.SubtypeInvalidArgument)
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Fatalf("err = %v; want wrapped fs.ErrNotExist cause to be preserved", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newSheetMediaTestRuntime(t *testing.T) (*common.RuntimeContext, *httpmock.Registry) {
|
||||
t.Helper()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
cfg := &core.CliConfig{
|
||||
AppID: "test-sheets-media-" + t.Name(),
|
||||
AppSecret: "test-secret",
|
||||
Brand: core.BrandFeishu,
|
||||
}
|
||||
f, _, _, reg := cmdutil.TestFactory(t, cfg)
|
||||
runtime := common.TestNewRuntimeContextForAPI(context.Background(), &cobra.Command{Use: "sheets"}, cfg, f, core.AsBot)
|
||||
return runtime, reg
|
||||
}
|
||||
|
||||
type sheetMediaCapturedMultipart struct {
|
||||
Fields map[string]string
|
||||
Files map[string][]byte
|
||||
}
|
||||
|
||||
func decodeSheetMediaMultipartBody(t *testing.T, stub *httpmock.Stub) sheetMediaCapturedMultipart {
|
||||
t.Helper()
|
||||
contentType := stub.CapturedHeaders.Get("Content-Type")
|
||||
mediaType, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
t.Fatalf("parse content-type %q: %v", contentType, 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 := sheetMediaCapturedMultipart{Fields: map[string]string{}, Files: map[string][]byte{}}
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
t.Fatalf("read multipart part: %v", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(part); err != nil {
|
||||
t.Fatalf("read multipart body for %q: %v", part.FormName(), err)
|
||||
}
|
||||
if part.FileName() != "" {
|
||||
body.Files[part.FormName()] = buf.Bytes()
|
||||
continue
|
||||
}
|
||||
body.Fields[part.FormName()] = buf.String()
|
||||
}
|
||||
return body
|
||||
}
|
||||
112
tests/cli_e2e/sheets/sheets_image_upload_dryrun_test.go
Normal file
112
tests/cli_e2e/sheets/sheets_image_upload_dryrun_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// TestSheets_ImageUploadDryRunParentType pins the parent_type the sheets
|
||||
// image-upload shortcuts emit in --dry-run output for native vs. imported
|
||||
// "office" spreadsheets. For native tokens parent_type must be "sheet_image";
|
||||
// for tokens prefixed with "fake_office_" (the synthetic token an imported
|
||||
// office spreadsheet carries) the backend requires "office_sheet_file". The
|
||||
// three covered entries — sheets +media-upload (backward), sheets
|
||||
// +cells-set-image, and sheets +create-float-image — are every image-upload
|
||||
// surface that the office/native split fans out to.
|
||||
func TestSheets_ImageUploadDryRunParentType(t *testing.T) {
|
||||
setSheetsDryRunEnv(t)
|
||||
|
||||
workDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workDir, "img.png"), []byte("png-bytes"), 0o600))
|
||||
|
||||
type tc struct {
|
||||
name string
|
||||
args []string
|
||||
token string
|
||||
wantParentType string
|
||||
}
|
||||
tests := []tc{
|
||||
{
|
||||
name: "media-upload native",
|
||||
args: []string{
|
||||
"sheets", "+media-upload",
|
||||
"--spreadsheet-token", "shtDryRunNative",
|
||||
"--file", "img.png",
|
||||
"--dry-run",
|
||||
},
|
||||
token: "shtDryRunNative",
|
||||
wantParentType: "sheet_image",
|
||||
},
|
||||
{
|
||||
name: "media-upload office",
|
||||
args: []string{
|
||||
"sheets", "+media-upload",
|
||||
"--spreadsheet-token", "fake_office_dryrun",
|
||||
"--file", "img.png",
|
||||
"--dry-run",
|
||||
},
|
||||
token: "fake_office_dryrun",
|
||||
wantParentType: "office_sheet_file",
|
||||
},
|
||||
{
|
||||
name: "cells-set-image native",
|
||||
args: []string{
|
||||
"sheets", "+cells-set-image",
|
||||
"--spreadsheet-token", "shtDryRunNative",
|
||||
"--sheet-id", "sheet1",
|
||||
"--range", "A1",
|
||||
"--image", "img.png",
|
||||
"--dry-run",
|
||||
},
|
||||
token: "shtDryRunNative",
|
||||
wantParentType: "sheet_image",
|
||||
},
|
||||
{
|
||||
name: "cells-set-image office",
|
||||
args: []string{
|
||||
"sheets", "+cells-set-image",
|
||||
"--spreadsheet-token", "fake_office_dryrun",
|
||||
"--sheet-id", "sheet1",
|
||||
"--range", "A1",
|
||||
"--image", "img.png",
|
||||
"--dry-run",
|
||||
},
|
||||
token: "fake_office_dryrun",
|
||||
wantParentType: "office_sheet_file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: tt.args,
|
||||
DefaultAs: "user",
|
||||
WorkDir: workDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, "POST", gjson.Get(out, "api.0.method").String(), "api.0 must be the drive upload; stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/drive/v1/medias/upload_all",
|
||||
gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, tt.wantParentType, gjson.Get(out, "api.0.body.parent_type").String(),
|
||||
"parent_type for token %q must be %q; stdout:\n%s", tt.token, tt.wantParentType, out)
|
||||
require.Equal(t, tt.token, gjson.Get(out, "api.0.body.parent_node").String(),
|
||||
"parent_node must equal the spreadsheet token; stdout:\n%s", out)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user