From 0ea0df8887287693d106c75939564a24fe7c97e5 Mon Sep 17 00:00:00 2001 From: Tim Walsh Date: Wed, 15 Jul 2026 00:02:18 -0700 Subject: [PATCH] refactor: rename module to github.com/timimsms/cu Fixes go install, which cannot work while the module path differs from the repository URL. ldflags -X paths in the Makefile and .goreleaser.yml are updated in lockstep to keep version stamping intact. Also fixes the release footer install instructions, narrows the Homebrew heading to macOS (casks are macOS-only), and adds the GoReleaser-documented post-install hook to strip com.apple.quarantine from the unsigned cask binary. Co-Authored-By: Claude Fable 5 --- .goreleaser.yml | 18 +- Makefile | 8 +- README.md | 2 +- cmd/cu/main.go | 2 +- docs/project-buildout/PHASE_1_FOUNDATION.md | 2 +- go.mod | 2 +- internal/api/client.go | 4 +- internal/api/retry.go | 2 +- internal/api/users.go | 2 +- internal/api/users_test.go | 2 +- internal/auth/auth.go | 2 +- internal/auth/mock/README.md | 2 +- internal/auth/mock/fixtures.go | 4 +- internal/auth/mock/mock.go | 4 +- internal/auth/mock/mock_test.go | 4 +- internal/cache/cache.go | 2 +- internal/cmd/api.go | 18 +- internal/cmd/api_test.go | 14 +- internal/cmd/auth.go | 4 +- internal/cmd/bulk.go | 4 +- internal/cmd/cache.go | 20 +-- internal/cmd/cache_test.go | 102 +++++------ internal/cmd/comment.go | 92 +++++----- internal/cmd/comment_test.go | 70 ++++---- internal/cmd/config.go | 4 +- internal/cmd/docs.go | 10 +- internal/cmd/export.go | 2 +- internal/cmd/export_test.go | 32 ++-- internal/cmd/interactive.go | 4 +- internal/cmd/list.go | 8 +- internal/cmd/me.go | 4 +- internal/cmd/root.go | 4 +- internal/cmd/space.go | 6 +- internal/cmd/task.go | 6 +- internal/cmd/task_test.go | 22 +-- internal/cmd/user.go | 6 +- internal/cmd/version.go | 2 +- internal/config/config.go | 6 +- internal/config/config_test_unix.go | 4 +- internal/interfaces/auth.go | 2 +- internal/mocks/auth.go | 2 +- internal/output/formatter.go | 4 +- internal/output/output_test.go | 30 ++-- internal/output/table_test.go | 102 +++++------ internal/output/wrapper_test.go | 178 ++++++++++---------- internal/testutil/ci.go | 2 +- 46 files changed, 416 insertions(+), 410 deletions(-) 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 +}