From 236bf7d2530dac902dd0e3b679d17f71098f15f6 Mon Sep 17 00:00:00 2001 From: "zhouyue.z" Date: Fri, 31 Jul 2026 12:18:18 +0800 Subject: [PATCH 1/2] fix(base): surface url resolve block list errors --- shortcuts/base/base_resolve.go | 21 +++++++++---- shortcuts/base/base_resolve_test.go | 46 ++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/shortcuts/base/base_resolve.go b/shortcuts/base/base_resolve.go index 31de0dbec6..73be53b7aa 100644 --- a/shortcuts/base/base_resolve.go +++ b/shortcuts/base/base_resolve.go @@ -195,7 +195,9 @@ func executeBaseURLResolve(runtime *common.RuntimeContext) error { switch classifyBaseURL(parsed) { case "base_url": out := resolveBaseURL(parsed) - enrichBaseResolveHint(runtime, out, resolveBaseURLSelection(parsed)) + if err := enrichBaseResolveHint(runtime, out, resolveBaseURLSelection(parsed)); err != nil { + return err + } runtime.OutFormat(out, nil, nil) return nil case "wiki_url": @@ -205,7 +207,9 @@ func executeBaseURLResolve(runtime *common.RuntimeContext) error { } selection := resolveBaseURLSelection(parsed) applyBaseURLSelection(out, selection) - enrichBaseResolveHint(runtime, out, selection) + if err := enrichBaseResolveHint(runtime, out, selection); err != nil { + return err + } runtime.OutFormat(out, nil, nil) return nil case "record_share_url": @@ -422,15 +426,19 @@ func executeBaseTitleResolve(runtime *common.RuntimeContext) error { } } -func enrichBaseResolveHint(runtime *common.RuntimeContext, out map[string]interface{}, selection baseURLSelection) { +func enrichBaseResolveHint(runtime *common.RuntimeContext, out map[string]interface{}, selection baseURLSelection) error { baseToken := strings.TrimSpace(common.GetString(out, "base_token")) selectedBlockID := strings.TrimSpace(common.GetString(out, "block_id")) if baseToken == "" || selectedBlockID == "" { out["hint"] = resolveHint("", nil) - return + return nil } - if block, found, err := resolveSelectedBaseBlock(runtime, baseToken, selectedBlockID); err == nil && found { + block, found, err := resolveSelectedBaseBlock(runtime, baseToken, selectedBlockID) + if err != nil { + return err + } + if found { out["block_type"] = block.Type if block.Name != "" { out["block_name"] = block.Name @@ -467,10 +475,11 @@ func enrichBaseResolveHint(runtime *common.RuntimeContext, out map[string]interf default: out["hint"] = resolveUnknownBlockHint() } - return + return nil } out["hint"] = resolveUnknownBlockHint() + return nil } type resolvedBaseBlock struct { diff --git a/shortcuts/base/base_resolve_test.go b/shortcuts/base/base_resolve_test.go index c1579e9c2a..93778b0b42 100644 --- a/shortcuts/base/base_resolve_test.go +++ b/shortcuts/base/base_resolve_test.go @@ -4,6 +4,7 @@ package base import ( + "errors" "net/http" "strings" "testing" @@ -67,7 +68,10 @@ func TestBaseURLResolveBaseURL(t *testing.T) { }) t.Run("unconfirmed selected block stays neutral", func(t *testing.T) { - factory, stdout, _ := newExecuteFactory(t) + factory, stdout, reg := newExecuteFactory(t) + reg.Register(baseBlockListResolveStub("bas123", + map[string]interface{}{"id": "tbl_other", "type": "table", "name": "Other"}, + )) err := runShortcutWithAuthTypes(t, BaseURLResolve, authTypes(), []string{ "+url-resolve", "--url", "https://example.larkoffice.com/base/bas123?table=tbl123&view=vew_stale&record=rec_stale", "--as", "user", }, factory, stdout) @@ -96,6 +100,46 @@ func TestBaseURLResolveBaseURL(t *testing.T) { } }) + t.Run("block list error is returned", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/base/v3/bases/bas123/blocks/list", + Body: map[string]interface{}{ + "code": 99991672, + "msg": "access denied", + "error": map[string]interface{}{ + "permission_violations": []interface{}{ + map[string]interface{}{"subject": "base:block:read"}, + }, + }, + }, + }) + err := runShortcutWithAuthTypes(t, BaseURLResolve, authTypes(), []string{ + "+url-resolve", "--url", "https://example.larkoffice.com/base/bas123?table=tbl123&view=vew_stale", "--as", "bot", + }, factory, stdout) + if err == nil { + t.Fatal("expected block-list error to be returned") + } + p, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("expected typed problem, got %T %v", err, err) + } + if p.Category != errs.CategoryAuthorization || p.Subtype != errs.SubtypeAppScopeNotApplied || p.Code != 99991672 { + t.Fatalf("unexpected problem: %#v", p) + } + var permissionErr *errs.PermissionError + if !errors.As(err, &permissionErr) { + t.Fatalf("expected PermissionError, got %T %v", err, err) + } + if len(permissionErr.MissingScopes) != 1 || permissionErr.MissingScopes[0] != "base:block:read" { + t.Fatalf("missing scopes=%v", permissionErr.MissingScopes) + } + if stdout.Len() != 0 { + t.Fatalf("stdout should stay empty when block-list fails: %s", stdout.String()) + } + }) + t.Run("field endpoint does not confirm untyped block", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) reg.Register(baseBlockListResolveStub("bas123", From 58d488e379cdfb8449134eb8de4edc06a9d0b2f3 Mon Sep 17 00:00:00 2001 From: "zhouyue.z" Date: Fri, 31 Jul 2026 12:56:27 +0800 Subject: [PATCH 2/2] test(base): cover wiki url resolve block errors --- shortcuts/base/base_resolve_test.go | 91 ++++++++++++++++++----------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/shortcuts/base/base_resolve_test.go b/shortcuts/base/base_resolve_test.go index 93778b0b42..5cfa363367 100644 --- a/shortcuts/base/base_resolve_test.go +++ b/shortcuts/base/base_resolve_test.go @@ -102,42 +102,11 @@ func TestBaseURLResolveBaseURL(t *testing.T) { t.Run("block list error is returned", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) - reg.Register(&httpmock.Stub{ - Method: "POST", - URL: "/open-apis/base/v3/bases/bas123/blocks/list", - Body: map[string]interface{}{ - "code": 99991672, - "msg": "access denied", - "error": map[string]interface{}{ - "permission_violations": []interface{}{ - map[string]interface{}{"subject": "base:block:read"}, - }, - }, - }, - }) + reg.Register(baseBlockListScopeErrorStub("bas123")) err := runShortcutWithAuthTypes(t, BaseURLResolve, authTypes(), []string{ "+url-resolve", "--url", "https://example.larkoffice.com/base/bas123?table=tbl123&view=vew_stale", "--as", "bot", }, factory, stdout) - if err == nil { - t.Fatal("expected block-list error to be returned") - } - p, ok := errs.ProblemOf(err) - if !ok { - t.Fatalf("expected typed problem, got %T %v", err, err) - } - if p.Category != errs.CategoryAuthorization || p.Subtype != errs.SubtypeAppScopeNotApplied || p.Code != 99991672 { - t.Fatalf("unexpected problem: %#v", p) - } - var permissionErr *errs.PermissionError - if !errors.As(err, &permissionErr) { - t.Fatalf("expected PermissionError, got %T %v", err, err) - } - if len(permissionErr.MissingScopes) != 1 || permissionErr.MissingScopes[0] != "base:block:read" { - t.Fatalf("missing scopes=%v", permissionErr.MissingScopes) - } - if stdout.Len() != 0 { - t.Fatalf("stdout should stay empty when block-list fails: %s", stdout.String()) - } + assertBaseBlockReadPermissionError(t, err, stdout) }) t.Run("field endpoint does not confirm untyped block", func(t *testing.T) { @@ -313,6 +282,22 @@ func baseBlockListResolveStub(baseToken string, blocks ...map[string]interface{} } } +func baseBlockListScopeErrorStub(baseToken string) *httpmock.Stub { + return &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/base/v3/bases/" + baseToken + "/blocks/list", + Body: map[string]interface{}{ + "code": 99991672, + "msg": "access denied", + "error": map[string]interface{}{ + "permission_violations": []interface{}{ + map[string]interface{}{"subject": "base:block:read"}, + }, + }, + }, + } +} + func TestBaseURLResolveWikiURL(t *testing.T) { t.Run("bitable", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) @@ -353,6 +338,19 @@ func TestBaseURLResolveWikiURL(t *testing.T) { } }) + t.Run("bitable table coordinates return block list error", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(wikiBaseNodeStub("wik123", "bas123", "Demo Base")) + reg.Register(baseBlockListScopeErrorStub("bas123")) + + err := runShortcutWithAuthTypes(t, BaseURLResolve, authTypes(), []string{ + "+url-resolve", + "--url", "https://example.larkoffice.com/wiki/wik123?table=tbl123&view=vew_stale", + "--as", "bot", + }, factory, stdout) + assertBaseBlockReadPermissionError(t, err, stdout) + }) + t.Run("bitable with dashboard selection", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) reg.Register(wikiBaseNodeStub("wik123", "bas123", "Demo Base")) @@ -420,6 +418,33 @@ func wikiBaseNodeStub(wikiToken, baseToken, title string) *httpmock.Stub { } } +func assertBaseBlockReadPermissionError(t *testing.T, err error, stdout interface { + Len() int + String() string +}) { + t.Helper() + if err == nil { + t.Fatal("expected block-list error to be returned") + } + p, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("expected typed problem, got %T %v", err, err) + } + if p.Category != errs.CategoryAuthorization || p.Subtype != errs.SubtypeAppScopeNotApplied || p.Code != 99991672 { + t.Fatalf("unexpected problem: %#v", p) + } + var permissionErr *errs.PermissionError + if !errors.As(err, &permissionErr) { + t.Fatalf("expected PermissionError, got %T %v", err, err) + } + if len(permissionErr.MissingScopes) != 1 || permissionErr.MissingScopes[0] != "base:block:read" { + t.Fatalf("missing scopes=%v", permissionErr.MissingScopes) + } + if stdout.Len() != 0 { + t.Fatalf("stdout should stay empty when block-list fails: %s", stdout.String()) + } +} + func TestBaseURLResolveRecordShareURL(t *testing.T) { t.Run("enriched", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t)