github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/pkg/prettyzone/prettyzone_test.go (about)

     1  package prettyzone
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"math/rand"
     8  	"testing"
     9  
    10  	"github.com/StackExchange/dnscontrol/v2/models"
    11  	"github.com/miekg/dns"
    12  	"github.com/miekg/dns/dnsutil"
    13  )
    14  
    15  func parseAndRegen(t *testing.T, buf *bytes.Buffer, expected string) {
    16  	// Take a zonefile, parse it, then generate a zone. We should
    17  	// get back the same string.
    18  	// This is used after any WriteZoneFile test as an extra verification step.
    19  
    20  	// Parse the output:
    21  	var parsed []dns.RR
    22  	for x := range dns.ParseZone(buf, "bosun.org", "bosun.org.zone") {
    23  		if x.Error != nil {
    24  			log.Fatalf("Error in zonefile: %v", x.Error)
    25  		} else {
    26  			parsed = append(parsed, x.RR)
    27  		}
    28  	}
    29  	// Generate it back:
    30  	buf2 := &bytes.Buffer{}
    31  	WriteZoneFileRR(buf2, parsed, "bosun.org")
    32  
    33  	// Compare:
    34  	if buf2.String() != expected {
    35  		t.Fatalf("Regenerated zonefile does not match: got=(\n%v\n)\nexpected=(\n%v\n)\n", buf2.String(), expected)
    36  	}
    37  }
    38  
    39  func TestMostCommonTtl(t *testing.T) {
    40  	var records []dns.RR
    41  	var g, e uint32
    42  	r1, _ := dns.NewRR("bosun.org. 100 IN A 1.1.1.1")
    43  	r2, _ := dns.NewRR("bosun.org. 200 IN A 1.1.1.1")
    44  	r3, _ := dns.NewRR("bosun.org. 300 IN A 1.1.1.1")
    45  	r4, _ := dns.NewRR("bosun.org. 400 IN NS foo.bosun.org.")
    46  	r5, _ := dns.NewRR("bosun.org. 400 IN NS bar.bosun.org.")
    47  
    48  	// All records are TTL=100
    49  	records = nil
    50  	records, e = append(records, r1, r1, r1), 100
    51  	x := models.RRstoRCs(records, "bosun.org")
    52  	g = mostCommonTTL(x)
    53  	if e != g {
    54  		t.Fatalf("expected %d; got %d\n", e, g)
    55  	}
    56  
    57  	// Mixture of TTLs with an obvious winner.
    58  	records = nil
    59  	records, e = append(records, r1, r2, r2), 200
    60  	g = mostCommonTTL(models.RRstoRCs(records, "bosun.org"))
    61  	if e != g {
    62  		t.Fatalf("expected %d; got %d\n", e, g)
    63  	}
    64  
    65  	// 3-way tie. Largest TTL should be used.
    66  	records = nil
    67  	records, e = append(records, r1, r2, r3), 300
    68  	g = mostCommonTTL(models.RRstoRCs(records, "bosun.org"))
    69  	if e != g {
    70  		t.Fatalf("expected %d; got %d\n", e, g)
    71  	}
    72  
    73  	// NS records are ignored.
    74  	records = nil
    75  	records, e = append(records, r1, r4, r5), 100
    76  	g = mostCommonTTL(models.RRstoRCs(records, "bosun.org"))
    77  	if e != g {
    78  		t.Fatalf("expected %d; got %d\n", e, g)
    79  	}
    80  
    81  }
    82  
    83  // func WriteZoneFile
    84  
    85  func TestWriteZoneFileSimple(t *testing.T) {
    86  	r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
    87  	r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
    88  	r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
    89  	buf := &bytes.Buffer{}
    90  	WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
    91  	expected := `$TTL 300
    92  @                IN A     192.30.252.153
    93                   IN A     192.30.252.154
    94  www              IN CNAME bosun.org.
    95  `
    96  	if buf.String() != expected {
    97  		t.Log(buf.String())
    98  		t.Log(expected)
    99  		t.Fatalf("Zone file does not match.")
   100  	}
   101  
   102  	parseAndRegen(t, buf, expected)
   103  }
   104  
   105  func TestWriteZoneFileSimpleTtl(t *testing.T) {
   106  	r1, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.153")
   107  	r2, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.154")
   108  	r3, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.155")
   109  	r4, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
   110  	buf := &bytes.Buffer{}
   111  	WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org")
   112  	expected := `$TTL 100
   113  @                IN A     192.30.252.153
   114                   IN A     192.30.252.154
   115                   IN A     192.30.252.155
   116  www        300   IN CNAME bosun.org.
   117  `
   118  	if buf.String() != expected {
   119  		t.Log(buf.String())
   120  		t.Log(expected)
   121  		t.Fatalf("Zone file does not match")
   122  	}
   123  
   124  	parseAndRegen(t, buf, expected)
   125  }
   126  
   127  func TestWriteZoneFileMx(t *testing.T) {
   128  	// sort by priority
   129  	r1, _ := dns.NewRR("aaa.bosun.org. IN MX 1 aaa.example.com.")
   130  	r2, _ := dns.NewRR("aaa.bosun.org. IN MX 5 aaa.example.com.")
   131  	r3, _ := dns.NewRR("aaa.bosun.org. IN MX 10 aaa.example.com.")
   132  	// same priority? sort by name
   133  	r4, _ := dns.NewRR("bbb.bosun.org. IN MX 10 ccc.example.com.")
   134  	r5, _ := dns.NewRR("bbb.bosun.org. IN MX 10 bbb.example.com.")
   135  	r6, _ := dns.NewRR("bbb.bosun.org. IN MX 10 aaa.example.com.")
   136  	// a mix
   137  	r7, _ := dns.NewRR("ccc.bosun.org. IN MX 40 zzz.example.com.")
   138  	r8, _ := dns.NewRR("ccc.bosun.org. IN MX 40 aaa.example.com.")
   139  	r9, _ := dns.NewRR("ccc.bosun.org. IN MX 1 ttt.example.com.")
   140  	buf := &bytes.Buffer{}
   141  	WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org")
   142  	if buf.String() != testdataZFMX {
   143  		t.Log(buf.String())
   144  		t.Log(testdataZFMX)
   145  		t.Fatalf("Zone file does not match.")
   146  	}
   147  	parseAndRegen(t, buf, testdataZFMX)
   148  }
   149  
   150  var testdataZFMX = `$TTL 3600
   151  aaa              IN MX    1 aaa.example.com.
   152                   IN MX    5 aaa.example.com.
   153                   IN MX    10 aaa.example.com.
   154  bbb              IN MX    10 aaa.example.com.
   155                   IN MX    10 bbb.example.com.
   156                   IN MX    10 ccc.example.com.
   157  ccc              IN MX    1 ttt.example.com.
   158                   IN MX    40 aaa.example.com.
   159                   IN MX    40 zzz.example.com.
   160  `
   161  
   162  func TestWriteZoneFileSrv(t *testing.T) {
   163  	// exhibits explicit ttls and long name
   164  	r1, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 9999 foo.com.`)
   165  	r2, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 20 5050 foo.com.`)
   166  	r3, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
   167  	r4, _ := dns.NewRR(`bosun.org. 300 IN SRV 20 10 5050 foo.com.`)
   168  	r5, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
   169  	buf := &bytes.Buffer{}
   170  	WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org")
   171  	if buf.String() != testdataZFSRV {
   172  		t.Log(buf.String())
   173  		t.Log(testdataZFSRV)
   174  		t.Fatalf("Zone file does not match.")
   175  	}
   176  	parseAndRegen(t, buf, testdataZFSRV)
   177  }
   178  
   179  var testdataZFSRV = `$TTL 300
   180  @                IN SRV   10 10 5050 foo.com.
   181                   IN SRV   10 10 5050 foo.com.
   182                   IN SRV   10 20 5050 foo.com.
   183                   IN SRV   20 10 5050 foo.com.
   184                   IN SRV   10 10 9999 foo.com.
   185  `
   186  
   187  func TestWriteZoneFilePtr(t *testing.T) {
   188  	// exhibits explicit ttls and long name
   189  	r1, _ := dns.NewRR(`bosun.org. 300 IN PTR chell.bosun.org`)
   190  	r2, _ := dns.NewRR(`bosun.org. 300 IN PTR barney.bosun.org.`)
   191  	r3, _ := dns.NewRR(`bosun.org. 300 IN PTR alex.bosun.org.`)
   192  	buf := &bytes.Buffer{}
   193  	WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org")
   194  	if buf.String() != testdataZFPTR {
   195  		t.Log(buf.String())
   196  		t.Log(testdataZFPTR)
   197  		t.Fatalf("Zone file does not match.")
   198  	}
   199  	parseAndRegen(t, buf, testdataZFPTR)
   200  }
   201  
   202  var testdataZFPTR = `$TTL 300
   203  @                IN PTR   alex.bosun.org.
   204                   IN PTR   barney.bosun.org.
   205                   IN PTR   chell.bosun.org.
   206  `
   207  
   208  func TestWriteZoneFileCaa(t *testing.T) {
   209  	// exhibits explicit ttls and long name
   210  	r1, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issuewild ";"`)
   211  	r2, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issue "letsencrypt.org"`)
   212  	r3, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "http://example.com"`)
   213  	r4, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.com"`)
   214  	r5, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.net"`)
   215  	r6, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "mailto:example.com"`)
   216  	buf := &bytes.Buffer{}
   217  	WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org")
   218  	if buf.String() != testdataZFCAA {
   219  		t.Log(buf.String())
   220  		t.Log(testdataZFCAA)
   221  		t.Fatalf("Zone file does not match.")
   222  	}
   223  	parseAndRegen(t, buf, testdataZFCAA)
   224  }
   225  
   226  var testdataZFCAA = `$TTL 300
   227  @                IN CAA   1 iodef "http://example.com"
   228                   IN CAA   1 iodef "mailto:example.com"
   229                   IN CAA   0 iodef "https://example.com"
   230                   IN CAA   0 iodef "https://example.net"
   231                   IN CAA   0 issue "letsencrypt.org"
   232                   IN CAA   0 issuewild ";"
   233  `
   234  
   235  // Test 1 of each record type
   236  
   237  func mustNewRR(s string) dns.RR {
   238  	r, err := dns.NewRR(s)
   239  	if err != nil {
   240  		panic(err)
   241  	}
   242  	return r
   243  }
   244  
   245  func TestWriteZoneFileEach(t *testing.T) {
   246  	// Each rtype should be listed in this test exactly once.
   247  	// If an rtype has more than one variations, add a test like TestWriteZoneFileCaa to test each.
   248  	var d []dns.RR
   249  	// #rtype_variations
   250  	d = append(d, mustNewRR(`4.5                  300 IN PTR   y.bosun.org.`)) // Wouldn't actually be in this domain.
   251  	d = append(d, mustNewRR(`bosun.org.           300 IN A     1.2.3.4`))
   252  	d = append(d, mustNewRR(`bosun.org.           300 IN MX    1 bosun.org.`))
   253  	d = append(d, mustNewRR(`bosun.org.           300 IN TXT   "my text"`))
   254  	d = append(d, mustNewRR(`bosun.org.           300 IN AAAA  4500:fe::1`))
   255  	d = append(d, mustNewRR(`bosun.org.           300 IN SRV   10 10 9999 foo.com.`))
   256  	d = append(d, mustNewRR(`bosun.org.           300 IN CAA   0 issue "letsencrypt.org"`))
   257  	d = append(d, mustNewRR(`_443._tcp.bosun.org. 300 IN TLSA  3 1 1 abcdef0`)) // Label must be _port._proto
   258  	d = append(d, mustNewRR(`sub.bosun.org.       300 IN NS    bosun.org.`))    // Must be a label with no other records.
   259  	d = append(d, mustNewRR(`x.bosun.org.         300 IN CNAME bosun.org.`))    // Must be a label with no other records.
   260  	buf := &bytes.Buffer{}
   261  	WriteZoneFileRR(buf, d, "bosun.org")
   262  	if buf.String() != testdataZFEach {
   263  		t.Log(buf.String())
   264  		t.Log(testdataZFEach)
   265  		t.Fatalf("Zone file does not match.")
   266  	}
   267  	parseAndRegen(t, buf, testdataZFEach)
   268  }
   269  
   270  var testdataZFEach = `$TTL 300
   271  @                IN A     1.2.3.4
   272                   IN AAAA  4500:fe::1
   273                   IN MX    1 bosun.org.
   274                   IN SRV   10 10 9999 foo.com.
   275                   IN TXT   "my text"
   276                   IN CAA   0 issue "letsencrypt.org"
   277  4.5              IN PTR   y.bosun.org.
   278  _443._tcp        IN TLSA  3 1 1 abcdef0
   279  sub              IN NS    bosun.org.
   280  x                IN CNAME bosun.org.
   281  `
   282  
   283  func TestWriteZoneFileSynth(t *testing.T) {
   284  	r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
   285  	r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
   286  	r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
   287  	rsynm := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
   288  	rsynm.SetLabel("myalias", "bosun.org")
   289  	rsynz := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
   290  	rsynz.SetLabel("zalias", "bosun.org")
   291  
   292  	recs := models.RRstoRCs([]dns.RR{r1, r2, r3}, "bosun.org")
   293  	recs = append(recs, rsynm)
   294  	recs = append(recs, rsynm)
   295  	recs = append(recs, rsynz)
   296  
   297  	buf := &bytes.Buffer{}
   298  	WriteZoneFileRC(buf, recs, "bosun.org", []string{"c1", "c2", "c3\nc4"})
   299  	expected := `$TTL 300
   300  ; c1
   301  ; c2
   302  ; c3
   303  ; c4
   304  @                IN A     192.30.252.153
   305                   IN A     192.30.252.154
   306  ;myalias          IN R53_ALIAS  atype= zone_id=
   307  ;myalias          IN R53_ALIAS  atype= zone_id=
   308  www              IN CNAME bosun.org.
   309  ;zalias           IN R53_ALIAS  atype= zone_id=
   310  `
   311  	if buf.String() != expected {
   312  		t.Log(buf.String())
   313  		t.Log(expected)
   314  		t.Fatalf("Zone file does not match.")
   315  	}
   316  }
   317  
   318  // Test sorting
   319  
   320  func TestWriteZoneFileOrder(t *testing.T) {
   321  	var records []dns.RR
   322  	for i, td := range []string{
   323  		"@",
   324  		"@",
   325  		"@",
   326  		"stackoverflow.com.",
   327  		"*",
   328  		"foo",
   329  		"bar.foo",
   330  		"hip.foo",
   331  		"mup",
   332  		"a.mup",
   333  		"bzt.mup",
   334  		"aaa.bzt.mup",
   335  		"zzz.bzt.mup",
   336  		"nnn.mup",
   337  		"zt.mup",
   338  		"zap",
   339  	} {
   340  		name := dnsutil.AddOrigin(td, "stackoverflow.com.")
   341  		r, _ := dns.NewRR(fmt.Sprintf("%s 300 IN A 1.2.3.%d", name, i))
   342  		records = append(records, r)
   343  	}
   344  
   345  	buf := &bytes.Buffer{}
   346  	WriteZoneFileRR(buf, records, "stackoverflow.com")
   347  	// Compare
   348  	if buf.String() != testdataOrder {
   349  		t.Log("Found:")
   350  		t.Log(buf.String())
   351  		t.Log("Expected:")
   352  		t.Log(testdataOrder)
   353  		t.Fatalf("Zone file does not match.")
   354  	}
   355  	parseAndRegen(t, buf, testdataOrder)
   356  
   357  	// Now shuffle the list many times and make sure it still works:
   358  	for iteration := 5; iteration > 0; iteration-- {
   359  		// Randomize the list:
   360  		perm := rand.Perm(len(records))
   361  		for i, v := range perm {
   362  			records[i], records[v] = records[v], records[i]
   363  		}
   364  		// Generate
   365  		buf := &bytes.Buffer{}
   366  		WriteZoneFileRR(buf, records, "stackoverflow.com")
   367  		// Compare
   368  		if buf.String() != testdataOrder {
   369  			t.Log(buf.String())
   370  			t.Log(testdataOrder)
   371  			t.Fatalf("Zone file does not match.")
   372  		}
   373  		parseAndRegen(t, buf, testdataOrder)
   374  	}
   375  }
   376  
   377  var testdataOrder = `$TTL 300
   378  @                IN A     1.2.3.0
   379                   IN A     1.2.3.1
   380                   IN A     1.2.3.2
   381                   IN A     1.2.3.3
   382  *                IN A     1.2.3.4
   383  foo              IN A     1.2.3.5
   384  bar.foo          IN A     1.2.3.6
   385  hip.foo          IN A     1.2.3.7
   386  mup              IN A     1.2.3.8
   387  a.mup            IN A     1.2.3.9
   388  bzt.mup          IN A     1.2.3.10
   389  aaa.bzt.mup      IN A     1.2.3.11
   390  zzz.bzt.mup      IN A     1.2.3.12
   391  nnn.mup          IN A     1.2.3.13
   392  zt.mup           IN A     1.2.3.14
   393  zap              IN A     1.2.3.15
   394  `
   395  
   396  // func formatLine
   397  
   398  func TestFormatLine(t *testing.T) {
   399  	tests := []struct {
   400  		lengths  []int
   401  		fields   []string
   402  		expected string
   403  	}{
   404  		{[]int{2, 2, 0}, []string{"a", "b", "c"}, "a  b  c"},
   405  		{[]int{2, 2, 0}, []string{"aaaaa", "b", "c"}, "aaaaa b c"},
   406  	}
   407  	for _, ts := range tests {
   408  		actual := formatLine(ts.lengths, ts.fields)
   409  		if actual != ts.expected {
   410  			t.Errorf("\"%s\" != \"%s\"", actual, ts.expected)
   411  		}
   412  	}
   413  }
   414  
   415  // func zoneLabelLess
   416  
   417  func TestZoneLabelLess(t *testing.T) {
   418  	/*
   419  			The zone should sort in prefix traversal order:
   420  
   421  		  @
   422  		  *
   423  		  foo
   424  		  bar.foo
   425  		  hip.foo
   426  		  mup
   427  		  a.mup
   428  		  bzt.mup
   429  		  *.bzt.mup
   430  		  1.bzt.mup
   431  		  2.bzt.mup
   432  		  10.bzt.mup
   433  		  aaa.bzt.mup
   434  		  zzz.bzt.mup
   435  		  nnn.mup
   436  		  zt.mup
   437  		  zap
   438  	*/
   439  
   440  	var tests = []struct {
   441  		e1, e2   string
   442  		expected bool
   443  	}{
   444  		{"@", "@", false},
   445  		{"@", "*", true},
   446  		{"@", "b", true},
   447  		{"*", "@", false},
   448  		{"*", "*", false},
   449  		{"*", "b", true},
   450  		{"foo", "foo", false},
   451  		{"foo", "bar", false},
   452  		{"bar", "foo", true},
   453  		{"a.mup", "mup", false},
   454  		{"mup", "a.mup", true},
   455  		{"a.mup", "a.mup", false},
   456  		{"a.mup", "bzt.mup", true},
   457  		{"a.mup", "aa.mup", true},
   458  		{"zt.mup", "aaa.bzt.mup", false},
   459  		{"aaa.bzt.mup", "mup", false},
   460  		{"*.bzt.mup", "aaa.bzt.mup", true},
   461  		{"1.bzt.mup", "aaa.bzt.mup", true},
   462  		{"1.bzt.mup", "2.bzt.mup", true},
   463  		{"10.bzt.mup", "2.bzt.mup", false},
   464  		{"nnn.mup", "aaa.bzt.mup", false},
   465  		{`www\.miek.nl`, `www.miek.nl`, false},
   466  	}
   467  
   468  	for _, test := range tests {
   469  		actual := zoneLabelLess(test.e1, test.e2)
   470  		if test.expected != actual {
   471  			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
   472  		}
   473  		actual = zoneLabelLess(test.e2, test.e1)
   474  		// The reverse should work too:
   475  		var expected bool
   476  		if test.e1 == test.e2 {
   477  			expected = false
   478  		} else {
   479  			expected = !test.expected
   480  		}
   481  		if expected != actual {
   482  			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
   483  		}
   484  	}
   485  }
   486  
   487  func TestZoneRrtypeLess(t *testing.T) {
   488  	/*
   489  		In zonefiles we want to list SOAs, then NSs, then all others.
   490  	*/
   491  
   492  	var tests = []struct {
   493  		e1, e2   string
   494  		expected bool
   495  	}{
   496  		{"SOA", "SOA", false},
   497  		{"SOA", "A", true},
   498  		{"SOA", "TXT", true},
   499  		{"SOA", "NS", true},
   500  		{"NS", "SOA", false},
   501  		{"NS", "A", true},
   502  		{"NS", "TXT", true},
   503  		{"NS", "NS", false},
   504  		{"A", "SOA", false},
   505  		{"A", "A", false},
   506  		{"A", "TXT", true},
   507  		{"A", "NS", false},
   508  		{"MX", "SOA", false},
   509  		{"MX", "A", false},
   510  		{"MX", "TXT", true},
   511  		{"MX", "NS", false},
   512  	}
   513  
   514  	for _, test := range tests {
   515  		actual := zoneRrtypeLess(test.e1, test.e2)
   516  		if test.expected != actual {
   517  			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
   518  		}
   519  		actual = zoneRrtypeLess(test.e2, test.e1)
   520  		// The reverse should work too:
   521  		var expected bool
   522  		if test.e1 == test.e2 {
   523  			expected = false
   524  		} else {
   525  			expected = !test.expected
   526  		}
   527  		if expected != actual {
   528  			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
   529  		}
   530  	}
   531  }