all repos

mugit @ c9e6e1a

馃惍 git server that your cow will love
4 files changed, 184 insertions(+), 566 deletions(-)
config: refactor validation
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-02-13 00:07:29 +0200
Authored at: 2026-02-12 22:12:48 +0200
Change ID: ssvzvsorrpvtsowowmplvspytozvlvmv
Parent: 86a3594
M internal/config/config.go
路路路
        5
        5
         	"fmt"

      
        6
        6
         	"os"

      
        7
        7
         	"path/filepath"

      
        8
        
        -	"time"

      
        9
        8
         

      
        10
        9
         	"gopkg.in/yaml.v2"

      
        11
        10
         )

      路路路
        119
        118
         	}

      
        120
        119
         }

      
        121
        120
         

      
        122
        
        -func (c Config) validate() error {

      
        123
        
        -	var errs []error

      
        124
        
        -

      
        125
        
        -	// server

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

      
        127
        
        -		errs = append(errs, err)

      
        128
        
        -	}

      
        129
        
        -

      
        130
        
        -	// meta

      
        131
        
        -	if c.Meta.Host == "" {

      
        132
        
        -		errs = append(errs, errors.New("meta.host is required"))

      
        133
        
        -	}

      
        134
        
        -

      
        135
        
        -	// repo

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

      
        137
        
        -		errs = append(errs, err)

      
        138
        
        -	}

      
        139
        
        -	if len(c.Repo.Readmes) == 0 {

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

      
        141
        
        -	}

      
        142
        
        -	if len(c.Repo.Masters) == 0 {

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

      
        144
        
        -	}

      
        145
        
        -

      
        146
        
        -	// ssh

      
        147
        
        -	if c.SSH.Enable {

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

      
        149
        
        -			errs = append(errs, err)

      
        150
        
        -		}

      
        151
        
        -		if c.SSH.Port == c.Server.Port {

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

      
        153
        
        -		}

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

      
        155
        
        -			errs = append(errs, err)

      
        156
        
        -		}

      
        157
        
        -		if len(c.SSH.Keys) == 0 {

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

      
        159
        
        -		}

      
        160
        
        -	}

      
        161
        
        -

      
        162
        
        -	// mirror

      
        163
        
        -	if c.Mirror.Enable {

      
        164
        
        -		if c.Mirror.Interval == "" {

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

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

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

      
        168
        
        -		}

      
        169
        
        -	}

      
        170
        
        -

      
        171
        
        -	return errors.Join(errs...)

      
        172
        
        -}

      
        173
        
        -

      
        174
        121
         func findConfigFile(userPath string) (string, error) {

      
        175
        122
         	if userPath != "" {

      
        176
        123
         		if _, err := os.Stat(userPath); err == nil {

      路路路
        198
        145
         	return "", ErrConfigNotFound

      
        199
        146
         }

      
        200
        147
         

      
        201
        
        -func validatePort(port int, fieldName string) error {

      
        202
        
        -	if port < 1 || port > 65535 {

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

      
        204
        
        -	}

      
        205
        
        -	return nil

      
        
        148
        +func isFileExists(path string) bool {

      
        
        149
        +	_, err := os.Stat(path)

      
        
        150
        +	return err == nil

      
        206
        151
         }

      
        207
        152
         

      
        208
        
        -func validateDirExists(path string, fieldName string) error {

      
        209
        
        -	if path == "" {

      
        210
        
        -		return fmt.Errorf("%s is required", fieldName)

      
        211
        
        -	}

      
        212
        
        -	info, err := os.Stat(path)

      
        
        153
        +func isDirExists(path string) bool {

      
        
        154
        +	i, err := os.Stat(path)

      
        213
        155
         	if err != nil {

      
        214
        
        -		if os.IsNotExist(err) {

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

      
        216
        
        -		}

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

      
        
        156
        +		return false

      
        218
        157
         	}

      
        219
        
        -	if !info.IsDir() {

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

      
        221
        
        -	}

      
        222
        
        -	return nil

      
        223
        
        -}

      
        224
        
        -

      
        225
        
        -func validateFileExists(path string, fieldName string) error {

      
        226
        
        -	if path == "" {

      
        227
        
        -		return fmt.Errorf("%s is required", fieldName)

      
        228
        
        -	}

      
        229
        
        -	info, err := os.Stat(path)

      
        230
        
        -	if err != nil {

      
        231
        
        -		if os.IsNotExist(err) {

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

      
        233
        
        -		}

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

      
        235
        
        -	}

      
        236
        
        -	if info.IsDir() {

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

      
        238
        
        -	}

      
        239
        
        -	return nil

      
        
        158
        +	return i.IsDir()

      
        240
        159
         }

      
M internal/config/config_test.go
路路路
        76
        76
         		}

      
        77
        77
         	})

      
        78
        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/validate.go
路路路
        
        1
        +package config

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"errors"

      
        
        5
        +	"fmt"

      
        
        6
        +	"time"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +func (c Config) validate() error {

      
        
        10
        +	var errs []error

      
        
        11
        +

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

      
        
        13
        +		// TODO: actually it should be a warning, host only used for go-import tag

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

      
        
        15
        +	}

      
        
        16
        +

      
        
        17
        +	if !isDirExists(c.Repo.Dir) {

      
        
        18
        +		errs = append(errs, fmt.Errorf("repo.dir seems to be an invalid path"))

      
        
        19
        +	}

      
        
        20
        +

      
        
        21
        +	if err := checkPort(c.Server.Port); err != nil {

      
        
        22
        +		errs = append(errs, fmt.Errorf("server.port %w", err))

      
        
        23
        +	}

      
        
        24
        +

      
        
        25
        +	if c.SSH.Enable {

      
        
        26
        +		if err := checkPort(c.SSH.Port); err != nil {

      
        
        27
        +			errs = append(errs, fmt.Errorf("ssh.port %w", err))

      
        
        28
        +		}

      
        
        29
        +

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

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

      
        
        32
        +		}

      
        
        33
        +

      
        
        34
        +		if !isFileExists(c.SSH.HostKey) {

      
        
        35
        +			errs = append(errs, fmt.Errorf("ssh.host_key seems to be an invalid path"))

      
        
        36
        +		}

      
        
        37
        +	}

      
        
        38
        +

      
        
        39
        +	if c.Mirror.Enable {

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

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

      
        
        42
        +		}

      
        
        43
        +	}

      
        
        44
        +

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

      
        
        46
        +}

      
        
        47
        +

      
        
        48
        +func checkPort(port int) error {

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

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

      
        
        51
        +	}

      
        
        52
        +	return nil

      
        
        53
        +}

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

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"testing"

      
        
        5
        +

      
        
        6
        +	"olexsmir.xyz/x/is"

      
        
        7
        +)

      
        
        8
        +

      
        
        9
        +func TestCheckPort(t *testing.T) {

      
        
        10
        +	is.Err(t, checkPort(1), nil)

      
        
        11
        +	is.Err(t, checkPort(80), nil)

      
        
        12
        +	is.Err(t, checkPort(65535), nil)

      
        
        13
        +

      
        
        14
        +	is.Err(t, checkPort(0), "must be between")

      
        
        15
        +	is.Err(t, checkPort(-1), "must be between")

      
        
        16
        +	is.Err(t, checkPort(65536), "must be between")

      
        
        17
        +}

      
        
        18
        +

      
        
        19
        +func TestConfig_Validate(t *testing.T) {

      
        
        20
        +	hostKey := "testdata/hostkey"

      
        
        21
        +	tests := []struct {

      
        
        22
        +		name     string

      
        
        23
        +		expected any

      
        
        24
        +		c        Config

      
        
        25
        +	}{

      
        
        26
        +		{

      
        
        27
        +			name: "minimal",

      
        
        28
        +			c: Config{

      
        
        29
        +				Meta: MetaConfig{Host: "example.com"},

      
        
        30
        +				Repo: RepoConfig{Dir: t.TempDir()},

      
        
        31
        +			},

      
        
        32
        +		},

      
        
        33
        +		{

      
        
        34
        +			name: "minimal with ssh",

      
        
        35
        +			c: Config{

      
        
        36
        +				Meta: MetaConfig{Host: "example.com"},

      
        
        37
        +				Repo: RepoConfig{Dir: t.TempDir()},

      
        
        38
        +				SSH: SSHConfig{

      
        
        39
        +					Enable:  true,

      
        
        40
        +					HostKey: hostKey,

      
        
        41
        +				},

      
        
        42
        +			},

      
        
        43
        +		},

      
        
        44
        +		{

      
        
        45
        +			name: "invalid meta.host",

      
        
        46
        +			expected: "meta.host",

      
        
        47
        +			c: Config{

      
        
        48
        +				Repo: RepoConfig{Dir: t.TempDir()},

      
        
        49
        +			},

      
        
        50
        +		},

      
        
        51
        +		{

      
        
        52
        +			name:     "invalid repo.dir",

      
        
        53
        +			expected: "repo.dir",

      
        
        54
        +			c: Config{

      
        
        55
        +				Meta: MetaConfig{Host: "example.com"},

      
        
        56
        +				Repo: RepoConfig{Dir: "nonexistent"},

      
        
        57
        +			},

      
        
        58
        +		},

      
        
        59
        +		{

      
        
        60
        +			name:     "invalid server port",

      
        
        61
        +			expected: "server.port",

      
        
        62
        +			c: Config{

      
        
        63
        +				Meta:   MetaConfig{Host: "example.com"},

      
        
        64
        +				Repo:   RepoConfig{Dir: t.TempDir()},

      
        
        65
        +				Server: ServerConfig{Port: -1},

      
        
        66
        +			},

      
        
        67
        +		},

      
        
        68
        +		{

      
        
        69
        +			name:     "invalid ssh port",

      
        
        70
        +			expected: "ssh.port",

      
        
        71
        +			c: Config{

      
        
        72
        +				Meta: MetaConfig{Host: "example.com"},

      
        
        73
        +				Repo: RepoConfig{Dir: t.TempDir()},

      
        
        74
        +				SSH: SSHConfig{

      
        
        75
        +					Enable:  true,

      
        
        76
        +					HostKey: hostKey,

      
        
        77
        +					Port:    100000,

      
        
        78
        +				},

      
        
        79
        +			},

      
        
        80
        +		},

      
        
        81
        +		{

      
        
        82
        +			name:     "same ssh and http ports",

      
        
        83
        +			expected: "ssh.port must differ",

      
        
        84
        +			c: Config{

      
        
        85
        +				Meta:   MetaConfig{Host: "example.com"},

      
        
        86
        +				Repo:   RepoConfig{Dir: t.TempDir()},

      
        
        87
        +				SSH:    SSHConfig{Enable: true, Port: 228},

      
        
        88
        +				Server: ServerConfig{Port: 228},

      
        
        89
        +			},

      
        
        90
        +		},

      
        
        91
        +		{

      
        
        92
        +			name:     "invalid ssh.host_key path",

      
        
        93
        +			expected: "ssh.host_key",

      
        
        94
        +			c: Config{

      
        
        95
        +				Meta: MetaConfig{Host: "example.com"},

      
        
        96
        +				Repo: RepoConfig{Dir: t.TempDir()},

      
        
        97
        +				SSH: SSHConfig{

      
        
        98
        +					Enable:  true,

      
        
        99
        +					HostKey: "/somewhere",

      
        
        100
        +				},

      
        
        101
        +			},

      
        
        102
        +		},

      
        
        103
        +		{

      
        
        104
        +			name:     "invalid mirror.interval duration format",

      
        
        105
        +			expected: "mirror.interval: invalid duration",

      
        
        106
        +			c: Config{

      
        
        107
        +				Meta: MetaConfig{Host: "example.com"},

      
        
        108
        +				Repo: RepoConfig{Dir: t.TempDir()},

      
        
        109
        +				Mirror: MirrorConfig{

      
        
        110
        +					Enable:   true,

      
        
        111
        +					Interval: "asdf",

      
        
        112
        +				},

      
        
        113
        +			},

      
        
        114
        +		},

      
        
        115
        +	}

      
        
        116
        +

      
        
        117
        +	for _, tt := range tests {

      
        
        118
        +		t.Run(tt.name, func(t *testing.T) {

      
        
        119
        +			tt.c.ensureDefaults()

      
        
        120
        +			err := tt.c.validate()

      
        
        121
        +			is.Err(t, err, tt.expected)

      
        
        122
        +		})

      
        
        123
        +	}

      
        
        124
        +}