4 files changed,
184 insertions(+),
566 deletions(-)
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 +}