Files
larksuite__cli/internal/errclass/classify_internal_test.go
zhaoyukun.yk 4dfe1b7dc0 feat(auth): support managed external credentials
Add Extended-only direct, credential-proxy, and platform-proxy modes while keeping Standard behavior and external-credential product details isolated behind source-neutral runtime capabilities.

Fail closed across helper execution, proxy routing, streams, managed file transfers, and unsupported event flows. Harden system configuration, edition updates, and release publication.

Cover Standard compatibility and Extended integration with contract tests, repository-local three-mode E2E, native trust checks, and CI and release gates.
2026-08-12 14:55:41 +08:00

141 lines
5.0 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package errclass
import (
"errors"
"strings"
"testing"
"github.com/larksuite/cli/errs"
)
// TestBuildAPIError_CategoryConfirmationFillsRiskAction pins fail-closed
// behaviour: a code mapped to CategoryConfirmation MUST yield a
// ConfirmationRequiredError whose Risk + Action are non-empty even when the
// CodeMeta itself carries no Risk/Action hints. Risk falls back to
// RiskUnknown; Action falls back to ctx.LarkCmd.
func TestBuildAPIError_CategoryConfirmationFillsRiskAction(t *testing.T) {
const stubCode = 99999991
codeMeta[stubCode] = CodeMeta{
Category: errs.CategoryConfirmation,
Subtype: errs.SubtypeConfirmationRequired,
}
t.Cleanup(func() { delete(codeMeta, stubCode) })
resp := map[string]any{"code": stubCode, "msg": "confirmation required"}
ctx := ClassifyContext{
Brand: "feishu",
AppID: "cli_test",
Identity: "user",
LarkCmd: "drive +delete",
}
err := BuildAPIError(resp, ctx)
var confirmErr *errs.ConfirmationRequiredError
if !errors.As(err, &confirmErr) {
t.Fatalf("expected *ConfirmationRequiredError, got %T: %v", err, err)
}
if confirmErr.Risk == "" {
t.Error("Risk empty; arm must fail-closed with RiskUnknown")
}
if confirmErr.Risk != errs.RiskUnknown {
t.Errorf("Risk = %q, want %q (CodeMeta carried no Risk hint)",
confirmErr.Risk, errs.RiskUnknown)
}
if confirmErr.Action == "" {
t.Error("Action empty; arm must fail-closed with command name from ClassifyContext")
}
if confirmErr.Action != "drive +delete" {
t.Errorf("Action = %q, want %q (ctx.LarkCmd fallback)",
confirmErr.Action, "drive +delete")
}
}
// TestBuildAPIError_CategoryConfirmationPrefersCodeMetaHints pins that when
// CodeMeta carries explicit Risk + Action, the dispatcher uses them rather
// than falling back to RiskUnknown / ctx.LarkCmd.
func TestBuildAPIError_CategoryConfirmationPrefersCodeMetaHints(t *testing.T) {
const stubCode = 99999992
codeMeta[stubCode] = CodeMeta{
Category: errs.CategoryConfirmation,
Subtype: errs.SubtypeConfirmationRequired,
Risk: errs.RiskHighRiskWrite,
Action: "wiki:delete-space",
}
t.Cleanup(func() { delete(codeMeta, stubCode) })
resp := map[string]any{"code": stubCode, "msg": "confirmation required"}
ctx := ClassifyContext{LarkCmd: "drive +delete"}
err := BuildAPIError(resp, ctx)
var confirmErr *errs.ConfirmationRequiredError
if !errors.As(err, &confirmErr) {
t.Fatalf("expected *ConfirmationRequiredError, got %T: %v", err, err)
}
if confirmErr.Risk != errs.RiskHighRiskWrite {
t.Errorf("Risk = %q, want %q (CodeMeta hint should win)",
confirmErr.Risk, errs.RiskHighRiskWrite)
}
if confirmErr.Action != "wiki:delete-space" {
t.Errorf("Action = %q, want %q (CodeMeta hint should win)",
confirmErr.Action, "wiki:delete-space")
}
}
// TestBuildAPIError_UnknownCategoryRoutesToInternalError pins fail-closed
// behaviour: an unrecognized Category routes to InternalError instead of
// emitting an empty Problem on the wire.
func TestBuildAPIError_UnknownCategoryRoutesToInternalError(t *testing.T) {
const stubCode = 99999993
codeMeta[stubCode] = CodeMeta{
Category: errs.Category("totally_unknown_category"),
Subtype: errs.SubtypeUnknown,
}
t.Cleanup(func() { delete(codeMeta, stubCode) })
resp := map[string]any{"code": stubCode, "msg": "weird"}
err := BuildAPIError(resp, ClassifyContext{})
var ie *errs.InternalError
if !errors.As(err, &ie) {
t.Fatalf("expected *InternalError, got %T: %v", err, err)
}
if ie.Category != errs.CategoryInternal {
t.Errorf("Category = %q, want %q", ie.Category, errs.CategoryInternal)
}
if ie.Subtype != errs.SubtypeSDKError {
t.Errorf("Subtype = %q, want %q", ie.Subtype, errs.SubtypeSDKError)
}
if ie.Code != stubCode {
t.Errorf("Code = %d, want %d (raw Lark code should propagate)", ie.Code, stubCode)
}
metadata, _ := errs.DiagnosticMetadataOf(err)
if metadata.Origin != larkErrorOrigin() {
t.Errorf("Origin = %q, want %q", metadata.Origin, larkErrorOrigin())
}
}
// TestBuildAPIError_ConfigInvalidClient_HasHint pins that when a
// CategoryConfig response (Lark code 10014 — "app secret invalid") flows
// through BuildAPIError, the resulting *ConfigError MUST carry the canonical
// recovery hint pointing the user at `lark-cli config init`.
func TestBuildAPIError_ConfigInvalidClient_HasHint(t *testing.T) {
const code = 10014
resp := map[string]any{"code": code, "msg": "app secret invalid"}
ctx := ClassifyContext{Brand: "feishu", AppID: "cli_test", Identity: "bot"}
err := BuildAPIError(resp, ctx)
var cfgErr *errs.ConfigError
if !errors.As(err, &cfgErr) {
t.Fatalf("expected *ConfigError, got %T: %v", err, err)
}
if cfgErr.Subtype != errs.SubtypeInvalidClient {
t.Errorf("Subtype = %q, want %q", cfgErr.Subtype, errs.SubtypeInvalidClient)
}
if cfgErr.Hint == "" {
t.Errorf("Hint is empty; canonical hint required for invalid_client")
}
if !strings.Contains(cfgErr.Hint, "lark-cli config init") {
t.Errorf("Hint should reference `lark-cli config init`; got %q", cfgErr.Hint)
}
}