sigs.k8s.io/external-dns@v0.14.1/provider/rfc2136/rfc2136_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package rfc2136
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"fmt"
    23  	"os"
    24  	"regexp"
    25  	"sort"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/miekg/dns"
    31  	log "github.com/sirupsen/logrus"
    32  	"github.com/stretchr/testify/assert"
    33  
    34  	"sigs.k8s.io/external-dns/endpoint"
    35  	"sigs.k8s.io/external-dns/plan"
    36  	"sigs.k8s.io/external-dns/provider"
    37  )
    38  
    39  type rfc2136Stub struct {
    40  	output     []*dns.Envelope
    41  	updateMsgs []*dns.Msg
    42  	createMsgs []*dns.Msg
    43  }
    44  
    45  func newStub() *rfc2136Stub {
    46  	return &rfc2136Stub{
    47  		output:     make([]*dns.Envelope, 0),
    48  		updateMsgs: make([]*dns.Msg, 0),
    49  		createMsgs: make([]*dns.Msg, 0),
    50  	}
    51  }
    52  
    53  func getSortedChanges(msgs []*dns.Msg) []string {
    54  	r := []string{}
    55  	for _, d := range msgs {
    56  		// only care about section after the ZONE SECTION: as the id: needs stripped out in order to sort and grantee the order when sorting
    57  		r = append(r, strings.Split(d.String(), "ZONE SECTION:")[1])
    58  	}
    59  	sort.Strings(r)
    60  	return r
    61  }
    62  
    63  func (r *rfc2136Stub) SendMessage(msg *dns.Msg) error {
    64  	zone := extractZoneFromMessage(msg.String())
    65  	// Make sure the zone starts with . to make sure HasSuffix does not match forbar.com for zone bar.com
    66  	if !strings.HasPrefix(zone, ".") {
    67  		zone = "." + zone
    68  	}
    69  	log.Infof("zone=%s", zone)
    70  	lines := extractUpdateSectionFromMessage(msg)
    71  	for _, line := range lines {
    72  		// break at first empty line
    73  		if len(strings.TrimSpace(line)) == 0 {
    74  			break
    75  		}
    76  
    77  		line = strings.Replace(line, "\t", " ", -1)
    78  		log.Info(line)
    79  		record := strings.Split(line, " ")[0]
    80  		if !strings.HasSuffix(record, zone) {
    81  			err := fmt.Errorf("Message contains updates outside of it's zone.  zone=%v record=%v", zone, record)
    82  			log.Error(err)
    83  			return err
    84  		}
    85  
    86  		if strings.Contains(line, " NONE ") {
    87  			r.updateMsgs = append(r.updateMsgs, msg)
    88  		} else if strings.Contains(line, " IN ") {
    89  			r.createMsgs = append(r.createMsgs, msg)
    90  		}
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  func (r *rfc2136Stub) setOutput(output []string) error {
    97  	r.output = make([]*dns.Envelope, len(output))
    98  	for i, e := range output {
    99  		rr, err := dns.NewRR(e)
   100  		if err != nil {
   101  			return err
   102  		}
   103  		r.output[i] = &dns.Envelope{
   104  			RR: []dns.RR{rr},
   105  		}
   106  	}
   107  	return nil
   108  }
   109  
   110  func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error) {
   111  	outChan := make(chan *dns.Envelope)
   112  	go func() {
   113  		for _, e := range r.output {
   114  			outChan <- e
   115  		}
   116  		close(outChan)
   117  	}()
   118  
   119  	return outChan, nil
   120  }
   121  
   122  func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) {
   123  	tlsConfig := TLSConfig{
   124  		UseTLS:                false,
   125  		SkipTLSVerify:         false,
   126  		CAFilePath:            "",
   127  		ClientCertFilePath:    "",
   128  		ClientCertKeyFilePath: "",
   129  	}
   130  	return NewRfc2136Provider("", 0, nil, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub)
   131  }
   132  
   133  func createRfc2136TLSStubProvider(stub *rfc2136Stub, tlsConfig TLSConfig) (provider.Provider, error) {
   134  	return NewRfc2136Provider("rfc2136-host", 0, nil, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub)
   135  }
   136  
   137  func createRfc2136StubProviderWithZones(stub *rfc2136Stub) (provider.Provider, error) {
   138  	tlsConfig := TLSConfig{
   139  		UseTLS:                false,
   140  		SkipTLSVerify:         false,
   141  		CAFilePath:            "",
   142  		ClientCertFilePath:    "",
   143  		ClientCertKeyFilePath: "",
   144  	}
   145  	zones := []string{"foo.com", "foobar.com"}
   146  	return NewRfc2136Provider("", 0, zones, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub)
   147  }
   148  
   149  func createRfc2136StubProviderWithZonesFilters(stub *rfc2136Stub) (provider.Provider, error) {
   150  	tlsConfig := TLSConfig{
   151  		UseTLS:                false,
   152  		SkipTLSVerify:         false,
   153  		CAFilePath:            "",
   154  		ClientCertFilePath:    "",
   155  		ClientCertKeyFilePath: "",
   156  	}
   157  	zones := []string{"foo.com", "foobar.com"}
   158  	return NewRfc2136Provider("", 0, zones, false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{Filters: zones}, false, 300*time.Second, false, "", "", "", 50, tlsConfig, stub)
   159  }
   160  
   161  func extractUpdateSectionFromMessage(msg fmt.Stringer) []string {
   162  	const searchPattern = "UPDATE SECTION:"
   163  	updateSectionOffset := strings.Index(msg.String(), searchPattern)
   164  	return strings.Split(strings.TrimSpace(msg.String()[updateSectionOffset+len(searchPattern):]), "\n")
   165  }
   166  
   167  func extractZoneFromMessage(msg string) string {
   168  	re := regexp.MustCompile(`ZONE SECTION:\n;(?P<ZONE>[\.,\-,\w,\d]+)\t`)
   169  	matches := re.FindStringSubmatch(msg)
   170  	return matches[re.SubexpIndex("ZONE")]
   171  }
   172  
   173  // TestRfc2136GetRecordsMultipleTargets simulates a single record with multiple targets.
   174  func TestRfc2136GetRecordsMultipleTargets(t *testing.T) {
   175  	stub := newStub()
   176  	err := stub.setOutput([]string{
   177  		"foo.com 3600 IN A 1.1.1.1",
   178  		"foo.com 3600 IN A 2.2.2.2",
   179  	})
   180  	assert.NoError(t, err)
   181  
   182  	provider, err := createRfc2136StubProvider(stub)
   183  	assert.NoError(t, err)
   184  
   185  	recs, err := provider.Records(context.Background())
   186  	assert.NoError(t, err)
   187  
   188  	assert.Equal(t, 1, len(recs), "expected single record")
   189  	assert.Equal(t, recs[0].DNSName, "foo.com")
   190  	assert.Equal(t, 2, len(recs[0].Targets), "expected two targets")
   191  	assert.True(t, recs[0].Targets[0] == "1.1.1.1" || recs[0].Targets[1] == "1.1.1.1") // ignore order
   192  	assert.True(t, recs[0].Targets[0] == "2.2.2.2" || recs[0].Targets[1] == "2.2.2.2") // ignore order
   193  	assert.Equal(t, recs[0].RecordType, "A")
   194  	assert.Equal(t, recs[0].RecordTTL, endpoint.TTL(3600))
   195  	assert.Equal(t, 0, len(recs[0].Labels), "expected no labels")
   196  	assert.Equal(t, 0, len(recs[0].ProviderSpecific), "expected no provider specific config")
   197  }
   198  
   199  func TestRfc2136TLSConfig(t *testing.T) {
   200  	stub := newStub()
   201  
   202  	caFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX.crt")
   203  	assert.NoError(t, err)
   204  	defer os.Remove(caFile.Name())
   205  	_, err = caFile.Write([]byte(
   206  		`-----BEGIN CERTIFICATE-----
   207  MIH+MIGxAhR2n1aQk0ONrQ8QQfa6GCzFWLmTXTAFBgMrZXAwITELMAkGA1UEBhMC
   208  REUxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yMzEwMjQwNzI5NDNaGA8yMTIzMDkz
   209  MDA3Mjk0M1owITELMAkGA1UEBhMCREUxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUG
   210  AytlcAMhAA1FzGJXuQdOpKv02SEl7SIA8SP8RVRI0QTi1bUFiFBLMAUGAytlcANB
   211  ADiCKRUGDMyafSSYhl0KXoiXrFOxvhrGM5l15L4q82JM5Qb8wv0gNrnbGTZlInuv
   212  ouB5ZN+05DzKCQhBekMnygQ=
   213  -----END CERTIFICATE-----
   214  `))
   215  
   216  	tlsConfig := TLSConfig{
   217  		UseTLS:                true,
   218  		SkipTLSVerify:         false,
   219  		CAFilePath:            caFile.Name(),
   220  		ClientCertFilePath:    "",
   221  		ClientCertKeyFilePath: "",
   222  	}
   223  
   224  	provider, err := createRfc2136TLSStubProvider(stub, tlsConfig)
   225  	assert.NoError(t, err)
   226  
   227  	rawProvider := provider.(*rfc2136Provider)
   228  
   229  	client, err := makeClient(*rawProvider)
   230  	assert.NoError(t, err)
   231  
   232  	assert.Equal(t, "tcp-tls", client.Net)
   233  	assert.Equal(t, false, client.TLSConfig.InsecureSkipVerify)
   234  	assert.Equal(t, "rfc2136-host", client.TLSConfig.ServerName)
   235  	assert.Equal(t, uint16(tls.VersionTLS13), client.TLSConfig.MinVersion)
   236  	assert.Equal(t, []string{"dot"}, client.TLSConfig.NextProtos)
   237  }
   238  
   239  func TestRfc2136TLSConfigNoVerify(t *testing.T) {
   240  	stub := newStub()
   241  
   242  	caFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX.crt")
   243  	assert.NoError(t, err)
   244  	defer os.Remove(caFile.Name())
   245  	_, err = caFile.Write([]byte(
   246  		`-----BEGIN CERTIFICATE-----
   247  MIH+MIGxAhR2n1aQk0ONrQ8QQfa6GCzFWLmTXTAFBgMrZXAwITELMAkGA1UEBhMC
   248  REUxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yMzEwMjQwNzI5NDNaGA8yMTIzMDkz
   249  MDA3Mjk0M1owITELMAkGA1UEBhMCREUxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUG
   250  AytlcAMhAA1FzGJXuQdOpKv02SEl7SIA8SP8RVRI0QTi1bUFiFBLMAUGAytlcANB
   251  ADiCKRUGDMyafSSYhl0KXoiXrFOxvhrGM5l15L4q82JM5Qb8wv0gNrnbGTZlInuv
   252  ouB5ZN+05DzKCQhBekMnygQ=
   253  -----END CERTIFICATE-----
   254  `))
   255  
   256  	tlsConfig := TLSConfig{
   257  		UseTLS:                true,
   258  		SkipTLSVerify:         true,
   259  		CAFilePath:            caFile.Name(),
   260  		ClientCertFilePath:    "",
   261  		ClientCertKeyFilePath: "",
   262  	}
   263  
   264  	provider, err := createRfc2136TLSStubProvider(stub, tlsConfig)
   265  	assert.NoError(t, err)
   266  
   267  	rawProvider := provider.(*rfc2136Provider)
   268  
   269  	client, err := makeClient(*rawProvider)
   270  	assert.NoError(t, err)
   271  
   272  	assert.Equal(t, "tcp-tls", client.Net)
   273  	assert.Equal(t, true, client.TLSConfig.InsecureSkipVerify)
   274  	assert.Equal(t, "rfc2136-host", client.TLSConfig.ServerName)
   275  	assert.Equal(t, uint16(tls.VersionTLS13), client.TLSConfig.MinVersion)
   276  	assert.Equal(t, []string{"dot"}, client.TLSConfig.NextProtos)
   277  }
   278  
   279  func TestRfc2136TLSConfigClientAuth(t *testing.T) {
   280  	stub := newStub()
   281  
   282  	caFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX.crt")
   283  	assert.NoError(t, err)
   284  	defer os.Remove(caFile.Name())
   285  	_, err = caFile.Write([]byte(
   286  		`-----BEGIN CERTIFICATE-----
   287  MIH+MIGxAhR2n1aQk0ONrQ8QQfa6GCzFWLmTXTAFBgMrZXAwITELMAkGA1UEBhMC
   288  REUxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yMzEwMjQwNzI5NDNaGA8yMTIzMDkz
   289  MDA3Mjk0M1owITELMAkGA1UEBhMCREUxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUG
   290  AytlcAMhAA1FzGJXuQdOpKv02SEl7SIA8SP8RVRI0QTi1bUFiFBLMAUGAytlcANB
   291  ADiCKRUGDMyafSSYhl0KXoiXrFOxvhrGM5l15L4q82JM5Qb8wv0gNrnbGTZlInuv
   292  ouB5ZN+05DzKCQhBekMnygQ=
   293  -----END CERTIFICATE-----
   294  `))
   295  
   296  	certFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX-client.crt")
   297  	assert.NoError(t, err)
   298  	defer os.Remove(certFile.Name())
   299  	_, err = certFile.Write([]byte(
   300  		`-----BEGIN CERTIFICATE-----
   301  MIIBfDCCAQICFANNDjPVDMTPm63C0jZ9M3H5I7GJMAoGCCqGSM49BAMCMCExCzAJ
   302  BgNVBAYTAkRFMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjMxMDI0MDcyMTU1WhgP
   303  MjEyMzA5MzAwNzIxNTVaMCExCzAJBgNVBAYTAkRFMRIwEAYDVQQDDAlsb2NhbGhv
   304  c3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQj7rjkeUEvjBT++IBMnIWgmI9VIjFx
   305  4VUGFmzPEawOckdnKW4fBdePiItsgePDVK4Oys5bzfSDhl6aAPCe16pwvljB7yIm
   306  xLJ+ytWk7OV/s10cmlaczrEtNeUjV1X9MTMwCgYIKoZIzj0EAwIDaAAwZQIwcZl8
   307  TrwwsyX3A0enXB1ih+nruF8Q9f9Rmm2pNcbEv24QIW/P2HGQm9qfx4lrYa7hAjEA
   308  goRP/fRfTTTLwLg8UBpUAmALX8A8HBSBaUlTTQcaImbcwU4DRSbv5JEA8tM1mWrA
   309  -----END CERTIFICATE-----
   310  `))
   311  
   312  	keyFile, err := os.CreateTemp("", "rfc2136-test-XXXXXXXX-client.key")
   313  	assert.NoError(t, err)
   314  	defer os.Remove(keyFile.Name())
   315  	_, err = keyFile.Write([]byte(
   316  		`-----BEGIN PRIVATE KEY-----
   317  MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD5B+aPE+TuHCvW1f7L
   318  U8jEPVXHv1fvCR8uBSsf1qdPo929XGpt5y5QfIGdW3NUeHWhZANiAAQj7rjkeUEv
   319  jBT++IBMnIWgmI9VIjFx4VUGFmzPEawOckdnKW4fBdePiItsgePDVK4Oys5bzfSD
   320  hl6aAPCe16pwvljB7yImxLJ+ytWk7OV/s10cmlaczrEtNeUjV1X9MTM=
   321  -----END PRIVATE KEY-----
   322  `))
   323  
   324  	tlsConfig := TLSConfig{
   325  		UseTLS:                true,
   326  		SkipTLSVerify:         false,
   327  		CAFilePath:            caFile.Name(),
   328  		ClientCertFilePath:    certFile.Name(),
   329  		ClientCertKeyFilePath: keyFile.Name(),
   330  	}
   331  
   332  	provider, err := createRfc2136TLSStubProvider(stub, tlsConfig)
   333  	log.Infof("provider, err is: %s", err)
   334  	assert.NoError(t, err)
   335  
   336  	rawProvider := provider.(*rfc2136Provider)
   337  
   338  	client, err := makeClient(*rawProvider)
   339  	log.Infof("client, err is: %v", client)
   340  	log.Infof("client, err is: %s", err)
   341  	assert.NoError(t, err)
   342  
   343  	assert.Equal(t, "tcp-tls", client.Net)
   344  	assert.Equal(t, false, client.TLSConfig.InsecureSkipVerify)
   345  	assert.Equal(t, "rfc2136-host", client.TLSConfig.ServerName)
   346  	assert.Equal(t, uint16(tls.VersionTLS13), client.TLSConfig.MinVersion)
   347  	assert.Equal(t, []string{"dot"}, client.TLSConfig.NextProtos)
   348  }
   349  
   350  func TestRfc2136GetRecords(t *testing.T) {
   351  	stub := newStub()
   352  	err := stub.setOutput([]string{
   353  		"v4.barfoo.com 3600 TXT test1",
   354  		"v1.foo.com 3600 TXT test2",
   355  		"v2.bar.com 3600 A 8.8.8.8",
   356  		"v3.bar.com 3600 TXT bbbb",
   357  		"v2.foo.com 3600 CNAME cccc",
   358  		"v1.foobar.com 3600 TXT dddd",
   359  	})
   360  	assert.NoError(t, err)
   361  
   362  	provider, err := createRfc2136StubProvider(stub)
   363  	assert.NoError(t, err)
   364  
   365  	recs, err := provider.Records(context.Background())
   366  	assert.NoError(t, err)
   367  
   368  	assert.Equal(t, 6, len(recs))
   369  	assert.True(t, contains(recs, "v1.foo.com"))
   370  	assert.True(t, contains(recs, "v2.bar.com"))
   371  	assert.True(t, contains(recs, "v2.foo.com"))
   372  }
   373  
   374  // Make sure the test version of SendMessage raises an error
   375  // if a zone update ever contains records outside of it's zone
   376  // as the TestRfc2136ApplyChanges tests all assume this
   377  func TestRfc2136SendMessage(t *testing.T) {
   378  	stub := newStub()
   379  
   380  	m := new(dns.Msg)
   381  	m.SetUpdate("foo.com.")
   382  	rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s", "v1.foo.com.", 0, "A", "1.2.3.4"))
   383  	m.Insert([]dns.RR{rr})
   384  
   385  	err = stub.SendMessage(m)
   386  	assert.NoError(t, err)
   387  
   388  	rr, err = dns.NewRR(fmt.Sprintf("%s %d %s %s", "v1.bar.com.", 0, "A", "1.2.3.4"))
   389  	m.Insert([]dns.RR{rr})
   390  
   391  	err = stub.SendMessage(m)
   392  	assert.Error(t, err)
   393  
   394  	m.SetUpdate(".")
   395  	err = stub.SendMessage(m)
   396  	assert.NoError(t, err)
   397  }
   398  
   399  // These tests are use the . root zone with no filters
   400  func TestRfc2136ApplyChanges(t *testing.T) {
   401  	stub := newStub()
   402  	provider, err := createRfc2136StubProvider(stub)
   403  	assert.NoError(t, err)
   404  
   405  	p := &plan.Changes{
   406  		Create: []*endpoint.Endpoint{
   407  			{
   408  				DNSName:    "v1.foo.com",
   409  				RecordType: "A",
   410  				Targets:    []string{"1.2.3.4"},
   411  				RecordTTL:  endpoint.TTL(400),
   412  			},
   413  			{
   414  				DNSName:    "v1.foobar.com",
   415  				RecordType: "TXT",
   416  				Targets:    []string{"boom"},
   417  			},
   418  			{
   419  				DNSName:    "ns.foobar.com",
   420  				RecordType: "NS",
   421  				Targets:    []string{"boom"},
   422  			},
   423  		},
   424  		Delete: []*endpoint.Endpoint{
   425  			{
   426  				DNSName:    "v2.foo.com",
   427  				RecordType: "A",
   428  				Targets:    []string{"1.2.3.4"},
   429  			},
   430  			{
   431  				DNSName:    "v2.foobar.com",
   432  				RecordType: "TXT",
   433  				Targets:    []string{"boom2"},
   434  			},
   435  		},
   436  	}
   437  
   438  	err = provider.ApplyChanges(context.Background(), p)
   439  	assert.NoError(t, err)
   440  
   441  	assert.Equal(t, 3, len(stub.createMsgs))
   442  	assert.True(t, strings.Contains(stub.createMsgs[0].String(), "v1.foo.com"))
   443  	assert.True(t, strings.Contains(stub.createMsgs[0].String(), "1.2.3.4"))
   444  
   445  	assert.True(t, strings.Contains(stub.createMsgs[1].String(), "v1.foobar.com"))
   446  	assert.True(t, strings.Contains(stub.createMsgs[1].String(), "boom"))
   447  
   448  	assert.True(t, strings.Contains(stub.createMsgs[2].String(), "ns.foobar.com"))
   449  	assert.True(t, strings.Contains(stub.createMsgs[2].String(), "boom"))
   450  
   451  	assert.Equal(t, 2, len(stub.updateMsgs))
   452  	assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "v2.foo.com"))
   453  	assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "v2.foobar.com"))
   454  }
   455  
   456  // These tests all use the foo.com and foobar.com zones with no filters
   457  // createMsgs and updateMsgs need sorted when are are used
   458  func TestRfc2136ApplyChangesWithZones(t *testing.T) {
   459  	stub := newStub()
   460  	provider, err := createRfc2136StubProviderWithZones(stub)
   461  	assert.NoError(t, err)
   462  
   463  	p := &plan.Changes{
   464  		Create: []*endpoint.Endpoint{
   465  			{
   466  				DNSName:    "v1.foo.com",
   467  				RecordType: "A",
   468  				Targets:    []string{"1.2.3.4"},
   469  				RecordTTL:  endpoint.TTL(400),
   470  			},
   471  			{
   472  				DNSName:    "v1.foobar.com",
   473  				RecordType: "TXT",
   474  				Targets:    []string{"boom"},
   475  			},
   476  			{
   477  				DNSName:    "ns.foobar.com",
   478  				RecordType: "NS",
   479  				Targets:    []string{"boom"},
   480  			},
   481  		},
   482  		Delete: []*endpoint.Endpoint{
   483  			{
   484  				DNSName:    "v2.foo.com",
   485  				RecordType: "A",
   486  				Targets:    []string{"1.2.3.4"},
   487  			},
   488  			{
   489  				DNSName:    "v2.foobar.com",
   490  				RecordType: "TXT",
   491  				Targets:    []string{"boom2"},
   492  			},
   493  		},
   494  	}
   495  
   496  	err = provider.ApplyChanges(context.Background(), p)
   497  	assert.NoError(t, err)
   498  
   499  	assert.Equal(t, 3, len(stub.createMsgs))
   500  	createMsgs := getSortedChanges(stub.createMsgs)
   501  	assert.Equal(t, 3, len(createMsgs))
   502  
   503  	assert.True(t, strings.Contains(createMsgs[0], "v1.foo.com"))
   504  	assert.True(t, strings.Contains(createMsgs[0], "1.2.3.4"))
   505  
   506  	assert.True(t, strings.Contains(createMsgs[1], "v1.foobar.com"))
   507  	assert.True(t, strings.Contains(createMsgs[1], "boom"))
   508  
   509  	assert.True(t, strings.Contains(createMsgs[2], "ns.foobar.com"))
   510  	assert.True(t, strings.Contains(createMsgs[2], "boom"))
   511  
   512  	assert.Equal(t, 2, len(stub.updateMsgs))
   513  	updateMsgs := getSortedChanges(stub.updateMsgs)
   514  	assert.Equal(t, 2, len(updateMsgs))
   515  
   516  	assert.True(t, strings.Contains(updateMsgs[0], "v2.foo.com"))
   517  	assert.True(t, strings.Contains(updateMsgs[1], "v2.foobar.com"))
   518  }
   519  
   520  // These tests use the foo.com and foobar.com zones and with filters set to both zones
   521  // createMsgs and updateMsgs need sorted when are are used
   522  func TestRfc2136ApplyChangesWithZonesFilters(t *testing.T) {
   523  	stub := newStub()
   524  	provider, err := createRfc2136StubProviderWithZonesFilters(stub)
   525  	assert.NoError(t, err)
   526  
   527  	p := &plan.Changes{
   528  		Create: []*endpoint.Endpoint{
   529  			{
   530  				DNSName:    "v1.foo.com",
   531  				RecordType: "A",
   532  				Targets:    []string{"1.2.3.4"},
   533  				RecordTTL:  endpoint.TTL(400),
   534  			},
   535  			{
   536  				DNSName:    "v1.foobar.com",
   537  				RecordType: "TXT",
   538  				Targets:    []string{"boom"},
   539  			},
   540  			{
   541  				DNSName:    "ns.foobar.com",
   542  				RecordType: "NS",
   543  				Targets:    []string{"boom"},
   544  			},
   545  			{
   546  				DNSName:    "filtered-out.foo.bar",
   547  				RecordType: "A",
   548  				Targets:    []string{"1.2.3.4"},
   549  				RecordTTL:  endpoint.TTL(400),
   550  			},
   551  		},
   552  		Delete: []*endpoint.Endpoint{
   553  			{
   554  				DNSName:    "v2.foo.com",
   555  				RecordType: "A",
   556  				Targets:    []string{"1.2.3.4"},
   557  			},
   558  			{
   559  				DNSName:    "v2.foobar.com",
   560  				RecordType: "TXT",
   561  				Targets:    []string{"boom2"},
   562  			},
   563  		},
   564  	}
   565  
   566  	err = provider.ApplyChanges(context.Background(), p)
   567  	assert.NoError(t, err)
   568  
   569  	assert.Equal(t, 3, len(stub.createMsgs))
   570  	createMsgs := getSortedChanges(stub.createMsgs)
   571  	assert.Equal(t, 3, len(createMsgs))
   572  
   573  	assert.True(t, strings.Contains(createMsgs[0], "v1.foo.com"))
   574  	assert.True(t, strings.Contains(createMsgs[0], "1.2.3.4"))
   575  
   576  	assert.True(t, strings.Contains(createMsgs[1], "v1.foobar.com"))
   577  	assert.True(t, strings.Contains(createMsgs[1], "boom"))
   578  
   579  	assert.True(t, strings.Contains(createMsgs[2], "ns.foobar.com"))
   580  	assert.True(t, strings.Contains(createMsgs[2], "boom"))
   581  
   582  	for _, s := range createMsgs {
   583  		assert.False(t, strings.Contains(s, "filtered-out.foo.bar"))
   584  	}
   585  
   586  	assert.Equal(t, 2, len(stub.updateMsgs))
   587  	updateMsgs := getSortedChanges(stub.updateMsgs)
   588  	assert.Equal(t, 2, len(updateMsgs))
   589  
   590  	assert.True(t, strings.Contains(updateMsgs[0], "v2.foo.com"))
   591  	assert.True(t, strings.Contains(updateMsgs[1], "v2.foobar.com"))
   592  
   593  }
   594  
   595  func TestRfc2136ApplyChangesWithDifferentTTLs(t *testing.T) {
   596  	stub := newStub()
   597  
   598  	provider, err := createRfc2136StubProvider(stub)
   599  	assert.NoError(t, err)
   600  
   601  	p := &plan.Changes{
   602  		Create: []*endpoint.Endpoint{
   603  			{
   604  				DNSName:    "v1.foo.com",
   605  				RecordType: "A",
   606  				Targets:    []string{"2.1.1.1"},
   607  				RecordTTL:  endpoint.TTL(400),
   608  			},
   609  			{
   610  				DNSName:    "v2.foo.com",
   611  				RecordType: "A",
   612  				Targets:    []string{"3.2.2.2"},
   613  				RecordTTL:  endpoint.TTL(200),
   614  			},
   615  			{
   616  				DNSName:    "v3.foo.com",
   617  				RecordType: "A",
   618  				Targets:    []string{"4.3.3.3"},
   619  			},
   620  		},
   621  	}
   622  
   623  	err = provider.ApplyChanges(context.Background(), p)
   624  	assert.NoError(t, err)
   625  
   626  	createRecords := extractUpdateSectionFromMessage(stub.createMsgs[0])
   627  	assert.Equal(t, 3, len(createRecords))
   628  	assert.True(t, strings.Contains(createRecords[0], "v1.foo.com"))
   629  	assert.True(t, strings.Contains(createRecords[0], "2.1.1.1"))
   630  	assert.True(t, strings.Contains(createRecords[0], "400"))
   631  	assert.True(t, strings.Contains(createRecords[1], "v2.foo.com"))
   632  	assert.True(t, strings.Contains(createRecords[1], "3.2.2.2"))
   633  	assert.True(t, strings.Contains(createRecords[1], "300"))
   634  	assert.True(t, strings.Contains(createRecords[2], "v3.foo.com"))
   635  	assert.True(t, strings.Contains(createRecords[2], "4.3.3.3"))
   636  	assert.True(t, strings.Contains(createRecords[2], "300"))
   637  }
   638  
   639  func TestRfc2136ApplyChangesWithUpdate(t *testing.T) {
   640  	stub := newStub()
   641  
   642  	provider, err := createRfc2136StubProvider(stub)
   643  	assert.NoError(t, err)
   644  
   645  	p := &plan.Changes{
   646  		Create: []*endpoint.Endpoint{
   647  			{
   648  				DNSName:    "v1.foo.com",
   649  				RecordType: "A",
   650  				Targets:    []string{"1.2.3.4"},
   651  				RecordTTL:  endpoint.TTL(400),
   652  			},
   653  			{
   654  				DNSName:    "v1.foobar.com",
   655  				RecordType: "TXT",
   656  				Targets:    []string{"boom"},
   657  			},
   658  		},
   659  	}
   660  
   661  	err = provider.ApplyChanges(context.Background(), p)
   662  	assert.NoError(t, err)
   663  
   664  	p = &plan.Changes{
   665  		UpdateOld: []*endpoint.Endpoint{
   666  			{
   667  				DNSName:    "v1.foo.com",
   668  				RecordType: "A",
   669  				Targets:    []string{"1.2.3.4"},
   670  				RecordTTL:  endpoint.TTL(400),
   671  			},
   672  			{
   673  				DNSName:    "v1.foobar.com",
   674  				RecordType: "TXT",
   675  				Targets:    []string{"boom"},
   676  			},
   677  		},
   678  		UpdateNew: []*endpoint.Endpoint{
   679  			{
   680  				DNSName:    "v1.foo.com",
   681  				RecordType: "A",
   682  				Targets:    []string{"1.2.3.5"},
   683  				RecordTTL:  endpoint.TTL(400),
   684  			},
   685  			{
   686  				DNSName:    "v1.foobar.com",
   687  				RecordType: "TXT",
   688  				Targets:    []string{"kablui"},
   689  			},
   690  		},
   691  	}
   692  
   693  	err = provider.ApplyChanges(context.Background(), p)
   694  	assert.NoError(t, err)
   695  
   696  	assert.Equal(t, 4, len(stub.createMsgs))
   697  	assert.Equal(t, 2, len(stub.updateMsgs))
   698  
   699  	assert.True(t, strings.Contains(stub.createMsgs[0].String(), "v1.foo.com"))
   700  	assert.True(t, strings.Contains(stub.createMsgs[0].String(), "1.2.3.4"))
   701  	assert.True(t, strings.Contains(stub.createMsgs[2].String(), "v1.foo.com"))
   702  	assert.True(t, strings.Contains(stub.createMsgs[2].String(), "1.2.3.5"))
   703  
   704  	assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "v1.foo.com"))
   705  	assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "1.2.3.4"))
   706  
   707  	assert.True(t, strings.Contains(stub.createMsgs[1].String(), "v1.foobar.com"))
   708  	assert.True(t, strings.Contains(stub.createMsgs[1].String(), "boom"))
   709  	assert.True(t, strings.Contains(stub.createMsgs[3].String(), "v1.foobar.com"))
   710  	assert.True(t, strings.Contains(stub.createMsgs[3].String(), "kablui"))
   711  
   712  	assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "v1.foobar.com"))
   713  	assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "boom"))
   714  }
   715  
   716  func TestChunkBy(t *testing.T) {
   717  	var records []*endpoint.Endpoint
   718  
   719  	for i := 0; i < 10; i++ {
   720  		records = append(records, &endpoint.Endpoint{
   721  			DNSName:    "v1.foo.com",
   722  			RecordType: "A",
   723  			Targets:    []string{"1.1.2.2"},
   724  			RecordTTL:  endpoint.TTL(400),
   725  		})
   726  	}
   727  
   728  	chunks := chunkBy(records, 2)
   729  	if len(chunks) != 5 {
   730  		t.Errorf("incorrect number of chunks returned")
   731  	}
   732  }
   733  
   734  func contains(arr []*endpoint.Endpoint, name string) bool {
   735  	for _, a := range arr {
   736  		if a.DNSName == name {
   737  			return true
   738  		}
   739  	}
   740  	return false
   741  }