mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
713 lines
22 KiB
Go
713 lines
22 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package doc
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
func TestDocResourceDownloadCoverDownloadsImageContent(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-download-app"))
|
|
documentID := "doxcnCoverDownload1"
|
|
coverToken := "cover_token_download_123"
|
|
reg.Register(docCoverMetadataStub(documentID, map[string]interface{}{
|
|
"token": coverToken,
|
|
"offset_ratio_x": 0.25,
|
|
"offset_ratio_y": 0.75,
|
|
}))
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/medias/" + coverToken + "/download",
|
|
Status: 200,
|
|
Body: []byte("png-data"),
|
|
Headers: http.Header{
|
|
"Content-Type": []string{"image/png"},
|
|
},
|
|
})
|
|
|
|
tmpDir := t.TempDir()
|
|
withDocsWorkingDir(t, tmpDir)
|
|
|
|
err := mountAndRunDocs(t, DocResourceDownload, []string{
|
|
"+resource-download",
|
|
"--doc", documentID,
|
|
"--type", "cover",
|
|
"--output", "cover",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
out := decodeDocResourceOutput(t, stdout)
|
|
data := out.Data
|
|
if data["type"] != "cover" {
|
|
t.Fatalf("type = %v, want cover", data["type"])
|
|
}
|
|
if data["content_type"] != "image/png" {
|
|
t.Fatalf("content_type = %v, want image/png", data["content_type"])
|
|
}
|
|
if int(data["size_bytes"].(float64)) != len("png-data") {
|
|
t.Fatalf("size_bytes = %v", data["size_bytes"])
|
|
}
|
|
savedPath, _ := data["saved_path"].(string)
|
|
if !strings.HasSuffix(savedPath, "cover.png") {
|
|
t.Fatalf("saved_path = %q, want cover.png suffix", savedPath)
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(tmpDir, "cover.png"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(cover.png) error: %v", err)
|
|
}
|
|
if string(content) != "png-data" {
|
|
t.Fatalf("downloaded content = %q", string(content))
|
|
}
|
|
cover := data["cover"].(map[string]interface{})
|
|
if cover["token"] != coverToken {
|
|
t.Fatalf("cover.token = %v, want %s", cover["token"], coverToken)
|
|
}
|
|
}
|
|
|
|
func TestDocResourceDownloadCoverEmptyReturnsErrorWithoutDownload(t *testing.T) {
|
|
f, _, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-empty-download-app"))
|
|
documentID := "doxcnCoverEmptyDownload1"
|
|
reg.Register(docCoverMetadataStub(documentID, map[string]interface{}{}))
|
|
|
|
tmpDir := t.TempDir()
|
|
withDocsWorkingDir(t, tmpDir)
|
|
|
|
err := mountAndRunDocs(t, DocResourceDownload, []string{
|
|
"+resource-download",
|
|
"--doc", documentID,
|
|
"--type", "cover",
|
|
"--output", "cover.png",
|
|
"--as", "bot",
|
|
}, f, nil)
|
|
if err == nil {
|
|
t.Fatal("expected empty cover error, got nil")
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeFailedPrecondition, "--type")
|
|
if _, statErr := os.Stat(filepath.Join(tmpDir, "cover.png")); !os.IsNotExist(statErr) {
|
|
t.Fatalf("cover.png should not be created, statErr=%v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestDocResourceDeleteCoverEmptyIsIdempotent(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-empty-delete-app"))
|
|
documentID := "doxcnCoverEmptyDelete1"
|
|
reg.Register(docCoverMetadataStub(documentID, map[string]interface{}{}))
|
|
|
|
err := mountAndRunDocs(t, DocResourceDelete, []string{
|
|
"+resource-delete",
|
|
"--doc", documentID,
|
|
"--type", "cover",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
data := decodeDocResourceOutput(t, stdout).Data
|
|
if data["deleted"] != false {
|
|
t.Fatalf("deleted = %v, want false", data["deleted"])
|
|
}
|
|
if data["already_empty"] != true {
|
|
t.Fatalf("already_empty = %v, want true", data["already_empty"])
|
|
}
|
|
}
|
|
|
|
func TestDocResourceDeleteCoverClearsExistingCover(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-delete-app"))
|
|
documentID := "doxcnCoverDelete1"
|
|
reg.Register(docCoverMetadataStub(documentID, map[string]interface{}{"token": "cover_token_delete_123"}))
|
|
patchStub := &httpmock.Stub{
|
|
Method: "PATCH",
|
|
URL: "/open-apis/docx/v1/documents/" + documentID,
|
|
Body: map[string]interface{}{"code": 0, "msg": "ok"},
|
|
}
|
|
reg.Register(patchStub)
|
|
|
|
err := mountAndRunDocs(t, DocResourceDelete, []string{
|
|
"+resource-delete",
|
|
"--doc", documentID,
|
|
"--type", "cover",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
body := string(patchStub.CapturedBody)
|
|
if !strings.Contains(body, `"update_cover"`) || !strings.Contains(body, `"cover":null`) {
|
|
t.Fatalf("PATCH body = %s, want update_cover.cover=null", body)
|
|
}
|
|
data := decodeDocResourceOutput(t, stdout).Data
|
|
if data["deleted"] != true {
|
|
t.Fatalf("deleted = %v, want true", data["deleted"])
|
|
}
|
|
if data["already_empty"] != false {
|
|
t.Fatalf("already_empty = %v, want false", data["already_empty"])
|
|
}
|
|
}
|
|
|
|
func TestDocResourceUpdateCoverUploadsFileAndReturnsFullTokenOnlyOnStdout(t *testing.T) {
|
|
f, stdout, stderr, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-update-app"))
|
|
documentID := "doxcnCoverUpdate1"
|
|
fileToken := "file_cover_uploaded_token_12345"
|
|
tmpDir := t.TempDir()
|
|
withDocsWorkingDir(t, tmpDir)
|
|
if err := os.WriteFile("cover.png", []byte("png-data"), 0644); err != nil {
|
|
t.Fatalf("WriteFile() error: %v", err)
|
|
}
|
|
|
|
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": fileToken},
|
|
},
|
|
}
|
|
reg.Register(uploadStub)
|
|
patchStub := &httpmock.Stub{
|
|
Method: "PATCH",
|
|
URL: "/open-apis/docx/v1/documents/" + documentID,
|
|
Body: map[string]interface{}{"code": 0, "msg": "ok"},
|
|
}
|
|
reg.Register(patchStub)
|
|
|
|
err := mountAndRunDocs(t, DocResourceUpdate, []string{
|
|
"+resource-update",
|
|
"--doc", documentID,
|
|
"--type", "cover",
|
|
"--file", "cover.png",
|
|
"--offset-ratio-x", "0.2",
|
|
"--offset-ratio-y", "0.8",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v — stderr: %s", err, stderr.String())
|
|
}
|
|
|
|
if !bytes.Contains(uploadStub.CapturedBody, []byte("png-data")) {
|
|
t.Fatalf("upload body does not contain file bytes")
|
|
}
|
|
uploadBody := string(uploadStub.CapturedBody)
|
|
if !strings.Contains(uploadBody, `name="parent_type"`) || !strings.Contains(uploadBody, "docx_image") {
|
|
t.Fatalf("upload body missing docx_image parent type: %s", uploadBody)
|
|
}
|
|
if !strings.Contains(uploadBody, "drive_route_token") || !strings.Contains(uploadBody, documentID) {
|
|
t.Fatalf("upload body missing drive_route_token extra: %s", uploadBody)
|
|
}
|
|
patchBody := string(patchStub.CapturedBody)
|
|
for _, want := range []string{`"update_cover"`, `"token":"` + fileToken + `"`, `"offset_ratio_x":0.2`, `"offset_ratio_y":0.8`} {
|
|
if !strings.Contains(patchBody, want) {
|
|
t.Fatalf("PATCH body = %s, missing %s", patchBody, want)
|
|
}
|
|
}
|
|
|
|
if strings.Contains(stderr.String(), fileToken) {
|
|
t.Fatalf("stderr leaked full file_token: %s", stderr.String())
|
|
}
|
|
data := decodeDocResourceOutput(t, stdout).Data
|
|
if data["file_token"] != fileToken {
|
|
t.Fatalf("stdout file_token = %v, want %s", data["file_token"], fileToken)
|
|
}
|
|
cover := data["cover"].(map[string]interface{})
|
|
if cover["token"] != fileToken {
|
|
t.Fatalf("stdout cover.token = %v, want %s", cover["token"], fileToken)
|
|
}
|
|
}
|
|
|
|
func TestDocResourceUpdateCoverRejectsMultipleSources(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-source-validation-app"))
|
|
|
|
err := mountAndRunDocs(t, DocResourceUpdate, []string{
|
|
"+resource-update",
|
|
"--doc", "doxcnCoverValidate1",
|
|
"--type", "cover",
|
|
"--file", "cover.png",
|
|
"--from-clipboard",
|
|
"--as", "bot",
|
|
}, f, nil)
|
|
if err == nil {
|
|
t.Fatal("expected mutual exclusion error, got nil")
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "", "--file", "--from-clipboard")
|
|
}
|
|
|
|
func TestDocResourceUpdateCoverRejectsMissingSource(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-source-required-app"))
|
|
|
|
err := mountAndRunDocs(t, DocResourceUpdate, []string{
|
|
"+resource-update",
|
|
"--doc", "doxcnCoverValidateRequired1",
|
|
"--type", "cover",
|
|
"--as", "bot",
|
|
}, f, nil)
|
|
if err == nil {
|
|
t.Fatal("expected missing source error, got nil")
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "", "--file", "--from-clipboard", "--url")
|
|
}
|
|
|
|
func TestDocResourceUpdateCoverRejectsUnsafeURLSource(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-url-validation-app"))
|
|
|
|
err := mountAndRunDocs(t, DocResourceUpdate, []string{
|
|
"+resource-update",
|
|
"--doc", "doxcnCoverURLValidate1",
|
|
"--type", "cover",
|
|
"--url", "https://127.0.0.1/cover.png",
|
|
"--as", "bot",
|
|
}, f, nil)
|
|
if err == nil {
|
|
t.Fatal("expected unsafe URL error, got nil")
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--url")
|
|
}
|
|
|
|
func TestDocCoverURLSyntaxValidation(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
ok bool
|
|
}{
|
|
{name: "https", raw: " https://example.com/cover.png ", ok: true},
|
|
{name: "http", raw: "http://example.com/cover.png"},
|
|
{name: "userinfo", raw: "https://user:pass@example.com/cover.png"},
|
|
{name: "empty host", raw: "https:///cover.png"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
u, err := parseDocCoverURLSyntax(tc.raw)
|
|
if tc.ok {
|
|
if err != nil {
|
|
t.Fatalf("parseDocCoverURLSyntax() error: %v", err)
|
|
}
|
|
if u.String() != "https://example.com/cover.png" {
|
|
t.Fatalf("URL = %q, want normalized https URL", u.String())
|
|
}
|
|
return
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--url")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocResourceCoverDryRunsPlanAPIs(t *testing.T) {
|
|
downloadRT := docValidateRuntime(t, map[string]string{
|
|
"doc": "doxcnCoverDryRunDownload",
|
|
"output": "cover",
|
|
}, nil, nil)
|
|
download := decodeDocDryRun(t, DocResourceDownload.DryRun(context.Background(), downloadRT))
|
|
if len(download.API) != 2 {
|
|
t.Fatalf("download dry-run API count = %d, want 2", len(download.API))
|
|
}
|
|
if got := download.API[0].URL; got != "/open-apis/docx/v1/documents/doxcnCoverDryRunDownload" {
|
|
t.Fatalf("download metadata URL = %q", got)
|
|
}
|
|
if got := download.API[1].URL; got != "/open-apis/drive/v1/medias/%3Ccover.token%3E/download" {
|
|
t.Fatalf("download media URL = %q", got)
|
|
}
|
|
|
|
updateRT := docValidateRuntime(t, map[string]string{
|
|
"doc": "https://example.larksuite.com/wiki/wikcnCoverDryRunUpdate",
|
|
"url": "https://example.com/cover.png",
|
|
}, nil, nil)
|
|
update := decodeDocDryRun(t, DocResourceUpdate.DryRun(context.Background(), updateRT))
|
|
if len(update.API) != 3 {
|
|
t.Fatalf("update dry-run API count = %d, want 3", len(update.API))
|
|
}
|
|
if got := update.API[0].URL; got != "/open-apis/wiki/v2/spaces/get_node" {
|
|
t.Fatalf("wiki resolve URL = %q", got)
|
|
}
|
|
if got := update.API[1].URL; got != "/open-apis/drive/v1/medias/upload_all" {
|
|
t.Fatalf("upload URL = %q", got)
|
|
}
|
|
if got := update.API[2].URL; got != "/open-apis/docx/v1/documents/%3Cresolved_docx_token%3E" {
|
|
t.Fatalf("patch URL = %q", got)
|
|
}
|
|
if got := update.API[1].Body["file"]; got != "https://example.com/cover.png" {
|
|
t.Fatalf("upload source = %#v", got)
|
|
}
|
|
|
|
deleteRT := docValidateRuntime(t, map[string]string{"doc": "doxcnCoverDryRunDelete"}, nil, nil)
|
|
deleteDry := decodeDocDryRun(t, DocResourceDelete.DryRun(context.Background(), deleteRT))
|
|
if len(deleteDry.API) != 2 {
|
|
t.Fatalf("delete dry-run API count = %d, want 2", len(deleteDry.API))
|
|
}
|
|
if got := deleteDry.API[1].URL; got != "/open-apis/docx/v1/documents/doxcnCoverDryRunDelete" {
|
|
t.Fatalf("delete patch URL = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDocResourceCoverDryRunReportsInvalidDoc(t *testing.T) {
|
|
rt := docValidateRuntime(t, map[string]string{"doc": "https://example.com/sheets/shtxxx"}, nil, nil)
|
|
dry := DocResourceDownload.DryRun(context.Background(), rt)
|
|
if got := dry.Format(); !strings.Contains(got, "error:") {
|
|
t.Fatalf("dry-run error output = %q, want error field", got)
|
|
}
|
|
}
|
|
|
|
func TestParseAndValidateDocCoverURLRejectsUnsafeIP(t *testing.T) {
|
|
_, err := parseAndValidateDocCoverURL(context.Background(), "https://127.0.0.1/cover.png")
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--url")
|
|
}
|
|
|
|
func TestValidateDocCoverURLHost(t *testing.T) {
|
|
for _, host := range []string{"", "localhost", "service.localhost", "127.0.0.1"} {
|
|
t.Run(host, func(t *testing.T) {
|
|
assertValidationContract(t, validateDocCoverURLHost(context.Background(), host), errs.SubtypeInvalidArgument, "--url")
|
|
})
|
|
}
|
|
if err := validateDocCoverURLHost(context.Background(), "1.1.1.1"); err != nil {
|
|
t.Fatalf("validateDocCoverURLHost(public IP) error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDocCoverIPSafetyBlocksSpecialRanges(t *testing.T) {
|
|
for _, rawIP := range []string{
|
|
"10.0.0.1",
|
|
"127.0.0.1",
|
|
"169.254.1.1",
|
|
"172.16.0.1",
|
|
"192.168.0.1",
|
|
"100.64.0.1",
|
|
"198.18.0.1",
|
|
"240.0.0.1",
|
|
} {
|
|
t.Run(rawIP, func(t *testing.T) {
|
|
if !isUnsafeDocCoverIP(net.ParseIP(rawIP)) {
|
|
t.Fatalf("%s was classified as safe", rawIP)
|
|
}
|
|
})
|
|
}
|
|
if isUnsafeDocCoverIP(net.ParseIP("1.1.1.1")) {
|
|
t.Fatal("public IPv4 address was classified as unsafe")
|
|
}
|
|
}
|
|
|
|
func TestDocCoverHTTPClientDoesNotUseProxy(t *testing.T) {
|
|
baseTransport := &http.Transport{Proxy: http.ProxyFromEnvironment}
|
|
baseClient := &http.Client{Transport: baseTransport}
|
|
|
|
client := newDocCoverHTTPClient(baseClient)
|
|
transport, ok := client.Transport.(*http.Transport)
|
|
if !ok {
|
|
t.Fatalf("client transport = %T, want *http.Transport", client.Transport)
|
|
}
|
|
if transport.Proxy != nil {
|
|
t.Fatal("cover URL downloader must not inherit proxy settings")
|
|
}
|
|
if baseTransport.Proxy == nil {
|
|
t.Fatal("base transport proxy was mutated")
|
|
}
|
|
}
|
|
|
|
func TestDocCoverHTTPClientRedirectValidation(t *testing.T) {
|
|
client := newDocCoverHTTPClient(&http.Client{})
|
|
req, err := http.NewRequest(http.MethodGet, "https://1.1.1.1/cover.png", nil)
|
|
if err != nil {
|
|
t.Fatalf("NewRequest() error: %v", err)
|
|
}
|
|
if err := client.CheckRedirect(req, []*http.Request{{}, {}, {}}); err == nil {
|
|
t.Fatal("expected too many redirects error")
|
|
}
|
|
|
|
prev, err := http.NewRequest(http.MethodGet, "https://1.1.1.1/start", nil)
|
|
if err != nil {
|
|
t.Fatalf("NewRequest(prev) error: %v", err)
|
|
}
|
|
downgrade, err := http.NewRequest(http.MethodGet, "http://1.1.1.1/cover.png", nil)
|
|
if err != nil {
|
|
t.Fatalf("NewRequest(downgrade) error: %v", err)
|
|
}
|
|
if err := client.CheckRedirect(downgrade, []*http.Request{prev}); err == nil {
|
|
t.Fatal("expected https-to-http redirect error")
|
|
}
|
|
}
|
|
|
|
func TestDocCoverConnRemoteIPValidation(t *testing.T) {
|
|
if err := validateDocCoverConnRemoteIP(nil); err == nil {
|
|
t.Fatal("expected nil connection error")
|
|
}
|
|
if err := validateDocCoverConnRemoteIP(docCoverRemoteAddrConn{}); err == nil {
|
|
t.Fatal("expected missing remote address error")
|
|
}
|
|
if err := validateDocCoverConnRemoteIP(docCoverRemoteAddrConn{addr: testAddr("not-ip")}); err == nil {
|
|
t.Fatal("expected invalid remote IP error")
|
|
}
|
|
if err := validateDocCoverConnRemoteIP(docCoverRemoteAddrConn{addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 443}}); err == nil {
|
|
t.Fatal("expected local remote IP error")
|
|
}
|
|
}
|
|
|
|
func TestDocCoverURLFileName(t *testing.T) {
|
|
cases := []struct {
|
|
raw string
|
|
ext string
|
|
want string
|
|
}{
|
|
{raw: "https://example.com/images/cover", ext: ".png", want: "cover.png"},
|
|
{raw: "https://example.com/images/cover.jpeg", ext: ".png", want: "cover.jpeg"},
|
|
{raw: "https://example.com/", ext: ".webp", want: "cover.webp"},
|
|
{raw: "https://example.com/images/%2Fescaped", ext: ".gif", want: "escaped.gif"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.want, func(t *testing.T) {
|
|
u, err := url.Parse(tc.raw)
|
|
if err != nil {
|
|
t.Fatalf("url.Parse(%q): %v", tc.raw, err)
|
|
}
|
|
if got := docCoverURLFileName(u, tc.ext); got != tc.want {
|
|
t.Fatalf("docCoverURLFileName() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDownloadDocCoverURLSuccess(t *testing.T) {
|
|
runtime, rawURL := newDocCoverURLTestRuntime(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write([]byte("png-data"))
|
|
}))
|
|
|
|
content, fileName, err := downloadDocCoverURL(context.Background(), runtime, rawURL)
|
|
if err != nil {
|
|
t.Fatalf("downloadDocCoverURL() error: %v", err)
|
|
}
|
|
if string(content) != "png-data" {
|
|
t.Fatalf("content = %q, want png-data", string(content))
|
|
}
|
|
if fileName != "cover.png" {
|
|
t.Fatalf("fileName = %q, want cover.png", fileName)
|
|
}
|
|
}
|
|
|
|
func TestDownloadDocCoverURLRejectsHTTPStatus(t *testing.T) {
|
|
runtime, rawURL := newDocCoverURLTestRuntime(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
_, _ = w.Write([]byte("unavailable"))
|
|
}))
|
|
|
|
_, _, err := downloadDocCoverURL(context.Background(), runtime, rawURL)
|
|
if err == nil {
|
|
t.Fatal("expected HTTP status error, got nil")
|
|
}
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("error = %T %v, want typed problem", err, err)
|
|
}
|
|
if p.Category != errs.CategoryNetwork {
|
|
t.Fatalf("problem category = %v, want %v", p.Category, errs.CategoryNetwork)
|
|
}
|
|
if p.Subtype != errs.SubtypeNetworkServer {
|
|
t.Fatalf("problem subtype = %v, want %v", p.Subtype, errs.SubtypeNetworkServer)
|
|
}
|
|
if p.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("problem code = %v, want %v", p.Code, http.StatusServiceUnavailable)
|
|
}
|
|
var networkErr *errs.NetworkError
|
|
if !errors.As(err, &networkErr) {
|
|
t.Fatalf("error = %T %v, want *errs.NetworkError", err, err)
|
|
}
|
|
if networkErr.Cause == nil {
|
|
t.Fatal("expected preserved underlying cause, got nil")
|
|
}
|
|
}
|
|
|
|
func TestDownloadDocCoverURLRejectsUnsupportedContentType(t *testing.T) {
|
|
runtime, rawURL := newDocCoverURLTestRuntime(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, _ = w.Write([]byte("not-image"))
|
|
}))
|
|
|
|
_, _, err := downloadDocCoverURL(context.Background(), runtime, rawURL)
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--url")
|
|
}
|
|
|
|
func TestDownloadDocCoverURLRejectsOversizeResponse(t *testing.T) {
|
|
runtime, rawURL := newDocCoverURLTestRuntime(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = io.CopyN(w, repeatByteReader('x'), docCoverURLMaxBytes+1)
|
|
}))
|
|
|
|
_, _, err := downloadDocCoverURL(context.Background(), runtime, rawURL)
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--url")
|
|
}
|
|
|
|
func TestDocCoverMetadataOutputAndOptionalFloats(t *testing.T) {
|
|
x := 0.25
|
|
y := 1.0
|
|
out := docCoverMetadata{
|
|
Token: "cover_token",
|
|
OffsetRatioX: &x,
|
|
OffsetRatioY: &y,
|
|
}.toOutput()
|
|
if out["token"] != "cover_token" || out["offset_ratio_x"] != x || out["offset_ratio_y"] != y {
|
|
t.Fatalf("cover output = %#v", out)
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"float": float64(1.5),
|
|
"int": 2,
|
|
"int64": int64(3),
|
|
"text": "4",
|
|
}
|
|
if got, ok := getOptionalFloat(data, "float"); !ok || got != 1.5 {
|
|
t.Fatalf("float optional = %v/%v, want 1.5/true", got, ok)
|
|
}
|
|
if got, ok := getOptionalFloat(data, "int"); !ok || got != 2 {
|
|
t.Fatalf("int optional = %v/%v, want 2/true", got, ok)
|
|
}
|
|
if got, ok := getOptionalFloat(data, "int64"); !ok || got != 3 {
|
|
t.Fatalf("int64 optional = %v/%v, want 3/true", got, ok)
|
|
}
|
|
if _, ok := getOptionalFloat(data, "text"); ok {
|
|
t.Fatal("string optional unexpectedly parsed as float")
|
|
}
|
|
if _, ok := getOptionalFloat(nil, "missing"); ok {
|
|
t.Fatal("nil map optional unexpectedly returned a value")
|
|
}
|
|
}
|
|
|
|
func TestDocCoverDryRunSource(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
str map[string]string
|
|
bools map[string]bool
|
|
want string
|
|
}{
|
|
{name: "clipboard", bools: map[string]bool{"from-clipboard": true}, want: "<clipboard image>"},
|
|
{name: "url", str: map[string]string{"url": "https://example.com/cover.png"}, want: "https://example.com/cover.png"},
|
|
{name: "file", str: map[string]string{"file": "cover.png"}, want: "@cover.png"},
|
|
{name: "empty", want: "<cover image>"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rt := docValidateRuntime(t, tc.str, tc.bools, nil)
|
|
if got := docCoverDryRunSource(rt); got != tc.want {
|
|
t.Fatalf("docCoverDryRunSource() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocShortcutsIncludeCoverResourceCommands(t *testing.T) {
|
|
got := map[string]bool{}
|
|
for _, shortcut := range Shortcuts() {
|
|
got[shortcut.Command] = true
|
|
}
|
|
for _, want := range []string{"+resource-download", "+resource-update", "+resource-delete"} {
|
|
if !got[want] {
|
|
t.Fatalf("Shortcuts() missing %s", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newDocCoverURLTestRuntime(t *testing.T, handler http.Handler) (*common.RuntimeContext, string) {
|
|
t.Helper()
|
|
|
|
server := httptest.NewTLSServer(handler)
|
|
t.Cleanup(server.Close)
|
|
|
|
f, _, _, _ := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-cover-url-download-app"))
|
|
targetAddr := server.Listener.Addr().String()
|
|
f.HttpClient = func() (*http.Client, error) {
|
|
return &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
var d net.Dialer
|
|
conn, err := d.DialContext(ctx, network, targetAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return docCoverRemoteAddrConn{
|
|
Conn: conn,
|
|
addr: &net.TCPAddr{IP: net.ParseIP("1.1.1.1"), Port: 443},
|
|
}, nil
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
return &common.RuntimeContext{Factory: f}, "https://1.1.1.1/assets/cover"
|
|
}
|
|
|
|
type docCoverRemoteAddrConn struct {
|
|
net.Conn
|
|
addr net.Addr
|
|
}
|
|
|
|
func (c docCoverRemoteAddrConn) RemoteAddr() net.Addr {
|
|
return c.addr
|
|
}
|
|
|
|
type testAddr string
|
|
|
|
func (a testAddr) Network() string {
|
|
return "test"
|
|
}
|
|
|
|
func (a testAddr) String() string {
|
|
return string(a)
|
|
}
|
|
|
|
type repeatByteReader byte
|
|
|
|
func (r repeatByteReader) Read(p []byte) (int, error) {
|
|
for i := range p {
|
|
p[i] = byte(r)
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func docCoverMetadataStub(documentID string, cover map[string]interface{}) *httpmock.Stub {
|
|
return &httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/docx/v1/documents/" + documentID,
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"document": map[string]interface{}{
|
|
"cover": cover,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
type docResourceOutput struct {
|
|
OK bool `json:"ok"`
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
|
|
func decodeDocResourceOutput(t *testing.T, stdout *bytes.Buffer) docResourceOutput {
|
|
t.Helper()
|
|
var out docResourceOutput
|
|
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
|
|
t.Fatalf("decode resource output: %v; output=%s", err, stdout.String())
|
|
}
|
|
return out
|
|
}
|