bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
414 lines
12 KiB
Go
414 lines
12 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"os"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
)
|
|
|
|
var commonDriveMediaUploadTestSeq atomic.Int64
|
|
|
|
func TestUploadDriveMediaAllTypedBuildsMultipartBody(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
parentNode *string
|
|
wantParentNode string
|
|
wantParentSet bool
|
|
}{
|
|
{
|
|
name: "includes parent_node when provided",
|
|
parentNode: strPtr("blk_parent"),
|
|
wantParentNode: "blk_parent",
|
|
wantParentSet: true,
|
|
},
|
|
{
|
|
name: "omits parent_node when not provided",
|
|
parentNode: nil,
|
|
wantParentSet: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
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_all_123"},
|
|
},
|
|
}
|
|
reg.Register(uploadStub)
|
|
|
|
filePath := writeDriveMediaUploadTestFile(t, "small.bin", 3)
|
|
fileToken, err := UploadDriveMediaAllTyped(runtime, DriveMediaUploadAllConfig{
|
|
FilePath: filePath,
|
|
FileName: "small.bin",
|
|
FileSize: 3,
|
|
ParentType: "docx_file",
|
|
ParentNode: tt.parentNode,
|
|
Extra: `{"drive_route_token":"doxcn123"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UploadDriveMediaAllTyped() error: %v", err)
|
|
}
|
|
if fileToken != "file_all_123" {
|
|
t.Fatalf("fileToken = %q, want %q", fileToken, "file_all_123")
|
|
}
|
|
|
|
body := decodeCapturedDriveMediaMultipartBody(t, uploadStub)
|
|
if got := body.Fields["file_name"]; got != "small.bin" {
|
|
t.Fatalf("file_name = %q, want %q", got, "small.bin")
|
|
}
|
|
if got := body.Fields["parent_type"]; got != "docx_file" {
|
|
t.Fatalf("parent_type = %q, want %q", got, "docx_file")
|
|
}
|
|
if got := body.Fields["size"]; got != "3" {
|
|
t.Fatalf("size = %q, want %q", got, "3")
|
|
}
|
|
if got := body.Fields["extra"]; got != `{"drive_route_token":"doxcn123"}` {
|
|
t.Fatalf("extra = %q, want drive route token payload", got)
|
|
}
|
|
if got := len(body.Files["file"]); got != 3 {
|
|
t.Fatalf("file size = %d, want %d", got, 3)
|
|
}
|
|
|
|
gotParentNode, hasParentNode := body.Fields["parent_node"]
|
|
if hasParentNode != tt.wantParentSet {
|
|
t.Fatalf("parent_node present = %v, want %v", hasParentNode, tt.wantParentSet)
|
|
}
|
|
if hasParentNode && gotParentNode != tt.wantParentNode {
|
|
t.Fatalf("parent_node = %q, want %q", gotParentNode, tt.wantParentNode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUploadDriveMediaMultipartTypedBuildsRequestBodies(t *testing.T) {
|
|
runtime, reg := newDriveMediaUploadTestRuntime(t)
|
|
withDriveMediaUploadWorkingDir(t, t.TempDir())
|
|
|
|
prepareStub := &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_123",
|
|
"block_size": float64(4 * 1024 * 1024),
|
|
"block_num": float64(6),
|
|
},
|
|
},
|
|
}
|
|
reg.Register(prepareStub)
|
|
|
|
partStubs := make([]*httpmock.Stub, 0, 6)
|
|
for i := 0; i < 6; i++ {
|
|
stub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/medias/upload_part",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
},
|
|
}
|
|
partStubs = append(partStubs, stub)
|
|
reg.Register(stub)
|
|
}
|
|
|
|
finishStub := &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_multi_123"},
|
|
},
|
|
}
|
|
reg.Register(finishStub)
|
|
|
|
filePath := writeDriveMediaUploadSizedFile(t, "large.bin", MaxDriveMediaUploadSinglePartSize+1)
|
|
fileToken, err := UploadDriveMediaMultipartTyped(runtime, DriveMediaMultipartUploadConfig{
|
|
FilePath: filePath,
|
|
FileName: "large.bin",
|
|
FileSize: MaxDriveMediaUploadSinglePartSize + 1,
|
|
ParentType: "ccm_import_open",
|
|
ParentNode: "",
|
|
Extra: `{"obj_type":"sheet","file_extension":"xlsx"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UploadDriveMediaMultipartTyped() error: %v", err)
|
|
}
|
|
if fileToken != "file_multi_123" {
|
|
t.Fatalf("fileToken = %q, want %q", fileToken, "file_multi_123")
|
|
}
|
|
|
|
prepareBody := decodeCapturedDriveMediaJSONBody(t, prepareStub)
|
|
if got, _ := prepareBody["parent_type"].(string); got != "ccm_import_open" {
|
|
t.Fatalf("prepare parent_type = %q, want %q", got, "ccm_import_open")
|
|
}
|
|
rawParentNode, ok := prepareBody["parent_node"]
|
|
if !ok {
|
|
t.Fatal("prepare body missing parent_node")
|
|
}
|
|
if got, ok := rawParentNode.(string); !ok || got != "" {
|
|
t.Fatalf("prepare parent_node = %#v, want empty string", rawParentNode)
|
|
}
|
|
if got, _ := prepareBody["extra"].(string); got != `{"obj_type":"sheet","file_extension":"xlsx"}` {
|
|
t.Fatalf("prepare extra = %q, want import payload", got)
|
|
}
|
|
if got, _ := prepareBody["size"].(float64); got != float64(MaxDriveMediaUploadSinglePartSize+1) {
|
|
t.Fatalf("prepare size = %v, want %d", got, MaxDriveMediaUploadSinglePartSize+1)
|
|
}
|
|
|
|
firstPart := decodeCapturedDriveMediaMultipartBody(t, partStubs[0])
|
|
if got := firstPart.Fields["upload_id"]; got != "upload_123" {
|
|
t.Fatalf("first part upload_id = %q, want %q", got, "upload_123")
|
|
}
|
|
if got := firstPart.Fields["seq"]; got != "0" {
|
|
t.Fatalf("first part seq = %q, want %q", got, "0")
|
|
}
|
|
if got := firstPart.Fields["size"]; got != "4194304" {
|
|
t.Fatalf("first part size = %q, want %q", got, "4194304")
|
|
}
|
|
|
|
lastPart := decodeCapturedDriveMediaMultipartBody(t, partStubs[len(partStubs)-1])
|
|
if got := lastPart.Fields["seq"]; got != "5" {
|
|
t.Fatalf("last part seq = %q, want %q", got, "5")
|
|
}
|
|
if got := lastPart.Fields["size"]; got != "1" {
|
|
t.Fatalf("last part size = %q, want %q", got, "1")
|
|
}
|
|
if got := len(lastPart.Files["file"]); got != 1 {
|
|
t.Fatalf("last part file size = %d, want %d", got, 1)
|
|
}
|
|
|
|
finishBody := decodeCapturedDriveMediaJSONBody(t, finishStub)
|
|
if got, _ := finishBody["upload_id"].(string); got != "upload_123" {
|
|
t.Fatalf("finish upload_id = %q, want %q", got, "upload_123")
|
|
}
|
|
if got, _ := finishBody["block_num"].(float64); got != 6 {
|
|
t.Fatalf("finish block_num = %v, want %d", got, 6)
|
|
}
|
|
}
|
|
|
|
func TestUploadDriveMediaMultipartTypedPrepareAPIFailure(t *testing.T) {
|
|
runtime, reg := newDriveMediaUploadTestRuntime(t)
|
|
withDriveMediaUploadWorkingDir(t, t.TempDir())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/medias/upload_prepare",
|
|
Body: map[string]interface{}{
|
|
"code": 999,
|
|
"msg": "prepare rejected",
|
|
},
|
|
})
|
|
|
|
filePath := writeDriveMediaUploadSizedFile(t, "large.bin", MaxDriveMediaUploadSinglePartSize+1)
|
|
_, err := UploadDriveMediaMultipartTyped(runtime, DriveMediaMultipartUploadConfig{
|
|
FilePath: filePath,
|
|
FileName: "large.bin",
|
|
FileSize: MaxDriveMediaUploadSinglePartSize + 1,
|
|
ParentType: "ccm_import_open",
|
|
ParentNode: "",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("expected typed problem, got %T (%v)", err, err)
|
|
}
|
|
if p.Category != errs.CategoryAPI || p.Code != 999 {
|
|
t.Fatalf("category/code = %s/%d, want api/999", p.Category, p.Code)
|
|
}
|
|
}
|
|
|
|
func TestUploadDriveMediaMultipartTypedFinishAPIFailure(t *testing.T) {
|
|
runtime, reg := newDriveMediaUploadTestRuntime(t)
|
|
withDriveMediaUploadWorkingDir(t, t.TempDir())
|
|
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_123",
|
|
"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": 999,
|
|
"msg": "finish rejected",
|
|
},
|
|
})
|
|
|
|
filePath := writeDriveMediaUploadSizedFile(t, "large.bin", MaxDriveMediaUploadSinglePartSize+1)
|
|
_, err := UploadDriveMediaMultipartTyped(runtime, DriveMediaMultipartUploadConfig{
|
|
FilePath: filePath,
|
|
FileName: "large.bin",
|
|
FileSize: MaxDriveMediaUploadSinglePartSize + 1,
|
|
ParentType: "ccm_import_open",
|
|
ParentNode: "",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("expected typed problem, got %T (%v)", err, err)
|
|
}
|
|
if p.Category != errs.CategoryAPI || p.Code != 999 {
|
|
t.Fatalf("category/code = %s/%d, want api/999", p.Category, p.Code)
|
|
}
|
|
}
|
|
|
|
type capturedDriveMediaMultipartBody struct {
|
|
Fields map[string]string
|
|
Files map[string][]byte
|
|
}
|
|
|
|
func newDriveMediaUploadTestRuntime(t *testing.T) (*RuntimeContext, *httpmock.Registry) {
|
|
t.Helper()
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
cfg := &core.CliConfig{
|
|
AppID: fmt.Sprintf("common-drive-media-test-%d", commonDriveMediaUploadTestSeq.Add(1)), AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
}
|
|
f, _, _, reg := cmdutil.TestFactory(t, cfg)
|
|
runtime := &RuntimeContext{
|
|
ctx: context.Background(),
|
|
Config: cfg,
|
|
Factory: f,
|
|
resolvedAs: core.AsBot,
|
|
}
|
|
return runtime, reg
|
|
}
|
|
|
|
func withDriveMediaUploadWorkingDir(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)
|
|
}
|
|
})
|
|
}
|
|
|
|
func writeDriveMediaUploadTestFile(t *testing.T, name string, size int) string {
|
|
t.Helper()
|
|
|
|
if err := os.WriteFile(name, bytes.Repeat([]byte("a"), size), 0644); err != nil {
|
|
t.Fatalf("WriteFile(%q) error: %v", name, err)
|
|
}
|
|
return name
|
|
}
|
|
|
|
func writeDriveMediaUploadSizedFile(t *testing.T, name string, size int64) string {
|
|
t.Helper()
|
|
|
|
fh, err := os.Create(name)
|
|
if err != nil {
|
|
t.Fatalf("Create(%q) error: %v", name, err)
|
|
}
|
|
if err := fh.Truncate(size); err != nil {
|
|
t.Fatalf("Truncate(%q) error: %v", name, err)
|
|
}
|
|
if err := fh.Close(); err != nil {
|
|
t.Fatalf("Close(%q) error: %v", name, err)
|
|
}
|
|
return name
|
|
}
|
|
|
|
func decodeCapturedDriveMediaJSONBody(t *testing.T, stub *httpmock.Stub) map[string]interface{} {
|
|
t.Helper()
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
|
|
t.Fatalf("decode captured JSON body: %v", err)
|
|
}
|
|
return body
|
|
}
|
|
|
|
func decodeCapturedDriveMediaMultipartBody(t *testing.T, stub *httpmock.Stub) capturedDriveMediaMultipartBody {
|
|
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 := capturedDriveMediaMultipartBody{
|
|
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
|
|
}
|
|
|
|
func strPtr(s string) *string {
|
|
return &s
|
|
}
|