mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
ef5da4b5ba
Cobra surfaced several command-line validation failures as plain errors. Classifying them by message text could report correctable input as internal/unknown, misleading agents and returning the wrong exit code. Classify errors at the boundary that produces them. Args and residual Cobra validation become validation/invalid_argument, while raw execution hooks and plugin failures become internal/unknown. Preserve typed errors, causes, bare exits, and partial failures. Make final-tree instrumentation stateless, type pre-callback framework failures at their source, and keep rendering and Shutdown lifecycle observations consistent. A Shutdown handler receives the error the command returned with its wrapping intact, so errors.Is still reaches the producer's sentinels, and it cannot change what the user was told because that is decided before the event fires. The envelope is still written after the event, keeping it the trailing content of stderr where readers look for it even when a failing hook warns on the same stream. Guard the error copier against a typed error being added without it, so handlers cannot silently start sharing a producer's value again. Add regression coverage for repeated execution, late help, lazy completion, writer failures, shortcut diagnostics, credential-provider classification, lifecycle isolation, and stderr write order.
592 lines
20 KiB
Go
592 lines
20 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmdutil
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
extcred "github.com/larksuite/cli/extension/credential"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/credential"
|
|
"github.com/larksuite/cli/internal/envvars"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/internal/recovery"
|
|
"github.com/larksuite/cli/internal/skillref"
|
|
"github.com/larksuite/cli/internal/surface"
|
|
)
|
|
|
|
func TestFactoryResolveSkillReference(t *testing.T) {
|
|
content := fstest.MapFS{
|
|
"lark-doc/SKILL.md": {Data: []byte("canonical")},
|
|
"acme-doc/SKILL.md": {Data: []byte("remapped")},
|
|
}
|
|
from, err := skillref.Parse("lark-doc")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
to, err := skillref.Parse("acme-doc")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resolver, err := skillref.New(content, []skillref.Mapping{{From: from, To: to}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("explicit remap", func(t *testing.T) {
|
|
f := &Factory{SkillContent: content, SkillReferences: resolver}
|
|
if got, ok := f.ResolveSkillReference("lark-doc"); !ok || got != "acme-doc" {
|
|
t.Fatalf("ResolveSkillReference() = %q, %v; want acme-doc, true", got, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("identity fallback", func(t *testing.T) {
|
|
f := &Factory{SkillContent: content}
|
|
if got, ok := f.ResolveSkillReference("lark-doc"); !ok || got != "lark-doc" {
|
|
t.Fatalf("ResolveSkillReference() = %q, %v; want lark-doc, true", got, ok)
|
|
}
|
|
if got, ok := f.ResolveSkillReference("missing"); ok || got != "" {
|
|
t.Fatalf("missing ResolveSkillReference() = %q, %v; want empty, false", got, ok)
|
|
}
|
|
if got, ok := f.ResolveSkillReference("../invalid"); ok || got != "" {
|
|
t.Fatalf("invalid ResolveSkillReference() = %q, %v; want empty, false", got, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("concealed skills read", func(t *testing.T) {
|
|
plan := surface.NewPlan(map[surface.CommandID]surface.CommandState{
|
|
surface.CommandSkillsRead: surface.CommandConcealed,
|
|
})
|
|
f := &Factory{
|
|
SkillContent: content,
|
|
SkillReferences: resolver,
|
|
Recovery: recovery.NewProjector(func() *surface.Plan { return plan }),
|
|
}
|
|
if got, ok := f.ResolveSkillReference("lark-doc"); ok || got != "" {
|
|
t.Fatalf("concealed ResolveSkillReference() = %q, %v; want empty, false", got, ok)
|
|
}
|
|
})
|
|
}
|
|
|
|
// newCmdWithAsFlag creates a cobra.Command with a --as string flag for testing.
|
|
func newCmdWithAsFlag(asValue string, changed bool) *cobra.Command {
|
|
cmd := &cobra.Command{Use: "test"}
|
|
cmd.Flags().String("as", "auto", "identity")
|
|
if changed {
|
|
_ = cmd.Flags().Set("as", asValue)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
// --- ResolveAs tests ---
|
|
|
|
func TestResolveAs_ExplicitAs(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
cmd := newCmdWithAsFlag("bot", true)
|
|
|
|
got := f.ResolveAs(context.Background(), cmd, core.AsBot)
|
|
if got != core.AsBot {
|
|
t.Errorf("want bot, got %s", got)
|
|
}
|
|
if f.IdentityAutoDetected {
|
|
t.Error("IdentityAutoDetected should be false for explicit --as")
|
|
}
|
|
if f.ResolvedIdentity != core.AsBot {
|
|
t.Errorf("ResolvedIdentity want bot, got %s", f.ResolvedIdentity)
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_ExplicitAsUser(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
cmd := newCmdWithAsFlag("user", true)
|
|
|
|
got := f.ResolveAs(context.Background(), cmd, core.AsUser)
|
|
if got != core.AsUser {
|
|
t.Errorf("want user, got %s", got)
|
|
}
|
|
if f.ResolvedIdentity != core.AsUser {
|
|
t.Errorf("ResolvedIdentity want user, got %s", f.ResolvedIdentity)
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_ExplicitAuto_FallsToAutoDetect(t *testing.T) {
|
|
// --as auto explicitly: should fall through to auto-detect
|
|
// Config has no UserOpenId → auto-detect returns bot
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
cmd := newCmdWithAsFlag("auto", true)
|
|
|
|
got := f.ResolveAs(context.Background(), cmd, "auto")
|
|
if got != core.AsBot {
|
|
t.Errorf("want bot (auto-detect, no login), got %s", got)
|
|
}
|
|
if !f.IdentityAutoDetected {
|
|
t.Error("IdentityAutoDetected should be true for auto-detect path")
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_DefaultAs_FromConfig(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{
|
|
AppID: "a", AppSecret: "s",
|
|
DefaultAs: "bot",
|
|
})
|
|
cmd := newCmdWithAsFlag("auto", false) // --as not changed
|
|
|
|
got := f.ResolveAs(context.Background(), cmd, "auto")
|
|
if got != core.AsBot {
|
|
t.Errorf("want bot (from default-as config), got %s", got)
|
|
}
|
|
if f.IdentityAutoDetected {
|
|
t.Error("IdentityAutoDetected should be false for default-as path")
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_DefaultAs_EnvDoesNotBypassConfigSource(t *testing.T) {
|
|
t.Setenv(envvars.CliDefaultAs, "user")
|
|
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
cmd := newCmdWithAsFlag("auto", false)
|
|
|
|
got := f.ResolveAs(context.Background(), cmd, "auto")
|
|
if got != core.AsBot {
|
|
t.Errorf("want bot (env default-as should not bypass config source), got %s", got)
|
|
}
|
|
if !f.IdentityAutoDetected {
|
|
t.Error("IdentityAutoDetected should be true when no account default-as is set")
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_DefaultAs_AutoValue_FallsToAutoDetect(t *testing.T) {
|
|
// default-as = "auto" should fall through to auto-detect
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{
|
|
AppID: "a", AppSecret: "s",
|
|
DefaultAs: "auto",
|
|
})
|
|
cmd := newCmdWithAsFlag("auto", false)
|
|
|
|
got := f.ResolveAs(context.Background(), cmd, "auto")
|
|
// No UserOpenId → auto-detect returns bot
|
|
if got != core.AsBot {
|
|
t.Errorf("want bot (auto-detect), got %s", got)
|
|
}
|
|
if !f.IdentityAutoDetected {
|
|
t.Error("IdentityAutoDetected should be true")
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_NilCmd_AutoDetect(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
|
|
got := f.ResolveAs(context.Background(), nil, "auto")
|
|
if got != core.AsBot {
|
|
t.Errorf("want bot, got %s", got)
|
|
}
|
|
}
|
|
|
|
// --- CheckIdentity tests ---
|
|
|
|
func TestCheckIdentity_Supported(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
|
|
err := f.CheckIdentity(core.AsBot, []string{"bot", "user"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if f.ResolvedIdentity != core.AsBot {
|
|
t.Errorf("ResolvedIdentity want bot, got %s", f.ResolvedIdentity)
|
|
}
|
|
}
|
|
|
|
func TestCheckIdentity_Supported_UserOnly(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
|
|
err := f.CheckIdentity(core.AsUser, []string{"user"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if f.ResolvedIdentity != core.AsUser {
|
|
t.Errorf("ResolvedIdentity want user, got %s", f.ResolvedIdentity)
|
|
}
|
|
}
|
|
|
|
func TestCheckIdentity_Unsupported_Explicit(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
f.IdentityAutoDetected = false // explicit --as
|
|
|
|
err := f.CheckIdentity(core.AsUser, []string{"bot"})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), "--as user is not supported") {
|
|
t.Errorf("unexpected error message: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "bot") {
|
|
t.Errorf("error should mention supported identity: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckIdentity_Unsupported_AutoDetected(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
f.IdentityAutoDetected = true
|
|
|
|
err := f.CheckIdentity(core.AsUser, []string{"bot"})
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T: %v", err, err)
|
|
}
|
|
if !strings.Contains(ve.Message, "resolved identity") {
|
|
t.Errorf("expected 'resolved identity' in message, got: %v", ve.Message)
|
|
}
|
|
if !strings.Contains(ve.Hint, "use --as bot") {
|
|
t.Errorf("expected hint to suggest --as bot, got: %v", ve.Hint)
|
|
}
|
|
}
|
|
|
|
// --- NewAPIClient / NewAPIClientWithConfig tests ---
|
|
|
|
func TestNewAPIClient(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", Brand: core.BrandLark}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
|
|
ac, err := f.NewAPIClient()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if ac.Config.AppID != "a" {
|
|
t.Errorf("want AppID a, got %s", ac.Config.AppID)
|
|
}
|
|
}
|
|
|
|
func TestNewAPIClientWithConfig(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", Brand: core.BrandLark}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
|
|
ac, err := f.NewAPIClientWithConfig(cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if ac.Config.AppID != "a" {
|
|
t.Errorf("want AppID a, got %s", ac.Config.AppID)
|
|
}
|
|
if ac.SDK == nil {
|
|
t.Error("SDK should not be nil")
|
|
}
|
|
if ac.HTTP == nil {
|
|
t.Error("HTTP should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestNewAPIClientWithConfig_NilIOStreams(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", Brand: core.BrandLark}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
f.IOStreams = nil
|
|
|
|
ac, err := f.NewAPIClientWithConfig(cfg)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if ac == nil {
|
|
t.Fatal("expected non-nil APIClient")
|
|
}
|
|
}
|
|
|
|
// --- ResolveStrictMode tests ---
|
|
|
|
func TestResolveStrictMode_Off(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
if got := f.ResolveStrictMode(context.Background()); got != core.StrictModeOff {
|
|
t.Errorf("expected off, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveStrictMode_BotFromAccount(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 2} // SupportsBot = 2
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
if got := f.ResolveStrictMode(context.Background()); got != core.StrictModeBot {
|
|
t.Errorf("expected bot, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveStrictMode_UserFromAccount(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 1} // SupportsUser = 1
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
if got := f.ResolveStrictMode(context.Background()); got != core.StrictModeUser {
|
|
t.Errorf("expected user, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveStrictMode_BothIdentities(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 3} // SupportsAll = 3
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
if got := f.ResolveStrictMode(context.Background()); got != core.StrictModeOff {
|
|
t.Errorf("expected off when both supported, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveStrictMode_NilCredential(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
f.Credential = nil
|
|
if got := f.ResolveStrictMode(context.Background()); got != core.StrictModeOff {
|
|
t.Errorf("expected off with nil credential, got %q", got)
|
|
}
|
|
}
|
|
|
|
// --- CheckStrictMode tests ---
|
|
|
|
func TestCheckStrictMode_BotMode_BotAllowed(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 2}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
if err := f.CheckStrictMode(context.Background(), core.AsBot); err != nil {
|
|
t.Errorf("bot should be allowed in bot mode, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckStrictMode_BotMode_UserBlocked(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 2}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
err := f.CheckStrictMode(context.Background(), core.AsUser)
|
|
if err == nil {
|
|
t.Fatal("expected error for user in bot mode")
|
|
}
|
|
if !strings.Contains(err.Error(), "strict mode") {
|
|
t.Errorf("error should mention strict mode, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckStrictMode_UserMode_UserAllowed(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 1}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
if err := f.CheckStrictMode(context.Background(), core.AsUser); err != nil {
|
|
t.Errorf("user should be allowed in user mode, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckStrictMode_UserMode_BotBlocked(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 1}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
err := f.CheckStrictMode(context.Background(), core.AsBot)
|
|
if err == nil {
|
|
t.Fatal("expected error for bot in user mode")
|
|
}
|
|
}
|
|
|
|
func TestCheckStrictMode_Off_BothAllowed(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
|
if err := f.CheckStrictMode(context.Background(), core.AsUser); err != nil {
|
|
t.Errorf("user should be allowed when off: %v", err)
|
|
}
|
|
if err := f.CheckStrictMode(context.Background(), core.AsBot); err != nil {
|
|
t.Errorf("bot should be allowed when off: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- ResolveAs strict mode tests ---
|
|
|
|
func TestResolveAs_StrictModeBot_ForceBot(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 2}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
cmd := newCmdWithAsFlag("auto", false)
|
|
got := f.ResolveAs(context.Background(), cmd, "auto")
|
|
if got != core.AsBot {
|
|
t.Errorf("bot mode should force bot, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_StrictModeUser_ForceUser(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 1}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
cmd := newCmdWithAsFlag("auto", false)
|
|
got := f.ResolveAs(context.Background(), cmd, "auto")
|
|
if got != core.AsUser {
|
|
t.Errorf("user mode should force user, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_StrictModeUser_PreservesExplicitBot(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 1}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
cmd := newCmdWithAsFlag("bot", true)
|
|
got := f.ResolveAs(context.Background(), cmd, core.AsBot)
|
|
if got != core.AsBot {
|
|
t.Errorf("explicit bot should be preserved for strict-mode validation, got %s", got)
|
|
}
|
|
if err := f.CheckStrictMode(context.Background(), got); err == nil {
|
|
t.Fatal("expected strict-mode error for explicit bot in user mode")
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_StrictModeBot_PreservesExplicitUser(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 2}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
cmd := newCmdWithAsFlag("user", true)
|
|
got := f.ResolveAs(context.Background(), cmd, core.AsUser)
|
|
if got != core.AsUser {
|
|
t.Errorf("explicit user should be preserved for strict-mode validation, got %s", got)
|
|
}
|
|
if err := f.CheckStrictMode(context.Background(), got); err == nil {
|
|
t.Fatal("expected strict-mode error for explicit user in bot mode")
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_StrictModeUser_ExplicitAutoForcesUser(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", SupportedIdentities: 1}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
cmd := newCmdWithAsFlag("auto", true)
|
|
got := f.ResolveAs(context.Background(), cmd, core.AsAuto)
|
|
if got != core.AsUser {
|
|
t.Errorf("--as auto should use strict-mode user identity, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveAs_StrictModeBot_IgnoresDefaultAsUser(t *testing.T) {
|
|
cfg := &core.CliConfig{AppID: "a", AppSecret: "s", DefaultAs: "user", SupportedIdentities: 2}
|
|
f, _, _, _ := TestFactory(t, cfg)
|
|
cmd := newCmdWithAsFlag("auto", false)
|
|
got := f.ResolveAs(context.Background(), cmd, "auto")
|
|
if got != core.AsBot {
|
|
t.Errorf("bot mode should override default-as user, got %s", got)
|
|
}
|
|
}
|
|
|
|
// stubExtProvider is a minimal extcred.Provider for testing external-provider guards.
|
|
type stubExtProvider struct {
|
|
name string
|
|
acct *extcred.Account
|
|
err error
|
|
}
|
|
|
|
func (s *stubExtProvider) Name() string { return s.name }
|
|
func (s *stubExtProvider) ResolveAccount(_ context.Context) (*extcred.Account, error) {
|
|
return s.acct, s.err
|
|
}
|
|
func (s *stubExtProvider) ResolveToken(_ context.Context, _ extcred.TokenSpec) (*extcred.Token, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestRequireBuiltinCredentialProvider_BlocksExternalProvider(t *testing.T) {
|
|
stub := &stubExtProvider{name: "env", acct: &extcred.Account{AppID: "app"}}
|
|
cred := credential.NewCredentialProvider([]extcred.Provider{stub}, nil, nil, nil)
|
|
f, _, _, _ := TestFactory(t, nil)
|
|
f.Credential = cred
|
|
|
|
err := f.RequireBuiltinCredentialProvider(context.Background(), "auth")
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("error type = %T, want *errs.ValidationError", err)
|
|
}
|
|
if got := output.ExitCodeOf(err); got != output.ExitValidation {
|
|
t.Errorf("exit code = %d, want %d", got, output.ExitValidation)
|
|
}
|
|
if ve.Message == "" {
|
|
t.Error("expected non-empty message")
|
|
}
|
|
if ve.Hint == "" {
|
|
t.Error("expected non-empty hint")
|
|
}
|
|
}
|
|
|
|
func TestRequireBuiltinCredentialProvider_AllowsBuiltinProvider(t *testing.T) {
|
|
// No extension providers → built-in path → no error
|
|
f, _, _, _ := TestFactory(t, nil)
|
|
err := f.RequireBuiltinCredentialProvider(context.Background(), "auth")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRequireBuiltinCredentialProvider_NilCredential(t *testing.T) {
|
|
f, _, _, _ := TestFactory(t, nil)
|
|
f.Credential = nil
|
|
err := f.RequireBuiltinCredentialProvider(context.Background(), "auth")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error with nil Credential: %v", err)
|
|
}
|
|
}
|
|
|
|
// A provider that already classified its own failure must keep that
|
|
// classification. Rewrapping it would replace, for example, a retryable
|
|
// network timeout with an internal fault and change the exit code with it.
|
|
func TestRequireBuiltinCredentialProvider_KeepsProviderClassification(t *testing.T) {
|
|
typed := errs.NewNetworkError(errs.SubtypeNetworkTimeout, "provider lookup timed out")
|
|
stub := &stubExtProvider{name: "env", err: typed}
|
|
cred := credential.NewCredentialProvider([]extcred.Provider{stub}, nil, nil, nil)
|
|
|
|
f, _, _, _ := TestFactory(t, nil)
|
|
f.Credential = cred
|
|
|
|
err := f.RequireBuiltinCredentialProvider(context.Background(), "auth")
|
|
|
|
problem, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("error %T carries no Problem", err)
|
|
}
|
|
if problem.Category != errs.CategoryNetwork || problem.Subtype != errs.SubtypeNetworkTimeout {
|
|
t.Errorf("classified as %s/%s, want %s/%s (the provider's own classification)",
|
|
problem.Category, problem.Subtype, errs.CategoryNetwork, errs.SubtypeNetworkTimeout)
|
|
}
|
|
var netErr *errs.NetworkError
|
|
if !errors.As(err, &netErr) {
|
|
t.Errorf("error = %T, want the provider's *errs.NetworkError preserved", err)
|
|
}
|
|
}
|
|
|
|
func TestRequireBuiltinCredentialProvider_PropagatesProviderError(t *testing.T) {
|
|
sentinel := errors.New("provider unavailable")
|
|
stub := &stubExtProvider{name: "env", err: sentinel}
|
|
cred := credential.NewCredentialProvider([]extcred.Provider{stub}, nil, nil, nil)
|
|
|
|
f, _, _, _ := TestFactory(t, nil)
|
|
f.Credential = cred
|
|
|
|
err := f.RequireBuiltinCredentialProvider(context.Background(), "auth")
|
|
if !errors.Is(err, sentinel) {
|
|
t.Fatalf("error = %v, want sentinel", err)
|
|
}
|
|
}
|
|
|
|
// A provider failure that carries no classification of its own is a fault in
|
|
// the tool, not in what the user typed. This runs in PersistentPreRunE, ahead
|
|
// of the command body, where an unclassified error is read as a bad command
|
|
// line — so the classification has to be applied here, and the exit code the
|
|
// user sees has to say internal fault rather than invalid input.
|
|
func TestRequireBuiltinCredentialProvider_UnclassifiedProviderErrorBecomesInternal(t *testing.T) {
|
|
sentinel := errors.New("provider unavailable")
|
|
stub := &stubExtProvider{name: "env", err: sentinel}
|
|
cred := credential.NewCredentialProvider([]extcred.Provider{stub}, nil, nil, nil)
|
|
|
|
f, _, _, _ := TestFactory(t, nil)
|
|
f.Credential = cred
|
|
|
|
err := f.RequireBuiltinCredentialProvider(context.Background(), "auth")
|
|
|
|
problem, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("error %T carries no Problem; an unclassified failure would be blamed on the user", err)
|
|
}
|
|
if problem.Category != errs.CategoryInternal || problem.Subtype != errs.SubtypeUnknown {
|
|
t.Errorf("classified as %s/%s, want %s/%s",
|
|
problem.Category, problem.Subtype, errs.CategoryInternal, errs.SubtypeUnknown)
|
|
}
|
|
var internalErr *errs.InternalError
|
|
if !errors.As(err, &internalErr) {
|
|
t.Errorf("error = %T, want *errs.InternalError", err)
|
|
}
|
|
if got := output.ExitCodeOf(err); got != output.ExitInternal {
|
|
t.Errorf("exit code = %d, want %d", got, output.ExitInternal)
|
|
}
|
|
if !errors.Is(err, sentinel) {
|
|
t.Error("classification dropped the provider's own error")
|
|
}
|
|
}
|