Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,20 @@ linters:
- forbidigo
# errs-typed-only enforced on paths already migrated to errs.NewXxxError.
# Add a path when its migration is complete.
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/calendar/|shortcuts/drive/|shortcuts/mail/|shortcuts/base/)
- path-except: (internal/auth/|internal/errcompat/|internal/errclass/|internal/client/|internal/cmdutil/factory\.go|cmd/auth/|cmd/config/|cmd/service/|shortcuts/common/mcp_client\.go|shortcuts/calendar/|shortcuts/drive/|shortcuts/mail/|shortcuts/base/|shortcuts/apps/)
text: errs-typed-only
linters:
- forbidigo
# errs-no-bare-wrap enforced on paths fully migrated to typed final
# errors. Scoped separately from errs-typed-only because cmd/auth/,
# cmd/config/ still have residual fmt.Errorf and must not be caught.
- path-except: (shortcuts/drive/|shortcuts/mail/|shortcuts/base/|shortcuts/calendar/|shortcuts/common/mcp_client\.go)
- path-except: (shortcuts/drive/|shortcuts/mail/|shortcuts/base/|shortcuts/calendar/|shortcuts/apps/|shortcuts/common/mcp_client\.go)
text: errs-no-bare-wrap
linters:
- forbidigo
# errs-no-legacy-helper enforced on domains whose shared validation/save
# helpers have migrated to typed final errors.
- path-except: (shortcuts/drive/|shortcuts/mail/|shortcuts/base/|shortcuts/calendar/)
- path-except: (shortcuts/drive/|shortcuts/mail/|shortcuts/base/|shortcuts/calendar/|shortcuts/apps/)
text: errs-no-legacy-helper
linters:
- forbidigo
Expand Down
15 changes: 15 additions & 0 deletions internal/errclass/codemeta_apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package errclass

import "github.com/larksuite/cli/errs"

// appsCodeMeta holds Miaoda apps-service Lark code -> CodeMeta mappings.
// Only stable endpoint semantics are registered globally; endpoint-specific
// recovery hints stay in shortcuts/apps.
var appsCodeMeta = map[int]CodeMeta{
90002: {Category: errs.CategoryAPI, Subtype: errs.SubtypeNotFound}, // app_id unknown or caller lacks access
}

func init() { mergeCodeMeta(appsCodeMeta, "apps") }
1 change: 1 addition & 0 deletions lint/errscontract/rule_no_legacy_common_helper_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
// legacy validation/save helpers are forbidden; callers must use the typed
// common replacements or construct an errs.* typed error directly.
var migratedCommonHelperPaths = []string{
"shortcuts/apps/",
"shortcuts/base/",
"shortcuts/drive/",
"shortcuts/mail/",
Expand Down
1 change: 1 addition & 0 deletions lint/errscontract/rule_no_legacy_envelope_literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
// call sites must return a typed errs.* error instead. Future domains opt in by
// appending their path prefix here.
var migratedEnvelopePaths = []string{
"shortcuts/apps/",
"shortcuts/base/",
"shortcuts/drive/",
"shortcuts/mail/",
Expand Down
5 changes: 2 additions & 3 deletions shortcuts/apps/apps_access_scope_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"io"
"strings"

"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
Expand All @@ -29,7 +28,7 @@
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
if strings.TrimSpace(rctx.Str("app-id")) == "" {
return output.ErrValidation("--app-id is required")
return appsValidationParamError("--app-id", "--app-id is required")

Check warning on line 31 in shortcuts/apps/apps_access_scope_get.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_get.go#L31

Added line #L31 was not covered by tests
}
return nil
},
Expand All @@ -42,7 +41,7 @@
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
appID := strings.TrimSpace(rctx.Str("app-id"))
path := fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID))
data, err := rctx.CallAPI("GET", path, nil, nil)
data, err := rctx.CallAPITyped("GET", path, nil, nil)
if err != nil {
return err
}
Expand Down
41 changes: 23 additions & 18 deletions shortcuts/apps/apps_access_scope_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"io"
"strings"

"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
Expand Down Expand Up @@ -40,7 +39,7 @@
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
if strings.TrimSpace(rctx.Str("app-id")) == "" {
return output.ErrValidation("--app-id is required")
return appsValidationParamError("--app-id", "--app-id is required")

Check warning on line 42 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L42

Added line #L42 was not covered by tests
}
return validateAccessScopeFlags(rctx)
},
Expand All @@ -64,7 +63,7 @@
}
appID := strings.TrimSpace(rctx.Str("app-id"))
path := fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID))
data, err := rctx.CallAPI("PUT", path, nil, body)
data, err := rctx.CallAPITyped("PUT", path, nil, body)
if err != nil {
return err
}
Expand All @@ -85,55 +84,61 @@
switch scope {
case "specific":
if targets == "" {
return output.ErrValidation("--targets is required when --scope=specific")
return appsValidationParamError("--targets", "--targets is required when --scope=specific")
}
if err := validateTargetsJSON(targets); err != nil {
return err
}
if approver != "" && !applyEnabled {
return output.ErrValidation("--approver requires --apply-enabled")
return appsValidationParamError("--approver", "--approver requires --apply-enabled")
}
if requireLogin {
return output.ErrValidation("--require-login is not allowed when --scope=specific")
return appsValidationParamError("--require-login", "--require-login is not allowed when --scope=specific")

Check warning on line 96 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L96

Added line #L96 was not covered by tests
}
case "public":
if targets != "" {
return output.ErrValidation("--targets is not allowed when --scope=public")
return appsValidationParamError("--targets", "--targets is not allowed when --scope=public")

Check warning on line 100 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L100

Added line #L100 was not covered by tests
}
if applyEnabled {
return output.ErrValidation("--apply-enabled is not allowed when --scope=public")
return appsValidationParamError("--apply-enabled", "--apply-enabled is not allowed when --scope=public")

Check warning on line 103 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L103

Added line #L103 was not covered by tests
}
if approver != "" {
return output.ErrValidation("--approver is not allowed when --scope=public")
return appsValidationParamError("--approver", "--approver is not allowed when --scope=public")
}
if !rctx.Cmd.Flags().Changed("require-login") {
return output.ErrValidation("--require-login is required when --scope=public (pass true or false explicitly; do not rely on the default)")
return appsValidationParamError("--require-login", "--require-login is required when --scope=public (pass true or false explicitly; do not rely on the default)")
}
case "tenant":
if targets != "" || applyEnabled || approver != "" || requireLogin {
return output.ErrValidation("no extra flags allowed when --scope=tenant")
return appsValidationError("no extra flags allowed when --scope=tenant").
WithParams(
appsInvalidParam("--targets", "not allowed when --scope=tenant"),
appsInvalidParam("--apply-enabled", "not allowed when --scope=tenant"),
appsInvalidParam("--approver", "not allowed when --scope=tenant"),
appsInvalidParam("--require-login", "not allowed when --scope=tenant"),
)
}
default:
return output.ErrValidation("--scope must be specific / public / tenant")
return appsValidationParamError("--scope", "--scope must be specific / public / tenant")

Check warning on line 122 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L122

Added line #L122 was not covered by tests
}
return nil
}

func validateTargetsJSON(targetsJSON string) error {
var items []map[string]interface{}
if err := json.Unmarshal([]byte(targetsJSON), &items); err != nil {
return output.ErrValidation("--targets is not valid JSON: %v", err)
return appsValidationParamError("--targets", "--targets is not valid JSON: %v", err).WithCause(err)

Check warning on line 130 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L130

Added line #L130 was not covered by tests
}
if len(items) == 0 {
return output.ErrValidation("--targets must contain at least one entry; specific scope requires concrete user/department/chat ids")
return appsValidationParamError("--targets", "--targets must contain at least one entry; specific scope requires concrete user/department/chat ids")
}
for i, t := range items {
typ, _ := t["type"].(string)
if !allowedAccessTargetTypes[typ] {
return output.ErrValidation("--targets[%d].type %q must be one of: user / department / chat", i, typ)
return appsValidationParamError("--targets", "--targets[%d].type %q must be one of: user / department / chat", i, typ)
}
if id, _ := t["id"].(string); strings.TrimSpace(id) == "" {
return output.ErrValidation("--targets[%d].id is empty", i)
return appsValidationParamError("--targets", "--targets[%d].id is empty", i)

Check warning on line 141 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L141

Added line #L141 was not covered by tests
}
}
return nil
Expand All @@ -152,7 +157,7 @@
scope := rctx.Str("scope")
enum, ok := scopeStringToServerEnum[scope]
if !ok {
return nil, output.ErrValidation("--scope must be specific / public / tenant, got %q", scope)
return nil, appsValidationParamError("--scope", "--scope must be specific / public / tenant, got %q", scope)

Check warning on line 160 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L160

Added line #L160 was not covered by tests
}
body := map[string]interface{}{"scope": enum}

Expand All @@ -161,7 +166,7 @@
// 用户传统一格式 [{type:user|department|chat, id:...}],body 里拆 3 个并列数组发后端。
var targets []map[string]interface{}
if err := json.Unmarshal([]byte(rctx.Str("targets")), &targets); err != nil {
return nil, output.ErrValidation("--targets is not valid JSON: %v", err)
return nil, appsValidationParamError("--targets", "--targets is not valid JSON: %v", err).WithCause(err)

Check warning on line 169 in shortcuts/apps/apps_access_scope_set.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_access_scope_set.go#L169

Added line #L169 was not covered by tests
}
users, departments, chats := splitAccessScopeTargets(targets)
if len(users) > 0 {
Expand Down
9 changes: 4 additions & 5 deletions shortcuts/apps/apps_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"io"
"strings"

"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
)

Expand All @@ -30,14 +29,14 @@
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
if strings.TrimSpace(rctx.Str("name")) == "" {
return output.ErrValidation("--name is required")
return appsValidationParamError("--name", "--name is required")

Check warning on line 32 in shortcuts/apps/apps_create.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_create.go#L32

Added line #L32 was not covered by tests
}
appType := strings.TrimSpace(rctx.Str("app-type"))
if appType == "" {
return output.ErrValidation("--app-type is required")
return appsValidationParamError("--app-type", "--app-type is required")

Check warning on line 36 in shortcuts/apps/apps_create.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_create.go#L36

Added line #L36 was not covered by tests
}
if !validAppTypes[appType] {
return output.ErrValidation(fmt.Sprintf("--app-type %q is not supported (allowed: HTML)", appType))
return appsValidationParamError("--app-type", "--app-type %q is not supported (allowed: HTML)", appType)
}
return nil
},
Expand All @@ -48,7 +47,7 @@
Body(buildAppsCreateBody(rctx))
},
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
data, err := rctx.CallAPI("POST", apiBasePath+"/apps", nil, buildAppsCreateBody(rctx))
data, err := rctx.CallAPITyped("POST", apiBasePath+"/apps", nil, buildAppsCreateBody(rctx))
if err != nil {
return err
}
Expand Down
26 changes: 26 additions & 0 deletions shortcuts/apps/apps_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/spf13/cobra"

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
Expand Down Expand Up @@ -46,6 +47,31 @@ func runAppsShortcut(t *testing.T, sc common.Shortcut, args []string, factory *c
return parent.ExecuteContext(context.Background())
}

func requireAppsProblem(t *testing.T, err error, category errs.Category) *errs.Problem {
t.Helper()
if err == nil {
t.Fatalf("expected error")
}
p, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed problem, got %T: %v", err, err)
}
if p.Category != category {
t.Fatalf("error category = %q, want %q", p.Category, category)
}
return p
}

func requireAppsValidationProblem(t *testing.T, err error) *errs.Problem {
t.Helper()
return requireAppsProblem(t, err, errs.CategoryValidation)
}

func requireAppsAPIProblem(t *testing.T, err error) *errs.Problem {
t.Helper()
return requireAppsProblem(t, err, errs.CategoryAPI)
}

// +create 测试

func TestAppsCreate_Success(t *testing.T) {
Expand Down
73 changes: 73 additions & 0 deletions shortcuts/apps/apps_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package apps

import (
"errors"

"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/client"
)

func appsValidationError(format string, args ...any) *errs.ValidationError {
return errs.NewValidationError(errs.SubtypeInvalidArgument, format, args...)
}

func appsValidationParamError(param, format string, args ...any) *errs.ValidationError {
return appsValidationError(format, args...).WithParam(param)
}

func appsInvalidParam(name, reason string) errs.InvalidParam {
return errs.InvalidParam{Name: name, Reason: reason}
}

func appsFailedPreconditionParamError(param, format string, args ...any) *errs.ValidationError {
return errs.NewValidationError(errs.SubtypeFailedPrecondition, format, args...).WithParam(param)
}

func appsInputPathError(err error) error {
if err == nil {
return nil

Check warning on line 32 in shortcuts/apps/apps_errors.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_errors.go#L32

Added line #L32 was not covered by tests
}
if errors.Is(err, fileio.ErrPathValidation) {
return appsValidationParamError("--path", "unsafe --path: %s", err).WithCause(err)

Check warning on line 35 in shortcuts/apps/apps_errors.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_errors.go#L35

Added line #L35 was not covered by tests
}
return appsValidationParamError("--path", "cannot read --path: %s", err).WithCause(err)
}

func appsInputPathEntryError(path string, err error) error {
if err == nil {
return nil

Check warning on line 42 in shortcuts/apps/apps_errors.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_errors.go#L42

Added line #L42 was not covered by tests
}
if errors.Is(err, fileio.ErrPathValidation) {
return appsValidationParamError("--path", "unsafe --path entry %s: %s", path, err).WithCause(err)

Check warning on line 45 in shortcuts/apps/apps_errors.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_errors.go#L45

Added line #L45 was not covered by tests
}
return appsValidationParamError("--path", "cannot read --path entry %s: %s", path, err).WithCause(err)
}

func appsFileIOError(err error, format string, args ...any) *errs.InternalError {
return errs.NewInternalError(errs.SubtypeFileIO, format, args...).WithCause(err)
}

func appsAPIBoundaryError(err error) error {
return client.WrapDoAPIError(err)
}

func enrichHTMLPublishAPIError(err error) error {
if err == nil {
return nil

Check warning on line 60 in shortcuts/apps/apps_errors.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_errors.go#L60

Added line #L60 was not covered by tests
}
p, ok := errs.ProblemOf(err)
if !ok {
return appsAPIBoundaryError(err)

Check warning on line 64 in shortcuts/apps/apps_errors.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/apps/apps_errors.go#L64

Added line #L64 was not covered by tests
}
if p.Message != "" {
p.Message = "html-publish failed: " + p.Message
}
if hint := buildHTMLPublishFailureHint(p.Code); hint != "" {
p.Hint = hint
}
return err
}
Loading
Loading