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
1132 lines
35 KiB
Go
1132 lines
35 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package wiki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"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"
|
|
)
|
|
|
|
// ── +space-list ──────────────────────────────────────────────────────────────
|
|
|
|
func TestWikiShortcutsIncludesSpaceListNodeListNodeCopy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
commands := map[string]bool{}
|
|
for _, s := range Shortcuts() {
|
|
commands[s.Command] = true
|
|
}
|
|
for _, want := range []string{"+space-list", "+node-list", "+node-copy"} {
|
|
if !commands[want] {
|
|
t.Errorf("Shortcuts() missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWikiListShortcutsDeclareNarrowScopes pins the per-endpoint scope
|
|
// choice. The framework's preflight does exact string matching, so a broad
|
|
// scope (e.g. wiki:wiki:readonly) would wrongly reject tokens carrying only
|
|
// the narrow per-API scope that the API actually accepts.
|
|
func TestWikiListShortcutsDeclareNarrowScopes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
shortcut common.Shortcut
|
|
want []string
|
|
}{
|
|
{"+space-list", WikiSpaceList, []string{"wiki:space:retrieve"}},
|
|
{"+node-list", WikiNodeList, []string{"wiki:node:retrieve"}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if !reflect.DeepEqual(tc.shortcut.Scopes, tc.want) {
|
|
t.Fatalf("%s scopes = %v, want %v", tc.name, tc.shortcut.Scopes, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWikiSpaceListReturnsPaginatedSpaces(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"items": []interface{}{
|
|
map[string]interface{}{
|
|
"space_id": "space_1",
|
|
"name": "Engineering Wiki",
|
|
"space_type": "team",
|
|
},
|
|
map[string]interface{}{
|
|
"space_id": "space_2",
|
|
"name": "Personal Library",
|
|
"space_type": "my_library",
|
|
},
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{"+space-list", "--as", "bot"}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var envelope struct {
|
|
OK bool `json:"ok"`
|
|
Data struct {
|
|
Spaces []map[string]interface{} `json:"spaces"`
|
|
HasMore bool `json:"has_more"`
|
|
PageToken string `json:"page_token"`
|
|
} `json:"data"`
|
|
Meta struct {
|
|
Count float64 `json:"count"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if !envelope.OK {
|
|
t.Fatalf("expected ok=true, got %s", stdout.String())
|
|
}
|
|
if envelope.Meta.Count != 2 {
|
|
t.Fatalf("meta.count = %v, want 2", envelope.Meta.Count)
|
|
}
|
|
if envelope.Data.HasMore {
|
|
t.Fatalf("has_more = true, want false on natural end")
|
|
}
|
|
if envelope.Data.Spaces[0]["name"] != "Engineering Wiki" {
|
|
t.Fatalf("spaces[0].name = %v, want %q", envelope.Data.Spaces[0]["name"], "Engineering Wiki")
|
|
}
|
|
}
|
|
|
|
// ── +node-list ───────────────────────────────────────────────────────────────
|
|
|
|
func TestWikiNodeListRequiresSpaceID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{"+node-list", "--as", "user"}, factory, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "required") {
|
|
t.Fatalf("expected required flag error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListRejectsNonNumericSpaceID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list", "--space-id", "wikcnABC", "--as", "user",
|
|
}, factory, nil)
|
|
if err == nil {
|
|
t.Fatalf("expected numeric space_id validation error, got nil")
|
|
}
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("ProblemOf() ok=false for %T: %v", err, err)
|
|
}
|
|
var validationErr *errs.ValidationError
|
|
if !errors.As(err, &validationErr) {
|
|
t.Fatalf("expected ValidationError, got %T: %v", err, err)
|
|
}
|
|
if p.Category != errs.CategoryValidation || p.Subtype != errs.SubtypeInvalidArgument || validationErr.Param != "--space-id" {
|
|
t.Fatalf("problem = %#v param=%q, want validation/invalid_argument/--space-id", p, validationErr.Param)
|
|
}
|
|
if !strings.Contains(p.Message, "--space-id must be a numeric wiki space_id") || !strings.Contains(p.Hint, "+space-list") {
|
|
t.Fatalf("expected numeric space_id validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListRejectsDocumentURLAsParentNodeToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list",
|
|
"--space-id", "7211568716812369922",
|
|
"--parent-node-token", "https://feishu.cn/docx/docxABC",
|
|
"--as", "user",
|
|
}, factory, nil)
|
|
if err == nil {
|
|
t.Fatalf("expected parent-node-token URL type validation error, got nil")
|
|
}
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("ProblemOf() ok=false for %T: %v", err, err)
|
|
}
|
|
var validationErr *errs.ValidationError
|
|
if !errors.As(err, &validationErr) {
|
|
t.Fatalf("expected ValidationError, got %T: %v", err, err)
|
|
}
|
|
if p.Category != errs.CategoryValidation || p.Subtype != errs.SubtypeInvalidArgument || validationErr.Param != "--parent-node-token" {
|
|
t.Fatalf("problem = %#v param=%q, want validation/invalid_argument/--parent-node-token", p, validationErr.Param)
|
|
}
|
|
if !strings.Contains(p.Message, "must identify a wiki node") || !strings.Contains(p.Hint, "+node-get") {
|
|
t.Fatalf("expected parent-node-token URL type validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListNormalizesWikiURLParentNodeToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
token, err := normalizeWikiNodeListParentToken("https://feishu.cn/wiki/wikcnPARENT?from=copy")
|
|
if err != nil {
|
|
t.Fatalf("normalizeWikiNodeListParentToken() error = %v", err)
|
|
}
|
|
if token != "wikcnPARENT" {
|
|
t.Fatalf("token = %q, want wikcnPARENT", token)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListAcceptsOpaqueParentNodeToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const opaqueNodeToken = "Q6ZM_EXAMPLE_TOKEN"
|
|
token, err := normalizeWikiNodeListParentToken(opaqueNodeToken)
|
|
if err != nil {
|
|
t.Fatalf("normalizeWikiNodeListParentToken() error = %v", err)
|
|
}
|
|
if token != opaqueNodeToken {
|
|
t.Fatalf("token = %q, want %q", token, opaqueNodeToken)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListRejectsAmbiguousSpaceAndParentTokens(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if err := validateWikiNodeListSpaceID("https://example.invalid/wiki/space"); err == nil {
|
|
t.Fatalf("expected URL space-id validation error")
|
|
} else {
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("ProblemOf() ok=false for %T: %v", err, err)
|
|
}
|
|
if !strings.Contains(p.Message, "not a URL or path") || !strings.Contains(p.Hint, "+space-list") {
|
|
t.Fatalf("problem = %#v, want URL/path message and +space-list hint", p)
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "partial wiki path",
|
|
input: "wik_placeholder/child",
|
|
wantMsg: "raw wiki node token",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := normalizeWikiNodeListParentToken(tt.input)
|
|
if err == nil {
|
|
t.Fatalf("expected parent token validation error")
|
|
}
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("ProblemOf() ok=false for %T: %v", err, err)
|
|
}
|
|
if !strings.Contains(p.Message, tt.wantMsg) {
|
|
t.Fatalf("message = %q, want substring %q", p.Message, tt.wantMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListAcceptsEmptyParentToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
token, err := normalizeWikiNodeListParentToken("")
|
|
if err != nil {
|
|
t.Fatalf("normalizeWikiNodeListParentToken(empty) error = %v", err)
|
|
}
|
|
if token != "" {
|
|
t.Fatalf("token = %q, want empty", token)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListProblemAddsActionableHint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := errs.NewAPIError(errs.SubtypeInvalidParameters, "param err: invalid page_token").WithCode(131002)
|
|
got := wikiNodeListProblem(err, nil)
|
|
p, ok := errs.ProblemOf(got)
|
|
if !ok {
|
|
t.Fatalf("ProblemOf() ok=false")
|
|
}
|
|
if !strings.Contains(p.Hint, "page token is invalid or stale") {
|
|
t.Fatalf("hint = %q, want invalid page token guidance", p.Hint)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListReturnsNodesForSpace(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/7211568716812369922/nodes",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"items": []interface{}{
|
|
map[string]interface{}{
|
|
"space_id": "7211568716812369922",
|
|
"node_token": "wik_node_1",
|
|
"obj_token": "docx_1",
|
|
"obj_type": "docx",
|
|
"parent_node_token": "",
|
|
"node_type": "origin",
|
|
"title": "Getting Started",
|
|
"has_child": true,
|
|
},
|
|
map[string]interface{}{
|
|
"space_id": "7211568716812369922",
|
|
"node_token": "wik_node_2",
|
|
"obj_token": "docx_2",
|
|
"obj_type": "docx",
|
|
"parent_node_token": "",
|
|
"node_type": "origin",
|
|
"title": "Architecture",
|
|
"has_child": false,
|
|
},
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list", "--space-id", "7211568716812369922", "--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var envelope struct {
|
|
OK bool `json:"ok"`
|
|
Data struct {
|
|
Nodes []map[string]interface{} `json:"nodes"`
|
|
HasMore bool `json:"has_more"`
|
|
PageToken string `json:"page_token"`
|
|
} `json:"data"`
|
|
Meta struct {
|
|
Count float64 `json:"count"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if !envelope.OK {
|
|
t.Fatalf("expected ok=true, got %s", stdout.String())
|
|
}
|
|
if envelope.Meta.Count != 2 {
|
|
t.Fatalf("meta.count = %v, want 2", envelope.Meta.Count)
|
|
}
|
|
if envelope.Data.Nodes[0]["title"] != "Getting Started" {
|
|
t.Fatalf("nodes[0].title = %v, want %q", envelope.Data.Nodes[0]["title"], "Getting Started")
|
|
}
|
|
if envelope.Data.Nodes[0]["has_child"] != true {
|
|
t.Fatalf("nodes[0].has_child = %v, want true", envelope.Data.Nodes[0]["has_child"])
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListPassesParentNodeToken(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
const parentNodeToken = "Q6ZM_EXAMPLE_TOKEN"
|
|
|
|
stub := &httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/7211568716812369922/nodes?page_size=50&parent_node_token=" + parentNodeToken,
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"items": []interface{}{
|
|
map[string]interface{}{
|
|
"space_id": "7211568716812369922",
|
|
"node_token": "wik_child",
|
|
"obj_token": "docx_child",
|
|
"obj_type": "docx",
|
|
"parent_node_token": parentNodeToken,
|
|
"node_type": "origin",
|
|
"title": "Child Doc",
|
|
"has_child": false,
|
|
},
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
}
|
|
reg.Register(stub)
|
|
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list", "--space-id", "7211568716812369922", "--parent-node-token", parentNodeToken, "--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
// Verify the correct node was returned (parent_node_token was passed correctly).
|
|
var envelope struct {
|
|
OK bool `json:"ok"`
|
|
Data struct {
|
|
Nodes []map[string]interface{} `json:"nodes"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if !envelope.OK {
|
|
t.Fatalf("expected ok=true, got %s", stdout.String())
|
|
}
|
|
if len(envelope.Data.Nodes) != 1 {
|
|
t.Fatalf("len(nodes) = %d, want 1", len(envelope.Data.Nodes))
|
|
}
|
|
if envelope.Data.Nodes[0]["parent_node_token"] != parentNodeToken {
|
|
t.Fatalf("nodes[0].parent_node_token = %v, want %q", envelope.Data.Nodes[0]["parent_node_token"], parentNodeToken)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListRejectsMyLibraryForBot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list", "--space-id", "my_library", "--as", "bot",
|
|
}, factory, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "bot identity does not support --space-id my_library") {
|
|
t.Fatalf("expected my_library bot rejection, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListResolvesMyLibraryForUser(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
// Step 1: resolve my_library to the real space_id.
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/my_library",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"space": map[string]interface{}{
|
|
"space_id": "7211568716812369923",
|
|
"name": "My Library",
|
|
"space_type": "my_library",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
// Step 2: list nodes in the resolved space.
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/7211568716812369923/nodes",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"items": []interface{}{
|
|
map[string]interface{}{
|
|
"space_id": "7211568716812369923",
|
|
"node_token": "wik_personal_1",
|
|
"title": "Personal Note",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list", "--space-id", "my_library", "--as", "user",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var envelope struct {
|
|
OK bool `json:"ok"`
|
|
Data struct {
|
|
Nodes []map[string]interface{} `json:"nodes"`
|
|
} `json:"data"`
|
|
Meta struct {
|
|
Count float64 `json:"count"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if envelope.Meta.Count != 1 {
|
|
t.Fatalf("meta.count = %v, want 1", envelope.Meta.Count)
|
|
}
|
|
if envelope.Data.Nodes[0]["space_id"] != "7211568716812369923" {
|
|
t.Fatalf("nodes[0].space_id = %v, want 7211568716812369923", envelope.Data.Nodes[0]["space_id"])
|
|
}
|
|
}
|
|
|
|
// ── +node-copy ───────────────────────────────────────────────────────────────
|
|
|
|
func TestWikiNodeCopyRequiresTargetSpaceOrParent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiNodeCopy, []string{
|
|
"+node-copy", "--space-id", "space_123", "--node-token", "wik_src", "--as", "bot",
|
|
}, factory, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "--target-space-id or --target-parent-node-token") {
|
|
t.Fatalf("expected target validation error, got %v", err)
|
|
}
|
|
requireWikiValidationParams(t, err, "--target-space-id", "--target-parent-node-token")
|
|
}
|
|
|
|
func TestWikiNodeCopyRejectsBothTargetFlags(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiNodeCopy, []string{
|
|
"+node-copy", "--space-id", "space_123", "--node-token", "wik_src",
|
|
"--target-space-id", "space_dst", "--target-parent-node-token", "wik_parent",
|
|
"--as", "bot",
|
|
}, factory, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
|
|
t.Fatalf("expected mutually exclusive error, got %v", err)
|
|
}
|
|
requireWikiValidationParams(t, err, "--target-space-id", "--target-parent-node-token")
|
|
}
|
|
|
|
// TestWikiNodeCopyDeclaredHighRiskWrite pins down the high-risk-write
|
|
// contract: invocation without --yes must fail with a confirmation_required
|
|
// error and must NOT issue the underlying API call. The aligned upstream
|
|
// schema flags this API as `danger: true`, and the shortcut now matches that
|
|
// risk classification.
|
|
func TestWikiNodeCopyDeclaredHighRiskWrite(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if WikiNodeCopy.Risk != "high-risk-write" {
|
|
t.Fatalf("WikiNodeCopy.Risk = %q, want %q", WikiNodeCopy.Risk, "high-risk-write")
|
|
}
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
// No HTTP stub registered — if the gate leaks, the request fires and
|
|
// httpmock errors with "no stub for POST ..." instead of the expected
|
|
// confirmation_required error, making the regression obvious.
|
|
err := mountAndRunWiki(t, WikiNodeCopy, []string{
|
|
"+node-copy",
|
|
"--space-id", "space_src",
|
|
"--node-token", "wik_src",
|
|
"--target-space-id", "space_dst",
|
|
"--as", "bot",
|
|
}, factory, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "requires confirmation") {
|
|
t.Fatalf("expected confirmation_required error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeCopyCopiesNodeToTargetSpace(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, stderr, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
stub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/wiki/v2/spaces/space_src/nodes/wik_src/copy",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_dst",
|
|
"node_token": "wik_copied",
|
|
"obj_token": "docx_copied",
|
|
"obj_type": "docx",
|
|
"parent_node_token": "",
|
|
"node_type": "origin",
|
|
"title": "Architecture (Copy)",
|
|
"has_child": false,
|
|
"url": "https://abc.feishu.cn/wiki/wik_copied_real",
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
}
|
|
reg.Register(stub)
|
|
|
|
err := mountAndRunWiki(t, WikiNodeCopy, []string{
|
|
"+node-copy",
|
|
"--space-id", "space_src",
|
|
"--node-token", "wik_src",
|
|
"--target-space-id", "space_dst",
|
|
"--title", "Architecture (Copy)",
|
|
"--yes",
|
|
"--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var envelope struct {
|
|
OK bool `json:"ok"`
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if !envelope.OK {
|
|
t.Fatalf("expected ok=true, got %s", stdout.String())
|
|
}
|
|
if envelope.Data["node_token"] != "wik_copied" {
|
|
t.Fatalf("node_token = %v, want %q", envelope.Data["node_token"], "wik_copied")
|
|
}
|
|
if envelope.Data["space_id"] != "space_dst" {
|
|
t.Fatalf("space_id = %v, want %q", envelope.Data["space_id"], "space_dst")
|
|
}
|
|
if got, want := envelope.Data["url"], "https://abc.feishu.cn/wiki/wik_copied_real"; got != want {
|
|
t.Fatalf("url = %#v, want %q (copy must surface the response url)", got, want)
|
|
}
|
|
|
|
var captured map[string]interface{}
|
|
if err := json.Unmarshal(stub.CapturedBody, &captured); err != nil {
|
|
t.Fatalf("unmarshal captured body: %v", err)
|
|
}
|
|
if captured["target_space_id"] != "space_dst" {
|
|
t.Fatalf("captured target_space_id = %v, want %q", captured["target_space_id"], "space_dst")
|
|
}
|
|
if captured["title"] != "Architecture (Copy)" {
|
|
t.Fatalf("captured title = %v, want %q", captured["title"], "Architecture (Copy)")
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "Copying wiki node") {
|
|
t.Fatalf("stderr = %q, want copy message", got)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeCopyCopiesNodeToTargetParent(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
stub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/wiki/v2/spaces/space_src/nodes/wik_src/copy",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_src",
|
|
"node_token": "wik_copied2",
|
|
"obj_token": "docx_copied2",
|
|
"obj_type": "docx",
|
|
"parent_node_token": "wik_parent_dst",
|
|
"node_type": "origin",
|
|
"title": "Architecture",
|
|
"has_child": false,
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
}
|
|
reg.Register(stub)
|
|
|
|
err := mountAndRunWiki(t, WikiNodeCopy, []string{
|
|
"+node-copy",
|
|
"--space-id", "space_src",
|
|
"--node-token", "wik_src",
|
|
"--target-parent-node-token", "wik_parent_dst",
|
|
"--yes",
|
|
"--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var captured map[string]interface{}
|
|
if err := json.Unmarshal(stub.CapturedBody, &captured); err != nil {
|
|
t.Fatalf("unmarshal captured body: %v", err)
|
|
}
|
|
if captured["target_parent_token"] != "wik_parent_dst" {
|
|
t.Fatalf("captured target_parent_token = %v, want %q", captured["target_parent_token"], "wik_parent_dst")
|
|
}
|
|
if _, hasTitle := captured["title"]; hasTitle {
|
|
t.Fatalf("title should not be in body when --title not provided, got %v", captured)
|
|
}
|
|
}
|
|
|
|
// ── +space-list / +node-list pagination & format ─────────────────────────────
|
|
|
|
func TestWikiSpaceListRejectsInvalidPageSize(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{
|
|
"+space-list", "--page-size", "0", "--as", "bot",
|
|
}, factory, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "--page-size must be between 1 and 50") {
|
|
t.Fatalf("expected page-size validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiSpaceListRejectsNegativePageLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
factory, _, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{
|
|
"+space-list", "--page-limit", "-1", "--as", "bot",
|
|
}, factory, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "--page-limit must be a non-negative integer") {
|
|
t.Fatalf("expected page-limit validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiSpaceListAutoPaginatesAcrossPages(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
// Page 1: has_more=true, page_token set. Loop must continue.
|
|
page1 := &httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": true,
|
|
"page_token": "tok_page2",
|
|
"items": []interface{}{
|
|
map[string]interface{}{"space_id": "sp_1", "name": "First"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
// Page 2: must receive page_token=tok_page2 in query. Captured to verify.
|
|
var page2Query string
|
|
page2 := &httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
OnMatch: func(req *http.Request) { page2Query = req.URL.RawQuery },
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"page_token": "",
|
|
"items": []interface{}{
|
|
map[string]interface{}{"space_id": "sp_2", "name": "Second"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reg.Register(page1)
|
|
reg.Register(page2)
|
|
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{"+space-list", "--page-all", "--as", "bot"}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var envelope struct {
|
|
Data struct {
|
|
Spaces []map[string]interface{} `json:"spaces"`
|
|
HasMore bool `json:"has_more"`
|
|
PageToken string `json:"page_token"`
|
|
} `json:"data"`
|
|
Meta struct {
|
|
Count float64 `json:"count"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if envelope.Meta.Count != 2 || len(envelope.Data.Spaces) != 2 {
|
|
t.Fatalf("merged spaces = %d / count=%v, want 2 / 2", len(envelope.Data.Spaces), envelope.Meta.Count)
|
|
}
|
|
if envelope.Data.HasMore || envelope.Data.PageToken != "" {
|
|
t.Fatalf("natural end should clear has_more/page_token, got has_more=%v page_token=%q", envelope.Data.HasMore, envelope.Data.PageToken)
|
|
}
|
|
q, _ := url.ParseQuery(page2Query)
|
|
if q.Get("page_token") != "tok_page2" {
|
|
t.Fatalf("page2 page_token = %q, want tok_page2", q.Get("page_token"))
|
|
}
|
|
}
|
|
|
|
func TestWikiSpaceListPageLimitTruncatesAndExposesNextCursor(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
// Only stub page 1; with --page-limit=1, the loop must stop BEFORE
|
|
// requesting page 2 — and surface has_more/page_token so the caller can resume.
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": true,
|
|
"page_token": "tok_next",
|
|
"items": []interface{}{
|
|
map[string]interface{}{"space_id": "sp_only", "name": "First"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{
|
|
"+space-list", "--page-all", "--page-limit", "1", "--as", "user",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var envelope struct {
|
|
Data struct {
|
|
Spaces []map[string]interface{} `json:"spaces"`
|
|
HasMore bool `json:"has_more"`
|
|
PageToken string `json:"page_token"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if len(envelope.Data.Spaces) != 1 {
|
|
t.Fatalf("spaces = %d, want 1 (capped)", len(envelope.Data.Spaces))
|
|
}
|
|
if !envelope.Data.HasMore || envelope.Data.PageToken != "tok_next" {
|
|
t.Fatalf("truncated state = has_more=%v page_token=%q, want true / tok_next", envelope.Data.HasMore, envelope.Data.PageToken)
|
|
}
|
|
}
|
|
|
|
func TestWikiSpaceListExplicitPageTokenStopsAfterOnePage(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
// Stub a page where has_more=true; auto-pagination should NOT trigger
|
|
// because the caller supplied an explicit --page-token cursor.
|
|
var capturedQuery string
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
OnMatch: func(req *http.Request) { capturedQuery = req.URL.RawQuery },
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": true,
|
|
"page_token": "tok_next",
|
|
"items": []interface{}{map[string]interface{}{"space_id": "sp_x"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{
|
|
"+space-list", "--page-token", "tok_input", "--as", "user",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
q, _ := url.ParseQuery(capturedQuery)
|
|
if q.Get("page_token") != "tok_input" {
|
|
t.Fatalf("captured page_token = %q, want tok_input", q.Get("page_token"))
|
|
}
|
|
}
|
|
|
|
func TestWikiSpaceListPrettyFormatRendersFields(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"items": []interface{}{
|
|
map[string]interface{}{
|
|
"space_id": "sp_1",
|
|
"name": "Engineering",
|
|
"description": "team docs",
|
|
"space_type": "team",
|
|
"visibility": "public",
|
|
"open_sharing": "open",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{
|
|
"+space-list", "--format", "pretty", "--as", "user",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
out := stdout.String()
|
|
for _, want := range []string{
|
|
"Engineering",
|
|
"space_id: sp_1",
|
|
"space_type: team",
|
|
"visibility: public",
|
|
"description: team docs",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("pretty output missing %q, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListDefaultIsSinglePage(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
// Only one stub registered; if the default tried to auto-paginate, the
|
|
// loop would attempt a 2nd request and httpmock would error. So this
|
|
// test pins down the "default = single page" contract.
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/7211568716812369922/nodes",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": true,
|
|
"page_token": "tok_next",
|
|
"items": []interface{}{
|
|
map[string]interface{}{"space_id": "7211568716812369922", "node_token": "wik_1", "title": "First"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list", "--space-id", "7211568716812369922", "--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
var envelope struct {
|
|
Data struct {
|
|
Nodes []map[string]interface{} `json:"nodes"`
|
|
HasMore bool `json:"has_more"`
|
|
PageToken string `json:"page_token"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if len(envelope.Data.Nodes) != 1 {
|
|
t.Fatalf("nodes = %d, want 1 (single page default)", len(envelope.Data.Nodes))
|
|
}
|
|
if !envelope.Data.HasMore || envelope.Data.PageToken != "tok_next" {
|
|
t.Fatalf("single-page default should surface upstream cursor, got has_more=%v page_token=%q", envelope.Data.HasMore, envelope.Data.PageToken)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeListPrettyFormatRendersFields(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/7211568716812369922/nodes",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"items": []interface{}{
|
|
map[string]interface{}{
|
|
"space_id": "7211568716812369922",
|
|
"node_token": "wik_1",
|
|
"obj_type": "docx",
|
|
"obj_token": "docx_1",
|
|
"title": "Getting Started",
|
|
"has_child": true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiNodeList, []string{
|
|
"+node-list", "--space-id", "7211568716812369922", "--format", "pretty", "--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
out := stdout.String()
|
|
for _, want := range []string{
|
|
"Getting Started",
|
|
"node_token: wik_1",
|
|
"obj_type: docx",
|
|
"has_child: true",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("pretty output missing %q, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── QA-driven fixes: empty slice + has_more hint + node-copy format ──
|
|
|
|
func TestWikiSpaceListEmptyResultReturnsEmptySliceNotNull(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": false,
|
|
"page_token": "",
|
|
"items": []interface{}{},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{"+space-list", "--as", "bot"}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
// Substring assertion is the only reliable way to distinguish [] from null
|
|
// in serialised JSON — unmarshalling both back into a Go slice would
|
|
// collapse the distinction.
|
|
if !strings.Contains(stdout.String(), `"spaces": []`) {
|
|
t.Fatalf("expected spaces to be empty array [], got:\n%s", stdout.String())
|
|
}
|
|
if strings.Contains(stdout.String(), `"spaces": null`) {
|
|
t.Fatalf("spaces serialised as null — JSON consumers expect []:\n%s", stdout.String())
|
|
}
|
|
|
|
var envelope struct {
|
|
Meta struct {
|
|
Count float64 `json:"count"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("unmarshal stdout: %v", err)
|
|
}
|
|
if envelope.Meta.Count != 0 {
|
|
t.Fatalf("meta.count = %v, want 0", envelope.Meta.Count)
|
|
}
|
|
}
|
|
|
|
func TestWikiSpaceListPrettyHintsWhenEmptyButHasMore(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"has_more": true,
|
|
"page_token": "tok_more",
|
|
"items": []interface{}{},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiSpaceList, []string{"+space-list", "--format", "pretty", "--as", "bot"}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
out := stdout.String()
|
|
// When the bot's first page is filtered out by upstream permissions, the
|
|
// blanket "No wiki spaces found." used to mislead users into thinking they
|
|
// had no access at all. Pretty mode must now distinguish that case.
|
|
if strings.Contains(out, "No wiki spaces found.") {
|
|
t.Fatalf("pretty output should not flatly claim 'No wiki spaces found.' when has_more=true; got:\n%s", out)
|
|
}
|
|
for _, want := range []string{
|
|
"Current page is empty but the server reports more pages.",
|
|
"tok_more",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("pretty output missing %q, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeCopyHasFormatPrettyRendersNode(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/wiki/v2/spaces/space_src/nodes/wik_src/copy",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_dst",
|
|
"node_token": "wik_copied",
|
|
"obj_token": "docx_copied",
|
|
"obj_type": "docx",
|
|
"parent_node_token": "wik_parent",
|
|
"node_type": "origin",
|
|
"title": "Architecture (Copy)",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiNodeCopy, []string{
|
|
"+node-copy",
|
|
"--space-id", "space_src",
|
|
"--node-token", "wik_src",
|
|
"--target-space-id", "space_dst",
|
|
"--title", "Architecture (Copy)",
|
|
"--format", "pretty",
|
|
"--yes",
|
|
"--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
out := stdout.String()
|
|
for _, want := range []string{
|
|
"Copied node:",
|
|
"title: Architecture (Copy)",
|
|
"node_token: wik_copied",
|
|
"space_id: space_dst",
|
|
"parent_node_token: wik_parent",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("pretty output missing %q, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|