diff --git a/.goreleaser.yml b/.goreleaser.yml index 2f20802..3f29104 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -24,10 +24,10 @@ builds: goarch: arm64 ldflags: - -s -w - - -X github.com/tim/cu/internal/version.Version={{.Version}} - - -X github.com/tim/cu/internal/version.Commit={{.Commit}} - - -X github.com/tim/cu/internal/version.Date={{.Date}} - - -X github.com/tim/cu/internal/version.BuiltBy=goreleaser + - -X github.com/timimsms/cu/internal/version.Version={{.Version}} + - -X github.com/timimsms/cu/internal/version.Commit={{.Commit}} + - -X github.com/timimsms/cu/internal/version.Date={{.Date}} + - -X github.com/timimsms/cu/internal/version.BuiltBy=goreleaser archives: - id: cu-archive @@ -83,6 +83,12 @@ homebrew_casks: directory: Casks homepage: "https://github.com/timimsms/cu" description: "A CLI for ClickUp — like gh is for GitHub" + hooks: + post: + install: | + if OS.mac? + system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/cu"] + end release: github: @@ -96,14 +102,14 @@ release: footer: | ## Installation - ### Homebrew (macOS/Linux) + ### Homebrew (macOS) ```bash brew install timimsms/cu/cu ``` ### Go ```bash - go install github.com/tim/cu/cmd/cu@v{{.Version}} + go install github.com/timimsms/cu/cmd/cu@v{{.Version}} ``` ### Direct Download diff --git a/Makefile b/Makefile index af00d12..3945361 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,10 @@ MAIN_PATH=./cmd/cu VERSION=$(shell git describe --tags --always --dirty) COMMIT=$(shell git rev-parse --short HEAD) DATE=$(shell date -u '+%Y-%m-%d_%H:%M:%S') -LDFLAGS=-ldflags "-X github.com/tim/cu/internal/version.Version=$(VERSION) \ - -X github.com/tim/cu/internal/version.Commit=$(COMMIT) \ - -X github.com/tim/cu/internal/version.Date=$(DATE) \ - -X github.com/tim/cu/internal/version.BuiltBy=make" +LDFLAGS=-ldflags "-X github.com/timimsms/cu/internal/version.Version=$(VERSION) \ + -X github.com/timimsms/cu/internal/version.Commit=$(COMMIT) \ + -X github.com/timimsms/cu/internal/version.Date=$(DATE) \ + -X github.com/timimsms/cu/internal/version.BuiltBy=make" # Default target all: build diff --git a/README.md b/README.md index ef6ddc6..b74768a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ npm install -g @clickup/cli ``` ### Direct Download -Download the latest release from the [releases page](https://github.com/tim/cu/releases). +Download the latest release from the [releases page](https://github.com/timimsms/cu/releases). ## Quick Start diff --git a/cmd/cu/main.go b/cmd/cu/main.go index aaab0b8..f98b354 100644 --- a/cmd/cu/main.go +++ b/cmd/cu/main.go @@ -3,7 +3,7 @@ package main import ( "os" - "github.com/tim/cu/internal/cmd" + "github.com/timimsms/cu/internal/cmd" ) func main() { diff --git a/docs/project-buildout/PHASE_1_FOUNDATION.md b/docs/project-buildout/PHASE_1_FOUNDATION.md index e9fd152..335c14b 100644 --- a/docs/project-buildout/PHASE_1_FOUNDATION.md +++ b/docs/project-buildout/PHASE_1_FOUNDATION.md @@ -12,7 +12,7 @@ Establish the core project structure, development environment, and foundational ## Task Checklist ### 1. Project Initialization -- [x] Initialize Go module: `go mod init github.com/tim/cu` +- [x] Initialize Go module: `go mod init github.com/timimsms/cu` - [x] Create standard Go project structure: ``` ├── cmd/ diff --git a/go.mod b/go.mod index ef96693..88aa89a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/tim/cu +module github.com/timimsms/cu go 1.24.4 diff --git a/internal/api/client.go b/internal/api/client.go index 08ab74b..5c87ee1 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -8,8 +8,8 @@ import ( "time" "github.com/raksul/go-clickup/clickup" - "github.com/tim/cu/internal/auth" - "github.com/tim/cu/internal/errors" + "github.com/timimsms/cu/internal/auth" + "github.com/timimsms/cu/internal/errors" ) // Client wraps the ClickUp API client diff --git a/internal/api/retry.go b/internal/api/retry.go index 8462d7e..d6988d6 100644 --- a/internal/api/retry.go +++ b/internal/api/retry.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/tim/cu/internal/errors" + "github.com/timimsms/cu/internal/errors" ) // retryTransport implements automatic retry with exponential backoff diff --git a/internal/api/users.go b/internal/api/users.go index bd63f6f..c6b92a9 100644 --- a/internal/api/users.go +++ b/internal/api/users.go @@ -8,7 +8,7 @@ import ( "sync" "github.com/raksul/go-clickup/clickup" - "github.com/tim/cu/internal/cache" + "github.com/timimsms/cu/internal/cache" ) // UserLookup provides user ID lookup functionality diff --git a/internal/api/users_test.go b/internal/api/users_test.go index 12b6b2f..64a73be 100644 --- a/internal/api/users_test.go +++ b/internal/api/users_test.go @@ -9,7 +9,7 @@ import ( "github.com/raksul/go-clickup/clickup" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tim/cu/internal/testutil" + "github.com/timimsms/cu/internal/testutil" ) // mockClient for testing UserLookup diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 5db3bf8..a429f57 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/tim/cu/internal/errors" + "github.com/timimsms/cu/internal/errors" "github.com/zalando/go-keyring" ) diff --git a/internal/auth/mock/README.md b/internal/auth/mock/README.md index 47261a5..7acb45c 100644 --- a/internal/auth/mock/README.md +++ b/internal/auth/mock/README.md @@ -17,7 +17,7 @@ The mock package includes: ```go import ( "testing" - "github.com/tim/cu/internal/auth/mock" + "github.com/timimsms/cu/internal/auth/mock" ) func TestMyCommand(t *testing.T) { diff --git a/internal/auth/mock/fixtures.go b/internal/auth/mock/fixtures.go index 0d56351..fc5597e 100644 --- a/internal/auth/mock/fixtures.go +++ b/internal/auth/mock/fixtures.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/tim/cu/internal/auth" - cuerrors "github.com/tim/cu/internal/errors" + "github.com/timimsms/cu/internal/auth" + cuerrors "github.com/timimsms/cu/internal/errors" ) // Common test tokens diff --git a/internal/auth/mock/mock.go b/internal/auth/mock/mock.go index c2fc55d..6dd1c13 100644 --- a/internal/auth/mock/mock.go +++ b/internal/auth/mock/mock.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/tim/cu/internal/auth" - cuerrors "github.com/tim/cu/internal/errors" + "github.com/timimsms/cu/internal/auth" + cuerrors "github.com/timimsms/cu/internal/errors" ) // AuthProvider is a mock implementation of auth.Manager for testing diff --git a/internal/auth/mock/mock_test.go b/internal/auth/mock/mock_test.go index 7a657e2..d8bf18d 100644 --- a/internal/auth/mock/mock_test.go +++ b/internal/auth/mock/mock_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tim/cu/internal/auth" - cuerrors "github.com/tim/cu/internal/errors" + "github.com/timimsms/cu/internal/auth" + cuerrors "github.com/timimsms/cu/internal/errors" ) func TestNewAuthProvider(t *testing.T) { diff --git a/internal/cache/cache.go b/internal/cache/cache.go index c3b78b3..8ede374 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/tim/cu/internal/config" + "github.com/timimsms/cu/internal/config" ) // Cache represents a simple file-based cache diff --git a/internal/cmd/api.go b/internal/cmd/api.go index f60373e..d77569b 100644 --- a/internal/cmd/api.go +++ b/internal/cmd/api.go @@ -11,13 +11,13 @@ import ( "time" "github.com/spf13/cobra" - "github.com/tim/cu/internal/auth" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/auth" + "github.com/timimsms/cu/internal/output" ) var ( - apiMethod string - apiData string + apiMethod string + apiData string apiHeaders []string ) @@ -60,7 +60,7 @@ For example, use "/team" for https://api.clickup.com/api/v2/team`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { endpoint := args[0] - + // Ensure endpoint starts with / if !strings.HasPrefix(endpoint, "/") { endpoint = "/" + endpoint @@ -100,7 +100,7 @@ For example, use "/team" for https://api.clickup.com/api/v2/team`, // Set headers req.Header.Set("Authorization", token.Value) req.Header.Set("Content-Type", "application/json") - + // Add custom headers for _, header := range apiHeaders { parts := strings.SplitN(header, ":", 2) @@ -113,7 +113,7 @@ For example, use "/team" for https://api.clickup.com/api/v2/team`, client := &http.Client{ Timeout: 30 * time.Second, } - + resp, err := client.Do(req) if err != nil { fmt.Fprintf(os.Stderr, "Request failed: %v\n", err) @@ -135,7 +135,7 @@ For example, use "/team" for https://api.clickup.com/api/v2/team`, // Check for non-2xx status codes if resp.StatusCode < 200 || resp.StatusCode >= 300 { fmt.Fprintf(os.Stderr, "API request failed with status %d: %s\n", resp.StatusCode, resp.Status) - + // Try to parse error response var errResp map[string]interface{} if err := json.Unmarshal(respBody, &errResp); err == nil { @@ -182,4 +182,4 @@ func init() { apiCmd.Flags().StringVarP(&apiMethod, "method", "X", "GET", "HTTP method (GET, POST, PUT, PATCH, DELETE)") apiCmd.Flags().StringVarP(&apiData, "data", "d", "", "Request body data (JSON)") apiCmd.Flags().StringArrayVarP(&apiHeaders, "header", "H", []string{}, "Custom headers (format: 'Header: value')") -} \ No newline at end of file +} diff --git a/internal/cmd/api_test.go b/internal/cmd/api_test.go index ddfab73..257e898 100644 --- a/internal/cmd/api_test.go +++ b/internal/cmd/api_test.go @@ -67,7 +67,7 @@ func TestEndpointNormalization(t *testing.T) { }, } - for _, tt := range tests { + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // This tests the logic that should add leading slash result := tt.input @@ -83,11 +83,11 @@ func TestEndpointNormalization(t *testing.T) { func TestHeaderParsing(t *testing.T) { tests := []struct { - name string - headers []string - expectedKey string - expectedValue string - shouldParse bool + name string + headers []string + expectedKey string + expectedValue string + shouldParse bool }{ { name: "valid header", @@ -129,4 +129,4 @@ func TestHeaderParsing(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index b0b41a7..c9541e9 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/spf13/cobra" - "github.com/tim/cu/internal/auth" - "github.com/tim/cu/internal/config" + "github.com/timimsms/cu/internal/auth" + "github.com/timimsms/cu/internal/config" ) var authCmd = &cobra.Command{ diff --git a/internal/cmd/bulk.go b/internal/cmd/bulk.go index fb82507..df954c2 100644 --- a/internal/cmd/bulk.go +++ b/internal/cmd/bulk.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/output" ) var bulkCmd = &cobra.Command{ diff --git a/internal/cmd/cache.go b/internal/cmd/cache.go index 3f52ae9..f82bf4d 100644 --- a/internal/cmd/cache.go +++ b/internal/cmd/cache.go @@ -6,8 +6,8 @@ import ( "time" "github.com/spf13/cobra" - "github.com/tim/cu/internal/cache" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/cache" + "github.com/timimsms/cu/internal/output" ) var cacheCmd = &cobra.Command{ @@ -102,7 +102,7 @@ func showCacheInfo(cmd *cobra.Command, args []string) error { } allCacheInfo = append(allCacheInfo, info) - + totalSize += stats.TotalSize totalEntries += stats.TotalEntries totalValid += stats.ValidEntries @@ -112,10 +112,10 @@ func showCacheInfo(cmd *cobra.Command, args []string) error { // Output based on format if outputFormat == "json" || outputFormat == "yaml" { result := map[string]interface{}{ - "caches": allCacheInfo, - "total_size": totalSize, - "total_entries": totalEntries, - "valid_entries": totalValid, + "caches": allCacheInfo, + "total_size": totalSize, + "total_entries": totalEntries, + "valid_entries": totalValid, "expired_entries": totalExpired, } return output.Format(outputFormat, result) @@ -253,12 +253,12 @@ func formatCacheTime(t time.Time) string { if t.IsZero() { return "never" } - + duration := time.Since(t) if duration < 0 { return t.Format("2006-01-02 15:04:05") } - + switch { case duration < time.Minute: return "just now" @@ -271,4 +271,4 @@ func formatCacheTime(t time.Time) string { default: return t.Format("2006-01-02") } -} \ No newline at end of file +} diff --git a/internal/cmd/cache_test.go b/internal/cmd/cache_test.go index 2c94d4e..3697468 100644 --- a/internal/cmd/cache_test.go +++ b/internal/cmd/cache_test.go @@ -54,7 +54,7 @@ func TestCacheCmd_Subcommands(t *testing.T) { t.Run("cache command has expected subcommands", func(t *testing.T) { cmd := cacheCmd subcommands := cmd.Commands() - + // Collect subcommand names subcommandNames := make(map[string]bool) for _, subcmd := range subcommands { @@ -63,7 +63,7 @@ func TestCacheCmd_Subcommands(t *testing.T) { // Check for expected subcommands assert.True(t, subcommandNames["info"], "Should have info subcommand") - assert.True(t, subcommandNames["clear"], "Should have clear subcommand") + assert.True(t, subcommandNames["clear"], "Should have clear subcommand") assert.True(t, subcommandNames["clean"], "Should have clean subcommand") }) @@ -113,14 +113,14 @@ func TestCacheCmd_Integration(t *testing.T) { t.Run("cache commands can be created without panic", func(t *testing.T) { // Test that we can create copies of the commands without panicking commands := []*cobra.Command{cacheCmd, cacheInfoCmd, cacheClearCmd, cacheCleanCmd} - + for _, originalCmd := range commands { testCmd := &cobra.Command{ Use: originalCmd.Use, Short: originalCmd.Short, Long: originalCmd.Long, } - + assert.NotNil(t, testCmd) assert.Equal(t, originalCmd.Use, testCmd.Use) assert.Equal(t, originalCmd.Short, testCmd.Short) @@ -133,16 +133,16 @@ func TestCacheCmd_Initialization(t *testing.T) { t.Run("cache command initialization", func(t *testing.T) { // Test that init() was called and commands are properly set up cmd := cacheCmd - + // Verify the main command has subcommands subcommands := cmd.Commands() assert.Greater(t, len(subcommands), 0, "Cache command should have subcommands") - + // Verify specific subcommands exist hasInfo := false hasClear := false hasClean := false - + for _, subcmd := range subcommands { switch subcmd.Name() { case "info": @@ -153,7 +153,7 @@ func TestCacheCmd_Initialization(t *testing.T) { hasClean = true } } - + assert.True(t, hasInfo, "Should have info subcommand") assert.True(t, hasClear, "Should have clear subcommand") assert.True(t, hasClean, "Should have clean subcommand") @@ -164,7 +164,7 @@ func TestCacheCmd_ErrorHandling(t *testing.T) { t.Run("commands have error handling capability", func(t *testing.T) { // Test that the commands use RunE (which supports error returns) // rather than Run (which doesn't) - + commands := map[string]*cobra.Command{ "info": cacheInfoCmd, "clear": cacheClearCmd, @@ -183,12 +183,12 @@ func TestCacheCmd_CommandTree(t *testing.T) { // Test the overall command tree structure root := cacheCmd assert.Equal(t, "cache", root.Use) - + // Test that each subcommand has the correct parent subcommands := root.Commands() for _, subcmd := range subcommands { assert.Equal(t, root, subcmd.Parent(), "Subcommand %s should have cache as parent", subcmd.Name()) - + // Test that subcommands don't have their own subcommands (these are leaf commands) grandchildren := subcmd.Commands() assert.Empty(t, grandchildren, "Cache subcommand %s should not have further subcommands", subcmd.Name()) @@ -210,9 +210,9 @@ func TestCacheCmd_Documentation(t *testing.T) { assert.NotEmpty(t, cmd.Use, "%s command should have Use field", name) assert.NotEmpty(t, cmd.Short, "%s command should have Short description", name) assert.NotEmpty(t, cmd.Long, "%s command should have Long description", name) - + // Long description should be longer than short description - assert.Greater(t, len(cmd.Long), len(cmd.Short), + assert.Greater(t, len(cmd.Long), len(cmd.Short), "%s command Long description should be longer than Short", name) } }) @@ -221,14 +221,14 @@ func TestCacheCmd_Documentation(t *testing.T) { func TestCacheCmd_MockExecutionStructure(t *testing.T) { t.Run("can simulate command execution structure", func(t *testing.T) { // Test that we understand the execution flow without actually running - + // Mock arguments that would be valid mockArgs := []string{} - + // Test that the commands accept the expected number of arguments // Cache subcommands should accept 0 arguments subcommands := []*cobra.Command{cacheInfoCmd, cacheClearCmd, cacheCleanCmd} - + for _, cmd := range subcommands { // These commands don't define Args, so should accept any number // But they're designed to work with 0 arguments @@ -268,22 +268,22 @@ func TestFormatBytes(t *testing.T) { func TestFormatCacheTime(t *testing.T) { t.Run("formats cache time correctly", func(t *testing.T) { now := time.Now() - + tests := []struct { name string time time.Time expected string }{ {"zero time", time.Time{}, "never"}, - {"future time", now.Add(5 * time.Minute), now.Add(5*time.Minute).Format("2006-01-02 15:04:05")}, + {"future time", now.Add(5 * time.Minute), now.Add(5 * time.Minute).Format("2006-01-02 15:04:05")}, {"just now", now.Add(-30 * time.Second), "just now"}, {"5 minutes ago", now.Add(-5 * time.Minute), "5 minutes ago"}, {"1 hour ago", now.Add(-1 * time.Hour), "1 hours ago"}, {"2 hours ago", now.Add(-2 * time.Hour), "2 hours ago"}, {"1 day ago", now.Add(-24 * time.Hour), "1 days ago"}, {"3 days ago", now.Add(-72 * time.Hour), "3 days ago"}, - {"1 week ago", now.Add(-7 * 24 * time.Hour), now.Add(-7*24*time.Hour).Format("2006-01-02")}, - {"2 weeks ago", now.Add(-14 * 24 * time.Hour), now.Add(-14*24*time.Hour).Format("2006-01-02")}, + {"1 week ago", now.Add(-7 * 24 * time.Hour), now.Add(-7 * 24 * time.Hour).Format("2006-01-02")}, + {"2 weeks ago", now.Add(-14 * 24 * time.Hour), now.Add(-14 * 24 * time.Hour).Format("2006-01-02")}, } for _, test := range tests { @@ -306,28 +306,28 @@ func TestShowCacheInfo_Function(t *testing.T) { t.Run("executes without panic", func(t *testing.T) { cmd := &cobra.Command{} args := []string{} - + // Capture output to prevent console noise during testing oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + var err error assert.NotPanics(t, func() { err = showCacheInfo(cmd, args) }) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // The function might error due to cache initialization issues, but shouldn't panic // In CI/test environments, cache may not be properly initialized if err != nil { @@ -346,28 +346,28 @@ func TestClearCache_Function(t *testing.T) { t.Run("executes without panic", func(t *testing.T) { cmd := &cobra.Command{} args := []string{} - + // Capture output to prevent console noise during testing oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + var err error assert.NotPanics(t, func() { err = clearCache(cmd, args) }) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // The function might error due to cache initialization issues, but shouldn't panic if err != nil { assert.Contains(t, err.Error(), "cache", "Error should be related to cache initialization") @@ -385,28 +385,28 @@ func TestCleanCache_Function(t *testing.T) { t.Run("executes without panic", func(t *testing.T) { cmd := &cobra.Command{} args := []string{} - + // Capture output to prevent console noise during testing oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + var err error assert.NotPanics(t, func() { err = cleanCache(cmd, args) }) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // The function might error due to cache initialization issues, but shouldn't panic if err != nil { assert.Contains(t, err.Error(), "cache", "Error should be related to cache initialization") @@ -419,26 +419,26 @@ func TestCacheCommands_RunEExecution(t *testing.T) { t.Run("cache info command RunE", func(t *testing.T) { cmd := cacheInfoCmd assert.NotNil(t, cmd.RunE) - + // Capture output oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + // Execute the RunE function err := cmd.RunE(cmd, []string{}) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // May fail due to cache initialization in test env, but should not panic if err != nil { assert.Error(t, err) @@ -448,25 +448,25 @@ func TestCacheCommands_RunEExecution(t *testing.T) { t.Run("cache clear command RunE", func(t *testing.T) { cmd := cacheClearCmd assert.NotNil(t, cmd.RunE) - + // Capture output oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + err := cmd.RunE(cmd, []string{}) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // May fail due to cache initialization in test env, but should not panic if err != nil { assert.Error(t, err) @@ -476,28 +476,28 @@ func TestCacheCommands_RunEExecution(t *testing.T) { t.Run("cache clean command RunE", func(t *testing.T) { cmd := cacheCleanCmd assert.NotNil(t, cmd.RunE) - + // Capture output oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + err := cmd.RunE(cmd, []string{}) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // May fail due to cache initialization in test env, but should not panic if err != nil { assert.Error(t, err) } }) -} \ No newline at end of file +} diff --git a/internal/cmd/comment.go b/internal/cmd/comment.go index 7da9c19..f6343de 100644 --- a/internal/cmd/comment.go +++ b/internal/cmd/comment.go @@ -10,8 +10,8 @@ import ( "github.com/raksul/go-clickup/clickup" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/output" ) var commentCmd = &cobra.Command{ @@ -25,12 +25,12 @@ Without subcommands, adds a comment to the specified task.`, } var ( - commentMessage string + commentMessage string commentAssignee string - notifyAll bool - listComments bool - deleteComment string - yesFlag bool + notifyAll bool + listComments bool + deleteComment string + yesFlag bool ) func init() { @@ -40,17 +40,17 @@ func init() { commentCmd.Flags().StringVarP(&commentMessage, "message", "m", "", "Comment text (opens editor if not provided)") commentCmd.Flags().StringVar(&commentAssignee, "assignee", "", "Assign comment to user") commentCmd.Flags().BoolVar(¬ifyAll, "notify-all", false, "Notify all task watchers") - + // List comments flag commentCmd.Flags().BoolVarP(&listComments, "list", "l", false, "List all comments on the task") - + // Delete comment flag commentCmd.Flags().StringVarP(&deleteComment, "delete", "d", "", "Delete comment by ID") - + // Subcommands commentCmd.AddCommand(listCommentsCmd) commentCmd.AddCommand(deleteCommentCmd) - + // Add yes flag to delete subcommand deleteCommentCmd.Flags().BoolVarP(&yesFlag, "yes", "y", false, "Skip confirmation prompt") } @@ -58,17 +58,17 @@ func init() { // addComment adds a new comment to a task func addComment(cmd *cobra.Command, args []string) error { taskID := args[0] - + // If listing comments, delegate to list function if listComments { return listTaskComments(cmd, []string{taskID}) } - + // If deleting comment, delegate to delete function if deleteComment != "" { return deleteTaskComment(cmd, []string{deleteComment}) } - + // Get comment text var text string if commentMessage != "" { @@ -79,7 +79,7 @@ func addComment(cmd *cobra.Command, args []string) error { scanner := bufio.NewScanner(os.Stdin) var lines []string emptyLineCount := 0 - + for scanner.Scan() { line := scanner.Text() if line == "" { @@ -93,31 +93,31 @@ func addComment(cmd *cobra.Command, args []string) error { lines = append(lines, line) fmt.Print("> ") } - + if err := scanner.Err(); err != nil { return fmt.Errorf("failed to read comment: %w", err) } - + text = strings.TrimSpace(strings.Join(lines, "\n")) if text == "" { return fmt.Errorf("comment text cannot be empty") } } - + // Create API client client, err := api.NewClient() if err != nil { return fmt.Errorf("failed to create API client: %w", err) } - + ctx := context.Background() - + // Create comment comment, err := client.CreateTaskComment(ctx, taskID, text, commentAssignee, notifyAll) if err != nil { return fmt.Errorf("failed to create comment: %w", err) } - + // Display result if outputFormat == "json" || outputFormat == "yaml" || outputFormat == "csv" { if err := output.Format(outputFormat, comment); err != nil { @@ -125,14 +125,14 @@ func addComment(cmd *cobra.Command, args []string) error { } return nil } - + // Human-readable output fmt.Printf("Comment added successfully!\n") fmt.Printf("ID: %d\n", comment.ID) if comment.Date != nil { fmt.Printf("Date: %s\n", comment.Date.String()) } - + return nil } @@ -145,21 +145,21 @@ var listCommentsCmd = &cobra.Command{ func listTaskComments(cmd *cobra.Command, args []string) error { taskID := args[0] - + // Create API client client, err := api.NewClient() if err != nil { return fmt.Errorf("failed to create API client: %w", err) } - + ctx := context.Background() - + // Get comments comments, err := client.GetTaskComments(ctx, taskID) if err != nil { return fmt.Errorf("failed to get comments: %w", err) } - + // Display results if outputFormat == "json" || outputFormat == "yaml" || outputFormat == "csv" { if err := output.Format(outputFormat, comments); err != nil { @@ -167,10 +167,10 @@ func listTaskComments(cmd *cobra.Command, args []string) error { } return nil } - + // Table output var rows [][]string - + for _, comment := range comments { text := comment.CommentText if len(text) > 50 { @@ -178,17 +178,17 @@ func listTaskComments(cmd *cobra.Command, args []string) error { } // Replace newlines with spaces for table display text = strings.ReplaceAll(text, "\n", " ") - + resolved := "" if comment.Resolved { resolved = "✓" } - + assignee := "" if comment.Assignee.ID != 0 { assignee = getUserDisplay(comment.Assignee) } - + rows = append(rows, []string{ fmt.Sprintf("%d", comment.ID), getUserDisplay(comment.User), @@ -198,13 +198,13 @@ func listTaskComments(cmd *cobra.Command, args []string) error { assignee, }) } - + // Print table if len(rows) > 0 { // Print header fmt.Printf("%-10s %-20s %-16s %-50s %-8s %-20s\n", "ID", "User", "Date", "Text", "Resolved", "Assignee") fmt.Println(strings.Repeat("-", 134)) - + // Print rows for _, row := range rows { fmt.Printf("%-10s %-20s %-16s %-50s %-8s %-20s\n", row[0], row[1], row[2], row[3], row[4], row[5]) @@ -212,9 +212,9 @@ func listTaskComments(cmd *cobra.Command, args []string) error { } else { fmt.Println("No comments found") } - + fmt.Printf("\nTotal comments: %d\n", len(comments)) - + return nil } @@ -227,7 +227,7 @@ var deleteCommentCmd = &cobra.Command{ func deleteTaskComment(cmd *cobra.Command, args []string) error { commentID := args[0] - + // Confirm deletion if !yesFlag { fmt.Printf("Are you sure you want to delete comment %s? (y/N): ", commentID) @@ -236,29 +236,29 @@ func deleteTaskComment(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("failed to read confirmation: %w", err) } - + response = strings.TrimSpace(strings.ToLower(response)) if response != "y" && response != "yes" { fmt.Println("Deletion cancelled") return nil } } - + // Create API client client, err := api.NewClient() if err != nil { return fmt.Errorf("failed to create API client: %w", err) } - + ctx := context.Background() - + // Delete comment if err := client.DeleteTaskComment(ctx, commentID); err != nil { return fmt.Errorf("failed to delete comment: %w", err) } - + fmt.Printf("Comment %s deleted successfully\n", commentID) - + return nil } @@ -313,11 +313,11 @@ func formatCommentDate(dateStr string) string { return dateStr // Return as-is if we can't parse it } } - + // Format relative time now := time.Now() diff := now.Sub(t) - + switch { case diff < time.Minute: return "just now" @@ -330,4 +330,4 @@ func formatCommentDate(dateStr string) string { default: return t.Format("2006-01-02 15:04") } -} \ No newline at end of file +} diff --git a/internal/cmd/comment_test.go b/internal/cmd/comment_test.go index ec1caa3..b05078b 100644 --- a/internal/cmd/comment_test.go +++ b/internal/cmd/comment_test.go @@ -27,7 +27,7 @@ func TestCommentCmd_Structure(t *testing.T) { t.Run("comment command has expected flags", func(t *testing.T) { cmd := commentCmd - + // Check message flag messageFlag := cmd.Flags().Lookup("message") assert.NotNil(t, messageFlag) @@ -116,7 +116,7 @@ func TestCommentCmd_FlagBehavior(t *testing.T) { func TestCommentCmd_ArgsValidation(t *testing.T) { t.Run("requires exactly one argument", func(t *testing.T) { cmd := commentCmd - + // Test no arguments err := cmd.Args(cmd, []string{}) assert.Error(t, err) @@ -181,7 +181,7 @@ func TestCommentCmd_GlobalVariables(t *testing.T) { originalMessage := commentMessage originalAssignee := commentAssignee originalNotifyAll := notifyAll - + // Modify variables commentMessage = "modified message" commentAssignee = "modified assignee" @@ -204,7 +204,7 @@ func TestCommentCmd_Integration(t *testing.T) { // This test ensures the command structure is sound // We don't execute it fully due to API dependencies cmd := commentCmd - + // Test that we can create a copy of the command testCmd := &cobra.Command{ Use: cmd.Use, @@ -212,7 +212,7 @@ func TestCommentCmd_Integration(t *testing.T) { Long: cmd.Long, Args: cmd.Args, } - + assert.NotNil(t, testCmd) assert.Equal(t, cmd.Use, testCmd.Use) assert.Equal(t, cmd.Short, testCmd.Short) @@ -225,7 +225,7 @@ func TestCommentHelpers(t *testing.T) { t.Run("comment command initialization", func(t *testing.T) { // Test that init() was called and flags are set up cmd := commentCmd - + // Verify flags were added during init() assert.NotNil(t, cmd.Flags().Lookup("message")) assert.NotNil(t, cmd.Flags().Lookup("assignee")) @@ -240,7 +240,7 @@ func TestCommentInput_Mock(t *testing.T) { t.Run("can mock stdin for testing", func(t *testing.T) { // This demonstrates how we could mock stdin for testing interactive input // though the actual function has complex API dependencies - + originalStdin := os.Stdin defer func() { os.Stdin = originalStdin }() @@ -257,7 +257,7 @@ func TestCommentInput_Mock(t *testing.T) { // Read the input (simulating what addComment would do) var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + // Verify we can read the mocked input content := buf.String() assert.Contains(t, content, "test comment") @@ -271,7 +271,7 @@ func TestCommentFormat_Mock(t *testing.T) { // we test similar logic that would be used comment := "This is a test comment" formatted := strings.TrimSpace(comment) - + assert.Equal(t, "This is a test comment", formatted) assert.NotContains(t, formatted, "\n") }) @@ -279,7 +279,7 @@ func TestCommentFormat_Mock(t *testing.T) { t.Run("handles multiline comments", func(t *testing.T) { comment := "Line 1\nLine 2\nLine 3" lines := strings.Split(comment, "\n") - + assert.Len(t, lines, 3) assert.Equal(t, "Line 1", lines[0]) assert.Equal(t, "Line 2", lines[1]) @@ -333,7 +333,7 @@ func TestFormatCommentDate(t *testing.T) { }{ {"empty string", "", ""}, {"RFC3339 format", "2022-01-01T15:04:05Z", "just now"}, // Will be formatted as relative time - {"unix timestamp ms", "1640995200000", "2021-12-31"}, // 2022-01-01 UTC timestamp + {"unix timestamp ms", "1640995200000", "2021-12-31"}, // 2022-01-01 UTC timestamp {"invalid format", "invalid-date", "invalid-date"}, } @@ -366,7 +366,7 @@ func TestAddComment_Function(t *testing.T) { // This test verifies the function exists and has the right signature // The actual execution will likely panic due to uninitialized API client // but we still get coverage of the function entry point - + // Reset global state origMessage := commentMessage origList := listComments @@ -376,22 +376,22 @@ func TestAddComment_Function(t *testing.T) { listComments = origList deleteComment = origDelete }() - + cmd := &cobra.Command{} args := []string{"test-task-id"} - + // Set a message to avoid interactive prompt commentMessage = "Test comment" listComments = false deleteComment = "" - + // Capture output to prevent console noise during testing oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + // We expect this to panic due to nil API client, but we still get some coverage defer func() { if r := recover(); r != nil { @@ -399,18 +399,18 @@ func TestAddComment_Function(t *testing.T) { assert.Contains(t, fmt.Sprintf("%v", r), "nil pointer dereference") } }() - + err := addComment(cmd, args) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // If we get here without panicking, check for error if err != nil { assert.Error(t, err) @@ -428,14 +428,14 @@ func TestListTaskComments_Function(t *testing.T) { t.Run("function can be called (may panic due to API dependencies)", func(t *testing.T) { cmd := &cobra.Command{} args := []string{"test-task-id"} - + // Capture output to prevent console noise during testing oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + // We expect this to panic due to nil API client, but we still get some coverage defer func() { if r := recover(); r != nil { @@ -443,18 +443,18 @@ func TestListTaskComments_Function(t *testing.T) { assert.Contains(t, fmt.Sprintf("%v", r), "nil pointer dereference") } }() - + err := listTaskComments(cmd, args) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // If we get here without panicking, check for error if err != nil { assert.Error(t, err) @@ -473,20 +473,20 @@ func TestDeleteTaskComment_Function(t *testing.T) { // Reset global state origYes := yesFlag defer func() { yesFlag = origYes }() - + cmd := &cobra.Command{} args := []string{"test-comment-id"} - + // Set yes flag to avoid interactive confirmation yesFlag = true - + // Capture output to prevent console noise during testing oldStdout := os.Stdout oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stdout = w os.Stderr = w - + // We expect this to panic due to nil API client, but we still get some coverage defer func() { if r := recover(); r != nil { @@ -494,21 +494,21 @@ func TestDeleteTaskComment_Function(t *testing.T) { assert.Contains(t, fmt.Sprintf("%v", r), "nil pointer dereference") } }() - + err := deleteTaskComment(cmd, args) - + // Restore output w.Close() os.Stdout = oldStdout os.Stderr = oldStderr - + // Read and discard output buf := make([]byte, 1024) _, _ = r.Read(buf) - + // Function may error due to API client initialization, but shouldn't panic if err != nil { assert.Error(t, err) } }) -} \ No newline at end of file +} diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 83c3eef..618a5e8 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -8,8 +8,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/tim/cu/internal/config" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/config" + "github.com/timimsms/cu/internal/output" ) var configCmd = &cobra.Command{ diff --git a/internal/cmd/docs.go b/internal/cmd/docs.go index 64a965a..23bea93 100644 --- a/internal/cmd/docs.go +++ b/internal/cmd/docs.go @@ -9,9 +9,9 @@ import ( ) var docsCmd = &cobra.Command{ - Use: "docs", - Short: "Generate documentation for cu", - Long: `Generate documentation for cu in various formats including Markdown, Man pages, and RST.`, + Use: "docs", + Short: "Generate documentation for cu", + Long: `Generate documentation for cu in various formats including Markdown, Man pages, and RST.`, Hidden: true, // Hide from regular help output } @@ -43,6 +43,6 @@ var genMarkdownCmd = &cobra.Command{ func init() { rootCmd.AddCommand(docsCmd) docsCmd.AddCommand(genMarkdownCmd) - + genMarkdownCmd.Flags().StringP("dir", "d", "./docs", "Directory to write documentation files") -} \ No newline at end of file +} diff --git a/internal/cmd/export.go b/internal/cmd/export.go index f0d3ddb..30eb544 100644 --- a/internal/cmd/export.go +++ b/internal/cmd/export.go @@ -12,7 +12,7 @@ import ( "github.com/raksul/go-clickup/clickup" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" + "github.com/timimsms/cu/internal/api" ) var exportCmd = &cobra.Command{ diff --git a/internal/cmd/export_test.go b/internal/cmd/export_test.go index 22c0d25..40fc011 100644 --- a/internal/cmd/export_test.go +++ b/internal/cmd/export_test.go @@ -29,23 +29,23 @@ func TestExportCmd_Structure(t *testing.T) { t.Run("export tasks has required flags", func(t *testing.T) { cmd := exportTasksCmd - + // Check for expected flags listFlag := cmd.Flags().Lookup("list") assert.NotNil(t, listFlag) - + formatFlag := cmd.Flags().Lookup("format") assert.NotNil(t, formatFlag) - + outputFlag := cmd.Flags().Lookup("output") assert.NotNil(t, outputFlag) - + statusFlag := cmd.Flags().Lookup("status") assert.NotNil(t, statusFlag) - + priorityFlag := cmd.Flags().Lookup("priority") assert.NotNil(t, priorityFlag) - + assigneeFlag := cmd.Flags().Lookup("assignee") assert.NotNil(t, assigneeFlag) }) @@ -61,7 +61,7 @@ func TestExportTasksCmd_Logic(t *testing.T) { isValid := lower == "csv" || lower == "json" || lower == "markdown" || lower == "md" assert.True(t, isValid, "Format %s should be valid", format) } - + invalidFormats := []string{"xml", "yaml", "txt", ""} for _, format := range invalidFormats { lower := strings.ToLower(format) @@ -85,7 +85,7 @@ func TestExportTasksCmd_Logic(t *testing.T) { "normal": 3, "low": 4, } - + for name, expectedID := range priorities { var p int switch name { @@ -103,7 +103,7 @@ func TestExportTasksCmd_Logic(t *testing.T) { }) } -// Note: The actual exportTasksToCSV, exportTasksToJSON, exportTasksToMarkdown +// Note: The actual exportTasksToCSV, exportTasksToJSON, exportTasksToMarkdown // functions are complex and depend on the clickup package structure. // These tests focus on command structure and logic validation. @@ -116,9 +116,9 @@ func TestExportCmd_FunctionExistence(t *testing.T) { var mdFunc func(*os.File, []clickup.Task) error = exportTasksToMarkdown var filterFunc func([]clickup.Task, string, string, string) []clickup.Task = filterTasksForExport var formatFunc func(string) string = formatTimestamp - + assert.NotNil(t, csvFunc) - assert.NotNil(t, jsonFunc) + assert.NotNil(t, jsonFunc) assert.NotNil(t, mdFunc) assert.NotNil(t, filterFunc) assert.NotNil(t, formatFunc) @@ -128,16 +128,16 @@ func TestExportCmd_FunctionExistence(t *testing.T) { func TestExportCmd_CommandFlags(t *testing.T) { t.Run("flags have correct properties", func(t *testing.T) { cmd := exportTasksCmd - + // Test flag defaults and properties listFlag := cmd.Flags().Lookup("list") assert.NotNil(t, listFlag) assert.Equal(t, "", listFlag.DefValue) - - formatFlag := cmd.Flags().Lookup("format") + + formatFlag := cmd.Flags().Lookup("format") assert.NotNil(t, formatFlag) assert.Equal(t, "csv", formatFlag.DefValue) - + outputFlag := cmd.Flags().Lookup("output") assert.NotNil(t, outputFlag) assert.Equal(t, "", outputFlag.DefValue) @@ -153,4 +153,4 @@ func TestExportCmd_Examples(t *testing.T) { assert.Contains(t, cmd.Long, "--format json") assert.Contains(t, cmd.Long, "--format markdown") }) -} \ No newline at end of file +} diff --git a/internal/cmd/interactive.go b/internal/cmd/interactive.go index 567cf64..8da3dca 100644 --- a/internal/cmd/interactive.go +++ b/internal/cmd/interactive.go @@ -9,8 +9,8 @@ import ( "github.com/manifoldco/promptui" "github.com/raksul/go-clickup/clickup" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/config" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/config" ) var interactiveCmd = &cobra.Command{ diff --git a/internal/cmd/list.go b/internal/cmd/list.go index e3e8ea4..b9562fb 100644 --- a/internal/cmd/list.go +++ b/internal/cmd/list.go @@ -6,10 +6,10 @@ import ( "os" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/cache" - "github.com/tim/cu/internal/config" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/cache" + "github.com/timimsms/cu/internal/config" + "github.com/timimsms/cu/internal/output" ) var ( diff --git a/internal/cmd/me.go b/internal/cmd/me.go index 4add3d0..4a2210f 100644 --- a/internal/cmd/me.go +++ b/internal/cmd/me.go @@ -6,8 +6,8 @@ import ( "os" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/output" ) var meCmd = &cobra.Command{ diff --git a/internal/cmd/root.go b/internal/cmd/root.go index cba4b11..3594f0e 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,8 +6,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/tim/cu/internal/config" - "github.com/tim/cu/internal/version" + "github.com/timimsms/cu/internal/config" + "github.com/timimsms/cu/internal/version" ) var ( diff --git a/internal/cmd/space.go b/internal/cmd/space.go index 290534a..a6d8112 100644 --- a/internal/cmd/space.go +++ b/internal/cmd/space.go @@ -6,9 +6,9 @@ import ( "os" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/cache" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/cache" + "github.com/timimsms/cu/internal/output" ) var spaceCmd = &cobra.Command{ diff --git a/internal/cmd/task.go b/internal/cmd/task.go index a391870..e867f9c 100644 --- a/internal/cmd/task.go +++ b/internal/cmd/task.go @@ -10,9 +10,9 @@ import ( "github.com/raksul/go-clickup/clickup" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/config" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/config" + "github.com/timimsms/cu/internal/output" ) var taskCmd = &cobra.Command{ diff --git a/internal/cmd/task_test.go b/internal/cmd/task_test.go index f872ee2..83d1ccd 100644 --- a/internal/cmd/task_test.go +++ b/internal/cmd/task_test.go @@ -123,7 +123,7 @@ func TestTruncate(t *testing.T) { assert.Contains(t, fmt.Sprintf("%v", r), "slice bounds out of range") } }() - + result := truncate(test.input, test.maxLen) // If we reach here, check that result length doesn't exceed maxLen assert.True(t, len(result) <= test.maxLen) @@ -250,7 +250,7 @@ func TestFormatRelativeTime(t *testing.T) { t.Run("formats past times correctly", func(t *testing.T) { now := time.Now() - + tests := []struct { name string time time.Time @@ -259,7 +259,7 @@ func TestFormatRelativeTime(t *testing.T) { {"5 minutes ago", now.Add(-5 * time.Minute), "5 minutes ago"}, {"2 hours ago", now.Add(-2 * time.Hour), "2 hours ago"}, {"3 days ago", now.Add(-72 * time.Hour), "3 days ago"}, - {"old date", now.Add(-30 * 24 * time.Hour), now.Add(-30*24*time.Hour).Format("Jan 2, 2006")}, + {"old date", now.Add(-30 * 24 * time.Hour), now.Add(-30 * 24 * time.Hour).Format("Jan 2, 2006")}, } for _, test := range tests { @@ -272,17 +272,17 @@ func TestFormatRelativeTime(t *testing.T) { t.Run("formats future times correctly", func(t *testing.T) { now := time.Now() - + tests := []struct { - name string - time time.Time - contains string // Check if result contains expected text + name string + time time.Time + contains string // Check if result contains expected text }{ {"in minutes", now.Add(5 * time.Minute), "minutes"}, {"in hours", now.Add(2 * time.Hour), "hour"}, {"tomorrow or hours", now.Add(25 * time.Hour), ""}, // Special case {"in days", now.Add(50 * time.Hour), "days"}, - {"future date", now.Add(30 * 24 * time.Hour), now.Add(30*24*time.Hour).Format("Jan 2, 2006")}, + {"future date", now.Add(30 * 24 * time.Hour), now.Add(30 * 24 * time.Hour).Format("Jan 2, 2006")}, } for _, test := range tests { @@ -339,7 +339,7 @@ func TestIsToday(t *testing.T) { t.Run("identifies today correctly", func(t *testing.T) { now := time.Now() - + tests := []struct { name string time time.Time @@ -370,7 +370,7 @@ func TestIsTomorrow(t *testing.T) { t.Run("identifies tomorrow correctly", func(t *testing.T) { now := time.Now() tomorrow := now.Add(24 * time.Hour) - + tests := []struct { name string time time.Time @@ -400,7 +400,7 @@ func TestIsThisWeek(t *testing.T) { t.Run("identifies this week correctly", func(t *testing.T) { now := time.Now() - + tests := []struct { name string time time.Time diff --git a/internal/cmd/user.go b/internal/cmd/user.go index d2a8789..8682c14 100644 --- a/internal/cmd/user.go +++ b/internal/cmd/user.go @@ -6,9 +6,9 @@ import ( "os" "github.com/spf13/cobra" - "github.com/tim/cu/internal/api" - "github.com/tim/cu/internal/cache" - "github.com/tim/cu/internal/output" + "github.com/timimsms/cu/internal/api" + "github.com/timimsms/cu/internal/cache" + "github.com/timimsms/cu/internal/output" ) var userCmd = &cobra.Command{ diff --git a/internal/cmd/version.go b/internal/cmd/version.go index 41f116f..0064435 100644 --- a/internal/cmd/version.go +++ b/internal/cmd/version.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/spf13/cobra" - "github.com/tim/cu/internal/version" + "github.com/timimsms/cu/internal/version" ) var versionCmd = &cobra.Command{ diff --git a/internal/config/config.go b/internal/config/config.go index f4125d7..b1e70ab 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -173,15 +173,15 @@ func SaveProjectConfig(settings map[string]interface{}) error { if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } - + // Convert to absolute for comparison absCwd, _ := filepath.Abs(cwd) - + // The config should be within the current directory tree if !strings.HasPrefix(absPath, absCwd) { return fmt.Errorf("invalid config path: outside current directory") } - + // Check for absolute path based on OS if runtime.GOOS == "windows" { // On Windows, absolute paths start with drive letter (e.g., C:\) diff --git a/internal/config/config_test_unix.go b/internal/config/config_test_unix.go index e8762b4..4792dfd 100644 --- a/internal/config/config_test_unix.go +++ b/internal/config/config_test_unix.go @@ -23,11 +23,11 @@ func TestInitProjectConfig_Unix(t *testing.T) { defer func() { _ = os.Chdir(oldWd) }() // Make directory read-only - require.NoError(t, os.Chmod(tmpDir, 0500)) // #nosec G302 - Test code intentionally testing permissions + require.NoError(t, os.Chmod(tmpDir, 0500)) // #nosec G302 - Test code intentionally testing permissions defer func() { _ = os.Chmod(tmpDir, 0750) }() // #nosec G302 - Restoring permissions after test err := InitProjectConfig() assert.Error(t, err) assert.Contains(t, err.Error(), "failed to write project config") }) -} \ No newline at end of file +} diff --git a/internal/interfaces/auth.go b/internal/interfaces/auth.go index a4c2a0d..85a9646 100644 --- a/internal/interfaces/auth.go +++ b/internal/interfaces/auth.go @@ -1,6 +1,6 @@ package interfaces -import "github.com/tim/cu/internal/auth" +import "github.com/timimsms/cu/internal/auth" // AuthManager defines the interface for authentication operations type AuthManager interface { diff --git a/internal/mocks/auth.go b/internal/mocks/auth.go index 616bf24..860fa5c 100644 --- a/internal/mocks/auth.go +++ b/internal/mocks/auth.go @@ -1,7 +1,7 @@ package mocks import ( - "github.com/tim/cu/internal/auth" + "github.com/timimsms/cu/internal/auth" ) // MockAuthManager is a mock implementation of AuthManager for testing diff --git a/internal/output/formatter.go b/internal/output/formatter.go index cb476ea..9b7b6e1 100644 --- a/internal/output/formatter.go +++ b/internal/output/formatter.go @@ -168,7 +168,7 @@ func (f *CSVFormatter) Format(data interface{}) error { } return writer.WriteAll(rows) } - + // Handle single struct if rv.Kind() == reflect.Struct || (rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Struct) { headers, err := structToHeaders(data) @@ -184,7 +184,7 @@ func (f *CSVFormatter) Format(data interface{}) error { } return writer.Write(values) } - + return fmt.Errorf("unsupported CSV data type") } } diff --git a/internal/output/output_test.go b/internal/output/output_test.go index d36abfa..b5882d4 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -183,7 +183,7 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("formats [][]string data", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + data := [][]string{ {"id", "name"}, {"1", "test"}, @@ -192,7 +192,7 @@ func TestCSVFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id,name") assert.Contains(t, output, "1,test") @@ -202,7 +202,7 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("formats []map[string]interface{} data", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + data := []map[string]interface{}{ {"id": 1, "name": "test", "active": true}, {"id": 2, "name": "test2", "active": false}, @@ -210,7 +210,7 @@ func TestCSVFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() // Headers should be present assert.Contains(t, output, "id") @@ -225,7 +225,7 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("handles empty []map[string]interface{}", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + data := []map[string]interface{}{} err := formatter.Format(data) @@ -236,7 +236,7 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("formats []map[string]string data", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + data := []map[string]string{ {"id": "1", "name": "test"}, {"id": "2", "name": "test2"}, @@ -244,7 +244,7 @@ func TestCSVFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "name") @@ -255,7 +255,7 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("handles empty []map[string]string", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + data := []map[string]string{} err := formatter.Format(data) @@ -266,18 +266,18 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("formats struct data", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + type TestStruct struct { ID int `json:"id"` Name string `json:"name"` } - + // Single struct should be converted to slice data := TestStruct{ID: 1, Name: "test"} err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "name") @@ -288,12 +288,12 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("formats slice of structs", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + type TestStruct struct { ID int `json:"id"` Name string `json:"name"` } - + data := []TestStruct{ {ID: 1, Name: "test1"}, {ID: 2, Name: "test2"}, @@ -301,7 +301,7 @@ func TestCSVFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "name") @@ -314,7 +314,7 @@ func TestCSVFormatter_Format(t *testing.T) { t.Run("handles unsupported data type", func(t *testing.T) { var buf bytes.Buffer formatter := &CSVFormatter{Writer: &buf} - + // Unsupported type data := 123 diff --git a/internal/output/table_test.go b/internal/output/table_test.go index c57fb77..7a664d4 100644 --- a/internal/output/table_test.go +++ b/internal/output/table_test.go @@ -13,7 +13,7 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats slice of maps", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + data := []map[string]string{ {"id": "1", "name": "John", "email": "john@example.com"}, {"id": "2", "name": "Jane", "email": "jane@example.com"}, @@ -21,7 +21,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "name") @@ -38,12 +38,12 @@ func TestTableFormatter_Format(t *testing.T) { Writer: &buf, ShowEmpty: true, } - + data := []map[string]string{} err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "No items found") }) @@ -54,7 +54,7 @@ func TestTableFormatter_Format(t *testing.T) { Writer: &buf, ShowEmpty: false, } - + data := []map[string]string{} err := formatter.Format(data) @@ -68,7 +68,7 @@ func TestTableFormatter_Format(t *testing.T) { Writer: &buf, NoHeader: true, } - + data := []map[string]string{ {"id": "1", "name": "John"}, {"id": "2", "name": "Jane"}, @@ -76,7 +76,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() // Should not have headers assert.NotContains(t, output, "id\tname") @@ -88,7 +88,7 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats map data", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + data := map[string]interface{}{ "id": 123, "name": "Test User", @@ -97,7 +97,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "KEY") assert.Contains(t, output, "VALUE") @@ -115,7 +115,7 @@ func TestTableFormatter_Format(t *testing.T) { Writer: &buf, NoHeader: true, } - + data := map[string]string{ "key1": "value1", "key2": "value2", @@ -123,7 +123,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() // Should not have headers assert.NotContains(t, output, "KEY\tVALUE") @@ -135,7 +135,7 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats struct data", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type TestStruct struct { ID int `json:"id"` Name string `json:"name"` @@ -143,7 +143,7 @@ func TestTableFormatter_Format(t *testing.T) { Tags []string `json:"tags"` Time time.Time `json:"time"` } - + data := TestStruct{ ID: 1, Name: "Test", @@ -154,7 +154,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "FIELD") assert.Contains(t, output, "VALUE") @@ -171,12 +171,12 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats pointer to struct", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type TestStruct struct { ID int `json:"id"` Name string `json:"name"` } - + data := &TestStruct{ ID: 1, Name: "Test", @@ -184,7 +184,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "1") @@ -195,18 +195,18 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats simple types", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + // String err := formatter.Format("simple string") assert.NoError(t, err) assert.Contains(t, buf.String(), "simple string") - + // Number buf.Reset() err = formatter.Format(42) assert.NoError(t, err) assert.Contains(t, buf.String(), "42") - + // Boolean buf.Reset() err = formatter.Format(true) @@ -217,13 +217,13 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats slice of structs", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type Person struct { ID int `json:"id"` Name string `json:"name"` Age int `json:"age"` } - + data := []Person{ {ID: 1, Name: "Alice", Age: 30}, {ID: 2, Name: "Bob", Age: 25}, @@ -231,7 +231,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "name") @@ -245,13 +245,13 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats struct with unexported fields", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type TestStruct struct { ID int `json:"id"` Name string `json:"name"` internal string // unexported, should be skipped } - + data := TestStruct{ ID: 1, Name: "Test", @@ -260,7 +260,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "name") @@ -270,7 +270,7 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("uses default writer when nil", func(t *testing.T) { formatter := &TableFormatter{Writer: nil} - + // Should not panic err := formatter.Format("test") assert.NoError(t, err) @@ -279,12 +279,12 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats struct with no json tags", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type TestStruct struct { ID int Name string } - + data := TestStruct{ ID: 1, Name: "Test", @@ -292,7 +292,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "ID") assert.Contains(t, output, "1") @@ -303,14 +303,14 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats struct with json tag options", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type TestStruct struct { ID int `json:"id,omitempty"` Name string `json:"name"` Internal string `json:"-"` // Should be skipped Renamed string `json:"custom_name"` } - + data := TestStruct{ ID: 1, Name: "Test", @@ -320,7 +320,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "id") assert.Contains(t, output, "name") @@ -333,11 +333,11 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats time values", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type TestStruct struct { Created time.Time `json:"created"` } - + testTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) data := TestStruct{ Created: testTime, @@ -345,7 +345,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "created") // Time should be formatted @@ -355,17 +355,17 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats nested structs", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type Address struct { Street string `json:"street"` City string `json:"city"` } - + type Person struct { Name string `json:"name"` Address Address `json:"address"` } - + data := Person{ Name: "John", Address: Address{ @@ -376,7 +376,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "name") assert.Contains(t, output, "John") @@ -388,7 +388,7 @@ func TestTableFormatter_Format(t *testing.T) { t.Run("formats relative time", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + // Test data with recent time now := time.Now() data := []map[string]interface{}{ @@ -397,7 +397,7 @@ func TestTableFormatter_Format(t *testing.T) { "created": now.Add(-5 * time.Minute), }, { - "id": "2", + "id": "2", "created": now.Add(-2 * time.Hour), }, { @@ -408,7 +408,7 @@ func TestTableFormatter_Format(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() // Should format times relatively assert.Contains(t, output, "ago") @@ -419,7 +419,7 @@ func TestTableFormatter_EdgeCases(t *testing.T) { t.Run("handles nil values in map", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + data := map[string]interface{}{ "key1": "value1", "key2": nil, @@ -428,7 +428,7 @@ func TestTableFormatter_EdgeCases(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + output := buf.String() assert.Contains(t, output, "key1") assert.Contains(t, output, "value1") @@ -439,13 +439,13 @@ func TestTableFormatter_EdgeCases(t *testing.T) { t.Run("handles empty struct", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type EmptyStruct struct{} data := EmptyStruct{} err := formatter.Format(data) assert.NoError(t, err) - + // Should have headers but no data rows output := buf.String() lines := strings.Split(strings.TrimSpace(output), "\n") @@ -455,12 +455,12 @@ func TestTableFormatter_EdgeCases(t *testing.T) { t.Run("handles struct with all unexported fields", func(t *testing.T) { var buf bytes.Buffer formatter := &TableFormatter{Writer: &buf} - + type PrivateStruct struct { internal1 string internal2 int } - + data := PrivateStruct{ internal1: "hidden", internal2: 42, @@ -468,10 +468,10 @@ func TestTableFormatter_EdgeCases(t *testing.T) { err := formatter.Format(data) assert.NoError(t, err) - + // Should not expose private fields output := buf.String() assert.NotContains(t, output, "hidden") assert.NotContains(t, output, "42") }) -} \ No newline at end of file +} diff --git a/internal/output/wrapper_test.go b/internal/output/wrapper_test.go index a833cd3..eb638a6 100644 --- a/internal/output/wrapper_test.go +++ b/internal/output/wrapper_test.go @@ -23,7 +23,7 @@ func TestNewFormatter(t *testing.T) { t.Run("creates formatter with config", func(t *testing.T) { config := &mockConfig{values: map[string]string{"output": "json"}} formatter := NewFormatter(config) - + assert.NotNil(t, formatter) assert.Equal(t, config, formatter.config) assert.True(t, formatter.colorOutput, "Should default to color output") @@ -32,7 +32,7 @@ func TestNewFormatter(t *testing.T) { t.Run("creates formatter with nil config", func(t *testing.T) { formatter := NewFormatter(nil) - + assert.NotNil(t, formatter) assert.Nil(t, formatter.config) assert.True(t, formatter.colorOutput) @@ -45,20 +45,20 @@ func TestFormatterWrapper_Print(t *testing.T) { t.Run("uses default table format", func(t *testing.T) { formatter := NewFormatter(nil) - + // Capture stdout oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - + err := formatter.Print(testData) - + _ = w.Close() os.Stdout = oldStdout - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + assert.NoError(t, err) assert.NotEmpty(t, buf.String()) }) @@ -66,20 +66,20 @@ func TestFormatterWrapper_Print(t *testing.T) { t.Run("uses config format", func(t *testing.T) { config := &mockConfig{values: map[string]string{"output": "json"}} formatter := NewFormatter(config) - + // Capture stdout oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - + err := formatter.Print(testData) - + _ = w.Close() os.Stdout = oldStdout - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + assert.NoError(t, err) assert.Contains(t, buf.String(), "key") assert.Contains(t, buf.String(), "value") @@ -92,9 +92,9 @@ func TestFormatterWrapper_PrintTo(t *testing.T) { t.Run("prints to specified writer", func(t *testing.T) { var buf bytes.Buffer formatter := NewFormatter(nil) - + err := formatter.PrintTo(&buf, testData) - + assert.NoError(t, err) assert.NotEmpty(t, buf.String()) }) @@ -103,9 +103,9 @@ func TestFormatterWrapper_PrintTo(t *testing.T) { var buf bytes.Buffer config := &mockConfig{values: map[string]string{"output": "json"}} formatter := NewFormatter(config) - + err := formatter.PrintTo(&buf, testData) - + assert.NoError(t, err) output := buf.String() assert.Contains(t, output, "key") @@ -116,40 +116,40 @@ func TestFormatterWrapper_PrintTo(t *testing.T) { func TestFormatterWrapper_PrintInfo(t *testing.T) { t.Run("prints info message when not quiet", func(t *testing.T) { formatter := NewFormatter(nil) - + // Capture stdout oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - + formatter.PrintInfo("test info") - + _ = w.Close() os.Stdout = oldStdout - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + assert.Contains(t, buf.String(), "test info") }) t.Run("does not print when quiet mode is enabled", func(t *testing.T) { formatter := NewFormatter(nil) formatter.SetQuiet(true) - + // Capture stdout oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - + formatter.PrintInfo("test info") - + _ = w.Close() os.Stdout = oldStdout - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + assert.Empty(t, buf.String()) }) } @@ -157,20 +157,20 @@ func TestFormatterWrapper_PrintInfo(t *testing.T) { func TestFormatterWrapper_PrintSuccess(t *testing.T) { t.Run("prints success message with check mark", func(t *testing.T) { formatter := NewFormatter(nil) - + // Capture stdout oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - + formatter.PrintSuccess("test success") - + _ = w.Close() os.Stdout = oldStdout - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + output := buf.String() assert.Contains(t, output, "✓") assert.Contains(t, output, "test success") @@ -179,40 +179,40 @@ func TestFormatterWrapper_PrintSuccess(t *testing.T) { t.Run("does not print when quiet mode is enabled", func(t *testing.T) { formatter := NewFormatter(nil) formatter.SetQuiet(true) - + // Capture stdout oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - + formatter.PrintSuccess("test success") - + _ = w.Close() os.Stdout = oldStdout - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + assert.Empty(t, buf.String()) }) t.Run("prints without color when color is disabled", func(t *testing.T) { formatter := NewFormatter(nil) formatter.SetColor(false) - + // Capture stdout oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - + formatter.PrintSuccess("test success") - + _ = w.Close() os.Stdout = oldStdout - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + output := buf.String() assert.Contains(t, output, "✓") assert.Contains(t, output, "test success") @@ -223,20 +223,20 @@ func TestFormatterWrapper_PrintError(t *testing.T) { t.Run("prints error message with X mark", func(t *testing.T) { formatter := NewFormatter(nil) testErr := errors.New("test error") - + // Capture stderr oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w - + formatter.PrintError(testErr) - + _ = w.Close() os.Stderr = oldStderr - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + output := buf.String() assert.Contains(t, output, "✗") assert.Contains(t, output, "test error") @@ -246,20 +246,20 @@ func TestFormatterWrapper_PrintError(t *testing.T) { formatter := NewFormatter(nil) formatter.SetQuiet(true) testErr := errors.New("test error") - + // Capture stderr oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w - + formatter.PrintError(testErr) - + _ = w.Close() os.Stderr = oldStderr - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + output := buf.String() assert.Contains(t, output, "✗") assert.Contains(t, output, "test error") @@ -269,20 +269,20 @@ func TestFormatterWrapper_PrintError(t *testing.T) { formatter := NewFormatter(nil) formatter.SetColor(false) testErr := errors.New("test error") - + // Capture stderr oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w - + formatter.PrintError(testErr) - + _ = w.Close() os.Stderr = oldStderr - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + output := buf.String() assert.Contains(t, output, "✗") assert.Contains(t, output, "test error") @@ -292,20 +292,20 @@ func TestFormatterWrapper_PrintError(t *testing.T) { func TestFormatterWrapper_PrintWarning(t *testing.T) { t.Run("prints warning message with warning sign", func(t *testing.T) { formatter := NewFormatter(nil) - + // Capture stderr oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w - + formatter.PrintWarning("test warning") - + _ = w.Close() os.Stderr = oldStderr - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + output := buf.String() assert.Contains(t, output, "⚠") assert.Contains(t, output, "test warning") @@ -314,40 +314,40 @@ func TestFormatterWrapper_PrintWarning(t *testing.T) { t.Run("does not print when quiet mode is enabled", func(t *testing.T) { formatter := NewFormatter(nil) formatter.SetQuiet(true) - + // Capture stderr oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w - + formatter.PrintWarning("test warning") - + _ = w.Close() os.Stderr = oldStderr - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + assert.Empty(t, buf.String()) }) t.Run("prints without color when color is disabled", func(t *testing.T) { formatter := NewFormatter(nil) formatter.SetColor(false) - + // Capture stderr oldStderr := os.Stderr r, w, _ := os.Pipe() os.Stderr = w - + formatter.PrintWarning("test warning") - + _ = w.Close() os.Stderr = oldStderr - + var buf bytes.Buffer _, _ = io.Copy(&buf, r) - + output := buf.String() assert.Contains(t, output, "⚠") assert.Contains(t, output, "test warning") @@ -357,12 +357,12 @@ func TestFormatterWrapper_PrintWarning(t *testing.T) { func TestFormatterWrapper_SetQuiet(t *testing.T) { t.Run("sets quiet mode", func(t *testing.T) { formatter := NewFormatter(nil) - + assert.False(t, formatter.quietMode) - + formatter.SetQuiet(true) assert.True(t, formatter.quietMode) - + formatter.SetQuiet(false) assert.False(t, formatter.quietMode) }) @@ -371,12 +371,12 @@ func TestFormatterWrapper_SetQuiet(t *testing.T) { func TestFormatterWrapper_SetColor(t *testing.T) { t.Run("sets color mode", func(t *testing.T) { formatter := NewFormatter(nil) - + assert.True(t, formatter.colorOutput) - + formatter.SetColor(false) assert.False(t, formatter.colorOutput) - + formatter.SetColor(true) assert.True(t, formatter.colorOutput) }) @@ -406,7 +406,7 @@ func TestFormatterWrapper_SetFormat(t *testing.T) { t.Run("accepts valid formats", func(t *testing.T) { validFormats := []string{"json", "yaml", "table", "csv"} - + for _, format := range validFormats { err := formatter.SetFormat(format) assert.NoError(t, err, "Should accept %s format", format) @@ -424,10 +424,10 @@ func TestFormatterWrapper_SetTableHeader(t *testing.T) { t.Run("accepts table headers", func(t *testing.T) { formatter := NewFormatter(nil) headers := []string{"ID", "Name", "Status"} - + // Should not panic formatter.SetTableHeader(headers) - + // Currently a no-op, so we just test it doesn't crash assert.NotNil(t, formatter) }) @@ -437,20 +437,20 @@ func TestFormatterWrapper_Integration(t *testing.T) { t.Run("full workflow with all methods", func(t *testing.T) { config := &mockConfig{values: map[string]string{"output": "json"}} formatter := NewFormatter(config) - + // Configure formatter formatter.SetQuiet(false) formatter.SetColor(true) - + // Test format operations assert.Equal(t, "json", formatter.GetFormat()) - + err := formatter.SetFormat("yaml") assert.NoError(t, err) - + // Test table headers (no-op currently) formatter.SetTableHeader([]string{"A", "B", "C"}) - + // Test data printing testData := map[string]string{"test": "data"} var buf bytes.Buffer @@ -464,7 +464,7 @@ func TestFormatterWrapper_EdgeCases(t *testing.T) { t.Run("handles nil data gracefully", func(t *testing.T) { formatter := NewFormatter(nil) var buf bytes.Buffer - + err := formatter.PrintTo(&buf, nil) // Should handle nil without crashing assert.NoError(t, err) @@ -479,12 +479,12 @@ func TestFormatterWrapper_EdgeCases(t *testing.T) { t.Run("handles case variations in SetFormat", func(t *testing.T) { formatter := NewFormatter(nil) - + // These should be valid (implementation doesn't do case conversion in SetFormat) err := formatter.SetFormat("JSON") assert.Error(t, err) // Current implementation is case-sensitive - + err = formatter.SetFormat("json") assert.NoError(t, err) }) -} \ No newline at end of file +} diff --git a/internal/testutil/ci.go b/internal/testutil/ci.go index 6ee9a8c..b80eef8 100644 --- a/internal/testutil/ci.go +++ b/internal/testutil/ci.go @@ -27,4 +27,4 @@ func SkipIfNoKeyring(t *testing.T) { if IsCI() { t.Skip("CI: keyring not available") } -} \ No newline at end of file +}