Compare commits

...

2 Commits

Author SHA1 Message Date
zhouyue.z
58d488e379 test(base): cover wiki url resolve block errors 2026-07-31 12:56:27 +08:00
zhouyue.z
236bf7d253 fix(base): surface url resolve block list errors 2026-07-31 12:18:18 +08:00
2 changed files with 85 additions and 7 deletions

View File

@@ -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 {

View File

@@ -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,15 @@ func TestBaseURLResolveBaseURL(t *testing.T) {
}
})
t.Run("block list error is returned", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
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)
assertBaseBlockReadPermissionError(t, err, stdout)
})
t.Run("field endpoint does not confirm untyped block", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(baseBlockListResolveStub("bas123",
@@ -269,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)
@@ -309,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"))
@@ -376,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)