Skip to content

Commit 986b98e

Browse files
test: strengthen doc fetch error coverage
1 parent a141ee8 commit 986b98e

2 files changed

Lines changed: 237 additions & 18 deletions

File tree

shortcuts/doc/docs_fetch_im_markdown_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,202 @@ func TestNewIMMarkdownContextExtractsBaseURL(t *testing.T) {
10831083
}
10841084
}
10851085

1086+
func TestIMMarkdownBaseURLFromInputEdges(t *testing.T) {
1087+
t.Parallel()
1088+
1089+
tests := []struct {
1090+
name string
1091+
input string
1092+
want string
1093+
ok bool
1094+
}{
1095+
{
1096+
name: "empty candidate before marker is skipped",
1097+
input: "///docx/doc_token",
1098+
},
1099+
{
1100+
name: "scheme candidate before marker is returned",
1101+
input: "//https://tenant.example.com/docx/doc_token",
1102+
want: "https://tenant.example.com",
1103+
ok: true,
1104+
},
1105+
{
1106+
name: "host without dot before marker is rejected",
1107+
input: "tenant/docx/doc_token",
1108+
},
1109+
{
1110+
name: "no document marker is rejected",
1111+
input: "tenant.example.com/path/doc_token",
1112+
},
1113+
}
1114+
1115+
for _, tt := range tests {
1116+
t.Run(tt.name, func(t *testing.T) {
1117+
t.Parallel()
1118+
1119+
got, ok := imMarkdownBaseURLFromInput(tt.input)
1120+
if ok != tt.ok {
1121+
t.Fatalf("ok = %v, want %v", ok, tt.ok)
1122+
}
1123+
if got != tt.want {
1124+
t.Fatalf("baseURL = %q, want %q", got, tt.want)
1125+
}
1126+
})
1127+
}
1128+
}
1129+
1130+
func TestIMMarkdownHandlerDirectFallbackBranches(t *testing.T) {
1131+
t.Parallel()
1132+
1133+
ctx := imMarkdownContext{baseURL: "https://tenant.example.com"}
1134+
cases := []struct {
1135+
name string
1136+
got string
1137+
want string
1138+
}{
1139+
{
1140+
name: "empty heading",
1141+
got: handleIMMarkdownHeading(2)("", " ", nil, ctx),
1142+
want: "",
1143+
},
1144+
{
1145+
name: "empty paragraph",
1146+
got: handleIMMarkdownParagraph("", " ", nil, ctx),
1147+
want: "",
1148+
},
1149+
{
1150+
name: "list item seq trims trailing dot",
1151+
got: handleIMMarkdownListItem("", "first\nsecond", map[string]string{"seq": "7."}, ctx),
1152+
want: "7. first\n second\n",
1153+
},
1154+
{
1155+
name: "list item seq auto uses bullet",
1156+
got: handleIMMarkdownListItem("", "first", map[string]string{"seq": "auto"}, ctx),
1157+
want: "- first\n",
1158+
},
1159+
{
1160+
name: "empty list item",
1161+
got: handleIMMarkdownListItem("", " ", map[string]string{"seq": "3"}, ctx),
1162+
want: "",
1163+
},
1164+
{
1165+
name: "empty callout",
1166+
got: handleIMMarkdownCallout("", "", nil, ctx),
1167+
want: "---\n---",
1168+
},
1169+
{
1170+
name: "empty blockquote",
1171+
got: handleIMMarkdownBlockquote("", " ", nil, ctx),
1172+
want: "",
1173+
},
1174+
{
1175+
name: "blockquote preserves blank quote lines",
1176+
got: handleIMMarkdownBlockquote("", "first\n\nsecond", nil, ctx),
1177+
want: "> first\n>\n> second",
1178+
},
1179+
{
1180+
name: "empty latex",
1181+
got: handleIMMarkdownLatex("", " ", nil, ctx),
1182+
want: "",
1183+
},
1184+
{
1185+
name: "image without URL",
1186+
got: handleIMMarkdownImage("", "", map[string]string{"alt": "A"}, ctx),
1187+
want: "",
1188+
},
1189+
{
1190+
name: "empty strong",
1191+
got: handleIMMarkdownStrong("", " ", nil, ctx),
1192+
want: "",
1193+
},
1194+
{
1195+
name: "empty emphasis",
1196+
got: handleIMMarkdownEmphasis("", " ", nil, ctx),
1197+
want: "",
1198+
},
1199+
{
1200+
name: "empty delete",
1201+
got: handleIMMarkdownDelete("", " ", nil, ctx),
1202+
want: "",
1203+
},
1204+
{
1205+
name: "anchor without href",
1206+
got: handleIMMarkdownAnchor("", "<b>plain</b>", nil, ctx),
1207+
want: "**plain**",
1208+
},
1209+
{
1210+
name: "table skips rows without cells",
1211+
got: handleIMMarkdownTable("<table><tr></tr></table>", "<tr></tr>", nil, ctx),
1212+
want: "`<table><tr></tr></table>`",
1213+
},
1214+
{
1215+
name: "empty normalized table cell",
1216+
got: normalizeIMMarkdownTableCell("<span> </span>"),
1217+
want: "",
1218+
},
1219+
{
1220+
name: "plain fenced code uses minimum fence",
1221+
got: imMarkdownFencedCode("plain", ""),
1222+
want: "```\nplain\n```",
1223+
},
1224+
}
1225+
1226+
for _, tt := range cases {
1227+
t.Run(tt.name, func(t *testing.T) {
1228+
t.Parallel()
1229+
1230+
if tt.got != tt.want {
1231+
t.Fatalf("got %q, want %q", tt.got, tt.want)
1232+
}
1233+
})
1234+
}
1235+
}
1236+
1237+
func TestIMMarkdownExtractionAndListBreakBranches(t *testing.T) {
1238+
t.Parallel()
1239+
1240+
rowBodies := extractIMMarkdownElementBodies(`</tr><tr/><tr>open`, imMarkdownRowTagRE)
1241+
if want := []string{""}; !reflect.DeepEqual(rowBodies, want) {
1242+
t.Fatalf("extractIMMarkdownElementBodies() = %#v, want %#v", rowBodies, want)
1243+
}
1244+
1245+
if _, _, ok := findIMMarkdownElementClosingTag(`<tr><td>open</td>`, len("<tr>"), imMarkdownRowTagRE); ok {
1246+
t.Fatal("findIMMarkdownElementClosingTag() found closing tag, want false")
1247+
}
1248+
1249+
if got := convertIMMarkdownListItems("", false, imMarkdownContext{}); got != "" {
1250+
t.Fatalf("empty list conversion = %q, want empty", got)
1251+
}
1252+
if got := convertIMMarkdownListItems("<li>open", false, imMarkdownContext{}); got != "" {
1253+
t.Fatalf("unclosed list conversion = %q, want empty", got)
1254+
}
1255+
if _, _, ok := findIMMarkdownListItemClosingTag(`<li>outer<li>inner</li>`, len("<li>")); ok {
1256+
t.Fatal("findIMMarkdownListItemClosingTag() found closing tag for unbalanced nested item")
1257+
}
1258+
}
1259+
1260+
func TestIMMarkdownLinkAndEncodingFallbackBranches(t *testing.T) {
1261+
t.Parallel()
1262+
1263+
text, href, ok := extractIMMarkdownInnerLink(`<a href='https://example.com/ref'></a>`)
1264+
if !ok {
1265+
t.Fatal("extractIMMarkdownInnerLink() ok = false, want true")
1266+
}
1267+
if text != "https://example.com/ref" || href != "https://example.com/ref" {
1268+
t.Fatalf("inner link = (%q, %q), want href fallback", text, href)
1269+
}
1270+
1271+
if got := escapeMarkdownLinkDestination("a%zz%"); got != "a%25zz%25" {
1272+
t.Fatalf("escaped invalid percent = %q, want %q", got, "a%25zz%25")
1273+
}
1274+
if got := escapeMarkdownLinkDestination("研发"); got != "%E7%A0%94%E5%8F%91" {
1275+
t.Fatalf("escaped unicode = %q, want encoded UTF-8 bytes", got)
1276+
}
1277+
if got := escapeMarkdownLinkDestination(string([]byte{'a', 0xff, 'b'})); got != "a%FFb" {
1278+
t.Fatalf("escaped invalid UTF-8 = %q, want %q", got, "a%FFb")
1279+
}
1280+
}
1281+
10861282
type imMarkdownCase struct {
10871283
name string
10881284
input string

shortcuts/doc/docs_fetch_v2_test.go

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ package doc
66
import (
77
"context"
88
"encoding/json"
9+
"errors"
910
"reflect"
1011
"strings"
1112
"testing"
1213

14+
"github.com/larksuite/cli/errs"
1315
"github.com/larksuite/cli/internal/cmdutil"
1416
"github.com/larksuite/cli/internal/core"
1517
"github.com/larksuite/cli/internal/httpmock"
@@ -252,9 +254,10 @@ func TestValidateReadModeFlagsRejectsInvalidScopeOptions(t *testing.T) {
252254
t.Parallel()
253255

254256
tests := []struct {
255-
name string
256-
setFlags map[string]string
257-
wantParam string
257+
name string
258+
setFlags map[string]string
259+
wantParam string
260+
wantParams []string
258261
}{
259262
{
260263
name: "negative context before",
@@ -288,7 +291,10 @@ func TestValidateReadModeFlagsRejectsInvalidScopeOptions(t *testing.T) {
288291
setFlags: map[string]string{
289292
"scope": "range",
290293
},
291-
wantParam: "--start-block-id",
294+
wantParams: []string{
295+
"--start-block-id",
296+
"--end-block-id",
297+
},
292298
},
293299
{
294300
name: "keyword needs keyword",
@@ -326,9 +332,7 @@ func TestValidateReadModeFlagsRejectsInvalidScopeOptions(t *testing.T) {
326332
if err == nil {
327333
t.Fatal("validateReadModeFlags() succeeded, want error")
328334
}
329-
if !strings.Contains(err.Error(), tt.wantParam) {
330-
t.Fatalf("error = %v, want param %s", err, tt.wantParam)
331-
}
335+
assertValidationContract(t, err, errs.SubtypeInvalidArgument, tt.wantParam, tt.wantParams...)
332336
})
333337
}
334338
}
@@ -389,23 +393,23 @@ func TestValidateFetchV2RejectsInvalidDocAndScope(t *testing.T) {
389393
t.Parallel()
390394

391395
tests := []struct {
392-
name string
393-
setFlags map[string]string
394-
want string
396+
name string
397+
setFlags map[string]string
398+
wantParam string
395399
}{
396400
{
397401
name: "invalid doc",
398402
setFlags: map[string]string{
399403
"doc": "https://example.com/sheets/sht_token",
400404
},
401-
want: "--doc",
405+
wantParam: "--doc",
402406
},
403407
{
404408
name: "invalid scope",
405409
setFlags: map[string]string{
406410
"scope": "bad",
407411
},
408-
want: "--scope",
412+
wantParam: "--scope",
409413
},
410414
}
411415

@@ -418,9 +422,7 @@ func TestValidateFetchV2RejectsInvalidDocAndScope(t *testing.T) {
418422
if err == nil {
419423
t.Fatal("validateFetchV2() succeeded, want error")
420424
}
421-
if !strings.Contains(err.Error(), tt.want) {
422-
t.Fatalf("error = %v, want %s", err, tt.want)
423-
}
425+
assertValidationContract(t, err, errs.SubtypeInvalidArgument, tt.wantParam)
424426
})
425427
}
426428
}
@@ -651,7 +653,7 @@ func TestDocsFetchV2ReturnsAPIError(t *testing.T) {
651653
Method: "POST",
652654
URL: "/open-apis/docs_ai/v1/documents/doxcnFetchAPIError/fetch",
653655
Body: map[string]interface{}{
654-
"code": 99991663,
656+
"code": 999999,
655657
"msg": "fetch failed",
656658
},
657659
})
@@ -664,8 +666,28 @@ func TestDocsFetchV2ReturnsAPIError(t *testing.T) {
664666
if err == nil {
665667
t.Fatal("mountAndRunDocs() succeeded, want API error")
666668
}
667-
if !strings.Contains(err.Error(), "fetch failed") {
668-
t.Fatalf("error = %v, want API message", err)
669+
var apiErr *errs.APIError
670+
if !errors.As(err, &apiErr) {
671+
t.Fatalf("error type = %T, want *errs.APIError (%v)", err, err)
672+
}
673+
p, ok := errs.ProblemOf(err)
674+
if !ok {
675+
t.Fatalf("ProblemOf() ok = false for %T (%v)", err, err)
676+
}
677+
if p.Category != errs.CategoryAPI {
678+
t.Errorf("category = %q, want %q", p.Category, errs.CategoryAPI)
679+
}
680+
if p.Subtype != errs.SubtypeUnknown {
681+
t.Errorf("subtype = %q, want %q", p.Subtype, errs.SubtypeUnknown)
682+
}
683+
if p.Code != 999999 {
684+
t.Errorf("code = %d, want 999999", p.Code)
685+
}
686+
if p.Message != "fetch failed" {
687+
t.Errorf("message = %q, want %q", p.Message, "fetch failed")
688+
}
689+
if cause := errors.Unwrap(err); cause != nil {
690+
t.Fatalf("unexpected wrapped cause for API response error: %T %v", cause, cause)
669691
}
670692
}
671693

@@ -754,6 +776,7 @@ func TestDocsFetchRejectsLegacyFlags(t *testing.T) {
754776
if err == nil {
755777
t.Fatal("expected v2-only validation error")
756778
}
779+
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--offset")
757780
for _, want := range tt.want {
758781
if !strings.Contains(err.Error(), want) {
759782
t.Fatalf("error missing %q: %v", want, err)

0 commit comments

Comments
 (0)