315 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|