refactor: extract url normalization to util

This commit is contained in:
2026-02-08 23:58:36 +02:00
parent 2b45a3b80d
commit e0acac5d7d
7 changed files with 172 additions and 18 deletions

52
cmd/info.go Normal file
View File

@@ -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
}

105
cmd/info_test.go Normal file
View File

@@ -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")
}
}

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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"`

View File

@@ -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")