diff --git a/cmd/info.go b/cmd/info.go new file mode 100644 index 0000000..7719592 --- /dev/null +++ b/cmd/info.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "fmt" + + "github.com/chenasraf/cospend-cli/internal/api" + "github.com/chenasraf/cospend-cli/internal/cache" + "github.com/chenasraf/cospend-cli/internal/config" + "github.com/spf13/cobra" +) + +// NewInfoCommand creates the info command +func NewInfoCommand() *cobra.Command { + return &cobra.Command{ + Use: "info", + Short: "Show account and configuration info", + Long: `Show the configured Nextcloud server, authenticated user, and user locale/language.`, + RunE: runInfo, + } +} + +func runInfo(cmd *cobra.Command, _ []string) error { + cmd.SilenceUsage = true + + cfg, err := config.Load() + if err != nil { + return err + } + + client := api.NewClient(cfg) + client.Debug = Debug + client.DebugWriter = cmd.ErrOrStderr() + + userInfo, ok := cache.LoadUserInfo() + if !ok { + userInfo, err = client.GetUserInfo() + if err != nil { + return fmt.Errorf("fetching user info: %w", err) + } + _ = cache.SaveUserInfo(userInfo) + } + + server := config.NormalizeURL(cfg.Domain) + + out := cmd.OutOrStdout() + _, _ = fmt.Fprintf(out, "Server: %s\n", server) + _, _ = fmt.Fprintf(out, "User: %s\n", cfg.User) + _, _ = fmt.Fprintf(out, "Locale: %s\n", userInfo.Locale) + _, _ = fmt.Fprintf(out, "Language: %s\n", userInfo.Language) + + return nil +} diff --git a/cmd/info_test.go b/cmd/info_test.go new file mode 100644 index 0000000..1f2025a --- /dev/null +++ b/cmd/info_test.go @@ -0,0 +1,105 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestInfoCommand(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ocs/v2.php/cloud/user" { + _ = json.NewEncoder(w).Encode(makeOCSResponse(200, map[string]string{ + "locale": "he_IL", + "language": "he", + })) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + cleanup := setupTestEnv(t, server.URL) + defer cleanup() + + cmd := NewInfoCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + + err := cmd.Execute() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + output := stdout.String() + + expected := []string{ + "Server: " + server.URL, + "User: testuser", + "Locale: he_IL", + "Language: he", + } + for _, exp := range expected { + if !strings.Contains(output, exp) { + t.Errorf("Output missing %q, got:\n%s", exp, output) + } + } +} + +func TestInfoCommandNormalizesURL(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ocs/v2.php/cloud/user" { + _ = json.NewEncoder(w).Encode(makeOCSResponse(200, map[string]string{ + "locale": "en_US", + "language": "en", + })) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + // Test with trailing slash — should be stripped + cleanup := setupTestEnv(t, server.URL+"/") + defer cleanup() + + cmd := NewInfoCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + + err := cmd.Execute() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + output := stdout.String() + if !strings.Contains(output, "Server: "+server.URL) { + t.Errorf("Expected trailing slash stripped, got:\n%s", output) + } + if strings.Contains(output, server.URL+"/") { + t.Errorf("Trailing slash should be stripped, got:\n%s", output) + } +} + +func TestInfoCommandAPIError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Internal Server Error")) + })) + defer server.Close() + + cleanup := setupTestEnv(t, server.URL) + defer cleanup() + + cmd := NewInfoCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + + err := cmd.Execute() + if err == nil { + t.Error("Expected error from API") + } +} diff --git a/cmd/init.go b/cmd/init.go index 3ce6e4c..19523bf 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -76,13 +76,7 @@ func runInit(cmd *cobra.Command, _ []string) error { if err != nil { return err } - domain = strings.TrimRight(domain, "/") - - // Auto-prepend https:// if no scheme provided - domainLower := strings.ToLower(domain) - if !strings.HasPrefix(domainLower, "http://") && !strings.HasPrefix(domainLower, "https://") { - domain = "https://" + domain - } + domain = config.NormalizeURL(domain) // Choose login method _, _ = fmt.Fprintln(cmd.OutOrStdout()) diff --git a/cmd/init_test.go b/cmd/init_test.go index d835555..15d76ad 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -262,13 +262,7 @@ func TestDomainAutoPrependHTTPS(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { - domain := tt.input - domain = strings.TrimRight(domain, "/") - domainLower := strings.ToLower(domain) - if !strings.HasPrefix(domainLower, "http://") && !strings.HasPrefix(domainLower, "https://") { - domain = "https://" + domain - } - + domain := config.NormalizeURL(tt.input) if domain != tt.expected { t.Errorf("Domain = %s, want %s", domain, tt.expected) } diff --git a/internal/api/client.go b/internal/api/client.go index 675cd6b..aea3b44 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -216,10 +216,7 @@ func (c *Client) debugf(format string, args ...interface{}) { } func (c *Client) doRequest(method, path string, body io.Reader) (*http.Response, error) { - baseURL := strings.TrimSuffix(c.config.Domain, "/") - if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { - baseURL = "https://" + baseURL - } + baseURL := config.NormalizeURL(c.config.Domain) fullURL := fmt.Sprintf("%s%s", baseURL, path) c.debugf("Request: %s %s", method, fullURL) diff --git a/internal/config/config.go b/internal/config/config.go index 9688d73..25814ea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/BurntSushi/toml" "github.com/adrg/xdg" @@ -14,6 +15,16 @@ import ( const appName = "cospend" +// NormalizeURL trims trailing slashes and prepends https:// if no scheme is present. +func NormalizeURL(url string) string { + url = strings.TrimRight(url, "/") + lower := strings.ToLower(url) + if !strings.HasPrefix(lower, "http://") && !strings.HasPrefix(lower, "https://") { + url = "https://" + url + } + return url +} + // Config holds the Nextcloud configuration type Config struct { Domain string `json:"domain" yaml:"domain" toml:"domain"` diff --git a/main.go b/main.go index 19671f8..05b54fb 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ func main() { rootCmd.AddCommand(cmd.NewListCommand()) rootCmd.AddCommand(cmd.NewDeleteCommand()) rootCmd.AddCommand(cmd.NewProjectsCommand()) + rootCmd.AddCommand(cmd.NewInfoCommand()) rootCmd.PersistentFlags().BoolVarP(&cmd.Debug, "debug", "d", false, "Enable debug output") rootCmd.PersistentFlags().StringVarP(&cmd.ProjectID, "project", "p", "", "Project ID")