github.com/mailgun/holster/v4@v4.20.0/mxresolv/mxresolv_test.go (about) 1 package mxresolv_test 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "math/rand" 8 "net" 9 "os" 10 "reflect" 11 "regexp" 12 "sort" 13 "testing" 14 "time" 15 16 "github.com/foxcpp/go-mockdns" 17 "github.com/mailgun/holster/v4/clock" 18 "github.com/mailgun/holster/v4/errors" 19 "github.com/mailgun/holster/v4/mxresolv" 20 "github.com/stretchr/testify/assert" 21 "github.com/stretchr/testify/require" 22 ) 23 24 func TestMain(m *testing.M) { 25 zones := map[string]mockdns.Zone{ 26 "test-a.definbox.com.": { 27 A: []string{"192.168.19.2"}, 28 }, 29 "test-cname.definbox.com.": { 30 CNAME: "definbox.com.", 31 }, 32 "definbox.com.": { 33 MX: []net.MX{ 34 {Host: "mxa.ninomail.com.", Pref: 10}, 35 {Host: "mxb.ninomail.com.", Pref: 10}, 36 }, 37 }, 38 "prefer.example.com.": { 39 MX: []net.MX{ 40 {Host: "mxa.example.com.", Pref: 20}, 41 {Host: "mxb.example.com.", Pref: 1}, 42 }, 43 }, 44 "prefer3.example.com.": { 45 MX: []net.MX{ 46 {Host: "mxa.example.com.", Pref: 1}, 47 {Host: "mxb.example.com.", Pref: 1}, 48 {Host: "mxc.example.com.", Pref: 2}, 49 }, 50 }, 51 "test-unicode.definbox.com.": { 52 MX: []net.MX{ 53 {Host: "mxa.definbox.com.", Pref: 1}, 54 {Host: "ex\\228mple.com.", Pref: 2}, 55 {Host: "mxb.definbox.com.", Pref: 3}, 56 }, 57 }, 58 "test-underscore.definbox.com.": { 59 MX: []net.MX{ 60 {Host: "foo_bar.definbox.com.", Pref: 1}, 61 }, 62 }, 63 "xn--test--xweh4bya7b6j.definbox.com.": { 64 MX: []net.MX{ 65 {Host: "xn--test---mofb0ab4b8camvcmn8gxd.definbox.com.", Pref: 10}, 66 }, 67 }, 68 "test-mx-ipv4.definbox.com.": { 69 MX: []net.MX{ 70 {Host: "34.150.176.225.", Pref: 10}, 71 }, 72 }, 73 "test-mx-ipv6.definbox.com.": { 74 MX: []net.MX{ 75 {Host: "::ffff:2296:b0e1.", Pref: 10}, 76 }, 77 }, 78 "example.com.": { 79 MX: []net.MX{ 80 {Host: ".", Pref: 0}, 81 }, 82 }, 83 "test-mx-zero.definbox.com.": { 84 MX: []net.MX{ 85 {Host: "0.0.0.0.", Pref: 0}, 86 }, 87 }, 88 "test-mx.definbox.com.": { 89 MX: []net.MX{ 90 {Host: "mxg.definbox.com.", Pref: 3}, 91 {Host: "mxa.definbox.com.", Pref: 1}, 92 {Host: "mxe.definbox.com.", Pref: 1}, 93 {Host: "mxi.definbox.com.", Pref: 1}, 94 {Host: "mxd.definbox.com.", Pref: 3}, 95 {Host: "mxc.definbox.com.", Pref: 2}, 96 {Host: "mxb.definbox.com.", Pref: 3}, 97 {Host: "mxf.definbox.com.", Pref: 3}, 98 {Host: "mxh.definbox.com.", Pref: 3}, 99 }, 100 }, 101 } 102 server, err := SpawnMockDNS(zones) 103 if err != nil { 104 panic(err) 105 } 106 107 server.Patch(mxresolv.Resolver) 108 exitVal := m.Run() 109 server.UnPatch(mxresolv.Resolver) 110 server.Stop() 111 os.Exit(exitVal) 112 } 113 114 func TestLookupWithPref(t *testing.T) { 115 for _, tc := range []struct { 116 desc string 117 inDomainName string 118 outMXHosts []*net.MX 119 outImplicitMX bool 120 }{{ 121 desc: "MX record preference is respected", 122 inDomainName: "test-mx.definbox.com", 123 outMXHosts: []*net.MX{ 124 {Host: "mxa.definbox.com", Pref: 1}, {Host: "mxe.definbox.com", Pref: 1}, {Host: "mxi.definbox.com", Pref: 1}, 125 {Host: "mxc.definbox.com", Pref: 2}, 126 {Host: "mxb.definbox.com", Pref: 3}, {Host: "mxd.definbox.com", Pref: 3}, {Host: "mxf.definbox.com", Pref: 3}, {Host: "mxg.definbox.com", Pref: 3}, {Host: "mxh.definbox.com", Pref: 3}, 127 }, 128 outImplicitMX: false, 129 }, { 130 inDomainName: "test-a.definbox.com", 131 outMXHosts: []*net.MX{{Host: "test-a.definbox.com", Pref: 1}}, 132 outImplicitMX: true, 133 }, { 134 inDomainName: "test-cname.definbox.com", 135 outMXHosts: []*net.MX{{Host: "mxa.ninomail.com", Pref: 10}, {Host: "mxb.ninomail.com", Pref: 10}}, 136 outImplicitMX: false, 137 }, { 138 inDomainName: "definbox.com", 139 outMXHosts: []*net.MX{{Host: "mxa.ninomail.com", Pref: 10}, {Host: "mxb.ninomail.com", Pref: 10}}, 140 outImplicitMX: false, 141 }, { 142 desc: "If an MX host returned by the resolver contains non ASCII " + 143 "characters then it is silently dropped from the returned list", 144 inDomainName: "test-unicode.definbox.com", 145 outMXHosts: []*net.MX{{Host: "mxa.definbox.com", Pref: 1}, {Host: "mxb.definbox.com", Pref: 3}}, 146 outImplicitMX: false, 147 }, { 148 desc: "Underscore is allowed in domain names", 149 inDomainName: "test-underscore.definbox.com", 150 outMXHosts: []*net.MX{{Host: "foo_bar.definbox.com", Pref: 1}}, 151 outImplicitMX: false, 152 }, { 153 inDomainName: "test-яндекс.definbox.com", 154 outMXHosts: []*net.MX{{Host: "xn--test---mofb0ab4b8camvcmn8gxd.definbox.com", Pref: 10}}, 155 outImplicitMX: false, 156 }, { 157 inDomainName: "xn--test--xweh4bya7b6j.definbox.com", 158 outMXHosts: []*net.MX{{Host: "xn--test---mofb0ab4b8camvcmn8gxd.definbox.com", Pref: 10}}, 159 outImplicitMX: false, 160 }, { 161 inDomainName: "test-mx-ipv4.definbox.com", 162 outMXHosts: []*net.MX{{Host: "34.150.176.225", Pref: 10}}, 163 outImplicitMX: false, 164 }, { 165 inDomainName: "test-mx-ipv6.definbox.com", 166 outMXHosts: []*net.MX{{Host: "::ffff:2296:b0e1", Pref: 10}}, 167 outImplicitMX: false, 168 }} { 169 t.Run(tc.inDomainName, func(t *testing.T) { 170 defer mxresolv.SetDeterministicInTests()() 171 172 // When 173 ctx, cancel := context.WithTimeout(context.Background(), 3*clock.Second) 174 defer cancel() 175 mxRecords, implicitMX, err := mxresolv.LookupWithPref(ctx, tc.inDomainName) 176 // Then 177 assert.NoError(t, err) 178 assert.Equal(t, tc.outMXHosts, mxRecords) 179 assert.Equal(t, tc.outImplicitMX, implicitMX) 180 }) 181 } 182 } 183 184 func TestLookup(t *testing.T) { 185 for _, tc := range []struct { 186 desc string 187 inDomainName string 188 outMXHosts []string 189 outImplicitMX bool 190 }{{ 191 desc: "MX record preference is respected", 192 inDomainName: "test-mx.definbox.com", 193 outMXHosts: []string{ 194 /* 1 */ "mxa.definbox.com", "mxi.definbox.com", "mxe.definbox.com", 195 /* 2 */ "mxc.definbox.com", 196 /* 3 */ "mxb.definbox.com", "mxf.definbox.com", "mxh.definbox.com", "mxd.definbox.com", "mxg.definbox.com", 197 }, 198 outImplicitMX: false, 199 }, { 200 inDomainName: "test-a.definbox.com", 201 outMXHosts: []string{"test-a.definbox.com"}, 202 outImplicitMX: true, 203 }, { 204 inDomainName: "test-cname.definbox.com", 205 outMXHosts: []string{"mxa.ninomail.com", "mxb.ninomail.com"}, 206 outImplicitMX: false, 207 }, { 208 inDomainName: "definbox.com", 209 outMXHosts: []string{"mxa.ninomail.com", "mxb.ninomail.com"}, 210 outImplicitMX: false, 211 }, { 212 desc: "If an MX host returned by the resolver contains non ASCII " + 213 "characters then it is silently dropped from the returned list", 214 inDomainName: "test-unicode.definbox.com", 215 outMXHosts: []string{"mxa.definbox.com", "mxb.definbox.com"}, 216 outImplicitMX: false, 217 }, { 218 desc: "Underscore is allowed in domain names", 219 inDomainName: "test-underscore.definbox.com", 220 outMXHosts: []string{"foo_bar.definbox.com"}, 221 outImplicitMX: false, 222 }, { 223 inDomainName: "test-яндекс.definbox.com", 224 outMXHosts: []string{"xn--test---mofb0ab4b8camvcmn8gxd.definbox.com"}, 225 outImplicitMX: false, 226 }, { 227 inDomainName: "xn--test--xweh4bya7b6j.definbox.com", 228 outMXHosts: []string{"xn--test---mofb0ab4b8camvcmn8gxd.definbox.com"}, 229 outImplicitMX: false, 230 }, { 231 inDomainName: "test-mx-ipv4.definbox.com", 232 outMXHosts: []string{"34.150.176.225"}, 233 outImplicitMX: false, 234 }, { 235 inDomainName: "test-mx-ipv6.definbox.com", 236 outMXHosts: []string{"::ffff:2296:b0e1"}, 237 outImplicitMX: false, 238 }} { 239 t.Run(tc.inDomainName, func(t *testing.T) { 240 defer mxresolv.SetDeterministicInTests()() 241 242 // When 243 ctx, cancel := context.WithTimeout(context.Background(), 3*clock.Second) 244 defer cancel() 245 mxHosts, implicitMX, err := mxresolv.Lookup(ctx, tc.inDomainName) 246 // Then 247 assert.NoError(t, err) 248 assert.Equal(t, tc.outMXHosts, mxHosts) 249 assert.Equal(t, tc.outImplicitMX, implicitMX) 250 }) 251 } 252 } 253 254 func TestLookupRegression(t *testing.T) { 255 defer mxresolv.SetDeterministicInTests()() 256 mxresolv.ResetCache() 257 258 // When 259 ctx, cancel := context.WithTimeout(context.Background(), 3*clock.Second) 260 defer cancel() 261 262 mxHosts, explictMX, err := mxresolv.Lookup(ctx, "test-mx.definbox.com") 263 // Then 264 require.NoError(t, err) 265 assert.Equal(t, []string{ 266 /* 1 */ "mxa.definbox.com", "mxi.definbox.com", "mxe.definbox.com", 267 /* 2 */ "mxc.definbox.com", 268 /* 3 */ "mxb.definbox.com", "mxf.definbox.com", "mxh.definbox.com", "mxd.definbox.com", "mxg.definbox.com", 269 }, mxHosts) 270 assert.Equal(t, false, explictMX) 271 272 // The second lookup returns the cached result, the cached result is shuffled. 273 mxHosts, explictMX, err = mxresolv.Lookup(ctx, "test-mx.definbox.com") 274 require.NoError(t, err) 275 assert.Equal(t, []string{ 276 /* 1 */ "mxe.definbox.com", "mxi.definbox.com", "mxa.definbox.com", 277 /* 2 */ "mxc.definbox.com", 278 /* 3 */ "mxh.definbox.com", "mxf.definbox.com", "mxg.definbox.com", "mxd.definbox.com", "mxb.definbox.com", 279 }, mxHosts) 280 assert.Equal(t, false, explictMX) 281 282 mxHosts, _, err = mxresolv.Lookup(ctx, "definbox.com") 283 require.NoError(t, err) 284 assert.Equal(t, []string{"mxb.ninomail.com", "mxa.ninomail.com"}, mxHosts) 285 286 // Should always prefer mxb over mxa since mxb has a lower pref than mxa 287 for i := 0; i < 100; i++ { 288 mxHosts, _, err = mxresolv.Lookup(ctx, "prefer.example.com") 289 require.NoError(t, err) 290 assert.Equal(t, []string{"mxb.example.com", "mxa.example.com"}, mxHosts) 291 } 292 293 // Should randomly order mxa and mxb. We make lookup 10 times and make sure 294 // that the returned result is not always the same. 295 mxHosts, _, err = mxresolv.Lookup(ctx, "prefer3.example.com") 296 require.NoError(t, err) 297 assert.Equal(t, []string{"mxb.example.com", "mxa.example.com", "mxc.example.com"}, mxHosts) 298 sameCount := 0 299 for i := 0; i < 10; i++ { 300 mxHosts2, _, err := mxresolv.Lookup(ctx, "prefer3.example.com") 301 assert.NoError(t, err) 302 if reflect.DeepEqual(mxHosts, mxHosts2) { 303 sameCount++ 304 } 305 } 306 assert.Less(t, sameCount, 10) 307 308 // mxc.example.com should always be last as it has a different priority, 309 // than the other two. 310 for i := 0; i < 100; i++ { 311 mxHosts, _, err = mxresolv.Lookup(ctx, "prefer3.example.com") 312 require.NoError(t, err) 313 assert.Equal(t, "mxc.example.com", mxHosts[2]) 314 } 315 } 316 317 func TestLookupError(t *testing.T) { 318 for _, tc := range []struct { 319 desc string 320 inDomainName string 321 outError string 322 }{ 323 { 324 inDomainName: "test-bogus.definbox.com", 325 outError: "lookup test-bogus.definbox.com.*: no such host", 326 }, 327 { 328 inDomainName: "", 329 outError: "lookup : no such host", 330 }, 331 { 332 inDomainName: "kaboom", 333 outError: "lookup kaboom.*: no such host", 334 }, 335 { 336 // MX 0 . 337 inDomainName: "example.com", 338 outError: "domain accepts no mail", 339 }, 340 { 341 // MX 10 0.0.0.0. 342 inDomainName: "test-mx-zero.definbox.com", 343 outError: "domain accepts no mail", 344 }, 345 } { 346 t.Run(tc.inDomainName, func(t *testing.T) { 347 // When 348 ctx, cancel := context.WithTimeout(context.Background(), 3*clock.Second) 349 defer cancel() 350 _, _, err := mxresolv.Lookup(ctx, tc.inDomainName) 351 352 // Then 353 require.Error(t, err) 354 assert.Regexp(t, regexp.MustCompile(tc.outError), err.Error()) 355 356 gotTemporary := false 357 var temporary interface{ Temporary() bool } 358 if errors.As(err, &temporary) { 359 gotTemporary = temporary.Temporary() 360 } 361 assert.False(t, gotTemporary) 362 363 // The second lookup returns the cached result, that only shows on the 364 // coverage report. 365 _, _, err = mxresolv.Lookup(ctx, tc.inDomainName) 366 assert.Regexp(t, regexp.MustCompile(tc.outError), err.Error()) 367 }) 368 } 369 } 370 371 // Shuffling does not cross preference group boundaries. 372 // 373 // Preference groups are: 374 // 375 // 1: mxa.definbox.com, mxe.definbox.com, mxi.definbox.com 376 // 2: mxc.definbox.com 377 // 3: mxb.definbox.com, mxd.definbox.com, mxf.definbox.com, mxg.definbox.com, mxh.definbox.com 378 func TestLookupShuffle(t *testing.T) { 379 defer mxresolv.SetDeterministicInTests()() 380 381 // When 382 ctx, cancel := context.WithTimeout(context.Background(), 3*clock.Second) 383 defer cancel() 384 shuffle1, _, err := mxresolv.Lookup(ctx, "test-mx.definbox.com") 385 assert.NoError(t, err) 386 shuffle2, _, err := mxresolv.Lookup(ctx, "test-mx.definbox.com") 387 assert.NoError(t, err) 388 389 // Then 390 assert.NotEqual(t, shuffle1[:3], shuffle2[:3]) 391 assert.NotEqual(t, shuffle1[4:], shuffle2[4:]) 392 393 sort.Strings(shuffle1[:3]) 394 sort.Strings(shuffle2[:3]) 395 assert.Equal(t, []string{"mxa.definbox.com", "mxe.definbox.com", "mxi.definbox.com"}, shuffle1[:3]) 396 assert.Equal(t, shuffle1[:3], shuffle2[:3]) 397 398 assert.Equal(t, "mxc.definbox.com", shuffle1[3]) 399 assert.Equal(t, shuffle1[3], shuffle2[3]) 400 401 sort.Strings(shuffle1[4:]) 402 sort.Strings(shuffle2[4:]) 403 assert.Equal(t, []string{"mxb.definbox.com", "mxd.definbox.com", "mxf.definbox.com", 404 "mxg.definbox.com", "mxh.definbox.com"}, shuffle1[4:]) 405 assert.Equal(t, shuffle1[4:], shuffle2[4:]) 406 } 407 408 func TestDistribution(t *testing.T) { 409 mxresolv.ResetCache() 410 411 // 2 host distribution should be uniform 412 dist := make(map[string]int, 2) 413 for i := 0; i < 1000; i++ { 414 s, _, _ := mxresolv.Lookup(context.Background(), "definbox.com") 415 _, ok := dist[s[0]] 416 if ok { 417 dist[s[0]] += 1 418 } else { 419 dist[s[0]] = 0 420 } 421 } 422 423 assertDistribution(t, dist, 35.0) 424 425 dist = make(map[string]int, 3) 426 for i := 0; i < 1000; i++ { 427 s, _, _ := mxresolv.Lookup(context.Background(), "test-mx.definbox.com") 428 _, ok := dist[s[0]] 429 if ok { 430 dist[s[0]] += 1 431 } else { 432 dist[s[0]] = 0 433 } 434 } 435 assertDistribution(t, dist, 35.0) 436 437 // This is what a standard distribution looks like when 3 hosts have the same MX priority 438 // spew.Dump(dist) 439 // (map[string]int) (len=3) { 440 // (string) (len=16) "mxa.definbox.com": (int) 324, 441 // (string) (len=16) "mxe.definbox.com": (int) 359, 442 // (string) (len=16) "mxi.definbox.com": (int) 314 443 // } 444 } 445 446 // Golang optimizes the allocation so there is no hit to performance or memory usage when calling 447 // `rand.New()` for each call to `shuffleNew()` over `rand.Shuffle()` which has a mutex. 448 // 449 // pkg: github.com/mailgun/holster/v4/mxresolv 450 // BenchmarkShuffleWithNew 451 // BenchmarkShuffleWithNew-10 61962 18434 ns/op 5376 B/op 1 allocs/op 452 // BenchmarkShuffleGlobal 453 // BenchmarkShuffleGlobal-10 65205 18480 ns/op 0 B/op 0 allocs/op 454 func BenchmarkShuffleWithNew(b *testing.B) { 455 for n := b.N; n > 0; n-- { 456 shuffleNew() 457 } 458 b.ReportAllocs() 459 } 460 461 func BenchmarkShuffleGlobal(b *testing.B) { 462 for n := b.N; n > 0; n-- { 463 shuffleGlobal() 464 } 465 b.ReportAllocs() 466 } 467 468 func shuffleNew() { 469 r := rand.New(rand.NewSource(time.Now().UnixNano())) 470 r.Shuffle(52, func(i, j int) {}) 471 } 472 473 func shuffleGlobal() { 474 rand.Shuffle(52, func(i, j int) {}) 475 } 476 477 func assertDistribution(t *testing.T, dist map[string]int, expected float64) { 478 t.Helper() 479 480 // Calculate the mean of the distribution 481 var sum int 482 for _, value := range dist { 483 sum += value 484 } 485 mean := float64(sum) / float64(len(dist)) 486 487 // Calculate the sum of squared differences 488 var squaredDifferences float64 489 for _, value := range dist { 490 diff := float64(value) - mean 491 squaredDifferences += diff * diff 492 } 493 494 // Calculate the variance and standard deviation 495 variance := squaredDifferences / float64(len(dist)) 496 stdDev := math.Sqrt(variance) 497 498 // The distribution of random hosts chosen should not exceed 35 499 assert.False(t, stdDev > expected, 500 fmt.Sprintf("Standard deviation is greater than %f:", expected)+"%.2f", stdDev) 501 502 }