feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/auth"
|
||||
"codeberg.org/petrbalvin/goget/internal/core"
|
||||
"codeberg.org/petrbalvin/interpres"
|
||||
)
|
||||
|
||||
// AuthConfig holds authentication configuration
|
||||
type AuthConfig struct {
|
||||
Username string `json:"username,omitempty" toml:"username,omitempty"`
|
||||
Password string `json:"password,omitempty" toml:"password,omitempty"`
|
||||
PasswordFile string `json:"password_file,omitempty" toml:"password_file,omitempty"`
|
||||
AuthType string `json:"auth_type,omitempty" toml:"auth_type,omitempty"` // basic, digest, auto
|
||||
OAuth auth.OAuthConfig `json:"oauth,omitempty" toml:"oauth,omitempty"`
|
||||
}
|
||||
|
||||
// RecursiveConfig holds recursive download configuration
|
||||
type RecursiveConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty" toml:"enabled,omitempty"`
|
||||
MaxDepth int `json:"max_depth,omitempty" toml:"max_depth,omitempty"`
|
||||
FollowExternal bool `json:"follow_external,omitempty" toml:"follow_external,omitempty"`
|
||||
ExcludePatterns []string `json:"exclude_patterns,omitempty" toml:"exclude_patterns,omitempty"`
|
||||
IncludePatterns []string `json:"include_patterns,omitempty" toml:"include_patterns,omitempty"`
|
||||
Delay string `json:"delay,omitempty" toml:"delay,omitempty"`
|
||||
Parallel int `json:"parallel,omitempty" toml:"parallel,omitempty"`
|
||||
}
|
||||
|
||||
// ExtractConfig holds archive extraction configuration
|
||||
type ExtractConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty" toml:"enabled,omitempty"`
|
||||
OutputDir string `json:"output_dir,omitempty" toml:"output_dir,omitempty"`
|
||||
StripComponents int `json:"strip_components,omitempty" toml:"strip_components,omitempty"`
|
||||
ExcludePatterns []string `json:"exclude_patterns,omitempty" toml:"exclude_patterns,omitempty"`
|
||||
PreservePermissions bool `json:"preserve_permissions,omitempty" toml:"preserve_permissions,omitempty"`
|
||||
AutoDetect bool `json:"auto_detect,omitempty" toml:"auto_detect,omitempty"`
|
||||
}
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
Timeout time.Duration `json:"timeout,omitempty" toml:"timeout,omitempty"`
|
||||
Parallel int `json:"parallel,omitempty" toml:"parallel,omitempty"`
|
||||
OutputDir string `json:"output_dir,omitempty" toml:"output_dir,omitempty"`
|
||||
Verbose bool `json:"verbose,omitempty" toml:"verbose,omitempty"`
|
||||
Debug bool `json:"debug,omitempty" toml:"debug,omitempty"`
|
||||
IPv4Fallback bool `json:"ipv4_fallback,omitempty" toml:"ipv4_fallback,omitempty"`
|
||||
Proxy string `json:"proxy,omitempty" toml:"proxy,omitempty"`
|
||||
UserAgent string `json:"user_agent,omitempty" toml:"user_agent,omitempty"`
|
||||
AutoDecompress bool `json:"auto_decompress,omitempty" toml:"auto_decompress,omitempty"`
|
||||
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty" toml:"insecure_skip_verify,omitempty"`
|
||||
ChecksumAlgo string `json:"checksum_algo,omitempty" toml:"checksum_algo,omitempty"`
|
||||
Auth AuthConfig `json:"auth,omitempty" toml:"auth,omitempty"`
|
||||
MaxSpeed int64 `json:"max_speed,omitempty" toml:"max_speed,omitempty"`
|
||||
AutoResume bool `json:"auto_resume,omitempty" toml:"auto_resume,omitempty"`
|
||||
KeepMetadata bool `json:"keep_metadata,omitempty" toml:"keep_metadata,omitempty"`
|
||||
Color string `json:"color,omitempty" toml:"color,omitempty"`
|
||||
ProgressStyle string `json:"progress_style,omitempty" toml:"progress_style,omitempty"`
|
||||
ShowETA bool `json:"show_eta,omitempty" toml:"show_eta,omitempty"`
|
||||
Recursive RecursiveConfig `json:"recursive,omitempty" toml:"recursive,omitempty"`
|
||||
Extract ExtractConfig `json:"extract,omitempty" toml:"extract,omitempty"`
|
||||
CookieJar string `json:"cookie_jar,omitempty" toml:"cookie_jar,omitempty"`
|
||||
DNSServers []string `json:"dns_servers,omitempty" toml:"dns_servers,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty" toml:"max_retries,omitempty"`
|
||||
configPath string
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Timeout: 30 * time.Minute,
|
||||
Parallel: 0,
|
||||
OutputDir: ".",
|
||||
Verbose: true,
|
||||
Debug: false,
|
||||
IPv4Fallback: true,
|
||||
Proxy: "",
|
||||
UserAgent: core.Name + "/" + core.Version,
|
||||
AutoDecompress: true,
|
||||
InsecureSkipVerify: false,
|
||||
ChecksumAlgo: "sha256",
|
||||
Auth: AuthConfig{},
|
||||
MaxSpeed: 0,
|
||||
AutoResume: true,
|
||||
KeepMetadata: false,
|
||||
Color: "auto",
|
||||
ProgressStyle: "unicode",
|
||||
ShowETA: true,
|
||||
Recursive: RecursiveConfig{
|
||||
Enabled: false,
|
||||
MaxDepth: 3,
|
||||
FollowExternal: false,
|
||||
ExcludePatterns: []string{},
|
||||
IncludePatterns: []string{},
|
||||
Delay: "100ms",
|
||||
},
|
||||
Extract: ExtractConfig{
|
||||
Enabled: false,
|
||||
OutputDir: "",
|
||||
StripComponents: 0,
|
||||
ExcludePatterns: []string{},
|
||||
PreservePermissions: true,
|
||||
AutoDetect: true,
|
||||
},
|
||||
CookieJar: "",
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads the configuration from file
|
||||
func Load(customPath string) (*Config, error) {
|
||||
cfg := DefaultConfig()
|
||||
configPath := customPath
|
||||
if configPath == "" {
|
||||
configPath = getConfigPath()
|
||||
}
|
||||
if configPath == "" {
|
||||
return cfg, nil
|
||||
}
|
||||
cfg.configPath = configPath
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return cfg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
if err := interpres.Unmarshal(data, cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config %s: %w", configPath, err)
|
||||
}
|
||||
if cfg.Auth.PasswordFile != "" && cfg.Auth.Password == "" {
|
||||
passwordPath := expandTilde(cfg.Auth.PasswordFile)
|
||||
if data, err := os.ReadFile(passwordPath); err == nil {
|
||||
cfg.Auth.Password = strings.TrimSpace(string(data))
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Save writes the configuration to disk
|
||||
func (c *Config) Save() error {
|
||||
if c.configPath == "" {
|
||||
c.configPath = getConfigPath()
|
||||
if c.configPath == "" {
|
||||
return fmt.Errorf("cannot determine config path")
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(c.configPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
cfgCopy := *c
|
||||
cfgCopy.Auth.Password = ""
|
||||
cfgCopy.Auth.OAuth.ClientSecret = ""
|
||||
cfgCopy.Auth.OAuth.AccessToken = ""
|
||||
cfgCopy.Auth.OAuth.RefreshToken = ""
|
||||
data, err := interpres.Marshal(cfgCopy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(c.configPath, data, 0600)
|
||||
}
|
||||
|
||||
// Path returns the config file path
|
||||
func (c *Config) Path() string {
|
||||
return c.configPath
|
||||
}
|
||||
|
||||
// CLIFlags holds CLI overrides to merge into the configuration.
|
||||
type CLIFlags struct {
|
||||
Timeout time.Duration
|
||||
Parallel int
|
||||
Verbose bool
|
||||
Debug bool
|
||||
MaxSpeed int64
|
||||
Proxy string
|
||||
NoIPv4 bool
|
||||
NoDecompress bool
|
||||
Username string
|
||||
Password string
|
||||
CookieJar string
|
||||
Recursive bool
|
||||
MaxDepth int
|
||||
FollowExternal bool
|
||||
ExcludePatterns []string
|
||||
Extract bool
|
||||
ExtractDir string
|
||||
StripComponents int
|
||||
}
|
||||
|
||||
// Merge merges CLI overrides into the configuration.
|
||||
func (c *Config) Merge(cli *CLIFlags) {
|
||||
if cli.Timeout > 0 {
|
||||
c.Timeout = cli.Timeout
|
||||
}
|
||||
if cli.Parallel >= 0 {
|
||||
c.Parallel = cli.Parallel
|
||||
}
|
||||
if cli.Verbose {
|
||||
c.Verbose = true
|
||||
}
|
||||
if cli.Debug {
|
||||
c.Debug = true
|
||||
}
|
||||
if cli.MaxSpeed >= 0 {
|
||||
c.MaxSpeed = cli.MaxSpeed
|
||||
}
|
||||
if cli.Proxy != "" {
|
||||
c.Proxy = cli.Proxy
|
||||
}
|
||||
if cli.NoIPv4 {
|
||||
c.IPv4Fallback = false
|
||||
}
|
||||
if cli.NoDecompress {
|
||||
c.AutoDecompress = false
|
||||
}
|
||||
if cli.Username != "" {
|
||||
c.Auth.Username = cli.Username
|
||||
c.Auth.Password = cli.Password
|
||||
}
|
||||
if cli.CookieJar != "" {
|
||||
c.CookieJar = cli.CookieJar
|
||||
}
|
||||
if cli.Recursive {
|
||||
c.Recursive.Enabled = true
|
||||
}
|
||||
if cli.MaxDepth > 0 {
|
||||
c.Recursive.MaxDepth = cli.MaxDepth
|
||||
}
|
||||
if cli.FollowExternal {
|
||||
c.Recursive.FollowExternal = true
|
||||
}
|
||||
if len(cli.ExcludePatterns) > 0 {
|
||||
c.Recursive.ExcludePatterns = cli.ExcludePatterns
|
||||
}
|
||||
if cli.Extract {
|
||||
c.Extract.Enabled = true
|
||||
}
|
||||
if cli.ExtractDir != "" {
|
||||
c.Extract.OutputDir = cli.ExtractDir
|
||||
}
|
||||
if cli.StripComponents >= 0 {
|
||||
c.Extract.StripComponents = cli.StripComponents
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigPath() string {
|
||||
if path := os.Getenv("GOGET_CONFIG"); path != "" {
|
||||
return path
|
||||
}
|
||||
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
|
||||
return filepath.Join(xdg, "goget", "config.toml")
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, ".config", "goget", "config.toml")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func expandTilde(path string) string {
|
||||
if strings.HasPrefix(path, "~/") {
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, path[2:])
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (c *Config) GetEffectiveTimeout(fileSize int64, userOverride time.Duration) time.Duration {
|
||||
tc := core.DefaultTimeoutConfig()
|
||||
return tc.CalculateTimeout(fileSize, userOverride)
|
||||
}
|
||||
|
||||
func (c *Config) GetParallelConnections(fileSize int64) int {
|
||||
if c.Parallel > 0 {
|
||||
return c.Parallel
|
||||
}
|
||||
if fileSize >= 100*1024*1024 {
|
||||
return 4
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (c *Config) ShouldUseColors() bool {
|
||||
switch c.Color {
|
||||
case "always":
|
||||
return true
|
||||
case "never":
|
||||
return false
|
||||
default:
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) IsExcluded(url string) bool {
|
||||
for _, pattern := range c.Recursive.ExcludePatterns {
|
||||
if strings.Contains(url, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Config) GetRecursiveDelay() time.Duration {
|
||||
if c.Recursive.Delay == "" {
|
||||
return 100 * time.Millisecond
|
||||
}
|
||||
d, err := time.ParseDuration(c.Recursive.Delay)
|
||||
if err != nil {
|
||||
return 100 * time.Millisecond
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (c *Config) GetExtractOutputDir(defaultDir string) string {
|
||||
if c.Extract.OutputDir != "" {
|
||||
return c.Extract.OutputDir
|
||||
}
|
||||
return defaultDir
|
||||
}
|
||||
|
||||
func ParseSpeed(s string) int64 {
|
||||
if s == "" || s == "0" {
|
||||
return 0
|
||||
}
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
var multiplier int64 = 1
|
||||
switch {
|
||||
case strings.HasSuffix(s, "tb/s"):
|
||||
multiplier = 1000 * 1000 * 1000 * 1000
|
||||
s = strings.TrimSuffix(s, "tb/s")
|
||||
case strings.HasSuffix(s, "gb/s"):
|
||||
multiplier = 1000 * 1000 * 1000
|
||||
s = strings.TrimSuffix(s, "gb/s")
|
||||
case strings.HasSuffix(s, "mb/s"):
|
||||
multiplier = 1000 * 1000
|
||||
s = strings.TrimSuffix(s, "mb/s")
|
||||
case strings.HasSuffix(s, "kb/s"):
|
||||
multiplier = 1000
|
||||
s = strings.TrimSuffix(s, "kb/s")
|
||||
case strings.HasSuffix(s, "/s"):
|
||||
s = strings.TrimSuffix(s, "/s")
|
||||
}
|
||||
var value float64
|
||||
fmt.Sscanf(s, "%f", &value)
|
||||
return int64(value * float64(multiplier))
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"codeberg.org/petrbalvin/interpres"
|
||||
)
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if cfg.Timeout != 30*time.Minute {
|
||||
t.Errorf("Expected default timeout 30m, got %v", cfg.Timeout)
|
||||
}
|
||||
if cfg.Parallel != 0 {
|
||||
t.Errorf("Expected default parallel 0, got %d", cfg.Parallel)
|
||||
}
|
||||
if cfg.Verbose != true {
|
||||
t.Errorf("Expected default verbose true, got %v", cfg.Verbose)
|
||||
}
|
||||
if cfg.AutoDecompress != true {
|
||||
t.Errorf("Expected default auto decompress true, got %v", cfg.AutoDecompress)
|
||||
}
|
||||
if cfg.ChecksumAlgo != "sha256" {
|
||||
t.Errorf("Expected default checksum algo sha256, got %s", cfg.ChecksumAlgo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigMerge(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
cfg.Merge(&CLIFlags{
|
||||
Timeout: 1 * time.Hour,
|
||||
Parallel: 4,
|
||||
Verbose: true,
|
||||
Debug: false,
|
||||
MaxSpeed: 1024 * 1024,
|
||||
Proxy: "http://proxy",
|
||||
NoIPv4: false,
|
||||
NoDecompress: false,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
CookieJar: "",
|
||||
Recursive: false,
|
||||
MaxDepth: 0,
|
||||
FollowExternal: false,
|
||||
ExcludePatterns: nil,
|
||||
Extract: false,
|
||||
ExtractDir: "",
|
||||
StripComponents: 0,
|
||||
})
|
||||
|
||||
if cfg.Timeout != 1*time.Hour {
|
||||
t.Errorf("Expected timeout 1h, got %v", cfg.Timeout)
|
||||
}
|
||||
if cfg.Parallel != 4 {
|
||||
t.Errorf("Expected parallel 4, got %d", cfg.Parallel)
|
||||
}
|
||||
if cfg.Proxy != "http://proxy" {
|
||||
t.Errorf("Expected proxy http://proxy, got %s", cfg.Proxy)
|
||||
}
|
||||
if cfg.Auth.Username != "user" {
|
||||
t.Errorf("Expected username user, got %s", cfg.Auth.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEffectiveTimeout(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// User override
|
||||
timeout := cfg.GetEffectiveTimeout(1000000, 1*time.Hour)
|
||||
if timeout != 1*time.Hour {
|
||||
t.Errorf("Expected 1h with user override, got %v", timeout)
|
||||
}
|
||||
|
||||
// Auto calculation for known size
|
||||
timeout = cfg.GetEffectiveTimeout(100*1024*1024, 0)
|
||||
if timeout < 30*time.Second {
|
||||
t.Errorf("Expected at least 30s for auto timeout, got %v", timeout)
|
||||
}
|
||||
|
||||
// Unknown size
|
||||
timeout = cfg.GetEffectiveTimeout(-1, 0)
|
||||
if timeout != 30*time.Minute {
|
||||
t.Errorf("Expected 30m for unknown size, got %v", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetParallelConnections(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Explicit parallel
|
||||
cfg.Parallel = 8
|
||||
if cfg.GetParallelConnections(1000) != 8 {
|
||||
t.Errorf("Expected 8 explicit connections, got %d", cfg.GetParallelConnections(1000))
|
||||
}
|
||||
|
||||
// Auto for large file
|
||||
cfg.Parallel = 0
|
||||
if cfg.GetParallelConnections(200*1024*1024) != 4 {
|
||||
t.Errorf("Expected 4 auto connections for large file, got %d", cfg.GetParallelConnections(200*1024*1024))
|
||||
}
|
||||
|
||||
// Auto for small file
|
||||
if cfg.GetParallelConnections(1024) != 1 {
|
||||
t.Errorf("Expected 1 connection for small file, got %d", cfg.GetParallelConnections(1024))
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldUseColors(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Always
|
||||
cfg.Color = "always"
|
||||
if !cfg.ShouldUseColors() {
|
||||
t.Error("Expected colors with always setting")
|
||||
}
|
||||
|
||||
// Never
|
||||
cfg.Color = "never"
|
||||
if cfg.ShouldUseColors() {
|
||||
t.Error("Expected no colors with never setting")
|
||||
}
|
||||
|
||||
// Auto (default)
|
||||
cfg.Color = "auto"
|
||||
// Can't test environment in unit tests, but verify it doesn't panic
|
||||
_ = cfg.ShouldUseColors()
|
||||
}
|
||||
|
||||
func TestIsExcluded(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Recursive.ExcludePatterns = []string{"example.com", "test.org"}
|
||||
|
||||
if !cfg.IsExcluded("https://example.com/file.zip") {
|
||||
t.Error("Expected example.com to be excluded")
|
||||
}
|
||||
if !cfg.IsExcluded("https://test.org/file.zip") {
|
||||
t.Error("Expected test.org to be excluded")
|
||||
}
|
||||
if cfg.IsExcluded("https://other.com/file.zip") {
|
||||
t.Error("Expected other.com to not be excluded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRecursiveDelay(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
delay := cfg.GetRecursiveDelay()
|
||||
if delay != 100*time.Millisecond {
|
||||
t.Errorf("Expected default delay 100ms, got %v", delay)
|
||||
}
|
||||
|
||||
cfg.Recursive.Delay = "500ms"
|
||||
delay = cfg.GetRecursiveDelay()
|
||||
if delay != 500*time.Millisecond {
|
||||
t.Errorf("Expected delay 500ms, got %v", delay)
|
||||
}
|
||||
|
||||
// Invalid delay should fall back to default
|
||||
cfg.Recursive.Delay = "invalid"
|
||||
delay = cfg.GetRecursiveDelay()
|
||||
if delay != 100*time.Millisecond {
|
||||
t.Errorf("Expected default delay for invalid value, got %v", delay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExtractOutputDir(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Empty config should return default
|
||||
dir := cfg.GetExtractOutputDir("/default")
|
||||
if dir != "/default" {
|
||||
t.Errorf("Expected /default, got %s", dir)
|
||||
}
|
||||
|
||||
// Config with output dir
|
||||
cfg.Extract.OutputDir = "/custom"
|
||||
dir = cfg.GetExtractOutputDir("/default")
|
||||
if dir != "/custom" {
|
||||
t.Errorf("Expected /custom, got %s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSpeed(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected int64
|
||||
}{
|
||||
{"", 0},
|
||||
{"0", 0},
|
||||
{"100", 100},
|
||||
{"100B/s", 100},
|
||||
{"1KB/s", 1000},
|
||||
{"1MB/s", 1000000},
|
||||
{"1GB/s", 1000000000},
|
||||
{"10MB/s", 10000000},
|
||||
{"100 KB/s", 100000},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := ParseSpeed(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("ParseSpeed(%s) = %d, expected %d", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandTilde(t *testing.T) {
|
||||
// Test with tilde
|
||||
path := expandTilde("~/test/file.txt")
|
||||
if path == "~/test/file.txt" {
|
||||
t.Error("Expected tilde to be expanded")
|
||||
}
|
||||
|
||||
// Test without tilde
|
||||
path = expandTilde("/absolute/path/file.txt")
|
||||
if path != "/absolute/path/file.txt" {
|
||||
t.Errorf("Expected unchanged path, got %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSave(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Verbose = true
|
||||
cfg.Parallel = 4
|
||||
|
||||
// Save should not fail (may create directories)
|
||||
err := cfg.Save()
|
||||
// We can't test the actual file creation in unit tests without a temp dir
|
||||
// but we verify the method doesn't panic
|
||||
_ = err
|
||||
}
|
||||
|
||||
func TestLoadNonExistentConfig(t *testing.T) {
|
||||
cfg, err := Load("/nonexistent/path/config.toml")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error for non-existent config, got %v", err)
|
||||
}
|
||||
if cfg == nil {
|
||||
t.Fatal("Expected default config for non-existent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalTOML(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Parallel = 4
|
||||
cfg.Verbose = true
|
||||
cfg.Timeout = 5 * time.Minute
|
||||
cfg.Auth.Username = "testuser"
|
||||
cfg.Auth.AuthType = "basic"
|
||||
cfg.Recursive.Enabled = true
|
||||
cfg.Recursive.MaxDepth = 5
|
||||
cfg.DNSServers = []string{"1.1.1.1", "8.8.8.8"}
|
||||
|
||||
data, err := interpres.Marshal(*cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("interpres.Marshal failed: %v", err)
|
||||
}
|
||||
|
||||
tomlStr := string(data)
|
||||
// Verify key elements are present
|
||||
if !strings.Contains(tomlStr, "parallel = 4") {
|
||||
t.Errorf("expected 'parallel = 4' in TOML output, got:\n%s", tomlStr)
|
||||
}
|
||||
if !strings.Contains(tomlStr, "verbose = true") {
|
||||
t.Errorf("expected 'verbose = true' in TOML output, got:\n%s", tomlStr)
|
||||
}
|
||||
if !strings.Contains(tomlStr, "[auth]") {
|
||||
t.Errorf("expected '[auth]' section in TOML output, got:\n%s", tomlStr)
|
||||
}
|
||||
if !strings.Contains(tomlStr, "username = \"testuser\"") {
|
||||
t.Errorf("expected 'username = \"testuser\"' in TOML output, got:\n%s", tomlStr)
|
||||
}
|
||||
if !strings.Contains(tomlStr, "[recursive]") {
|
||||
t.Errorf("expected '[recursive]' section in TOML output, got:\n%s", tomlStr)
|
||||
}
|
||||
if !strings.Contains(tomlStr, "dns_servers = [") {
|
||||
t.Errorf("expected 'dns_servers' array in TOML output, got:\n%s", tomlStr)
|
||||
}
|
||||
|
||||
// Roundtrip: unmarshal back and verify
|
||||
var cfg2 Config
|
||||
if err := interpres.Unmarshal(data, &cfg2); err != nil {
|
||||
t.Fatalf("interpres.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg2.Parallel != 4 {
|
||||
t.Errorf("roundtrip: expected Parallel 4, got %d", cfg2.Parallel)
|
||||
}
|
||||
if cfg2.Verbose != true {
|
||||
t.Errorf("roundtrip: expected Verbose true, got %v", cfg2.Verbose)
|
||||
}
|
||||
if cfg2.Auth.Username != "testuser" {
|
||||
t.Errorf("roundtrip: expected Auth.Username testuser, got %s", cfg2.Auth.Username)
|
||||
}
|
||||
if cfg2.Recursive.MaxDepth != 5 {
|
||||
t.Errorf("roundtrip: expected Recursive.MaxDepth 5, got %d", cfg2.Recursive.MaxDepth)
|
||||
}
|
||||
if len(cfg2.DNSServers) != 2 || cfg2.DNSServers[0] != "1.1.1.1" {
|
||||
t.Errorf("roundtrip: expected DNSServers [1.1.1.1, 8.8.8.8], got %v", cfg2.DNSServers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTOMLDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
data, err := interpres.Marshal(*cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("interpres.Marshal with default config failed: %v", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
t.Error("expected non-empty TOML output for default config")
|
||||
}
|
||||
|
||||
// Verify it can be parsed back
|
||||
var cfg2 Config
|
||||
if err := interpres.Unmarshal(data, &cfg2); err != nil {
|
||||
t.Fatalf("interpres.Unmarshal of default output failed: %v", err)
|
||||
}
|
||||
if cfg2.OutputDir != "." {
|
||||
t.Errorf("expected OutputDir '.', got %s", cfg2.OutputDir)
|
||||
}
|
||||
if cfg2.ChecksumAlgo != "sha256" {
|
||||
t.Errorf("expected ChecksumAlgo sha256, got %s", cfg2.ChecksumAlgo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPath(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
path := cfg.Path()
|
||||
// Path should be empty when loaded with DefaultConfig
|
||||
_ = path
|
||||
}
|
||||
Reference in New Issue
Block a user