Files
iceBear67 a006483bbc
ci / test (push) Canceled after 0s
docker / build (push) Canceled after 0s
init
2026-08-14 07:13:22 +00:00

315 lines
7.1 KiB
Go

package config
import (
"strings"
"testing"
"time"
)
const minimal = `
[[repo]]
name = "demo"
src = "https://git.example.com/demo.git"
dst = "git@github.com:me/demo.git"
`
func mustParse(t *testing.T, doc string) *Config {
t.Helper()
cfg, err := Parse([]byte(doc))
if err != nil {
t.Fatalf("parse: %v", err)
}
return cfg
}
func TestDefaultsAreApplied(t *testing.T) {
cfg := mustParse(t, minimal)
if cfg.WorkDir != DefaultWorkDir {
t.Errorf("work_dir = %q, want %q", cfg.WorkDir, DefaultWorkDir)
}
if cfg.LogLevel != "info" || cfg.LogFormat != "text" {
t.Errorf("log defaults = %q/%q", cfg.LogLevel, cfg.LogFormat)
}
if cfg.Concurrency < 1 {
t.Errorf("concurrency = %d, want >= 1", cfg.Concurrency)
}
j := cfg.Jobs[0]
if j.Interval != DefaultInterval || j.Timeout != DefaultTimeout {
t.Errorf("interval/timeout = %s/%s", j.Interval, j.Timeout)
}
if !j.Prune || !j.Force {
t.Error("prune and force should default to true for a mirror")
}
if j.Atomic || j.AllowEmpty {
t.Error("atomic and allow_empty should default to false")
}
if strings.Join(j.Refs, ",") != strings.Join(DefaultRefs, ",") {
t.Errorf("refs = %v, want %v", j.Refs, DefaultRefs)
}
if !strings.HasSuffix(j.Dir, "mirrors/demo.git") {
t.Errorf("mirror dir = %q", j.Dir)
}
}
func TestGlobalDefaultsCascadeAndRepoOverrides(t *testing.T) {
cfg := mustParse(t, `
[global]
work_dir = "/data"
interval = "10m"
prune = false
ssh_key = "/keys/shared"
[[repo]]
name = "inherits"
src = "https://example.com/a.git"
dst = "git@github.com:me/a.git"
[[repo]]
name = "overrides"
src = "https://example.com/b.git"
dst = "git@github.com:me/b.git"
interval = "30s"
prune = true
ssh_key = "/keys/b"
`)
byName := map[string]Job{}
for _, j := range cfg.Jobs {
byName[j.Name] = j
}
a := byName["inherits"]
if a.Interval != 10*time.Minute {
t.Errorf("inherited interval = %s, want 10m", a.Interval)
}
if a.Prune {
t.Error("inherited prune should be false")
}
if a.Dst.SSHKey != "/keys/shared" {
t.Errorf("inherited ssh_key = %q", a.Dst.SSHKey)
}
b := byName["overrides"]
if b.Interval != 30*time.Second {
t.Errorf("overridden interval = %s, want 30s", b.Interval)
}
if !b.Prune {
t.Error("overridden prune should be true")
}
if b.Dst.SSHKey != "/keys/b" {
t.Errorf("overridden ssh_key = %q", b.Dst.SSHKey)
}
}
func TestEndpointAcceptsStringOrTable(t *testing.T) {
cfg := mustParse(t, `
[[repo]]
name = "demo"
src = "https://git.example.com/demo.git"
[repo.dst]
url = "git@github.com:me/demo.git"
ssh_key = "/keys/demo"
known_hosts = "/etc/syncbot/known_hosts"
strict_host_key = "yes"
`)
j := cfg.Jobs[0]
if j.Src.URL != "https://git.example.com/demo.git" {
t.Errorf("src url = %q", j.Src.URL)
}
if j.Dst.URL != "git@github.com:me/demo.git" {
t.Errorf("dst url = %q", j.Dst.URL)
}
if j.Dst.SSHKey != "/keys/demo" || j.Dst.KnownHosts != "/etc/syncbot/known_hosts" {
t.Errorf("dst ssh settings = %+v", j.Dst)
}
if j.Dst.StrictHostKey != "yes" {
t.Errorf("strict_host_key = %q", j.Dst.StrictHostKey)
}
// src has no key of its own and none was inherited.
if j.Src.SSHKey != "" {
t.Errorf("src ssh_key = %q, want empty", j.Src.SSHKey)
}
}
func TestStrictHostKeyDefaultFollowsKnownHosts(t *testing.T) {
cfg := mustParse(t, minimal)
if got := cfg.Jobs[0].Dst.StrictHostKey; got != "accept-new" {
t.Errorf("without known_hosts: %q, want accept-new", got)
}
cfg = mustParse(t, minimal+`
[global]
known_hosts = "/etc/syncbot/known_hosts"
`)
if got := cfg.Jobs[0].Dst.StrictHostKey; got != "yes" {
t.Errorf("with known_hosts: %q, want yes", got)
}
}
func TestEnvExpansion(t *testing.T) {
t.Setenv("SYNCBOT_TEST_TOKEN", "s3cr#t$")
cfg := mustParse(t, `
[[repo]]
name = "demo"
src = "https://x-access-token:${SYNCBOT_TEST_TOKEN}@github.com/me/demo.git"
dst = "git@github.com:me/mirror.git"
`)
want := "https://x-access-token:s3cr#t$@github.com/me/demo.git"
if got := cfg.Jobs[0].Src.URL; got != want {
t.Errorf("expanded src = %q, want %q", got, want)
}
}
func TestUndefinedEnvVarIsAnError(t *testing.T) {
_, err := Parse([]byte(`
[[repo]]
name = "demo"
src = "https://${SYNCBOT_DEFINITELY_UNSET}@example.com/a.git"
dst = "git@github.com:me/a.git"
`))
if err == nil || !strings.Contains(err.Error(), "SYNCBOT_DEFINITELY_UNSET") {
t.Fatalf("want an error naming the missing variable, got %v", err)
}
}
func TestDisabledRepoIsSkipped(t *testing.T) {
cfg := mustParse(t, `
[[repo]]
name = "on"
src = "https://example.com/a.git"
dst = "git@github.com:me/a.git"
[[repo]]
name = "off"
enabled = false
src = "https://example.com/b.git"
dst = "git@github.com:me/b.git"
`)
if len(cfg.Jobs) != 1 || cfg.Jobs[0].Name != "on" {
t.Fatalf("want only the enabled repo, got %d: %+v", len(cfg.Jobs), cfg.Jobs)
}
}
func TestValidationErrors(t *testing.T) {
cases := []struct{ name, doc, want string }{
{"no repos", `[global]
work_dir = "/data"`, "nothing to sync"},
{"missing name", `[[repo]]
src = "a"
dst = "b"`, "name must match"},
{"bad name", `[[repo]]
name = "../escape"
src = "a"
dst = "b"`, "name must match"},
{"duplicate name", `[[repo]]
name = "x"
src = "a"
dst = "b"
[[repo]]
name = "x"
src = "c"
dst = "d"`, "duplicate name"},
{"missing dst", `[[repo]]
name = "x"
src = "a"`, "dst is required"},
{"same src and dst", `[[repo]]
name = "x"
src = "a"
dst = "a"`, "same repository"},
{"unknown key", `[[repo]]
name = "x"
src = "a"
dst = "b"
intervall = "5m"`, "unknown config key"},
{"bad duration", `[[repo]]
name = "x"
src = "a"
dst = "b"
interval = "5 minutes"`, "invalid duration"},
{"relative ssh key", `[[repo]]
name = "x"
src = "a"
dst = "b"
ssh_key = "keys/x"`, "absolute path"},
{"ref without prefix", `[[repo]]
name = "x"
src = "a"
dst = "b"
refs = ["heads/*"]`, `must start with "refs/"`},
{"two globs", `[[repo]]
name = "x"
src = "a"
dst = "b"
refs = ["refs/*/*"]`, `at most one`},
{"bad log level", `[global]
log_level = "verbose"
[[repo]]
name = "x"
src = "a"
dst = "b"`, "log_level"},
{"bad endpoint key", `[[repo]]
name = "x"
src = "a"
[repo.dst]
url = "b"
sshkey = "/k"`, "unknown key"},
{"endpoint table without url", `[[repo]]
name = "x"
src = "a"
[repo.dst]
ssh_key = "/k"`, "url"},
{"bad git_config", `[[repo]]
name = "x"
src = "a"
dst = "b"
git_config = ["pack.threads"]`, "key=value"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := Parse([]byte(tc.doc))
if err == nil {
t.Fatalf("want an error mentioning %q", tc.want)
}
if !strings.Contains(err.Error(), tc.want) {
t.Errorf("error = %q, want it to mention %q", err, tc.want)
}
})
}
}
func TestMaxBackoffNeverBelowInterval(t *testing.T) {
cfg := mustParse(t, `
[[repo]]
name = "x"
src = "a"
dst = "b"
interval = "10m"
max_backoff = "1m"
`)
if got := cfg.Jobs[0].MaxBackoff; got != 10*time.Minute {
t.Errorf("max_backoff = %s, want it raised to the interval (10m)", got)
}
}
func TestJobsAreSortedForStableDiffs(t *testing.T) {
cfg := mustParse(t, `
[[repo]]
name = "zulu"
src = "a"
dst = "b"
[[repo]]
name = "alpha"
src = "c"
dst = "d"
`)
if cfg.Jobs[0].Name != "alpha" || cfg.Jobs[1].Name != "zulu" {
t.Errorf("jobs not sorted: %s, %s", cfg.Jobs[0].Name, cfg.Jobs[1].Name)
}
}