all repos

mugit @ 3167cb3

馃惍 git server that your cow will love
5 files changed, 699 insertions(+), 38 deletions(-)
config: validate, thanks to my boy Claude
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-02-04 23:34:07 +0200
Authored at: 2026-02-03 20:57:24 +0200
Change ID: llzpqltrnktzpynqmvzxpkkvzrtysopo
Parent: 7711375
M go.mod
路路路
        12
        12
         	golang.org/x/crypto v0.47.0

      
        13
        13
         	golang.org/x/sync v0.19.0

      
        14
        14
         	gopkg.in/yaml.v2 v2.4.0

      
        
        15
        +	olexsmir.xyz/x v0.1.2

      
        15
        16
         )

      
        16
        17
         

      
        17
        18
         require (

      
M go.sum
路路路
        109
        109
         gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

      
        110
        110
         gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

      
        111
        111
         gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

      
        
        112
        +olexsmir.xyz/x v0.1.2 h1:etNRQijfG4Oduip6XClBDIaP/gqugAd9Viu5MNyLcoA=

      
        
        113
        +olexsmir.xyz/x v0.1.2/go.mod h1:mScl7AVsDCUUxvhzh6X2sLGwjEBg2K79AlkE9dgHXUA=

      
M internal/config/config.go
路路路
        5
        5
         	"fmt"

      
        6
        6
         	"os"

      
        7
        7
         	"path/filepath"

      
        
        8
        +	"time"

      
        8
        9
         

      
        9
        10
         	"gopkg.in/yaml.v2"

      
        10
        11
         )

      
        11
        12
         

      
        12
        13
         var ErrConfigNotFound = errors.New("no config file found")

      
        13
        14
         

      
        
        15
        +type ServerConfig struct {

      
        
        16
        +	Host string `yaml:"host"`

      
        
        17
        +	Port int    `yaml:"port"`

      
        
        18
        +}

      
        
        19
        +

      
        
        20
        +type MetaConfig struct {

      
        
        21
        +	Title       string `yaml:"title"`

      
        
        22
        +	Description string `yaml:"description"`

      
        
        23
        +	Host        string `yaml:"host"`

      
        
        24
        +}

      
        
        25
        +

      
        
        26
        +type RepoConfig struct {

      
        
        27
        +	Dir     string   `yaml:"dir"`

      
        
        28
        +	Readmes []string `yaml:"readmes"`

      
        
        29
        +	Masters []string `yaml:"masters"`

      
        
        30
        +}

      
        
        31
        +

      
        
        32
        +type SSHConfig struct {

      
        
        33
        +	Enable  bool     `yaml:"enable"`

      
        
        34
        +	Port    int      `yaml:"port"`

      
        
        35
        +	HostKey string   `yaml:"host_key"`

      
        
        36
        +	Keys    []string `yaml:"keys"`

      
        
        37
        +}

      
        
        38
        +

      
        
        39
        +type MirrorConfig struct {

      
        
        40
        +	Enable      bool   `yaml:"enable"`

      
        
        41
        +	Interval    string `yaml:"interval"`

      
        
        42
        +	GithubToken string `yaml:"github_token"`

      
        
        43
        +}

      
        
        44
        +

      
        14
        45
         type Config struct {

      
        15
        
        -	Server struct {

      
        16
        
        -		Host string `yaml:"host"`

      
        17
        
        -		Port int    `yaml:"port"`

      
        18
        
        -	} `yaml:"server"`

      
        19
        
        -	Meta struct {

      
        20
        
        -		Title       string `yaml:"title"`

      
        21
        
        -		Description string `yaml:"description"`

      
        22
        
        -		Host        string `yaml:"host"`

      
        23
        
        -	} `yaml:"meta"`

      
        24
        
        -	Repo struct {

      
        25
        
        -		Dir     string   `yaml:"dir"`

      
        26
        
        -		Readmes []string `yaml:"readmes"`

      
        27
        
        -		Masters []string `yaml:"masters"`

      
        28
        
        -	} `yaml:"repo"`

      
        29
        
        -	SSH struct {

      
        30
        
        -		Enable  bool     `yaml:"enable"`

      
        31
        
        -		Port    int      `yaml:"port"`

      
        32
        
        -		HostKey string   `yaml:"host_key"`

      
        33
        
        -		Keys    []string `yaml:"keys"`

      
        34
        
        -	} `yaml:"ssh"`

      
        35
        
        -	Mirror struct {

      
        36
        
        -		Enable      bool   `yaml:"enable"`

      
        37
        
        -		Interval    string `yaml:"interval"`

      
        38
        
        -		GithubToken string `yaml:"github_token"`

      
        39
        
        -	} `yaml:"mirror"`

      
        
        46
        +	Server ServerConfig `yaml:"server"`

      
        
        47
        +	Meta   MetaConfig   `yaml:"meta"`

      
        
        48
        +	Repo   RepoConfig   `yaml:"repo"`

      
        
        49
        +	SSH    SSHConfig    `yaml:"ssh"`

      
        
        50
        +	Mirror MirrorConfig `yaml:"mirror"`

      
        40
        51
         }

      
        41
        52
         

      
        42
        53
         // Load loads configuration with the following priority:

      
        43
        54
         // 1. User provided fpath (if provided and exists)

      
        44
        
        -// 2. $XDG_CONFIG_HOME/mugit/config.yaml

      
        45
        
        -// 3. $HOME/.config/mugit/config.yaml (fallback if XDG_CONFIG_HOME not set)

      
        46
        
        -// 4. /etc/mugit/config.yaml

      
        
        55
        +// 2. /var/lib/mugit/config.yaml

      
        
        56
        +// 3. $XDG_CONFIG_HOME/mugit/config.yaml or $HOME/.config/mugit/config.yaml

      
        47
        57
         func Load(fpath string) (*Config, error) {

      
        
        58
        +	// 4. /etc/mugit/config.yaml

      
        48
        59
         	configPath, err := findConfigFile(fpath)

      
        49
        60
         	if err != nil {

      
        50
        61
         		return nil, err

      路路路
        72
        83
         }

      
        73
        84
         

      
        74
        85
         func (c Config) validate() error {

      
        75
        
        -	// var errs []error

      
        76
        
        -	// return errors.Join(errs...)

      
        77
        
        -	return nil

      
        
        86
        +	var errs []error

      
        
        87
        +

      
        
        88
        +	// server

      
        
        89
        +	if err := validatePort(c.Server.Port, "server.port"); err != nil {

      
        
        90
        +		errs = append(errs, err)

      
        
        91
        +	}

      
        
        92
        +

      
        
        93
        +	// meta

      
        
        94
        +	if c.Meta.Host == "" {

      
        
        95
        +		errs = append(errs, errors.New("meta.host is required"))

      
        
        96
        +	}

      
        
        97
        +

      
        
        98
        +	// repo

      
        
        99
        +	if err := validateDirExists(c.Repo.Dir, "repo.dir"); err != nil {

      
        
        100
        +		errs = append(errs, err)

      
        
        101
        +	}

      
        
        102
        +	if len(c.Repo.Readmes) == 0 {

      
        
        103
        +		errs = append(errs, errors.New("repo.readmes must have at least one value"))

      
        
        104
        +	}

      
        
        105
        +	if len(c.Repo.Masters) == 0 {

      
        
        106
        +		errs = append(errs, errors.New("repo.masters must have at least one value"))

      
        
        107
        +	}

      
        
        108
        +

      
        
        109
        +	// ssh

      
        
        110
        +	if c.SSH.Enable {

      
        
        111
        +		if err := validatePort(c.SSH.Port, "ssh.port"); err != nil {

      
        
        112
        +			errs = append(errs, err)

      
        
        113
        +		}

      
        
        114
        +		if c.SSH.Port == c.Server.Port {

      
        
        115
        +			errs = append(errs, fmt.Errorf("ssh.port must differ from server.port (both are %d)", c.Server.Port))

      
        
        116
        +		}

      
        
        117
        +		if err := validateFileExists(c.SSH.HostKey, "ssh.host_key"); err != nil {

      
        
        118
        +			errs = append(errs, err)

      
        
        119
        +		}

      
        
        120
        +		if len(c.SSH.Keys) == 0 {

      
        
        121
        +			errs = append(errs, errors.New("ssh.keys must have at least one value when ssh is enabled"))

      
        
        122
        +		}

      
        
        123
        +	}

      
        
        124
        +

      
        
        125
        +	// mirror

      
        
        126
        +	if c.Mirror.Enable {

      
        
        127
        +		if c.Mirror.Interval == "" {

      
        
        128
        +			errs = append(errs, errors.New("mirror.interval is required when mirror is enabled"))

      
        
        129
        +		} else if _, err := time.ParseDuration(c.Mirror.Interval); err != nil {

      
        
        130
        +			errs = append(errs, fmt.Errorf("mirror.interval: invalid duration format: %w", err))

      
        
        131
        +		}

      
        
        132
        +	}

      
        
        133
        +

      
        
        134
        +	return errors.Join(errs...)

      
        78
        135
         }

      
        79
        136
         

      
        80
        137
         func findConfigFile(userPath string) (string, error) {

      路路路
        84
        141
         		}

      
        85
        142
         	}

      
        86
        143
         

      
        87
        
        -	paths := []string{}

      
        88
        
        -	if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {

      
        89
        
        -		paths = append(paths, filepath.Join(xdg, "mugit", "config.yaml"))

      
        90
        
        -	} else if home := os.Getenv("HOME"); home != "" {

      
        91
        
        -		paths = append(paths, filepath.Join(home, ".config", "mugit", "config.yaml"))

      
        
        144
        +	path := "/var/lib/mugit/config.yaml"

      
        
        145
        +	if _, err := os.Stat(path); err == nil {

      
        
        146
        +		return path, nil

      
        92
        147
         	}

      
        93
        148
         

      
        94
        
        -	paths = append(paths, "/etc/mugit/config.yaml")

      
        95
        
        -	for _, p := range paths {

      
        
        149
        +	if configDir, err := os.UserConfigDir(); err == nil {

      
        
        150
        +		p := filepath.Join(configDir, "mugit", "config.yaml")

      
        96
        151
         		if _, err := os.Stat(p); err == nil {

      
        97
        152
         			return p, nil

      
        98
        153
         		}

      
        99
        154
         	}

      
        100
        155
         

      
        
        156
        +	path = "/etc/mugit/config.yaml"

      
        
        157
        +	if _, err := os.Stat(path); err == nil {

      
        
        158
        +		return path, nil

      
        
        159
        +	}

      
        
        160
        +

      
        101
        161
         	return "", ErrConfigNotFound

      
        102
        162
         }

      
        
        163
        +

      
        
        164
        +func validatePort(port int, fieldName string) error {

      
        
        165
        +	if port < 1 || port > 65535 {

      
        
        166
        +		return fmt.Errorf("%s must be between 1 and 65535, got %d", fieldName, port)

      
        
        167
        +	}

      
        
        168
        +	return nil

      
        
        169
        +}

      
        
        170
        +

      
        
        171
        +func validateDirExists(path string, fieldName string) error {

      
        
        172
        +	if path == "" {

      
        
        173
        +		return fmt.Errorf("%s is required", fieldName)

      
        
        174
        +	}

      
        
        175
        +	info, err := os.Stat(path)

      
        
        176
        +	if err != nil {

      
        
        177
        +		if os.IsNotExist(err) {

      
        
        178
        +			return fmt.Errorf("%s: directory does not exist: %s", fieldName, path)

      
        
        179
        +		}

      
        
        180
        +		return fmt.Errorf("%s: cannot access directory: %w", fieldName, err)

      
        
        181
        +	}

      
        
        182
        +	if !info.IsDir() {

      
        
        183
        +		return fmt.Errorf("%s: path exists but is not a directory: %s", fieldName, path)

      
        
        184
        +	}

      
        
        185
        +	return nil

      
        
        186
        +}

      
        
        187
        +

      
        
        188
        +func validateFileExists(path string, fieldName string) error {

      
        
        189
        +	if path == "" {

      
        
        190
        +		return fmt.Errorf("%s is required", fieldName)

      
        
        191
        +	}

      
        
        192
        +	info, err := os.Stat(path)

      
        
        193
        +	if err != nil {

      
        
        194
        +		if os.IsNotExist(err) {

      
        
        195
        +			return fmt.Errorf("%s: file does not exist: %s", fieldName, path)

      
        
        196
        +		}

      
        
        197
        +		return fmt.Errorf("%s: cannot access file: %w", fieldName, err)

      
        
        198
        +	}

      
        
        199
        +	if info.IsDir() {

      
        
        200
        +		return fmt.Errorf("%s: path is a directory, not a file: %s", fieldName, path)

      
        
        201
        +	}

      
        
        202
        +	return nil

      
        
        203
        +}

      
A internal/config/config_test.go
路路路
        
        1
        +package config

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"os"

      
        
        5
        +	"path/filepath"

      
        
        6
        +	"testing"

      
        
        7
        +

      
        
        8
        +	"olexsmir.xyz/x/is"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +func TestFindConfigFile(t *testing.T) {

      
        
        12
        +	t.Run("returns user provided path when it exists", func(t *testing.T) {

      
        
        13
        +		path, err := findConfigFile("testdata/hostkey")

      
        
        14
        +		is.Err(t, err, nil)

      
        
        15
        +		is.Equal(t, path, "testdata/hostkey")

      
        
        16
        +	})

      
        
        17
        +

      
        
        18
        +	t.Run("falls back when user path doesn't exist", func(t *testing.T) {

      
        
        19
        +		path, err := findConfigFile("/nonexistent/user/config.yaml")

      
        
        20
        +		if err != nil {

      
        
        21
        +			is.Err(t, err, ErrConfigNotFound)

      
        
        22
        +		} else {

      
        
        23
        +			_, statErr := os.Stat(path)

      
        
        24
        +			is.Err(t, statErr, nil)

      
        
        25
        +		}

      
        
        26
        +	})

      
        
        27
        +

      
        
        28
        +	t.Run("finds config in user config directory", func(t *testing.T) {

      
        
        29
        +		tmpDir := t.TempDir()

      
        
        30
        +		configDir := filepath.Join(tmpDir, "mugit")

      
        
        31
        +		if err := os.MkdirAll(configDir, 0o755); err != nil {

      
        
        32
        +			t.Fatal(err)

      
        
        33
        +		}

      
        
        34
        +		configFile := filepath.Join(configDir, "config.yaml")

      
        
        35
        +		if err := os.WriteFile(configFile, []byte("test"), 0o644); err != nil {

      
        
        36
        +			t.Fatal(err)

      
        
        37
        +		}

      
        
        38
        +

      
        
        39
        +		t.Setenv("XDG_CONFIG_HOME", tmpDir)

      
        
        40
        +

      
        
        41
        +		path, err := findConfigFile("")

      
        
        42
        +		is.Err(t, err, nil)

      
        
        43
        +		is.Equal(t, path, configFile)

      
        
        44
        +	})

      
        
        45
        +

      
        
        46
        +	t.Run("returns error when no config found anywhere", func(t *testing.T) {

      
        
        47
        +		t.Setenv("XDG_CONFIG_HOME", "/nonexistent")

      
        
        48
        +		t.Setenv("HOME", "/nonexistent")

      
        
        49
        +

      
        
        50
        +		path, err := findConfigFile("/nonexistent/config.yaml")

      
        
        51
        +		is.Err(t, err, ErrConfigNotFound)

      
        
        52
        +		is.Equal(t, path, "")

      
        
        53
        +	})

      
        
        54
        +

      
        
        55
        +	t.Run("prefers data directory over user config", func(t *testing.T) {

      
        
        56
        +		tmpDir := t.TempDir()

      
        
        57
        +		configDir := filepath.Join(tmpDir, "mugit")

      
        
        58
        +		if err := os.MkdirAll(configDir, 0o755); err != nil {

      
        
        59
        +			t.Fatal(err)

      
        
        60
        +		}

      
        
        61
        +		userConfigFile := filepath.Join(configDir, "config.yaml")

      
        
        62
        +		if err := os.WriteFile(userConfigFile, []byte("user config"), 0o644); err != nil {

      
        
        63
        +			t.Fatal(err)

      
        
        64
        +		}

      
        
        65
        +

      
        
        66
        +		t.Setenv("XDG_CONFIG_HOME", tmpDir)

      
        
        67
        +

      
        
        68
        +		path, err := findConfigFile("")

      
        
        69
        +		is.Err(t, err, nil)

      
        
        70
        +

      
        
        71
        +		if path == "/var/lib/mugit/config.yaml" {

      
        
        72
        +			_, statErr := os.Stat(path)

      
        
        73
        +			is.Err(t, statErr, nil)

      
        
        74
        +		} else {

      
        
        75
        +			is.Equal(t, path, userConfigFile)

      
        
        76
        +		}

      
        
        77
        +	})

      
        
        78
        +}

      
        
        79
        +

      
        
        80
        +func TestValidatePort(t *testing.T) {

      
        
        81
        +	t.Run("accepts standard port numbers", func(t *testing.T) {

      
        
        82
        +		is.Err(t, validatePort(1, "test"), nil)

      
        
        83
        +		is.Err(t, validatePort(80, "test"), nil)

      
        
        84
        +		is.Err(t, validatePort(8080, "test"), nil)

      
        
        85
        +		is.Err(t, validatePort(65535, "test"), nil)

      
        
        86
        +	})

      
        
        87
        +

      
        
        88
        +	t.Run("rejects out of range ports", func(t *testing.T) {

      
        
        89
        +		is.Err(t, validatePort(0, "test"), "must be between")

      
        
        90
        +		is.Err(t, validatePort(-1, "test"), "must be between")

      
        
        91
        +		is.Err(t, validatePort(65536, "test"), "must be between")

      
        
        92
        +		is.Err(t, validatePort(100000, "test"), "must be between")

      
        
        93
        +	})

      
        
        94
        +}

      
        
        95
        +

      
        
        96
        +func TestValidateDirExists(t *testing.T) {

      
        
        97
        +	t.Run("accepts existing directories", func(t *testing.T) {

      
        
        98
        +		tmpDir := t.TempDir()

      
        
        99
        +		is.Err(t, validateDirExists(tmpDir, "test"), nil)

      
        
        100
        +	})

      
        
        101
        +

      
        
        102
        +	t.Run("rejects nonexistent paths", func(t *testing.T) {

      
        
        103
        +		is.Err(t, validateDirExists("/nonexistent/path/to/dir", "test"), "does not exist")

      
        
        104
        +	})

      
        
        105
        +

      
        
        106
        +	t.Run("rejects empty paths", func(t *testing.T) {

      
        
        107
        +		is.Err(t, validateDirExists("", "test"), "is required")

      
        
        108
        +	})

      
        
        109
        +

      
        
        110
        +	t.Run("rejects files when directory expected", func(t *testing.T) {

      
        
        111
        +		tmpDir := t.TempDir()

      
        
        112
        +		tmpFile := filepath.Join(tmpDir, "file.txt")

      
        
        113
        +		if err := os.WriteFile(tmpFile, []byte("test"), 0o644); err != nil {

      
        
        114
        +			t.Fatal(err)

      
        
        115
        +		}

      
        
        116
        +		is.Err(t, validateDirExists(tmpFile, "test"), "not a directory")

      
        
        117
        +	})

      
        
        118
        +}

      
        
        119
        +

      
        
        120
        +func TestValidateFileExists(t *testing.T) {

      
        
        121
        +	t.Run("accepts existing files", func(t *testing.T) {

      
        
        122
        +		tmpDir := t.TempDir()

      
        
        123
        +		tmpFile := filepath.Join(tmpDir, "file.txt")

      
        
        124
        +		if err := os.WriteFile(tmpFile, []byte("test"), 0o644); err != nil {

      
        
        125
        +			t.Fatal(err)

      
        
        126
        +		}

      
        
        127
        +		is.Err(t, validateFileExists(tmpFile, "test"), nil)

      
        
        128
        +	})

      
        
        129
        +

      
        
        130
        +	t.Run("rejects nonexistent files", func(t *testing.T) {

      
        
        131
        +		is.Err(t, validateFileExists("/nonexistent/file.txt", "test"), "does not exist")

      
        
        132
        +	})

      
        
        133
        +

      
        
        134
        +	t.Run("rejects empty paths", func(t *testing.T) {

      
        
        135
        +		is.Err(t, validateFileExists("", "test"), "is required")

      
        
        136
        +	})

      
        
        137
        +

      
        
        138
        +	t.Run("rejects directories when file expected", func(t *testing.T) {

      
        
        139
        +		tmpDir := t.TempDir()

      
        
        140
        +		is.Err(t, validateFileExists(tmpDir, "test"), "is a directory")

      
        
        141
        +	})

      
        
        142
        +}

      
        
        143
        +

      
        
        144
        +func TestConfigValidate(t *testing.T) {

      
        
        145
        +	tmpDir := t.TempDir()

      
        
        146
        +	hostKeyPath := "testdata/hostkey"

      
        
        147
        +

      
        
        148
        +	t.Run("accepts minimal valid configuration", func(t *testing.T) {

      
        
        149
        +		cfg := Config{

      
        
        150
        +			Server: ServerConfig{

      
        
        151
        +				Host: "localhost",

      
        
        152
        +				Port: 8080,

      
        
        153
        +			},

      
        
        154
        +			Meta: MetaConfig{

      
        
        155
        +				Title:       "Test",

      
        
        156
        +				Description: "Test description",

      
        
        157
        +				Host:        "example.com",

      
        
        158
        +			},

      
        
        159
        +			Repo: RepoConfig{

      
        
        160
        +				Dir:     tmpDir,

      
        
        161
        +				Readmes: []string{"README.md"},

      
        
        162
        +				Masters: []string{"main"},

      
        
        163
        +			},

      
        
        164
        +			SSH: SSHConfig{

      
        
        165
        +				Enable: false,

      
        
        166
        +			},

      
        
        167
        +			Mirror: MirrorConfig{

      
        
        168
        +				Enable: false,

      
        
        169
        +			},

      
        
        170
        +		}

      
        
        171
        +		is.Err(t, cfg.validate(), nil)

      
        
        172
        +	})

      
        
        173
        +

      
        
        174
        +	t.Run("accepts configuration with SSH enabled", func(t *testing.T) {

      
        
        175
        +		cfg := Config{

      
        
        176
        +			Server: ServerConfig{

      
        
        177
        +				Host: "localhost",

      
        
        178
        +				Port: 8080,

      
        
        179
        +			},

      
        
        180
        +			Meta: MetaConfig{

      
        
        181
        +				Title:       "Test",

      
        
        182
        +				Description: "Test description",

      
        
        183
        +				Host:        "example.com",

      
        
        184
        +			},

      
        
        185
        +			Repo: RepoConfig{

      
        
        186
        +				Dir:     tmpDir,

      
        
        187
        +				Readmes: []string{"README.md"},

      
        
        188
        +				Masters: []string{"main"},

      
        
        189
        +			},

      
        
        190
        +			SSH: SSHConfig{

      
        
        191
        +				Enable:  true,

      
        
        192
        +				Port:    2222,

      
        
        193
        +				HostKey: hostKeyPath,

      
        
        194
        +				Keys:    []string{"ssh-rsa AAAAB3..."},

      
        
        195
        +			},

      
        
        196
        +			Mirror: MirrorConfig{

      
        
        197
        +				Enable: false,

      
        
        198
        +			},

      
        
        199
        +		}

      
        
        200
        +		is.Err(t, cfg.validate(), nil)

      
        
        201
        +	})

      
        
        202
        +

      
        
        203
        +	t.Run("accepts configuration with mirroring enabled", func(t *testing.T) {

      
        
        204
        +		cfg := Config{

      
        
        205
        +			Server: ServerConfig{

      
        
        206
        +				Host: "localhost",

      
        
        207
        +				Port: 8080,

      
        
        208
        +			},

      
        
        209
        +			Meta: MetaConfig{

      
        
        210
        +				Title:       "Test",

      
        
        211
        +				Description: "Test description",

      
        
        212
        +				Host:        "example.com",

      
        
        213
        +			},

      
        
        214
        +			Repo: RepoConfig{

      
        
        215
        +				Dir:     tmpDir,

      
        
        216
        +				Readmes: []string{"README.md"},

      
        
        217
        +				Masters: []string{"main"},

      
        
        218
        +			},

      
        
        219
        +			SSH: SSHConfig{

      
        
        220
        +				Enable: false,

      
        
        221
        +			},

      
        
        222
        +			Mirror: MirrorConfig{

      
        
        223
        +				Enable:      true,

      
        
        224
        +				Interval:    "1h",

      
        
        225
        +				GithubToken: "ghp_token",

      
        
        226
        +			},

      
        
        227
        +		}

      
        
        228
        +		is.Err(t, cfg.validate(), nil)

      
        
        229
        +	})

      
        
        230
        +

      
        
        231
        +	t.Run("rejects invalid server port", func(t *testing.T) {

      
        
        232
        +		cfg := Config{

      
        
        233
        +			Server: ServerConfig{

      
        
        234
        +				Port: 0,

      
        
        235
        +			},

      
        
        236
        +			Meta: MetaConfig{

      
        
        237
        +				Host: "example.com",

      
        
        238
        +			},

      
        
        239
        +			Repo: RepoConfig{

      
        
        240
        +				Dir:     tmpDir,

      
        
        241
        +				Readmes: []string{"README.md"},

      
        
        242
        +				Masters: []string{"main"},

      
        
        243
        +			},

      
        
        244
        +		}

      
        
        245
        +		is.Err(t, cfg.validate(), "server.port")

      
        
        246
        +	})

      
        
        247
        +

      
        
        248
        +	t.Run("rejects missing meta host", func(t *testing.T) {

      
        
        249
        +		cfg := Config{

      
        
        250
        +			Server: ServerConfig{

      
        
        251
        +				Port: 8080,

      
        
        252
        +			},

      
        
        253
        +			Meta: MetaConfig{

      
        
        254
        +				Host: "",

      
        
        255
        +			},

      
        
        256
        +			Repo: RepoConfig{

      
        
        257
        +				Dir:     tmpDir,

      
        
        258
        +				Readmes: []string{"README.md"},

      
        
        259
        +				Masters: []string{"main"},

      
        
        260
        +			},

      
        
        261
        +		}

      
        
        262
        +		is.Err(t, cfg.validate(), "meta.host")

      
        
        263
        +	})

      
        
        264
        +

      
        
        265
        +	t.Run("rejects nonexistent repository directory", func(t *testing.T) {

      
        
        266
        +		cfg := Config{

      
        
        267
        +			Server: ServerConfig{

      
        
        268
        +				Port: 8080,

      
        
        269
        +			},

      
        
        270
        +			Meta: MetaConfig{

      
        
        271
        +				Host: "example.com",

      
        
        272
        +			},

      
        
        273
        +			Repo: RepoConfig{

      
        
        274
        +				Dir:     "/nonexistent/path",

      
        
        275
        +				Readmes: []string{"README.md"},

      
        
        276
        +				Masters: []string{"main"},

      
        
        277
        +			},

      
        
        278
        +		}

      
        
        279
        +		is.Err(t, cfg.validate(), "repo.dir")

      
        
        280
        +	})

      
        
        281
        +

      
        
        282
        +	t.Run("rejects empty readme list", func(t *testing.T) {

      
        
        283
        +		cfg := Config{

      
        
        284
        +			Server: ServerConfig{

      
        
        285
        +				Port: 8080,

      
        
        286
        +			},

      
        
        287
        +			Meta: MetaConfig{

      
        
        288
        +				Host: "example.com",

      
        
        289
        +			},

      
        
        290
        +			Repo: RepoConfig{

      
        
        291
        +				Dir:     tmpDir,

      
        
        292
        +				Readmes: []string{},

      
        
        293
        +				Masters: []string{"main"},

      
        
        294
        +			},

      
        
        295
        +		}

      
        
        296
        +		is.Err(t, cfg.validate(), "repo.readmes")

      
        
        297
        +	})

      
        
        298
        +

      
        
        299
        +	t.Run("rejects empty master branches list", func(t *testing.T) {

      
        
        300
        +		cfg := Config{

      
        
        301
        +			Server: ServerConfig{

      
        
        302
        +				Port: 8080,

      
        
        303
        +			},

      
        
        304
        +			Meta: MetaConfig{

      
        
        305
        +				Host: "example.com",

      
        
        306
        +			},

      
        
        307
        +			Repo: RepoConfig{

      
        
        308
        +				Dir:     tmpDir,

      
        
        309
        +				Readmes: []string{"README.md"},

      
        
        310
        +				Masters: []string{},

      
        
        311
        +			},

      
        
        312
        +		}

      
        
        313
        +		is.Err(t, cfg.validate(), "repo.masters")

      
        
        314
        +	})

      
        
        315
        +

      
        
        316
        +	t.Run("rejects invalid SSH port when SSH enabled", func(t *testing.T) {

      
        
        317
        +		cfg := Config{

      
        
        318
        +			Server: ServerConfig{

      
        
        319
        +				Port: 8080,

      
        
        320
        +			},

      
        
        321
        +			Meta: MetaConfig{

      
        
        322
        +				Host: "example.com",

      
        
        323
        +			},

      
        
        324
        +			Repo: RepoConfig{

      
        
        325
        +				Dir:     tmpDir,

      
        
        326
        +				Readmes: []string{"README.md"},

      
        
        327
        +				Masters: []string{"main"},

      
        
        328
        +			},

      
        
        329
        +			SSH: SSHConfig{

      
        
        330
        +				Enable:  true,

      
        
        331
        +				Port:    0,

      
        
        332
        +				HostKey: hostKeyPath,

      
        
        333
        +				Keys:    []string{"ssh-rsa AAAAB3..."},

      
        
        334
        +			},

      
        
        335
        +		}

      
        
        336
        +		is.Err(t, cfg.validate(), "ssh.port")

      
        
        337
        +	})

      
        
        338
        +

      
        
        339
        +	t.Run("rejects SSH port same as HTTP port", func(t *testing.T) {

      
        
        340
        +		cfg := Config{

      
        
        341
        +			Server: ServerConfig{

      
        
        342
        +				Port: 8080,

      
        
        343
        +			},

      
        
        344
        +			Meta: MetaConfig{

      
        
        345
        +				Host: "example.com",

      
        
        346
        +			},

      
        
        347
        +			Repo: RepoConfig{

      
        
        348
        +				Dir:     tmpDir,

      
        
        349
        +				Readmes: []string{"README.md"},

      
        
        350
        +				Masters: []string{"main"},

      
        
        351
        +			},

      
        
        352
        +			SSH: SSHConfig{

      
        
        353
        +				Enable:  true,

      
        
        354
        +				Port:    8080,

      
        
        355
        +				HostKey: hostKeyPath,

      
        
        356
        +				Keys:    []string{"ssh-rsa AAAAB3..."},

      
        
        357
        +			},

      
        
        358
        +		}

      
        
        359
        +		is.Err(t, cfg.validate(), "must differ")

      
        
        360
        +	})

      
        
        361
        +

      
        
        362
        +	t.Run("rejects nonexistent SSH host key file", func(t *testing.T) {

      
        
        363
        +		cfg := Config{

      
        
        364
        +			Server: ServerConfig{

      
        
        365
        +				Port: 8080,

      
        
        366
        +			},

      
        
        367
        +			Meta: MetaConfig{

      
        
        368
        +				Host: "example.com",

      
        
        369
        +			},

      
        
        370
        +			Repo: RepoConfig{

      
        
        371
        +				Dir:     tmpDir,

      
        
        372
        +				Readmes: []string{"README.md"},

      
        
        373
        +				Masters: []string{"main"},

      
        
        374
        +			},

      
        
        375
        +			SSH: SSHConfig{

      
        
        376
        +				Enable:  true,

      
        
        377
        +				Port:    2222,

      
        
        378
        +				HostKey: "/nonexistent/key",

      
        
        379
        +				Keys:    []string{"ssh-rsa AAAAB3..."},

      
        
        380
        +			},

      
        
        381
        +		}

      
        
        382
        +		is.Err(t, cfg.validate(), "ssh.host_key")

      
        
        383
        +	})

      
        
        384
        +

      
        
        385
        +	t.Run("rejects empty SSH keys list when SSH enabled", func(t *testing.T) {

      
        
        386
        +		cfg := Config{

      
        
        387
        +			Server: ServerConfig{

      
        
        388
        +				Port: 8080,

      
        
        389
        +			},

      
        
        390
        +			Meta: MetaConfig{

      
        
        391
        +				Host: "example.com",

      
        
        392
        +			},

      
        
        393
        +			Repo: RepoConfig{

      
        
        394
        +				Dir:     tmpDir,

      
        
        395
        +				Readmes: []string{"README.md"},

      
        
        396
        +				Masters: []string{"main"},

      
        
        397
        +			},

      
        
        398
        +			SSH: SSHConfig{

      
        
        399
        +				Enable:  true,

      
        
        400
        +				Port:    2222,

      
        
        401
        +				HostKey: hostKeyPath,

      
        
        402
        +				Keys:    []string{},

      
        
        403
        +			},

      
        
        404
        +		}

      
        
        405
        +		is.Err(t, cfg.validate(), "ssh.keys")

      
        
        406
        +	})

      
        
        407
        +

      
        
        408
        +	t.Run("rejects empty mirror interval", func(t *testing.T) {

      
        
        409
        +		cfg := Config{

      
        
        410
        +			Server: ServerConfig{

      
        
        411
        +				Port: 8080,

      
        
        412
        +			},

      
        
        413
        +			Meta: MetaConfig{

      
        
        414
        +				Host: "example.com",

      
        
        415
        +			},

      
        
        416
        +			Repo: RepoConfig{

      
        
        417
        +				Dir:     tmpDir,

      
        
        418
        +				Readmes: []string{"README.md"},

      
        
        419
        +				Masters: []string{"main"},

      
        
        420
        +			},

      
        
        421
        +			Mirror: MirrorConfig{

      
        
        422
        +				Enable:   true,

      
        
        423
        +				Interval: "",

      
        
        424
        +			},

      
        
        425
        +		}

      
        
        426
        +		is.Err(t, cfg.validate(), "mirror.interval")

      
        
        427
        +	})

      
        
        428
        +

      
        
        429
        +	t.Run("rejects invalid mirror interval format", func(t *testing.T) {

      
        
        430
        +		cfg := Config{

      
        
        431
        +			Server: ServerConfig{

      
        
        432
        +				Port: 8080,

      
        
        433
        +			},

      
        
        434
        +			Meta: MetaConfig{

      
        
        435
        +				Host: "example.com",

      
        
        436
        +			},

      
        
        437
        +			Repo: RepoConfig{

      
        
        438
        +				Dir:     tmpDir,

      
        
        439
        +				Readmes: []string{"README.md"},

      
        
        440
        +				Masters: []string{"main"},

      
        
        441
        +			},

      
        
        442
        +			Mirror: MirrorConfig{

      
        
        443
        +				Enable:   true,

      
        
        444
        +				Interval: "1hour",

      
        
        445
        +			},

      
        
        446
        +		}

      
        
        447
        +		is.Err(t, cfg.validate(), "invalid duration")

      
        
        448
        +	})

      
        
        449
        +

      
        
        450
        +	t.Run("collects and reports multiple validation errors", func(t *testing.T) {

      
        
        451
        +		cfg := Config{

      
        
        452
        +			Server: ServerConfig{

      
        
        453
        +				Port: 0,

      
        
        454
        +			},

      
        
        455
        +			Meta: MetaConfig{

      
        
        456
        +				Host: "",

      
        
        457
        +			},

      
        
        458
        +			Repo: RepoConfig{

      
        
        459
        +				Dir:     "/nonexistent",

      
        
        460
        +				Readmes: []string{},

      
        
        461
        +				Masters: []string{},

      
        
        462
        +			},

      
        
        463
        +		}

      
        
        464
        +		err := cfg.validate()

      
        
        465
        +		is.Err(t, err, "server.port")

      
        
        466
        +		is.Err(t, err, "meta.host")

      
        
        467
        +		is.Err(t, err, "repo.dir")

      
        
        468
        +		is.Err(t, err, "repo.readmes")

      
        
        469
        +		is.Err(t, err, "repo.masters")

      
        
        470
        +	})

      
        
        471
        +

      
        
        472
        +	t.Run("accepts multiple readme and master branch names", func(t *testing.T) {

      
        
        473
        +		cfg := Config{

      
        
        474
        +			Server: ServerConfig{

      
        
        475
        +				Port: 8080,

      
        
        476
        +			},

      
        
        477
        +			Meta: MetaConfig{

      
        
        478
        +				Host: "example.com",

      
        
        479
        +			},

      
        
        480
        +			Repo: RepoConfig{

      
        
        481
        +				Dir:     tmpDir,

      
        
        482
        +				Readmes: []string{"README.md", "readme.txt", "README"},

      
        
        483
        +				Masters: []string{"main", "master", "trunk"},

      
        
        484
        +			},

      
        
        485
        +		}

      
        
        486
        +		is.Err(t, cfg.validate(), nil)

      
        
        487
        +	})

      
        
        488
        +

      
        
        489
        +	t.Run("ignores invalid SSH fields when SSH disabled", func(t *testing.T) {

      
        
        490
        +		cfg := Config{

      
        
        491
        +			Server: ServerConfig{

      
        
        492
        +				Port: 8080,

      
        
        493
        +			},

      
        
        494
        +			Meta: MetaConfig{

      
        
        495
        +				Host: "example.com",

      
        
        496
        +			},

      
        
        497
        +			Repo: RepoConfig{

      
        
        498
        +				Dir:     tmpDir,

      
        
        499
        +				Readmes: []string{"README.md"},

      
        
        500
        +				Masters: []string{"main"},

      
        
        501
        +			},

      
        
        502
        +			SSH: SSHConfig{

      
        
        503
        +				Enable:  false,

      
        
        504
        +				Port:    0,

      
        
        505
        +				HostKey: "/nonexistent",

      
        
        506
        +				Keys:    []string{},

      
        
        507
        +			},

      
        
        508
        +		}

      
        
        509
        +		is.Err(t, cfg.validate(), nil)

      
        
        510
        +	})

      
        
        511
        +

      
        
        512
        +	t.Run("ignores invalid mirror fields when mirroring disabled", func(t *testing.T) {

      
        
        513
        +		cfg := Config{

      
        
        514
        +			Server: ServerConfig{

      
        
        515
        +				Port: 8080,

      
        
        516
        +			},

      
        
        517
        +			Meta: MetaConfig{

      
        
        518
        +				Host: "example.com",

      
        
        519
        +			},

      
        
        520
        +			Repo: RepoConfig{

      
        
        521
        +				Dir:     tmpDir,

      
        
        522
        +				Readmes: []string{"README.md"},

      
        
        523
        +				Masters: []string{"main"},

      
        
        524
        +			},

      
        
        525
        +			Mirror: MirrorConfig{

      
        
        526
        +				Enable:   false,

      
        
        527
        +				Interval: "invalid",

      
        
        528
        +			},

      
        
        529
        +		}

      
        
        530
        +		is.Err(t, cfg.validate(), nil)

      
        
        531
        +	})

      
        
        532
        +

      
        
        533
        +	t.Run("accepts various time duration formats", func(t *testing.T) {

      
        
        534
        +		durations := []string{"1h", "30m", "1h30m", "1h30m45s", "24h"}

      
        
        535
        +		for _, duration := range durations {

      
        
        536
        +			cfg := Config{

      
        
        537
        +				Server: ServerConfig{

      
        
        538
        +					Port: 8080,

      
        
        539
        +				},

      
        
        540
        +				Meta: MetaConfig{

      
        
        541
        +					Host: "example.com",

      
        
        542
        +				},

      
        
        543
        +				Repo: RepoConfig{

      
        
        544
        +					Dir:     tmpDir,

      
        
        545
        +					Readmes: []string{"README.md"},

      
        
        546
        +					Masters: []string{"main"},

      
        
        547
        +				},

      
        
        548
        +				Mirror: MirrorConfig{

      
        
        549
        +					Enable:   true,

      
        
        550
        +					Interval: duration,

      
        
        551
        +				},

      
        
        552
        +			}

      
        
        553
        +			is.Err(t, cfg.validate(), nil)

      
        
        554
        +		}

      
        
        555
        +	})

      
        
        556
        +}

      
A internal/config/testdata/hostkey
路路路
        
        1
        +test host key content