package main import ( "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "testing" ) // withProvider adds a connection to the settings. func withProvider(s Settings, name, typ string) Settings { obj(obj(s, "router"), "dnsProviders")[name] = map[string]any{"type": typ, "secretFile": "/var/lib/router/dns-secrets/" + name} return s } func TestDDNSRoundTrip(t *testing.T) { s := withProvider(mustParse(t), "cf", "cloudflare") if f := s.ddns(); f.Enabled || f.Provider != "" || f.Interval != "5min" || !f.IPv4 || f.IPv6 { t.Fatalf("defaults = %+v", f) } f := DDNSForm{Enabled: true, Provider: "cf", Zone: "example.org", Domains: "Home.example.org, *.k8s.example.org\nvpn.example.org", IPv4: true, IPv6: true, SSL: true, Interval: "10min"} if err := s.applyDDNS(f); err != nil { t.Fatal(err) } out := string(mustMarshal(t, s)) for _, want := range []string{`"provider": "cf"`, `"home.example.org"`, `"*.k8s.example.org"`, `"ipv6": true`, `"zone": "example.org"`} { if !strings.Contains(out, want) { t.Errorf("%s missing:\n%s", want, out) } } got := s.ddns() if got.Provider != "cf" || got.Domains != "home.example.org\n*.k8s.example.org\nvpn.example.org" { t.Errorf("read back = %+v", got) } f.Enabled = false // switching off keeps the rest if err := s.applyDDNS(f); err != nil { t.Fatal(err) } if got := s.ddns(); got.Enabled || got.Zone != "example.org" || got.Provider != "cf" { t.Errorf("after switching off = %+v", got) } } func TestDDNSIsChecked(t *testing.T) { good := DDNSForm{Enabled: true, Provider: "cf", Domains: "a.example.org", IPv4: true, SSL: true, Interval: "5min"} for name, mod := range map[string]func(*DDNSForm){ "domains": func(f *DDNSForm) { f.Domains = "not a host!" }, "provider": func(f *DDNSForm) { f.Provider = "nothing" }, "interval": func(f *DDNSForm) { f.Interval = "soon" }, "ipv4": func(f *DDNSForm) { f.IPv4 = false }, } { f := good mod(&f) if err := withProvider(mustParse(t), "cf", "cloudflare").applyDDNS(f); err == nil || err.(FormErrors)[name] == "" { t.Errorf("%s accepted: %v", name, err) } } f := good f.Provider = "" if err := withProvider(mustParse(t), "cf", "cloudflare").applyDDNS(f); err == nil || err.(FormErrors)["provider"] == "" { t.Errorf("enabled without an account accepted: %v", err) } f = good f.Domains = "" if err := withProvider(mustParse(t), "cf", "cloudflare").applyDDNS(f); err == nil { t.Error("enabled without names accepted") } f.Enabled, f.Provider = false, "" if err := mustParse(t).applyDDNS(f); err != nil { t.Errorf("disabled needs neither names nor an account: %v", err) } } func TestDNSProvidersRoundTrip(t *testing.T) { s := mustParse(t) dir := t.TempDir() rows := []DNSProvider{ {Name: "CF", Type: "Cloudflare", Username: "token", Secret: " s3cret\n", New: true}, {Name: "dyn", Type: "dyndns2", Server: "dynupdate.no-ip.com", Username: "u", Secret: "p", New: true}, {New: true, Type: "cloudflare"}, // the empty add row } writes, deletes, err := s.applyDNSProviders(rows, dir) if err != nil || len(writes) != 2 || len(deletes) != 0 { t.Fatalf("writes=%v deletes=%v err=%v", writes, deletes, err) } if writes[0].Path != filepath.Join(dir, "cf") || writes[0].Secret != "s3cret" { t.Errorf("writes = %+v", writes) } out := string(mustMarshal(t, s)) if strings.Contains(out, "s3cret") || !strings.Contains(out, `"secretFile": "`+dir+`/cf"`) || !strings.Contains(out, `"server": "dynupdate.no-ip.com"`) { t.Errorf("settings:\n%s", out) } // Edited without a new secret: the file stays. A used connection cannot go. obj(obj(s, "router"), "ddns")["provider"] = "cf" rows = []DNSProvider{{Name: "cf", Type: "cloudflare", Username: "token"}, {Name: "dyn", Type: "dyndns2", Remove: true}} if writes, deletes, err = s.applyDNSProviders(rows, dir); err != nil || len(writes) != 0 || len(deletes) != 1 { t.Fatalf("writes=%v deletes=%v err=%v", writes, deletes, err) } rows = []DNSProvider{{Name: "cf", Type: "cloudflare", Remove: true}} if _, _, err = s.applyDNSProviders(rows, dir); err == nil || !strings.Contains(err.(FormErrors)["prov[0].name"], "Dynamic DNS") { t.Errorf("a connection in use was removed: %v", err) } } func TestDNSProvidersAreChecked(t *testing.T) { good := DNSProvider{Name: "cf", Type: "cloudflare", Secret: "x", New: true} for name, mod := range map[string]func(*DNSProvider){ "name": func(r *DNSProvider) { r.Name = "Not A Name!" }, "type": func(r *DNSProvider) { r.Type = "no way" }, "server": func(r *DNSProvider) { r.Server = "https://x.example" }, "secret": func(r *DNSProvider) { r.Secret = "" }, } { r := good mod(&r) if _, _, err := mustParse(t).applyDNSProviders([]DNSProvider{r}, "/x"); err == nil || err.(FormErrors)["prov[0]."+name] == "" { t.Errorf("%s accepted: %v", name, err) } } s := withProvider(mustParse(t), "cf", "cloudflare") if _, _, err := s.applyDNSProviders([]DNSProvider{good}, "/x"); err == nil { t.Error("a new connection with a taken name accepted") } } func TestDNSProvidersAndDDNSPages(t *testing.T) { a, sys := newTestApp(t) h := a.routes() cookie := do(h, "POST", "/login", url.Values{"password": {"hunter2hunter2"}}, nil).Result().Cookies()[0] body := do(h, "GET", "/dnsproviders", nil, cookie).Body.String() if !strings.Contains(body, `name="providers" value="1"`) { t.Fatalf("page:\n%s", body) } token := regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(body)[1] form := url.Values{"csrf": {token}, "providers": {"1"}, "prov.0.new": {"1"}, "prov.0.name": {"cf"}, "prov.0.type": {"cloudflare"}, "prov.0.username": {"token"}} if rec := do(h, "POST", "/dnsproviders", form, cookie); rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "API token") { t.Fatalf("no secret: %d\n%s", rec.Code, rec.Body) } form.Set("prov.0.secret", "tok3n") if rec := do(h, "POST", "/dnsproviders", form, cookie); rec.Code != http.StatusSeeOther { t.Fatalf("save: %d\n%s", rec.Code, rec.Body) } path := filepath.Join(sys.StateDir, "dns-secrets", "cf") data, err := os.ReadFile(path) if err != nil || string(data) != "tok3n" { t.Fatalf("secret file: %q %v", data, err) } if fi, _ := os.Stat(path); fi.Mode().Perm() != 0o600 { t.Errorf("mode %v", fi.Mode()) } if body := do(h, "GET", "/dnsproviders", nil, cookie).Body.String(); strings.Contains(body, "tok3n") || !strings.Contains(body, "leave empty to keep") { t.Errorf("the page shows the secret or lacks the hint:\n%s", body) } // Dynamic DNS picks the connection. body = do(h, "GET", "/ddns", nil, cookie).Body.String() if !strings.Contains(body, `