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 }