mirror of
https://github.com/larksuite/cli.git
synced 2026-07-05 15:47:54 +08:00
* feat: add markdown +diff shortcut Change-Id: I7da27889517707ac6f1d5e8c429e4bdfb49fdcf8 * fix: harden markdown diff downloads Change-Id: I0020e14ebee780617d790836af1368db851b8cf1 * refactor: address markdown diff review feedback Change-Id: I0ddb852218ec4784c0f9491896796c3007f04122
380 lines
13 KiB
Go
380 lines
13 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package markdown
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
"github.com/larksuite/cli/internal/output"
|
|
)
|
|
|
|
func TestMarkdownDiffRejectsUnsupportedFormat(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--from-version", "7633658129540910621",
|
|
"--format", "table",
|
|
}, f, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "only supports --format json or pretty") {
|
|
t.Fatalf("expected format validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffRejectsToVersionWithoutFromVersion(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--to-version", "7633658129540910628",
|
|
}, f, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "--to-version requires --from-version") {
|
|
t.Fatalf("expected version validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffRemoteVsRemoteJSON(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910621",
|
|
Status: 200,
|
|
RawBody: []byte("# Title\n\n- alpha\n- beta\n"),
|
|
Headers: http.Header{
|
|
"Content-Disposition": []string{`attachment; filename="README.md"`},
|
|
},
|
|
})
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910628",
|
|
Status: 200,
|
|
RawBody: []byte("# Title\n\n- alpha\n- beta updated\n- gamma\n"),
|
|
Headers: http.Header{
|
|
"Content-Disposition": []string{`attachment; filename="README.md"`},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--from-version", "7633658129540910621",
|
|
"--to-version", "7633658129540910628",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var env struct {
|
|
OK bool `json:"ok"`
|
|
Data struct {
|
|
Changed bool `json:"changed"`
|
|
Mode string `json:"mode"`
|
|
FromVersion string `json:"from_version"`
|
|
ToVersion string `json:"to_version"`
|
|
AddedLines int `json:"added_lines"`
|
|
DeletedLines int `json:"deleted_lines"`
|
|
Diff string `json:"diff"`
|
|
Hunks []markdownDiffHunk `json:"hunks"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
|
|
t.Fatalf("json unmarshal error: %v\n%s", err, stdout.String())
|
|
}
|
|
if !env.OK {
|
|
t.Fatalf("expected ok=true, got false: %s", stdout.String())
|
|
}
|
|
if !env.Data.Changed {
|
|
t.Fatalf("expected changed=true: %s", stdout.String())
|
|
}
|
|
if env.Data.Mode != markdownDiffModeRemoteVsRemote {
|
|
t.Fatalf("mode = %q, want %q", env.Data.Mode, markdownDiffModeRemoteVsRemote)
|
|
}
|
|
if env.Data.FromVersion != "7633658129540910621" || env.Data.ToVersion != "7633658129540910628" {
|
|
t.Fatalf("versions = %q -> %q", env.Data.FromVersion, env.Data.ToVersion)
|
|
}
|
|
if env.Data.AddedLines != 2 || env.Data.DeletedLines != 1 {
|
|
t.Fatalf("added/deleted = %d/%d, want 2/1", env.Data.AddedLines, env.Data.DeletedLines)
|
|
}
|
|
if len(env.Data.Hunks) != 1 {
|
|
t.Fatalf("len(hunks) = %d, want 1", len(env.Data.Hunks))
|
|
}
|
|
if !strings.Contains(env.Data.Diff, "@@") || !strings.Contains(env.Data.Diff, "+- gamma") {
|
|
t.Fatalf("diff missing expected content: %s", env.Data.Diff)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffRemoteVsLocalPretty(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download",
|
|
Status: 200,
|
|
RawBody: []byte("# Title\n\nhello old\n"),
|
|
Headers: http.Header{
|
|
"Content-Disposition": []string{`attachment; filename="README.md"`},
|
|
},
|
|
})
|
|
|
|
tmpDir := t.TempDir()
|
|
withMarkdownWorkingDir(t, tmpDir)
|
|
if err := os.WriteFile("local.md", []byte("# Title\n\nhello new\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile() error: %v", err)
|
|
}
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--file", "./local.md",
|
|
"--format", "pretty",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(stdout.String(), "@@") {
|
|
t.Fatalf("pretty output missing hunk header: %s", stdout.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), output.Red+"-hello old"+output.Reset) {
|
|
t.Fatalf("pretty output missing removed line color: %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), output.Green+"+hello new"+output.Reset) {
|
|
t.Fatalf("pretty output missing added line color: %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffRejectsOversizedRemoteContent(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download",
|
|
Status: 200,
|
|
RawBody: bytes.Repeat([]byte("x"), markdownDiffMaxContentBytes+1),
|
|
})
|
|
|
|
tmpDir := t.TempDir()
|
|
withMarkdownWorkingDir(t, tmpDir)
|
|
if err := os.WriteFile("local.md", []byte("# Title\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile() error: %v", err)
|
|
}
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--file", "./local.md",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "remote Markdown content exceeds 10.0 MB markdown +diff content limit") {
|
|
t.Fatalf("expected remote content size error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffRejectsOversizedLocalContent(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download",
|
|
Status: 200,
|
|
RawBody: []byte("# Title\n"),
|
|
})
|
|
|
|
tmpDir := t.TempDir()
|
|
withMarkdownWorkingDir(t, tmpDir)
|
|
if err := os.WriteFile("local.md", bytes.Repeat([]byte("x"), markdownDiffMaxContentBytes+1), 0o644); err != nil {
|
|
t.Fatalf("WriteFile() error: %v", err)
|
|
}
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--file", "./local.md",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "local Markdown file exceeds 10.0 MB markdown +diff content limit") {
|
|
t.Fatalf("expected local content size error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDownloadErrorPreservesStructuredErrors(t *testing.T) {
|
|
apiErr := output.ErrAPI(99991663, "permission denied", map[string]interface{}{"permission": "drive:file:download"})
|
|
if got := wrapMarkdownDownloadError(apiErr); got != apiErr {
|
|
t.Fatalf("wrapMarkdownDownloadError() = %v, want original API error", got)
|
|
}
|
|
|
|
got := wrapMarkdownDownloadError(errors.New("dial tcp timeout"))
|
|
var exitErr *output.ExitError
|
|
if !errors.As(got, &exitErr) {
|
|
t.Fatalf("wrapMarkdownDownloadError() = %T, want *output.ExitError", got)
|
|
}
|
|
if exitErr.Code != output.ExitNetwork {
|
|
t.Fatalf("exit code = %d, want %d", exitErr.Code, output.ExitNetwork)
|
|
}
|
|
if !strings.Contains(got.Error(), "download failed: dial tcp timeout") {
|
|
t.Fatalf("wrapped error = %q", got.Error())
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffIncludesNoNewlineMarker(t *testing.T) {
|
|
diffText, changed, added, deleted, hunks := summarizeMarkdownDiff(
|
|
"a/test.md",
|
|
"b/test.md",
|
|
"# Title\n\nhello old",
|
|
"# Title\n\nhello new",
|
|
3,
|
|
)
|
|
if !changed {
|
|
t.Fatalf("expected changed=true")
|
|
}
|
|
if added != 1 || deleted != 1 {
|
|
t.Fatalf("added/deleted = %d/%d, want 1/1", added, deleted)
|
|
}
|
|
if len(hunks) != 1 {
|
|
t.Fatalf("len(hunks) = %d, want 1", len(hunks))
|
|
}
|
|
if strings.Count(diffText, "\\ No newline at end of file") != 2 {
|
|
t.Fatalf("diff should contain two no-newline markers: %q", diffText)
|
|
}
|
|
if !strings.Contains(diffText, "-hello old\n\\ No newline at end of file\n+hello new\n\\ No newline at end of file\n") {
|
|
t.Fatalf("diff missing expected no-newline marker sequence: %q", diffText)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffRemoteVsRemoteJSONMultipleHunks(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910621",
|
|
Status: 200,
|
|
RawBody: []byte("line1\nline2\nline3\nline4\nline5\nline6\n"),
|
|
Headers: http.Header{
|
|
"Content-Disposition": []string{`attachment; filename="README.md"`},
|
|
},
|
|
})
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910628",
|
|
Status: 200,
|
|
RawBody: []byte("line1\nline2 changed\nline3\nline4\nline5 changed\nline6\n"),
|
|
Headers: http.Header{
|
|
"Content-Disposition": []string{`attachment; filename="README.md"`},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--from-version", "7633658129540910621",
|
|
"--to-version", "7633658129540910628",
|
|
"--context-lines", "0",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var env struct {
|
|
OK bool `json:"ok"`
|
|
Data struct {
|
|
Changed bool `json:"changed"`
|
|
AddedLines int `json:"added_lines"`
|
|
DeletedLines int `json:"deleted_lines"`
|
|
Hunks []markdownDiffHunk `json:"hunks"`
|
|
Diff string `json:"diff"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
|
|
t.Fatalf("json unmarshal error: %v\n%s", err, stdout.String())
|
|
}
|
|
if !env.OK || !env.Data.Changed {
|
|
t.Fatalf("expected changed=true: %s", stdout.String())
|
|
}
|
|
if env.Data.AddedLines != 2 || env.Data.DeletedLines != 2 {
|
|
t.Fatalf("added/deleted = %d/%d, want 2/2", env.Data.AddedLines, env.Data.DeletedLines)
|
|
}
|
|
if len(env.Data.Hunks) != 2 {
|
|
t.Fatalf("len(hunks) = %d, want 2", len(env.Data.Hunks))
|
|
}
|
|
if !strings.Contains(env.Data.Diff, "-line2") || !strings.Contains(env.Data.Diff, "+line5 changed") {
|
|
t.Fatalf("diff missing expected content: %s", env.Data.Diff)
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffNoChangesPretty(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download?version=7633658129540910621",
|
|
Status: 200,
|
|
RawBody: []byte("# Title\n"),
|
|
})
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/drive/v1/files/box_md_diff/download",
|
|
Status: 200,
|
|
RawBody: []byte("# Title\n"),
|
|
})
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--from-version", "7633658129540910621",
|
|
"--format", "pretty",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got := strings.TrimSpace(stdout.String()); got != "No differences." {
|
|
t.Fatalf("pretty no-change output = %q, want %q", got, "No differences.")
|
|
}
|
|
}
|
|
|
|
func TestMarkdownDiffDryRunRemoteVsLocal(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig())
|
|
|
|
tmpDir := t.TempDir()
|
|
withMarkdownWorkingDir(t, tmpDir)
|
|
localPath := filepath.Join(".", "local.md")
|
|
if err := os.WriteFile(localPath, []byte("# local\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile() error: %v", err)
|
|
}
|
|
|
|
err := mountAndRunMarkdown(t, MarkdownDiff, []string{
|
|
"+diff",
|
|
"--file-token", "box_md_diff",
|
|
"--file", localPath,
|
|
"--dry-run",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(stdout.String(), "/open-apis/drive/v1/files/:file_token/download") && !strings.Contains(stdout.String(), "/open-apis/drive/v1/files/box_md_diff/download") {
|
|
t.Fatalf("dry-run missing download call: %s", stdout.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), `"local_file": "local.md"`) && !strings.Contains(stdout.String(), `"local_file": "./local.md"`) {
|
|
t.Fatalf("dry-run missing local file metadata: %s", stdout.String())
|
|
}
|
|
}
|