1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Integration Testing framework (#46)

* integration test started

* details

* More tests.

* idn tests and punycode (not tested fully because I'm on an aiplane)

* test for dual provider compatibility

* readme for tests

* vendor idna

* fix casing
This commit is contained in:
Craig Peterson
2017-03-16 22:42:53 -07:00
committed by GitHub
parent 0906f5c383
commit 101916a6e4
12 changed files with 547 additions and 8 deletions

2
.gitignore vendored
View File

@ -5,8 +5,8 @@ dnscontrol.exe
dnscontrol dnscontrol
dnsconfig.js dnsconfig.js
creds.json creds.json
integration
ExternalDNS ExternalDNS
docs/_site docs/_site
powershell.log powershell.log
zones/ zones/
integrationTest/.env

View File

@ -26,6 +26,8 @@ For Google cloud authentication, DNSControl requires a JSON 'Service Account Key
} }
{% endhighlight %} {% endhighlight %}
**Note**: The `project_id`, `private_key`, and `client_email`, are the only fields that are strictly required, but it is sometimes easier to just paste the entire json object in. Either way is fine.
See [the Activation section](#activation) for some tips on obtaining these credentials. See [the Activation section](#activation) for some tips on obtaining these credentials.
## Metadata ## Metadata

View File

@ -0,0 +1,213 @@
package main
import (
"flag"
"testing"
"fmt"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/nameservers"
"github.com/StackExchange/dnscontrol/providers"
_ "github.com/StackExchange/dnscontrol/providers/_all"
"github.com/StackExchange/dnscontrol/providers/config"
"github.com/miekg/dns/dnsutil"
)
var providerToRun = flag.String("provider", "", "Provider to run")
func init() {
flag.Parse()
}
func getProvider(t *testing.T) (providers.DNSServiceProvider, string) {
if *providerToRun == "" {
t.Log("No provider specified with -provider")
return nil, ""
}
jsons, err := config.LoadProviderConfigs("providers.json")
if err != nil {
t.Fatalf("Error loading provider configs: %s", err)
}
for name, cfg := range jsons {
if *providerToRun != name {
continue
}
provider, err := providers.CreateDNSProvider(name, cfg, nil)
if err != nil {
t.Fatal(err)
}
return provider, cfg["domain"]
}
t.Fatalf("Provider %s not found", *providerToRun)
return nil, ""
}
func TestDNSProviders(t *testing.T) {
provider, domain := getProvider(t)
if provider == nil {
return
}
t.Run(fmt.Sprintf("%s", domain), func(t *testing.T) {
runTests(t, provider, domain)
})
}
func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvider, domainName string) *models.DomainConfig {
dc := &models.DomainConfig{
Name: domainName,
}
// fix up nameservers
ns, err := prv.GetNameservers(domainName)
if err != nil {
t.Fatal("Failed getting nameservers", err)
}
dc.Nameservers = ns
nameservers.AddNSRecords(dc)
return dc
}
func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string) {
dc := getDomainConfigWithNameservers(t, prv, domainName)
// run tests one at a time
for i, tst := range tests {
if t.Failed() {
break
}
t.Run(fmt.Sprintf("%d: %s", i, tst.Desc), func(t *testing.T) {
dom, _ := dc.Copy()
for _, r := range tst.Records {
rc := models.RecordConfig(*r)
rc.NameFQDN = dnsutil.AddOrigin(rc.Name, domainName)
dom.Records = append(dom.Records, &rc)
}
dom2, _ := dom.Copy()
// get corrections for first time
corrections, err := prv.GetDomainCorrections(dom)
if err != nil {
t.Fatal(err)
}
if i != 0 && len(corrections) == 0 {
t.Fatalf("Expect changes for all tests, but got none")
}
for _, c := range corrections {
err = c.F()
if err != nil {
t.Fatal(err)
}
}
//run a second time and expect zero corrections
corrections, err = prv.GetDomainCorrections(dom2)
if err != nil {
t.Fatal(err)
}
if len(corrections) != 0 {
t.Fatalf("Expected 0 corrections on second run, but found %d.", len(corrections))
}
})
}
}
func TestDualProviders(t *testing.T) {
p, domain := getProvider(t)
if p == nil {
return
}
dc := getDomainConfigWithNameservers(t, p, domain)
// clear everything
run := func() {
cs, err := p.GetDomainCorrections(dc)
if err != nil {
t.Fatal(err)
}
for i, c := range cs {
t.Logf("#%d: %s", i+1, c.Msg)
if err = c.F(); err != nil {
t.Fatal(err)
}
}
}
t.Log("Clearing everything")
run()
// add bogus nameservers
dc.Records = []*models.RecordConfig{}
dc.Nameservers = append(dc.Nameservers, models.StringsToNameservers([]string{"ns1.otherdomain.tld", "ns2.otherdomain.tld"})...)
nameservers.AddNSRecords(dc)
t.Log("Adding nameservers from another provider")
run()
// run again to make sure no corrections
t.Log("Running again to ensure stability")
cs, err := p.GetDomainCorrections(dc)
if err != nil {
t.Fatal(err)
}
if len(cs) != 0 {
t.Fatal("Expect no corrections on second run")
}
}
type TestCase struct {
Desc string
Records []*rec
}
type rec models.RecordConfig
func a(name, target string) *rec {
return makeRec(name, target, "A")
}
func cname(name, target string) *rec {
return makeRec(name, target, "CNAME")
}
func makeRec(name, target, typ string) *rec {
return &rec{
Name: name,
Type: typ,
Target: target,
TTL: 300,
}
}
func (r *rec) ttl(t uint32) *rec {
r.TTL = t
return r
}
func tc(desc string, recs ...*rec) *TestCase {
return &TestCase{
Desc: desc,
Records: recs,
}
}
var tests = []*TestCase{
// A
tc("Empty"),
tc("Create an A record", a("@", "1.1.1.1")),
tc("Change it", a("@", "1.2.3.4")),
tc("Add another", a("@", "1.2.3.4"), a("www", "1.2.3.4")),
tc("Add another(same name)", a("@", "1.2.3.4"), a("www", "1.2.3.4"), a("www", "5.6.7.8")),
tc("Change a ttl", a("@", "1.2.3.4").ttl(100), a("www", "1.2.3.4"), a("www", "5.6.7.8")),
tc("Change single target from set", a("@", "1.2.3.4").ttl(100), a("www", "2.2.2.2"), a("www", "5.6.7.8")),
tc("Change all ttls", a("@", "1.2.3.4").ttl(500), a("www", "2.2.2.2").ttl(400), a("www", "5.6.7.8").ttl(400)),
tc("Delete one", a("@", "1.2.3.4").ttl(500), a("www", "5.6.7.8").ttl(400)),
tc("Add back and change ttl", a("www", "5.6.7.8").ttl(700), a("www", "1.2.3.4").ttl(700)),
tc("Change targets and ttls", a("www", "1.1.1.1"), a("www", "2.2.2.2")),
// CNAMES
tc("Empty"),
tc("Create a CNAME", cname("foo", "google.com.")),
tc("Change it", cname("foo", "google2.com.")),
tc("Change to A record", a("foo", "1.2.3.4")),
tc("Change back to CNAME", cname("foo", "google.com.")),
//IDNAs
tc("Internationalized name", a("ööö", "1.2.3.4")),
tc("Change IDN", a("ööö", "2.2.2.2")),
tc("Internationalized CNAME Target", cname("a", "ööö.com.")),
tc("IDN CNAME AND Target", cname("öoö", "ööö.ööö.")),
}

View File

@ -0,0 +1,8 @@
{
"GCLOUD": {
"domain": "$GCLOUD_DOMAIN",
"project_id": "$GCLOUD_PROJECT",
"private_key": "$GCLOUD_PRIVATEKEY",
"client_email": "$GCLOUD_EMAIL",
}
}

16
integrationTest/readme.md Normal file
View File

@ -0,0 +1,16 @@
### Integration Tests
This is a simple framework for testing dns providers by making real requests.
There is a sequence of changes that are defined in the test file that are run against your chosen provider.
For each step, it will run the config once and expect changes. It will run it again and expect no changes. This should give us much higher confidence that providers will work in real life.
## Configuration
`providers.json` should have an object for each provider type under test. This is identical to the json expected in creds.json for dnscontrol, except it also has a "domain" field specified for the domain to test. The domain does not even need to be registered for most providers. Note that `providers.json` expects environment variables to be specified with the relevant info.
## Running a test
1. Define all environment variables expected for the provider you wish to run. I setup a local `.env` file with the appropriate values and use [zoo](https://github.com/jsonmaur/zoo) to run my commands.
2. run `go test -v -provider $NAME` where $NAME is the name of the provider you wish to run.

View File

@ -12,6 +12,7 @@ import (
"github.com/StackExchange/dnscontrol/transform" "github.com/StackExchange/dnscontrol/transform"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/idna"
) )
const DefaultTTL = uint32(300) const DefaultTTL = uint32(300)
@ -164,6 +165,32 @@ func (r *RecordConfig) Copy() (*RecordConfig, error) {
return newR, err return newR, err
} }
//Punycode will convert all records to punycode format.
//It will encode:
//- Name
//- NameFQDN
//- Target (CNAME and MX only)
func (dc *DomainConfig) Punycode() error {
var err error
for _, rec := range dc.Records {
rec.Name, err = idna.ToASCII(rec.Name)
if err != nil {
return err
}
rec.NameFQDN, err = idna.ToASCII(rec.NameFQDN)
if err != nil {
return err
}
if rec.Type == "MX" || rec.Type == "CNAME" {
rec.Target, err = idna.ToASCII(rec.Target)
if err != nil {
return err
}
}
}
return nil
}
func copyObj(input interface{}, output interface{}) error { func copyObj(input interface{}, output interface{}) error {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
enc := gob.NewEncoder(buf) enc := gob.NewEncoder(buf)

View File

@ -34,14 +34,11 @@ func LoadProviderConfigs(fname string) (map[string]map[string]string, error) {
} }
func replaceEnvVars(m map[string]map[string]string) error { func replaceEnvVars(m map[string]map[string]string) error {
for provider, keys := range m { for _, keys := range m {
for k, v := range keys { for k, v := range keys {
if strings.HasPrefix(v, "$") { if strings.HasPrefix(v, "$") {
env := v[1:] env := v[1:]
newVal := os.Getenv(env) newVal := os.Getenv(env)
if newVal == "" {
return fmt.Errorf("Provider %s references environment variable %s, but has no value.", provider, env)
}
keys[k] = newVal keys[k] = newVal
} }
} }

View File

@ -98,7 +98,9 @@ func keyForRec(r *models.RecordConfig) key {
} }
func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
if err := dc.Punycode(); err != nil {
return nil, err
}
rrs, zoneName, err := g.getRecords(dc.Name) rrs, zoneName, err := g.getRecords(dc.Name)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -59,7 +59,7 @@ func createRegistrar(rType string, config map[string]string) (Registrar, error)
return initer(config) return initer(config)
} }
func createDNSProvider(dType string, config map[string]string, meta json.RawMessage) (DNSServiceProvider, error) { func CreateDNSProvider(dType string, config map[string]string, meta json.RawMessage) (DNSServiceProvider, error) {
initer, ok := dspTypes[dType] initer, ok := dspTypes[dType]
if !ok { if !ok {
return nil, fmt.Errorf("DSP type %s not declared", dType) return nil, fmt.Errorf("DSP type %s not declared", dType)
@ -93,7 +93,7 @@ func CreateDsps(d *models.DNSConfig, providerConfigs map[string]map[string]strin
if !ok { if !ok {
return nil, fmt.Errorf("DNSServiceProvider %s not listed in -providers file", dsp.Name) return nil, fmt.Errorf("DNSServiceProvider %s not listed in -providers file", dsp.Name)
} }
provider, err := createDNSProvider(dsp.Type, rawMsg, dsp.Metadata) provider, err := CreateDNSProvider(dsp.Type, rawMsg, dsp.Metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }

68
vendor/golang.org/x/net/idna/idna.go generated vendored Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package idna implements IDNA2008 (Internationalized Domain Names for
// Applications), defined in RFC 5890, RFC 5891, RFC 5892, RFC 5893 and
// RFC 5894.
package idna // import "golang.org/x/net/idna"
import (
"strings"
"unicode/utf8"
)
// TODO(nigeltao): specify when errors occur. For example, is ToASCII(".") or
// ToASCII("foo\x00") an error? See also http://www.unicode.org/faq/idn.html#11
// acePrefix is the ASCII Compatible Encoding prefix.
const acePrefix = "xn--"
// ToASCII converts a domain or domain label to its ASCII form. For example,
// ToASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
// ToASCII("golang") is "golang".
func ToASCII(s string) (string, error) {
if ascii(s) {
return s, nil
}
labels := strings.Split(s, ".")
for i, label := range labels {
if !ascii(label) {
a, err := encode(acePrefix, label)
if err != nil {
return "", err
}
labels[i] = a
}
}
return strings.Join(labels, "."), nil
}
// ToUnicode converts a domain or domain label to its Unicode form. For example,
// ToUnicode("xn--bcher-kva.example.com") is "bücher.example.com", and
// ToUnicode("golang") is "golang".
func ToUnicode(s string) (string, error) {
if !strings.Contains(s, acePrefix) {
return s, nil
}
labels := strings.Split(s, ".")
for i, label := range labels {
if strings.HasPrefix(label, acePrefix) {
u, err := decode(label[len(acePrefix):])
if err != nil {
return "", err
}
labels[i] = u
}
}
return strings.Join(labels, "."), nil
}
func ascii(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}

200
vendor/golang.org/x/net/idna/punycode.go generated vendored Normal file
View File

@ -0,0 +1,200 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package idna
// This file implements the Punycode algorithm from RFC 3492.
import (
"fmt"
"math"
"strings"
"unicode/utf8"
)
// These parameter values are specified in section 5.
//
// All computation is done with int32s, so that overflow behavior is identical
// regardless of whether int is 32-bit or 64-bit.
const (
base int32 = 36
damp int32 = 700
initialBias int32 = 72
initialN int32 = 128
skew int32 = 38
tmax int32 = 26
tmin int32 = 1
)
// decode decodes a string as specified in section 6.2.
func decode(encoded string) (string, error) {
if encoded == "" {
return "", nil
}
pos := 1 + strings.LastIndex(encoded, "-")
if pos == 1 {
return "", fmt.Errorf("idna: invalid label %q", encoded)
}
if pos == len(encoded) {
return encoded[:len(encoded)-1], nil
}
output := make([]rune, 0, len(encoded))
if pos != 0 {
for _, r := range encoded[:pos-1] {
output = append(output, r)
}
}
i, n, bias := int32(0), initialN, initialBias
for pos < len(encoded) {
oldI, w := i, int32(1)
for k := base; ; k += base {
if pos == len(encoded) {
return "", fmt.Errorf("idna: invalid label %q", encoded)
}
digit, ok := decodeDigit(encoded[pos])
if !ok {
return "", fmt.Errorf("idna: invalid label %q", encoded)
}
pos++
i += digit * w
if i < 0 {
return "", fmt.Errorf("idna: invalid label %q", encoded)
}
t := k - bias
if t < tmin {
t = tmin
} else if t > tmax {
t = tmax
}
if digit < t {
break
}
w *= base - t
if w >= math.MaxInt32/base {
return "", fmt.Errorf("idna: invalid label %q", encoded)
}
}
x := int32(len(output) + 1)
bias = adapt(i-oldI, x, oldI == 0)
n += i / x
i %= x
if n > utf8.MaxRune || len(output) >= 1024 {
return "", fmt.Errorf("idna: invalid label %q", encoded)
}
output = append(output, 0)
copy(output[i+1:], output[i:])
output[i] = n
i++
}
return string(output), nil
}
// encode encodes a string as specified in section 6.3 and prepends prefix to
// the result.
//
// The "while h < length(input)" line in the specification becomes "for
// remaining != 0" in the Go code, because len(s) in Go is in bytes, not runes.
func encode(prefix, s string) (string, error) {
output := make([]byte, len(prefix), len(prefix)+1+2*len(s))
copy(output, prefix)
delta, n, bias := int32(0), initialN, initialBias
b, remaining := int32(0), int32(0)
for _, r := range s {
if r < 0x80 {
b++
output = append(output, byte(r))
} else {
remaining++
}
}
h := b
if b > 0 {
output = append(output, '-')
}
for remaining != 0 {
m := int32(0x7fffffff)
for _, r := range s {
if m > r && r >= n {
m = r
}
}
delta += (m - n) * (h + 1)
if delta < 0 {
return "", fmt.Errorf("idna: invalid label %q", s)
}
n = m
for _, r := range s {
if r < n {
delta++
if delta < 0 {
return "", fmt.Errorf("idna: invalid label %q", s)
}
continue
}
if r > n {
continue
}
q := delta
for k := base; ; k += base {
t := k - bias
if t < tmin {
t = tmin
} else if t > tmax {
t = tmax
}
if q < t {
break
}
output = append(output, encodeDigit(t+(q-t)%(base-t)))
q = (q - t) / (base - t)
}
output = append(output, encodeDigit(q))
bias = adapt(delta, h+1, h == b)
delta = 0
h++
remaining--
}
delta++
n++
}
return string(output), nil
}
func decodeDigit(x byte) (digit int32, ok bool) {
switch {
case '0' <= x && x <= '9':
return int32(x - ('0' - 26)), true
case 'A' <= x && x <= 'Z':
return int32(x - 'A'), true
case 'a' <= x && x <= 'z':
return int32(x - 'a'), true
}
return 0, false
}
func encodeDigit(digit int32) byte {
switch {
case 0 <= digit && digit < 26:
return byte(digit + 'a')
case 26 <= digit && digit < 36:
return byte(digit + ('0' - 26))
}
panic("idna: internal error in punycode encoding")
}
// adapt is the bias adaptation function specified in section 6.1.
func adapt(delta, numPoints int32, firstTime bool) int32 {
if firstTime {
delta /= damp
} else {
delta /= 2
}
delta += delta / numPoints
k := int32(0)
for delta > ((base-tmin)*tmax)/2 {
delta /= base - tmin
k += base
}
return k + (base-tmin+1)*delta/(delta+skew)
}

6
vendor/vendor.json vendored
View File

@ -338,6 +338,12 @@
"revision": "4971afdc2f162e82d185353533d3cf16188a9f4e", "revision": "4971afdc2f162e82d185353533d3cf16188a9f4e",
"revisionTime": "2016-11-15T21:05:04Z" "revisionTime": "2016-11-15T21:05:04Z"
}, },
{
"checksumSHA1": "GIGmSrYACByf5JDIP9ByBZksY80=",
"path": "golang.org/x/net/idna",
"revision": "a6577fac2d73be281a500b310739095313165611",
"revisionTime": "2017-03-08T20:54:49Z"
},
{ {
"checksumSHA1": "XH7CgbL5Z8COUc+MKrYqS3FFosY=", "checksumSHA1": "XH7CgbL5Z8COUc+MKrYqS3FFosY=",
"path": "golang.org/x/oauth2", "path": "golang.org/x/oauth2",