diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/root.go | 59 | ||||
| -rw-r--r-- | internal/cli/root_test.go | 128 |
2 files changed, 187 insertions, 0 deletions
diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..6029e81 --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,59 @@ +package cli + +import ( + "fmt" + + timr "codeberg.org/snonux/timr/internal" + "codeberg.org/snonux/timr/internal/config" + "github.com/spf13/cobra" +) + +var loadedConfig = config.Default() + +// Execute runs the root command. +func Execute() error { + return NewRootCmd().Execute() +} + +// NewRootCmd creates the Cobra root command. +func NewRootCmd() *cobra.Command { + var configPath string + var showVersion bool + + cmd := &cobra.Command{ + Use: "timr", + Short: "Track time from your terminal", + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if showVersion { + return nil + } + + cfg, err := config.Load(configPath) + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + loadedConfig = cfg + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if showVersion { + _, err := fmt.Fprintln(cmd.OutOrStdout(), timr.Version) + return err + } + + return cmd.Help() + }, + } + + cmd.Flags().BoolVar(&showVersion, "version", false, "Print version and exit") + cmd.PersistentFlags().StringVar(&configPath, "config", "", "Path to config file") + + return cmd +} + +// CurrentConfig returns the config loaded in PersistentPreRunE. +func CurrentConfig() config.Config { + return loadedConfig +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 0000000..c900027 --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,128 @@ +package cli + +import ( + "bytes" + "io" + "os" + "path/filepath" + "strings" + "testing" + + timr "codeberg.org/snonux/timr/internal" +) + +func TestRootVersionFlag(t *testing.T) { + loadedConfig = CurrentConfig() + + var out bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&out) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"--version"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + + if strings.TrimSpace(out.String()) != timr.Version { + t.Fatalf("output = %q, want %q", strings.TrimSpace(out.String()), timr.Version) + } +} + +func TestRootLoadsConfigInPersistentPreRun(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + + cfgPath := filepath.Join(t.TempDir(), "config.json") + content := `{ + "hostname": "from-config", + "worktime_db_dir": "~/custom-db" +}` + if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + cmd := NewRootCmd() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"--config", cfgPath}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + + cfg := CurrentConfig() + if cfg.Hostname != "from-config" { + t.Fatalf("Hostname = %q, want %q", cfg.Hostname, "from-config") + } + + wantDir := filepath.Join(tempHome, "custom-db") + if cfg.WorktimeDBDir != wantDir { + t.Fatalf("WorktimeDBDir = %q, want %q", cfg.WorktimeDBDir, wantDir) + } +} + +func TestRootInvalidConfigFileReturnsError(t *testing.T) { + cfgPath := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(cfgPath, []byte(`{"hostname":`), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + cmd := NewRootCmd() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"--config", cfgPath}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Execute() error = nil, want config parse error") + } + if !strings.Contains(err.Error(), "load config") { + t.Fatalf("Execute() error = %v, want load config context", err) + } +} + +func TestVersionSkipsConfigLoading(t *testing.T) { + cfgPath := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(cfgPath, []byte(`{"hostname":`), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + var out bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&out) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"--config", cfgPath, "--version"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + + if strings.TrimSpace(out.String()) != timr.Version { + t.Fatalf("output = %q, want %q", strings.TrimSpace(out.String()), timr.Version) + } +} + +func TestRootUsesDefaultConfigWhenNoFileExists(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + t.Setenv("XDG_CONFIG_HOME", filepath.Join(tempHome, ".config")) + + cmd := NewRootCmd() + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + + cfg := CurrentConfig() + if cfg.WeekWorkHours != 40 { + t.Fatalf("WeekWorkHours = %v, want 40", cfg.WeekWorkHours) + } + + wantDir := filepath.Join(tempHome, "git", "worktime") + if cfg.WorktimeDBDir != wantDir { + t.Fatalf("WorktimeDBDir = %q, want %q", cfg.WorktimeDBDir, wantDir) + } +} |
