feat: add logout command

This commit is contained in:
2026-03-23 22:40:06 +02:00
parent 59cb401946
commit 345d7c0cd1
5 changed files with 202 additions and 0 deletions

View File

@@ -408,6 +408,16 @@ cospend config get default-project
---
### Logging Out
```bash
cospend logout
```
Removes the configuration file (stored credentials) and clears all cached data.
---
## Caching
Project data (members, categories, payment methods, currencies) is cached locally to avoid repeated

58
cmd/logout.go Normal file
View File

@@ -0,0 +1,58 @@
package cmd
import (
"fmt"
"os"
"github.com/chenasraf/cospend-cli/internal/cache"
"github.com/chenasraf/cospend-cli/internal/config"
"github.com/spf13/cobra"
)
// NewLogoutCommand creates the logout command
func NewLogoutCommand() *cobra.Command {
return &cobra.Command{
Use: "logout",
Short: "Remove configuration and cached data",
Long: `Remove the configuration file and clear all cached data.
This effectively logs you out by removing your stored credentials
and any cached project data.`,
RunE: runLogout,
}
}
func runLogout(cmd *cobra.Command, _ []string) error {
cmd.SilenceUsage = true
out := cmd.OutOrStdout()
removed := false
// Remove config file
configPath := config.GetConfigPath()
if configPath != "" {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("removing config file: %w", err)
}
_, _ = fmt.Fprintf(out, "Removed config: %s\n", configPath)
removed = true
}
// Remove cache directory
cacheDir := cache.GetCacheDir()
if info, err := os.Stat(cacheDir); err == nil && info.IsDir() {
if err := os.RemoveAll(cacheDir); err != nil {
return fmt.Errorf("removing cache directory: %w", err)
}
_, _ = fmt.Fprintf(out, "Removed cache: %s\n", cacheDir)
removed = true
}
if !removed {
_, _ = fmt.Fprintln(out, "Nothing to remove (no config or cache found).")
} else {
_, _ = fmt.Fprintln(out, "Logged out successfully.")
}
return nil
}

128
cmd/logout_test.go Normal file
View File

@@ -0,0 +1,128 @@
package cmd
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestNewLogoutCommand(t *testing.T) {
cmd := NewLogoutCommand()
if cmd.Use != "logout" {
t.Errorf("Wrong Use: %s", cmd.Use)
}
}
func TestLogoutRemovesConfigAndCache(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tempDir)
t.Setenv("XDG_CACHE_HOME", tempDir)
t.Setenv("HOME", tempDir)
// Create config file
configDir := filepath.Join(tempDir, "cospend")
if err := os.MkdirAll(configDir, 0700); err != nil {
t.Fatalf("Failed to create config dir: %v", err)
}
configPath := filepath.Join(configDir, "cospend.json")
if err := os.WriteFile(configPath, []byte(`{"domain":"x","user":"u","password":"p"}`), 0600); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
// Create cache directory with a file in a separate temp dir
cacheTempDir := t.TempDir()
t.Setenv("XDG_CACHE_HOME", cacheTempDir)
cacheDir := filepath.Join(cacheTempDir, "cospend")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
t.Fatalf("Failed to create cache dir: %v", err)
}
cachePath := filepath.Join(cacheDir, "test-project.json")
if err := os.WriteFile(cachePath, []byte(`{}`), 0600); err != nil {
t.Fatalf("Failed to write cache: %v", err)
}
cmd := NewLogoutCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
err := cmd.Execute()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Config should be removed
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
t.Error("Config file should have been removed")
}
// Cache dir should be removed
if _, err := os.Stat(cacheDir); !os.IsNotExist(err) {
t.Error("Cache directory should have been removed")
}
output := stdout.String()
if !bytes.Contains([]byte(output), []byte("Logged out successfully")) {
t.Errorf("Expected success message, got: %s", output)
}
}
func TestLogoutNothingToRemove(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tempDir)
t.Setenv("XDG_CACHE_HOME", tempDir)
t.Setenv("HOME", tempDir)
cmd := NewLogoutCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
err := cmd.Execute()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
output := stdout.String()
if !bytes.Contains([]byte(output), []byte("Nothing to remove")) {
t.Errorf("Expected 'Nothing to remove' message, got: %s", output)
}
}
func TestLogoutConfigOnly(t *testing.T) {
tempDir := t.TempDir()
cacheTempDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tempDir)
t.Setenv("XDG_CACHE_HOME", cacheTempDir)
t.Setenv("HOME", tempDir)
// Create config file only (no cache)
configDir := filepath.Join(tempDir, "cospend")
if err := os.MkdirAll(configDir, 0700); err != nil {
t.Fatalf("Failed to create config dir: %v", err)
}
configPath := filepath.Join(configDir, "cospend.json")
if err := os.WriteFile(configPath, []byte(`{"domain":"x","user":"u","password":"p"}`), 0600); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
cmd := NewLogoutCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
err := cmd.Execute()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
t.Error("Config file should have been removed")
}
output := stdout.String()
if !bytes.Contains([]byte(output), []byte("Removed config")) {
t.Errorf("Expected 'Removed config' message, got: %s", output)
}
if !bytes.Contains([]byte(output), []byte("Logged out successfully")) {
t.Errorf("Expected success message, got: %s", output)
}
}

View File

@@ -149,6 +149,11 @@ func getCacheHome() string {
return xdg.CacheHome
}
// GetCacheDir returns the cache directory path
func GetCacheDir() string {
return filepath.Join(getCacheHome(), appName)
}
// getCachePath returns the cache file path for a project
func getCachePath(projectID string) (string, error) {
cacheDir := filepath.Join(getCacheHome(), appName)

View File

@@ -38,6 +38,7 @@ func main() {
rootCmd.AddCommand(cmd.NewProjectsCommand())
rootCmd.AddCommand(cmd.NewInfoCommand())
rootCmd.AddCommand(cmd.NewConfigCommand())
rootCmd.AddCommand(cmd.NewLogoutCommand())
rootCmd.PersistentFlags().BoolVarP(&cmd.Debug, "debug", "D", false, "Enable debug output")
rootCmd.PersistentFlags().StringVarP(&cmd.ProjectID, "project", "p", "", "Project ID")