Skip to content
Merged
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
201 changes: 201 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package cmd

import (
"strings"
"testing"

"github.com/Use-Tusk/tusk-cli/internal/cliconfig"
"github.com/stretchr/testify/require"
)

func TestConfigCmdHelp(t *testing.T) {
// configCmd.Run calls cmd.Help(); verify it executes without panic
configCmd.Run(configCmd, []string{})
}

func TestConfigGetCmd(t *testing.T) {
origConfig := cliconfig.CLIConfig
t.Cleanup(func() { cliconfig.CLIConfig = origConfig })

t.Run("analytics", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{AnalyticsEnabled: true}
err := configGetCmd.RunE(configGetCmd, []string{"analytics"})
require.NoError(t, err)
})

t.Run("darkmode nil", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configGetCmd.RunE(configGetCmd, []string{"darkMode"})
require.NoError(t, err)
})

t.Run("darkmode set", func(t *testing.T) {
val := true
cliconfig.CLIConfig = &cliconfig.Config{DarkMode: &val}
err := configGetCmd.RunE(configGetCmd, []string{"darkMode"})
require.NoError(t, err)
})

t.Run("autoupdate", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{AutoUpdate: true}
err := configGetCmd.RunE(configGetCmd, []string{"autoUpdate"})
require.NoError(t, err)
})

t.Run("autocheckupdates nil", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configGetCmd.RunE(configGetCmd, []string{"autoCheckUpdates"})
require.NoError(t, err)
})

t.Run("autocheckupdates set", func(t *testing.T) {
val := false
cliconfig.CLIConfig = &cliconfig.Config{AutoCheckUpdates: &val}
err := configGetCmd.RunE(configGetCmd, []string{"autoCheckUpdates"})
require.NoError(t, err)
})

t.Run("unknown key returns error", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configGetCmd.RunE(configGetCmd, []string{"unknownKey"})
require.Error(t, err)
require.Contains(t, err.Error(), "unknown config key")
})
}

func TestConfigSetCmd(t *testing.T) {
origConfig := cliconfig.CLIConfig
t.Cleanup(func() { cliconfig.CLIConfig = origConfig })

// Sandbox all config resolution paths across OSes:
// - Linux typically honors XDG_CONFIG_HOME
// - macOS uses HOME/Library/Application Support via os.UserConfigDir
// - Windows uses APPDATA/LOCALAPPDATA via os.UserConfigDir
sandbox := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", sandbox)
t.Setenv("HOME", sandbox)
t.Setenv("APPDATA", sandbox)
t.Setenv("LOCALAPPDATA", sandbox)

cfgPath := cliconfig.GetPath()
require.NotEmpty(t, cfgPath)
require.True(t, strings.HasPrefix(cfgPath, sandbox))

t.Run("analytics true clears developer mode", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{IsTuskDeveloper: true}
err := configSetCmd.RunE(configSetCmd, []string{"analytics", "true"})
require.NoError(t, err)
require.True(t, cliconfig.CLIConfig.AnalyticsEnabled)
require.False(t, cliconfig.CLIConfig.IsTuskDeveloper)
})

t.Run("analytics false", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{AnalyticsEnabled: true}
err := configSetCmd.RunE(configSetCmd, []string{"analytics", "false"})
require.NoError(t, err)
require.False(t, cliconfig.CLIConfig.AnalyticsEnabled)
})

t.Run("analytics invalid value", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"analytics", "maybe"})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid value for analytics")
})

t.Run("darkmode true", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"darkMode", "true"})
require.NoError(t, err)
require.NotNil(t, cliconfig.CLIConfig.DarkMode)
require.True(t, *cliconfig.CLIConfig.DarkMode)
})

t.Run("darkmode invalid value", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"darkMode", "maybe"})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid value for darkMode")
})

t.Run("autoupdate true", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"autoUpdate", "true"})
require.NoError(t, err)
require.True(t, cliconfig.CLIConfig.AutoUpdate)
})

t.Run("autoupdate invalid value", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"autoUpdate", "maybe"})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid value for autoUpdate")
})

t.Run("autocheckupdates false", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"autoCheckUpdates", "false"})
require.NoError(t, err)
require.NotNil(t, cliconfig.CLIConfig.AutoCheckUpdates)
require.False(t, *cliconfig.CLIConfig.AutoCheckUpdates)
})

t.Run("autocheckupdates invalid value", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"autoCheckUpdates", "maybe"})
require.Error(t, err)
require.Contains(t, err.Error(), "invalid value for autoCheckUpdates")
})

t.Run("unknown key", func(t *testing.T) {
cliconfig.CLIConfig = &cliconfig.Config{}
err := configSetCmd.RunE(configSetCmd, []string{"unknownKey", "true"})
require.Error(t, err)
require.Contains(t, err.Error(), "unknown config key")
})
}

func TestParseBool(t *testing.T) {
tests := []struct {
name string
input string
expected bool
expectError bool
}{
// True values
{name: "true", input: "true", expected: true},
{name: "TRUE uppercase", input: "TRUE", expected: true},
{name: "True mixed case", input: "True", expected: true},
{name: "1", input: "1", expected: true},
{name: "yes", input: "yes", expected: true},
{name: "YES uppercase", input: "YES", expected: true},
{name: "on", input: "on", expected: true},
{name: "ON uppercase", input: "ON", expected: true},
// False values
{name: "false", input: "false", expected: false},
{name: "FALSE uppercase", input: "FALSE", expected: false},
{name: "False mixed case", input: "False", expected: false},
{name: "0", input: "0", expected: false},
{name: "no", input: "no", expected: false},
{name: "NO uppercase", input: "NO", expected: false},
{name: "off", input: "off", expected: false},
{name: "OFF uppercase", input: "OFF", expected: false},
// Invalid values
{name: "empty string", input: "", expectError: true},
{name: "random string", input: "maybe", expectError: true},
{name: "2", input: "2", expectError: true},
{name: "tru", input: "tru", expectError: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseBool(tt.input)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, got)
}
})
}
}
136 changes: 136 additions & 0 deletions cmd/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cmd

import (
"errors"
"fmt"
"testing"

"github.com/Use-Tusk/tusk-cli/internal/api"
"github.com/stretchr/testify/require"
)

func TestCapitalizeFirst(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{name: "empty string", input: "", expected: ""},
{name: "lowercase first letter", input: "hello world", expected: "Hello world"},
{name: "already uppercase", input: "Hello world", expected: "Hello world"},
{name: "single lowercase", input: "a", expected: "A"},
{name: "single uppercase", input: "A", expected: "A"},
{name: "non-alpha first char", input: "1abc", expected: "1abc"},
{name: "special char first", input: "!hello", expected: "!hello"},
{name: "all lowercase", input: "abc", expected: "Abc"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := capitalizeFirst(tt.input)
require.Equal(t, tt.expected, got)
})
}
}

func TestFormatApiError(t *testing.T) {
tests := []struct {
name string
err error
expectedNil bool
expectedMsg string
}{
{
name: "nil error returns nil",
err: nil,
expectedNil: true,
},
{
name: "non-API error passes through unchanged",
err: errors.New("some generic error"),
expectedMsg: "some generic error",
},
{
name: "401 returns unauthorized message",
err: &api.ApiError{
StatusCode: 401,
Message: "unauthorized",
},
expectedMsg: fmt.Sprintf("Not authorized. Your credentials may be expired or invalid.\nRun `tusk auth login` or set TUSK_API_KEY.\nGet started: %s", api.DocsSetupURL),
},
{
name: "403 returns unauthorized message",
err: &api.ApiError{
StatusCode: 403,
Message: "forbidden",
},
expectedMsg: fmt.Sprintf("Not authorized. Your credentials may be expired or invalid.\nRun `tusk auth login` or set TUSK_API_KEY.\nGet started: %s", api.DocsSetupURL),
},
{
name: "404 with message uses capitalized message",
err: &api.ApiError{
StatusCode: 404,
Message: "resource not available",
},
expectedMsg: "Resource not available.",
},
{
name: "404 with empty message uses default",
err: &api.ApiError{
StatusCode: 404,
Message: "",
},
expectedMsg: "Resource not found.",
},
{
name: "500 returns service error",
err: &api.ApiError{
StatusCode: 500,
Message: "internal error",
},
expectedMsg: "Tusk service error (HTTP 500). Please try again.\nIf the issue persists, please contact support@usetusk.ai.",
},
{
name: "503 returns service error with correct status code",
err: &api.ApiError{
StatusCode: 503,
Message: "service unavailable",
},
expectedMsg: "Tusk service error (HTTP 503). Please try again.\nIf the issue persists, please contact support@usetusk.ai.",
},
{
name: "other status with message returns message with status code",
err: &api.ApiError{
StatusCode: 422,
Message: "validation failed",
},
expectedMsg: "validation failed (HTTP 422)",
},
{
name: "other status with empty message returns original error",
err: &api.ApiError{
StatusCode: 400,
Message: "",
RawBody: "bad request body",
},
expectedMsg: "http 400: bad request body",
},
{
name: "wrapped API error is unwrapped correctly",
err: fmt.Errorf("wrapped: %w", &api.ApiError{StatusCode: 401, Message: "unauth"}),
expectedMsg: fmt.Sprintf("Not authorized. Your credentials may be expired or invalid.\nRun `tusk auth login` or set TUSK_API_KEY.\nGet started: %s", api.DocsSetupURL),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatApiError(tt.err)
if tt.expectedNil {
require.NoError(t, got)
return
}
require.Error(t, got)
require.Equal(t, tt.expectedMsg, got.Error())
})
}
}
Loading
Loading