Files
dingtalk-workspace-cli/internal/shortcut/doc/pagination_test.go

145 lines
5.7 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package doc
import (
"errors"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageDocSearchPageAllContract(t *testing.T) {
caller := &docCoverageCaller{responses: map[string][]map[string]any{
"search_documents": {
{"documents": []any{map[string]any{"nodeId": "a", "name": "A"}}, "hasMore": true, "nextPageToken": "p2"},
{"documents": []any{map[string]any{"nodeId": "b", "name": "B"}, map[string]any{"nodeId": "a", "name": "A"}}, "hasMore": false},
},
}}
if err := runDocCoverage(t, Search, caller, "--query", "report", "--page-all", "--limit", "2"); err != nil {
t.Fatal(err)
}
if len(caller.history) != 2 || caller.history[1].params["pageToken"] != "p2" {
t.Fatalf("pagination calls = %#v", caller.history)
}
}
func TestCrossPlatformCoverageDocPaginationFailsClosedOnStalledCursor(t *testing.T) {
caller := &docCoverageCaller{responses: map[string][]map[string]any{
"list_nodes": {
{"nodes": []any{map[string]any{"nodeId": "a"}}, "hasMore": true, "nextPageToken": "p2"},
{"nodes": []any{map[string]any{"nodeId": "b"}}, "hasMore": true, "nextPageToken": "p2"},
},
}}
err := runDocCoverage(t, List, caller, "--folder", "f", "--page-all", "--limit", "1")
if err == nil || len(caller.history) != 2 {
t.Fatalf("stalled pagination err=%v history=%#v", err, caller.history)
}
}
func TestCrossPlatformCoverageDocPaginationMaxItemsStopsAtPageBoundary(t *testing.T) {
caller := &docCoverageCaller{responses: map[string][]map[string]any{
"search_documents": {
{"documents": []any{map[string]any{"nodeId": "a"}, map[string]any{"nodeId": "b"}}, "hasMore": true, "nextPageToken": "p2"},
{"documents": []any{map[string]any{"nodeId": "c"}}, "hasMore": true, "nextPageToken": "p3"},
},
}}
if err := runDocCoverage(t, Search, caller, "--query", "report", "--page-all", "--limit", "2", "--max-items", "3"); err != nil {
t.Fatal(err)
}
if len(caller.history) != 2 || caller.history[1].params["pageSize"] != 1 || caller.history[1].params["pageToken"] != "p2" {
t.Fatalf("max-items pagination calls = %#v", caller.history)
}
}
func TestCrossPlatformCoverageDocPaginationRejectsServerPageOverflow(t *testing.T) {
caller := &docCoverageCaller{responses: map[string][]map[string]any{
"search_documents": {{
"documents": []any{map[string]any{"nodeId": "a"}, map[string]any{"nodeId": "b"}},
"hasMore": true, "nextPageToken": "p2",
}},
}}
err := runDocCoverage(t, Search, caller, "--query", "report", "--page-all", "--limit", "2", "--max-items", "1")
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "doc_pagination_page_size_exceeded" {
t.Fatalf("page overflow error = %#v", err)
}
if len(caller.history) != 1 || caller.history[0].params["pageSize"] != 1 {
t.Fatalf("page overflow calls = %#v", caller.history)
}
}
func TestCrossPlatformCoverageDocPaginationFinalBranchMatrix(t *testing.T) {
run := func(t *testing.T, caller *docCoverageCaller, options docPageOptions) (map[string]any, error) {
t.Helper()
declaration := Search
declaration.Execute = func(rt *shortcut.RuntimeContext) error {
result, err := collectDocPages(rt, "search_documents", "documents", map[string]any{"base": true}, searchDocsProject, options)
if err == nil {
err = rt.Output(result)
}
return err
}
err := runDocCoverage(t, declaration, caller)
return nil, err
}
defaults := &docCoverageCaller{responses: map[string][]map[string]any{
"search_documents": {{"documents": []any{map[string]any{"url": "u"}}}},
}}
if _, err := run(t, defaults, docPageOptions{Cursor: " initial "}); err != nil {
t.Fatal(err)
}
if len(defaults.history) != 1 || defaults.history[0].params["pageSize"] != 30 || defaults.history[0].params["pageToken"] != "initial" {
t.Fatalf("default pagination call = %#v", defaults.history)
}
readFailure := &docCoverageCaller{failAt: 1, responses: map[string][]map[string]any{}}
if _, err := run(t, readFailure, docPageOptions{PageAll: true}); err == nil {
t.Fatal("page read failure succeeded")
}
duplicate := &docCoverageCaller{responses: map[string][]map[string]any{
"search_documents": {{
"documents": []any{
map[string]any{"nodeId": "a"}, map[string]any{"nodeId": "a"}, map[string]any{"name": "no-key"},
},
"nextPageToken": "p2",
}},
}}
if _, err := run(t, duplicate, docPageOptions{PageAll: false, PageSize: 3, MaxPages: 2, MaxItems: 10}); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
response map[string]any
maxPages int
wantError bool
}{
{"unproven", map[string]any{"documents": []any{map[string]any{"nodeId": "a"}}}, 2, true},
{"missing cursor", map[string]any{"documents": []any{}, "hasMore": true}, 2, true},
{"max pages", map[string]any{"documents": []any{}, "hasMore": true, "nextPageToken": "p2"}, 1, false},
} {
t.Run(tc.name, func(t *testing.T) {
caller := &docCoverageCaller{responses: map[string][]map[string]any{"search_documents": {tc.response}}}
_, err := run(t, caller, docPageOptions{PageAll: true, PageSize: 1, MaxPages: tc.maxPages, MaxItems: 10})
if (err != nil) != tc.wantError {
t.Fatalf("err=%v wantError=%v", err, tc.wantError)
}
})
}
if got := pageItemKey(map[string]any{"url": " u "}); got != "url:u" {
t.Fatalf("URL page key = %q", got)
}
if got := pageItemKey(map[string]any{"id": 1}); got != "" {
t.Fatalf("invalid page key = %q", got)
}
if more, known, next := docPageState(map[string]any{"data": map[string]any{"has_more": true, "nextCursor": "c"}}); !more || !known || next != "c" {
t.Fatalf("nested page state = %v/%v/%q", more, known, next)
}
}