4 files changed,
184 insertions(+),
566 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-02-13 00:07:29 +0200
Change ID:
ssvzvsorrpvtsowowmplvspytozvlvmv
Parent:
86a3594
M
internal/config/config.go
@@ -5,7 +5,6 @@ "errors"
"fmt" "os" "path/filepath" - "time" "gopkg.in/yaml.v2" )@@ -119,58 +118,6 @@ c.Mirror.Interval = "8h"
} } -func (c Config) validate() error { - var errs []error - - // server - if err := validatePort(c.Server.Port, "server.port"); err != nil { - errs = append(errs, err) - } - - // meta - if c.Meta.Host == "" { - errs = append(errs, errors.New("meta.host is required")) - } - - // repo - if err := validateDirExists(c.Repo.Dir, "repo.dir"); err != nil { - errs = append(errs, err) - } - if len(c.Repo.Readmes) == 0 { - errs = append(errs, errors.New("repo.readmes must have at least one value")) - } - if len(c.Repo.Masters) == 0 { - errs = append(errs, errors.New("repo.masters must have at least one value")) - } - - // ssh - if c.SSH.Enable { - if err := validatePort(c.SSH.Port, "ssh.port"); err != nil { - errs = append(errs, err) - } - if c.SSH.Port == c.Server.Port { - errs = append(errs, fmt.Errorf("ssh.port must differ from server.port (both are %d)", c.Server.Port)) - } - if err := validateFileExists(c.SSH.HostKey, "ssh.host_key"); err != nil { - errs = append(errs, err) - } - if len(c.SSH.Keys) == 0 { - errs = append(errs, errors.New("ssh.keys must have at least one value when ssh is enabled")) - } - } - - // mirror - if c.Mirror.Enable { - if c.Mirror.Interval == "" { - errs = append(errs, errors.New("mirror.interval is required when mirror is enabled")) - } else if _, err := time.ParseDuration(c.Mirror.Interval); err != nil { - errs = append(errs, fmt.Errorf("mirror.interval: invalid duration format: %w", err)) - } - } - - return errors.Join(errs...) -} - func findConfigFile(userPath string) (string, error) { if userPath != "" { if _, err := os.Stat(userPath); err == nil {@@ -198,43 +145,15 @@
return "", ErrConfigNotFound } -func validatePort(port int, fieldName string) error { - if port < 1 || port > 65535 { - return fmt.Errorf("%s must be between 1 and 65535, got %d", fieldName, port) - } - return nil +func isFileExists(path string) bool { + _, err := os.Stat(path) + return err == nil } -func validateDirExists(path string, fieldName string) error { - if path == "" { - return fmt.Errorf("%s is required", fieldName) - } - info, err := os.Stat(path) +func isDirExists(path string) bool { + i, err := os.Stat(path) if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("%s: directory does not exist: %s", fieldName, path) - } - return fmt.Errorf("%s: cannot access directory: %w", fieldName, err) + return false } - if !info.IsDir() { - return fmt.Errorf("%s: path exists but is not a directory: %s", fieldName, path) - } - return nil -} - -func validateFileExists(path string, fieldName string) error { - if path == "" { - return fmt.Errorf("%s is required", fieldName) - } - info, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("%s: file does not exist: %s", fieldName, path) - } - return fmt.Errorf("%s: cannot access file: %w", fieldName, err) - } - if info.IsDir() { - return fmt.Errorf("%s: path is a directory, not a file: %s", fieldName, path) - } - return nil + return i.IsDir() }
M
internal/config/config_test.go
@@ -76,481 +76,3 @@ is.Equal(t, path, userConfigFile)
} }) } - -func TestValidatePort(t *testing.T) { - t.Run("accepts standard port numbers", func(t *testing.T) { - is.Err(t, validatePort(1, "test"), nil) - is.Err(t, validatePort(80, "test"), nil) - is.Err(t, validatePort(8080, "test"), nil) - is.Err(t, validatePort(65535, "test"), nil) - }) - - t.Run("rejects out of range ports", func(t *testing.T) { - is.Err(t, validatePort(0, "test"), "must be between") - is.Err(t, validatePort(-1, "test"), "must be between") - is.Err(t, validatePort(65536, "test"), "must be between") - is.Err(t, validatePort(100000, "test"), "must be between") - }) -} - -func TestValidateDirExists(t *testing.T) { - t.Run("accepts existing directories", func(t *testing.T) { - tmpDir := t.TempDir() - is.Err(t, validateDirExists(tmpDir, "test"), nil) - }) - - t.Run("rejects nonexistent paths", func(t *testing.T) { - is.Err(t, validateDirExists("/nonexistent/path/to/dir", "test"), "does not exist") - }) - - t.Run("rejects empty paths", func(t *testing.T) { - is.Err(t, validateDirExists("", "test"), "is required") - }) - - t.Run("rejects files when directory expected", func(t *testing.T) { - tmpDir := t.TempDir() - tmpFile := filepath.Join(tmpDir, "file.txt") - if err := os.WriteFile(tmpFile, []byte("test"), 0o644); err != nil { - t.Fatal(err) - } - is.Err(t, validateDirExists(tmpFile, "test"), "not a directory") - }) -} - -func TestValidateFileExists(t *testing.T) { - t.Run("accepts existing files", func(t *testing.T) { - tmpDir := t.TempDir() - tmpFile := filepath.Join(tmpDir, "file.txt") - if err := os.WriteFile(tmpFile, []byte("test"), 0o644); err != nil { - t.Fatal(err) - } - is.Err(t, validateFileExists(tmpFile, "test"), nil) - }) - - t.Run("rejects nonexistent files", func(t *testing.T) { - is.Err(t, validateFileExists("/nonexistent/file.txt", "test"), "does not exist") - }) - - t.Run("rejects empty paths", func(t *testing.T) { - is.Err(t, validateFileExists("", "test"), "is required") - }) - - t.Run("rejects directories when file expected", func(t *testing.T) { - tmpDir := t.TempDir() - is.Err(t, validateFileExists(tmpDir, "test"), "is a directory") - }) -} - -func TestConfigValidate(t *testing.T) { - tmpDir := t.TempDir() - hostKeyPath := "testdata/hostkey" - - t.Run("accepts minimal valid configuration", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Host: "localhost", - Port: 8080, - }, - Meta: MetaConfig{ - Title: "Test", - Description: "Test description", - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: false, - }, - Mirror: MirrorConfig{ - Enable: false, - }, - } - is.Err(t, cfg.validate(), nil) - }) - - t.Run("accepts configuration with SSH enabled", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Host: "localhost", - Port: 8080, - }, - Meta: MetaConfig{ - Title: "Test", - Description: "Test description", - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: true, - Port: 2222, - HostKey: hostKeyPath, - Keys: []string{"ssh-rsa AAAAB3..."}, - }, - Mirror: MirrorConfig{ - Enable: false, - }, - } - is.Err(t, cfg.validate(), nil) - }) - - t.Run("accepts configuration with mirroring enabled", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Host: "localhost", - Port: 8080, - }, - Meta: MetaConfig{ - Title: "Test", - Description: "Test description", - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: false, - }, - Mirror: MirrorConfig{ - Enable: true, - Interval: "1h", - GithubToken: "ghp_token", - }, - } - is.Err(t, cfg.validate(), nil) - }) - - t.Run("rejects invalid server port", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 0, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - } - is.Err(t, cfg.validate(), "server.port") - }) - - t.Run("rejects missing meta host", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - } - is.Err(t, cfg.validate(), "meta.host") - }) - - t.Run("rejects nonexistent repository directory", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: "/nonexistent/path", - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - } - is.Err(t, cfg.validate(), "repo.dir") - }) - - t.Run("rejects empty readme list", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{}, - Masters: []string{"main"}, - }, - } - is.Err(t, cfg.validate(), "repo.readmes") - }) - - t.Run("rejects empty master branches list", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{}, - }, - } - is.Err(t, cfg.validate(), "repo.masters") - }) - - t.Run("rejects invalid SSH port when SSH enabled", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: true, - Port: 0, - HostKey: hostKeyPath, - Keys: []string{"ssh-rsa AAAAB3..."}, - }, - } - is.Err(t, cfg.validate(), "ssh.port") - }) - - t.Run("rejects SSH port same as HTTP port", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: true, - Port: 8080, - HostKey: hostKeyPath, - Keys: []string{"ssh-rsa AAAAB3..."}, - }, - } - is.Err(t, cfg.validate(), "must differ") - }) - - t.Run("rejects nonexistent SSH host key file", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: true, - Port: 2222, - HostKey: "/nonexistent/key", - Keys: []string{"ssh-rsa AAAAB3..."}, - }, - } - is.Err(t, cfg.validate(), "ssh.host_key") - }) - - t.Run("rejects empty SSH keys list when SSH enabled", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: true, - Port: 2222, - HostKey: hostKeyPath, - Keys: []string{}, - }, - } - is.Err(t, cfg.validate(), "ssh.keys") - }) - - t.Run("rejects empty mirror interval", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - Mirror: MirrorConfig{ - Enable: true, - Interval: "", - }, - } - is.Err(t, cfg.validate(), "mirror.interval") - }) - - t.Run("rejects invalid mirror interval format", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - Mirror: MirrorConfig{ - Enable: true, - Interval: "1hour", - }, - } - is.Err(t, cfg.validate(), "invalid duration") - }) - - t.Run("collects and reports multiple validation errors", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 0, - }, - Meta: MetaConfig{ - Host: "", - }, - Repo: RepoConfig{ - Dir: "/nonexistent", - Readmes: []string{}, - Masters: []string{}, - }, - } - err := cfg.validate() - is.Err(t, err, "server.port") - is.Err(t, err, "meta.host") - is.Err(t, err, "repo.dir") - is.Err(t, err, "repo.readmes") - is.Err(t, err, "repo.masters") - }) - - t.Run("accepts multiple readme and master branch names", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md", "readme.txt", "README"}, - Masters: []string{"main", "master", "trunk"}, - }, - } - is.Err(t, cfg.validate(), nil) - }) - - t.Run("ignores invalid SSH fields when SSH disabled", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - SSH: SSHConfig{ - Enable: false, - Port: 0, - HostKey: "/nonexistent", - Keys: []string{}, - }, - } - is.Err(t, cfg.validate(), nil) - }) - - t.Run("ignores invalid mirror fields when mirroring disabled", func(t *testing.T) { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - Mirror: MirrorConfig{ - Enable: false, - Interval: "invalid", - }, - } - is.Err(t, cfg.validate(), nil) - }) - - t.Run("accepts various time duration formats", func(t *testing.T) { - durations := []string{"1h", "30m", "1h30m", "1h30m45s", "24h"} - for _, duration := range durations { - cfg := Config{ - Server: ServerConfig{ - Port: 8080, - }, - Meta: MetaConfig{ - Host: "example.com", - }, - Repo: RepoConfig{ - Dir: tmpDir, - Readmes: []string{"README.md"}, - Masters: []string{"main"}, - }, - Mirror: MirrorConfig{ - Enable: true, - Interval: duration, - }, - } - is.Err(t, cfg.validate(), nil) - } - }) -}
A
internal/config/validate.go
@@ -0,0 +1,53 @@
+package config + +import ( + "errors" + "fmt" + "time" +) + +func (c Config) validate() error { + var errs []error + + if c.Meta.Host == "" { + // TODO: actually it should be a warning, host only used for go-import tag + errs = append(errs, errors.New("meta.host is required")) + } + + if !isDirExists(c.Repo.Dir) { + errs = append(errs, fmt.Errorf("repo.dir seems to be an invalid path")) + } + + if err := checkPort(c.Server.Port); err != nil { + errs = append(errs, fmt.Errorf("server.port %w", err)) + } + + if c.SSH.Enable { + if err := checkPort(c.SSH.Port); err != nil { + errs = append(errs, fmt.Errorf("ssh.port %w", err)) + } + + if c.SSH.Port == c.Server.Port { + errs = append(errs, fmt.Errorf("ssh.port must differ from server.port (both are %d)", c.Server.Port)) + } + + if !isFileExists(c.SSH.HostKey) { + errs = append(errs, fmt.Errorf("ssh.host_key seems to be an invalid path")) + } + } + + if c.Mirror.Enable { + if _, err := time.ParseDuration(c.Mirror.Interval); err != nil { + errs = append(errs, fmt.Errorf("mirror.interval: invalid duration format: %w", err)) + } + } + + return errors.Join(errs...) +} + +func checkPort(port int) error { + if port < 1 || port > 65535 { + return fmt.Errorf("must be between 1 and 65535, got %d", port) + } + return nil +}
A
internal/config/validate_test.go
@@ -0,0 +1,124 @@
+package config + +import ( + "testing" + + "olexsmir.xyz/x/is" +) + +func TestCheckPort(t *testing.T) { + is.Err(t, checkPort(1), nil) + is.Err(t, checkPort(80), nil) + is.Err(t, checkPort(65535), nil) + + is.Err(t, checkPort(0), "must be between") + is.Err(t, checkPort(-1), "must be between") + is.Err(t, checkPort(65536), "must be between") +} + +func TestConfig_Validate(t *testing.T) { + hostKey := "testdata/hostkey" + tests := []struct { + name string + expected any + c Config + }{ + { + name: "minimal", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: t.TempDir()}, + }, + }, + { + name: "minimal with ssh", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: t.TempDir()}, + SSH: SSHConfig{ + Enable: true, + HostKey: hostKey, + }, + }, + }, + { + name: "invalid meta.host", + expected: "meta.host", + c: Config{ + Repo: RepoConfig{Dir: t.TempDir()}, + }, + }, + { + name: "invalid repo.dir", + expected: "repo.dir", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: "nonexistent"}, + }, + }, + { + name: "invalid server port", + expected: "server.port", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: t.TempDir()}, + Server: ServerConfig{Port: -1}, + }, + }, + { + name: "invalid ssh port", + expected: "ssh.port", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: t.TempDir()}, + SSH: SSHConfig{ + Enable: true, + HostKey: hostKey, + Port: 100000, + }, + }, + }, + { + name: "same ssh and http ports", + expected: "ssh.port must differ", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: t.TempDir()}, + SSH: SSHConfig{Enable: true, Port: 228}, + Server: ServerConfig{Port: 228}, + }, + }, + { + name: "invalid ssh.host_key path", + expected: "ssh.host_key", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: t.TempDir()}, + SSH: SSHConfig{ + Enable: true, + HostKey: "/somewhere", + }, + }, + }, + { + name: "invalid mirror.interval duration format", + expected: "mirror.interval: invalid duration", + c: Config{ + Meta: MetaConfig{Host: "example.com"}, + Repo: RepoConfig{Dir: t.TempDir()}, + Mirror: MirrorConfig{ + Enable: true, + Interval: "asdf", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.c.ensureDefaults() + err := tt.c.validate() + is.Err(t, err, tt.expected) + }) + } +}