Files
dingtalk-workspace-cli/internal/errors/errors_test.go
T
2026-07-31 14:36:19 +08:00

413 lines
12 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package errors
import (
stderrors "errors"
"strings"
"testing"
"time"
)
func TestExitCodeByCategory(t *testing.T) {
t.Parallel()
cases := []struct {
err error
want int
}{
{err: NewAPI("api"), want: 1},
{err: NewAuth("auth"), want: 2},
{err: NewValidation("validation"), want: 3},
{err: NewDiscovery("discovery"), want: 6},
{err: NewInternal("internal"), want: 5},
{err: stderrors.New("plain"), want: 5},
}
for _, tc := range cases {
if got := ExitCode(tc.err); got != tc.want {
t.Fatalf("ExitCode(%v) = %d, want %d", tc.err, got, tc.want)
}
}
}
func TestPrintJSON(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintJSON(&b, NewValidation(
"bad flag",
WithReason("missing_required_flag"),
WithHint("Pass the required flag and retry."),
WithRetryable(true),
WithActions("dws schema doc.create_document", "retry command"),
WithSnapshot("/tmp/dws-recovery/snapshot.json"),
)); err != nil {
t.Fatalf("PrintJSON() error = %v", err)
}
got := b.String()
if !strings.Contains(got, "\"category\": \"validation\"") {
t.Fatalf("expected validation category in output, got %q", got)
}
if !strings.Contains(got, "\"message\": \"bad flag\"") {
t.Fatalf("expected error message in output, got %q", got)
}
if !strings.Contains(got, "\"reason\": \"missing_required_flag\"") {
t.Fatalf("expected reason in output, got %q", got)
}
if !strings.Contains(got, "\"retryable\": true") {
t.Fatalf("expected retryable in output, got %q", got)
}
if !strings.Contains(got, "\"hint\": \"Pass the required flag and retry.\"") {
t.Fatalf("expected hint in output, got %q", got)
}
if !strings.Contains(got, "\"snapshot_path\": \"/tmp/dws-recovery/snapshot.json\"") {
t.Fatalf("expected snapshot path in output, got %q", got)
}
}
func TestCrossPlatformCoverageRetryabilityTriStateAndRetryTiming(t *testing.T) {
t.Parallel()
next := time.Date(2026, time.July, 30, 4, 5, 6, 0, time.FixedZone("CST", 8*60*60))
tests := []struct {
name string
err error
wantRetryable string
wantRetryAfter bool
wantNextRetryAt bool
}{
{
name: "unknown is omitted",
err: NewAPI("unknown"),
},
{
name: "explicit false is preserved",
err: NewValidation("terminal", WithRetryable(false)),
wantRetryable: `"retryable": false`,
},
{
name: "explicit true with timing",
err: NewAPI("transient", WithRetryable(true), WithRetryAfterSeconds(30), WithNextRetryAt(next)),
wantRetryable: `"retryable": true`,
wantRetryAfter: true,
wantNextRetryAt: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var jsonOut strings.Builder
if err := PrintJSON(&jsonOut, tt.err); err != nil {
t.Fatalf("PrintJSON() error = %v", err)
}
gotJSON := jsonOut.String()
if tt.wantRetryable == "" {
if strings.Contains(gotJSON, `"retryable"`) {
t.Fatalf("unknown retryability must be omitted: %s", gotJSON)
}
} else if !strings.Contains(gotJSON, tt.wantRetryable) {
t.Fatalf("missing %s in %s", tt.wantRetryable, gotJSON)
}
if got := strings.Contains(gotJSON, `"retry_after_seconds": 30`); got != tt.wantRetryAfter {
t.Fatalf("retry_after_seconds presence = %v, want %v: %s", got, tt.wantRetryAfter, gotJSON)
}
if got := strings.Contains(gotJSON, `"next_retry_at": "2026-07-29T20:05:06Z"`); got != tt.wantNextRetryAt {
t.Fatalf("next_retry_at presence = %v, want %v: %s", got, tt.wantNextRetryAt, gotJSON)
}
var humanOut strings.Builder
if err := PrintHuman(&humanOut, tt.err); err != nil {
t.Fatalf("PrintHuman() error = %v", err)
}
gotHuman := humanOut.String()
if tt.wantRetryable == "" {
if strings.Contains(gotHuman, "Retryable:") {
t.Fatalf("unknown retryability must be omitted: %s", gotHuman)
}
} else {
want := "Retryable: true"
if strings.Contains(tt.wantRetryable, "false") {
want = "Retryable: false"
}
if !strings.Contains(gotHuman, want) {
t.Fatalf("missing %q in %s", want, gotHuman)
}
}
if tt.wantRetryAfter && !strings.Contains(gotHuman, "Retry After: 30s") {
t.Fatalf("missing retry delay in %s", gotHuman)
}
if tt.wantNextRetryAt && !strings.Contains(gotHuman, "Next Retry At: 2026-07-29T20:05:06Z") {
t.Fatalf("missing next retry time in %s", gotHuman)
}
})
}
}
func TestCrossPlatformCoverageRetryTimingOptionsIgnoreInvalidValues(t *testing.T) {
t.Parallel()
err := NewAPI(
"invalid timing",
WithRetryAfterSeconds(-1),
WithNextRetryAt(time.Time{}),
).(*Error)
if err.RetryAfterSeconds != nil || err.NextRetryAt != nil {
t.Fatalf("invalid retry timing was retained: %#v", err)
}
}
func TestPrintJSON_AvailableFlags(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintJSON(&b, NewValidation(
"unknown flag: --foo",
WithReason("unknown_flag"),
WithHint("Did you mean --bar?"),
WithAvailableFlags("bar", "baz"),
)); err != nil {
t.Fatalf("PrintJSON() error = %v", err)
}
got := b.String()
if !strings.Contains(got, `"available_flags"`) {
t.Fatalf("expected available_flags in output, got %q", got)
}
if !strings.Contains(got, `"bar"`) || !strings.Contains(got, `"baz"`) {
t.Fatalf("expected flag names in output, got %q", got)
}
}
func TestPrintJSON_FourPartGuidance(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintJSON(&b, NewValidation(
"缺少参数",
WithReason("必须提供时间范围"),
WithActions("补充开始时间", "补充结束时间"),
WithExamples(`dws chat message list-all --start "..." --end "..." --format json`),
)); err != nil {
t.Fatalf("PrintJSON() error = %v", err)
}
got := b.String()
for _, want := range []string{`"error_message"`, `"reason"`, `"suggested_actions"`, `"examples"`} {
if !strings.Contains(got, want) {
t.Fatalf("four-part JSON output missing %s: %q", want, got)
}
}
}
func TestPrintHuman_FourPartGuidance(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintHuman(&b, NewValidation(
"缺少参数",
WithReason("必须提供时间范围"),
WithActions("补充开始和结束时间"),
WithExamples(`dws chat message list-all --start "..." --end "..."`),
)); err != nil {
t.Fatalf("PrintHuman() error = %v", err)
}
got := b.String()
for _, want := range []string{"错误信息:", "原因:", "建议操作:", "示例:"} {
if !strings.Contains(got, want) {
t.Fatalf("four-part human output missing %q: %q", want, got)
}
}
}
func TestPrintHuman(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintHumanAt(&b, NewValidation(
"bad flag",
WithReason("missing_required_flag"),
WithOperation("calendar.list"),
WithServerKey("calendar"),
WithHint("Pass the required flag and retry."),
WithRetryable(true),
WithActions("retry command"),
WithSnapshot("/tmp/dws-recovery/snapshot.json"),
), VerbosityVerbose); err != nil {
t.Fatalf("PrintHuman() error = %v", err)
}
got := b.String()
if !strings.Contains(got, "Error: [VALIDATION] bad flag") {
t.Fatalf("expected formatted header in output, got %q", got)
}
if !strings.Contains(got, "Reason: missing_required_flag") {
t.Fatalf("expected reason in output, got %q", got)
}
if !strings.Contains(got, "Hint: Pass the required flag and retry.") {
t.Fatalf("expected hint in output, got %q", got)
}
if !strings.Contains(got, "Action: retry command") {
t.Fatalf("expected action in output, got %q", got)
}
if !strings.Contains(got, "Snapshot: /tmp/dws-recovery/snapshot.json") {
t.Fatalf("expected snapshot in verbose output, got %q", got)
}
if !strings.Contains(got, "Retryable: true") {
t.Fatalf("expected retryable marker in output, got %q", got)
}
}
func TestPrintHuman_NormalMode(t *testing.T) {
t.Parallel()
var b strings.Builder
PrintHuman(&b, NewValidation(
"bad flag",
WithHint("fix it"),
WithRetryable(true),
WithActions("retry"),
WithServerDiag(ServerDiagnostics{TraceID: "trace-abc", ServerErrorCode: "PARAM_ERROR"}),
))
got := b.String()
if !strings.Contains(got, "Error: [VALIDATION] bad flag") {
t.Fatalf("expected header, got %q", got)
}
if !strings.Contains(got, "Trace ID: trace-abc") {
t.Fatalf("expected trace id in normal output, got %q", got)
}
if !strings.Contains(got, "Server Code: PARAM_ERROR") {
t.Fatalf("expected server code in normal output, got %q", got)
}
}
func TestPrintJSONIncludesServerDiag(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintJSON(&b, NewAPI(
"server error",
WithServerDiag(ServerDiagnostics{
TraceID: "trace-xyz",
ServerErrorCode: "TIMEOUT_ERROR",
TechnicalDetail: "deadline exceeded",
FriendlyHint: "请开通消息搜索权益",
ActionURL: "https://example.test/enable-search",
}),
)); err != nil {
t.Fatalf("PrintJSON() error = %v", err)
}
got := b.String()
if !strings.Contains(got, `"trace_id": "trace-xyz"`) {
t.Fatalf("expected trace_id in output, got %q", got)
}
if !strings.Contains(got, `"server_error_code": "TIMEOUT_ERROR"`) {
t.Fatalf("expected server_error_code in output, got %q", got)
}
if !strings.Contains(got, `"technical_detail": "deadline exceeded"`) {
t.Fatalf("expected technical_detail in output, got %q", got)
}
if !strings.Contains(got, `"friendly_hint": "请开通消息搜索权益"`) {
t.Fatalf("expected server friendly_hint in output, got %q", got)
}
if !strings.Contains(got, `"action_url": "https://example.test/enable-search"`) {
t.Fatalf("expected server action_url in output, got %q", got)
}
}
func TestPrintHumanIncludesServerGuidance(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintHuman(&b, NewAPI(
"search entitlement required",
WithServerDiag(ServerDiagnostics{
ServerErrorCode: "SEARCH_ENTITLEMENT_REQUIRED",
FriendlyHint: "请联系管理员开通消息搜索权益",
ActionURL: "https://example.test/enable-search",
}),
)); err != nil {
t.Fatalf("PrintHuman() error = %v", err)
}
got := b.String()
if !strings.Contains(got, "Hint: 请联系管理员开通消息搜索权益") {
t.Fatalf("expected server guidance in output, got %q", got)
}
if !strings.Contains(got, "Action: 开启地址: https://example.test/enable-search") {
t.Fatalf("expected server action URL in output, got %q", got)
}
}
func TestPrintJSONIncludesRPCCodeAndData(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintJSON(&b, NewAPI(
"JSON-RPC tools/call failed with code -32602: invalid arguments",
WithReason("tools_call_jsonrpc_invalid_params"),
WithRPCCode(-32602),
WithRPCData([]byte(`{"field":"base_id","error":"required"}`)),
)); err != nil {
t.Fatalf("PrintJSON() error = %v", err)
}
got := b.String()
if !strings.Contains(got, `"rpc_code": -32602`) {
t.Fatalf("expected rpc_code in output, got %q", got)
}
if !strings.Contains(got, `"field"`) || !strings.Contains(got, `"base_id"`) {
t.Fatalf("expected rpc_data content in output, got %q", got)
}
}
func TestPrintHumanIncludesRPCCode_Debug(t *testing.T) {
t.Parallel()
var b strings.Builder
if err := PrintHumanAt(&b, NewValidation(
"invalid params",
WithRPCCode(-32602),
WithRPCData([]byte(`"missing field"`)),
), VerbosityDebug); err != nil {
t.Fatalf("PrintHuman() error = %v", err)
}
got := b.String()
if !strings.Contains(got, "RPC Code: -32602") {
t.Fatalf("expected RPC Code in debug output, got %q", got)
}
if !strings.Contains(got, "RPC Data:") {
t.Fatalf("expected RPC Data in debug output, got %q", got)
}
}
func TestPrintHumanHidesRPCCode_Normal(t *testing.T) {
t.Parallel()
var b strings.Builder
PrintHuman(&b, NewValidation(
"invalid params",
WithRPCCode(-32602),
))
got := b.String()
if strings.Contains(got, "RPC Code:") {
t.Fatalf("normal mode should not show RPC Code, got %q", got)
}
}