github.com/rbisecke/kafka-go@v0.4.27/dialer_test.go (about) 1 package kafka 2 3 import ( 4 "context" 5 "crypto/tls" 6 "crypto/x509" 7 "fmt" 8 "io" 9 "net" 10 "reflect" 11 "sort" 12 "testing" 13 "time" 14 ) 15 16 func TestDialer(t *testing.T) { 17 tests := []struct { 18 scenario string 19 function func(*testing.T, context.Context, *Dialer) 20 }{ 21 { 22 scenario: "looking up partitions returns the list of available partitions for a topic", 23 function: testDialerLookupPartitions, 24 }, 25 } 26 27 for _, test := range tests { 28 testFunc := test.function 29 t.Run(test.scenario, func(t *testing.T) { 30 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 31 defer cancel() 32 33 testFunc(t, ctx, &Dialer{}) 34 }) 35 } 36 } 37 38 func testDialerLookupPartitions(t *testing.T, ctx context.Context, d *Dialer) { 39 client, topic, shutdown := newLocalClientAndTopic() 40 defer shutdown() 41 42 // Write a message to ensure the partition gets created. 43 w := &Writer{ 44 Addr: TCP("localhost:9092"), 45 Topic: topic, 46 Transport: client.Transport, 47 } 48 w.WriteMessages(ctx, Message{}) 49 w.Close() 50 51 partitions, err := d.LookupPartitions(ctx, "tcp", "localhost:9092", topic) 52 if err != nil { 53 t.Error(err) 54 return 55 } 56 57 sort.Slice(partitions, func(i int, j int) bool { 58 return partitions[i].ID < partitions[j].ID 59 }) 60 61 want := []Partition{ 62 { 63 Topic: topic, 64 Leader: Broker{Host: "localhost", Port: 9092, ID: 1}, 65 Replicas: []Broker{{Host: "localhost", Port: 9092, ID: 1}}, 66 Isr: []Broker{{Host: "localhost", Port: 9092, ID: 1}}, 67 ID: 0, 68 }, 69 } 70 if !reflect.DeepEqual(partitions, want) { 71 t.Errorf("bad partitions:\ngot: %+v\nwant: %+v", partitions, want) 72 } 73 } 74 75 func tlsConfig(t *testing.T) *tls.Config { 76 const ( 77 certPEM = `-----BEGIN CERTIFICATE----- 78 MIID2zCCAsOgAwIBAgIJAMSqbewCgw4xMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV 79 BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp 80 c2NvMRAwDgYDVQQKDAdTZWdtZW50MRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTcx 81 MjIzMTU1NzAxWhcNMjcxMjIxMTU1NzAxWjBgMQswCQYDVQQGEwJVUzETMBEGA1UE 82 CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEQMA4GA1UECgwH 83 U2VnbWVudDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOC 84 AQ8AMIIBCgKCAQEAtda9OWKYNtINe/BKAoB+/zLg2qbaTeHN7L722Ug7YoY6zMVB 85 aQEHrUmshw/TOrT7GLN/6e6rFN74UuNg72C1tsflZvxqkGdrup3I3jxMh2ApAxLi 86 zem/M6Eke2OAqt+SzRPqc5GXH/nrWVd3wqg48DZOAR0jVTY2e0fWy+Er/cPJI1lc 87 L6ZMIRJikHTXkaiFj2Jct1iWvgizx5HZJBxXJn2Awix5nvc+zmXM0ZhoedbJRoBC 88 dGkRXd3xv2F4lqgVHtP3Ydjc/wYoPiGudSAkhyl9tnkHjvIjA/LeRNshWHbCIaQX 89 yemnXIcyyf+W+7EK0gXio7uiP+QSoM5v/oeVMQIDAQABo4GXMIGUMHoGA1UdIwRz 90 MHGhZKRiMGAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYD 91 VQQHDA1TYW4gRnJhbmNpc2NvMRAwDgYDVQQKDAdTZWdtZW50MRIwEAYDVQQDDAls 92 b2NhbGhvc3SCCQCBYUuEuypDMTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DANBgkq 93 hkiG9w0BAQsFAAOCAQEATk6IlVsXtNp4C1yeegaM+jE8qgKJfNm1sV27zKx8HPiO 94 F7LvTGYIG7zd+bf3pDSwRxfBhsLEwmN9TUN1d6Aa9zeu95qOnR76POfHILgttu2w 95 IzegO8I7BycnLjU9o/l9gCpusnN95tIYQhfD08ygUpYTQRuI0cmZ/Dp3xb0S9f5N 96 miYTuUoStYSA4RWbDWo+Is9YWPu7rwieziOZ96oguGz3mtqvkjxVAQH1xZr3bKHr 97 HU9LpQh0i6oTK0UCqnDwlhJl1c7A3UooxFpc3NGxyjogzTfI/gnBKfPo7eeswwsV 98 77rjIkhBW49L35KOo1uyblgK1vTT7VPtzJnuDq3ORg== 99 -----END CERTIFICATE-----` 100 101 keyPEM = `-----BEGIN RSA PRIVATE KEY----- 102 MIIEowIBAAKCAQEAtda9OWKYNtINe/BKAoB+/zLg2qbaTeHN7L722Ug7YoY6zMVB 103 aQEHrUmshw/TOrT7GLN/6e6rFN74UuNg72C1tsflZvxqkGdrup3I3jxMh2ApAxLi 104 zem/M6Eke2OAqt+SzRPqc5GXH/nrWVd3wqg48DZOAR0jVTY2e0fWy+Er/cPJI1lc 105 L6ZMIRJikHTXkaiFj2Jct1iWvgizx5HZJBxXJn2Awix5nvc+zmXM0ZhoedbJRoBC 106 dGkRXd3xv2F4lqgVHtP3Ydjc/wYoPiGudSAkhyl9tnkHjvIjA/LeRNshWHbCIaQX 107 yemnXIcyyf+W+7EK0gXio7uiP+QSoM5v/oeVMQIDAQABAoIBAQCa6roHW8JGYipu 108 vsau3v5TOOtsHN67n3arDf6MGwfM5oLN1ffmF6SMs8myv36781hBMRv3FwjWHSf+ 109 pgz9o6zsbd05Ii8/m3yiXq609zZT107ZeYuU1mG5AL5uCNWjvhn5cdA6aX0RFwC0 110 +tnjEyJ/NCS8ujBR9n/wA8IxrEKoTGcxRb6qFPPKWYoBevu34td1Szf0kH8AKjtQ 111 rdPK0Of/ZEiAUxNMLTBEOmC0ZabxJV/YGWcUU4DpmEDZSgQSr4yLT4BFUwF2VC8t 112 8VXn5dBP3RMo4h7JlteulcKYsMQZXD6KvUwY2LaEpFM/b14r+TZTUQGhwS+Ha11m 113 xa4eNwFhAoGBANshGlpR9cUUq8vNex0Wb63P9BTRTXwg1yEJVMSua+DlaaqaX/hS 114 hOxl3K4y2V5OCK31C+SOAqqbrGtMXVym5c5pX8YyC11HupFJwdFLUEc74uF3CtWY 115 GMMvEvItCK5ZvYvS5I2CQGcp1fhEMle/Uz+hFi1eeWepMqgHbVx5vkdtAoGBANRv 116 XYQsTAGSkhcHB++/ASDskAew5EoHfwtJzSX0BZC6DCACF/U4dCKzBVndOrELOPXs 117 2CZXCG4ptWzNgt6YTlMX9U7nLei5pPjoivIJsMudnc22DrDS7C94rCk++M3JeLOM 118 KSN0ou9+1iEdE7rQdMgZMryaY71OBonCIDsWgJZVAoGAB+k0CFq5IrpSUXNDpJMw 119 yPee+jlsMLUGzzyFAOzDHEVsASq9mDtybQ5oXymay1rJ2W3lVgUCd6JTITSKklO8 120 LC2FtaQM4Ps78w7Unne3mDrDQByKGZf6HOHQL0oM7C51N10Pv0Qaix7piKL9pklT 121 +hIYuN6WR3XGTGaoPhRvGCkCgYBqaQ5y8q1v7Dd5iXAUS50JHPZYo+b2niKpSOKW 122 LFHNWSRRtDrD/u9Nolb/2K1ZmcGCjo0HR3lVlVbnlVoEnk49mTaru2lntfZJKFLR 123 QsFofR9at+NL95uPe+bhEkYW7uCjL4Y72GT1ipdAJwyG+3xD7ztW9g8X+EmWH8N9 124 VZw7sQKBgGxp820jbjWhG1O9RnYLwflcZzUlSkhWJDg9tKJXBjD+hFX98Okuf0gu 125 DUpdbxbJHSi0xAjOjLVswNws4pVwzgtZVK8R7k8j3Z5TtYTJTSQLfgVowuyEdAaI 126 C8OxVJ/At/IJGnWSIz8z+/YCUf7p4jd2LJgmZVVzXeDsOFcH62gu 127 -----END RSA PRIVATE KEY-----` 128 129 caPEM = `-----BEGIN CERTIFICATE----- 130 MIIDPDCCAiQCCQCBYUuEuypDMTANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJV 131 UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEQ 132 MA4GA1UECgwHU2VnbWVudDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE3MTIyMzE1 133 NTMxOVoXDTI3MTIyMTE1NTMxOVowYDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh 134 bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoMB1NlZ21l 135 bnQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 136 AQoCggEBAJwB+Yp6MyUepgtaRDxVjpMI2RmlAaV1qApMWu60LWGKJs4KWoIoLl6p 137 oSEqnWrpMmb38pyGP99X1+t3uZjiK9L8nFhuKZ581tsTKLxaSl+YVg7JbH5LVCS6 138 opsfB5ON1gJxf1HA9YyMqKHkBFh8/hdOGR0T6Bll9TPO1NQB/UqMy/tKr3sA3KZm 139 XVDbRKSuUAQWz5J9/hLPmVMU41F/uD7mvyDY+x8GymInZjUXG4e0oq2RJgU6SYZ8 140 mkscM6qhKY3mL487w/kHVFtFlMkOhvI7LIh3zVvWwgGSAoAv9yai9BDZNFSk0cEb 141 bb/IK7BQW9sNI3lcnGirdbnjV94X9/sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA 142 MJLeGdYO3dpsPx2R39Bw0qa5cUh42huPf8n7rp4a4Ca5jJjcAlCYV8HzqOzpiKYy 143 ZNuHy8LnNVYYh5Qoh8EO45bplMV1wnHfi6hW6DY5j3SQdcxkoVsW5R7rBF7a7SDg 144 6uChVRPHgsnALUUc7Wvvd3sAs/NKHzHu86mgD3EefkdqWAaCapzcqT9mo9KXkWJM 145 DhSJS+/iIaroc8umDnbPfhhgnlMf0/D4q0TjiLSSqyLzVifxnv9yHz56TrhHG/QP 146 E/8+FEGCHYKM4JLr5smGlzv72Kfx9E1CkG6TgFNIHjipVv1AtYDvaNMdPF2533+F 147 wE3YmpC3Q0g9r44nEbz4Bw== 148 -----END CERTIFICATE-----` 149 ) 150 151 // Define TLS configuration 152 certificate, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM)) 153 if err != nil { 154 t.Error(err) 155 t.FailNow() 156 } 157 158 caCertPool := x509.NewCertPool() 159 if ok := caCertPool.AppendCertsFromPEM([]byte(caPEM)); !ok { 160 t.Error(err) 161 t.FailNow() 162 } 163 164 return &tls.Config{ 165 Certificates: []tls.Certificate{certificate}, 166 RootCAs: caCertPool, 167 InsecureSkipVerify: true, 168 } 169 } 170 171 func TestDialerTLS(t *testing.T) { 172 client, topic, shutdown := newLocalClientAndTopic() 173 defer shutdown() 174 175 // Write a message to ensure the partition gets created. 176 w := &Writer{ 177 Addr: TCP("localhost:9092"), 178 Topic: topic, 179 Transport: client.Transport, 180 } 181 w.WriteMessages(context.Background(), Message{}) 182 w.Close() 183 184 // Create an SSL proxy using the tls.Config that connects to the 185 // docker-composed kafka 186 config := tlsConfig(t) 187 l, err := tls.Listen("tcp", "127.0.0.1:", config) 188 if err != nil { 189 t.Error(err) 190 return 191 } 192 defer l.Close() 193 194 go func() { 195 for { 196 conn, err := l.Accept() 197 if err != nil { 198 return // intentionally ignored 199 } 200 201 go func(in net.Conn) { 202 out, err := net.Dial("tcp", "localhost:9092") 203 if err != nil { 204 t.Error(err) 205 return 206 } 207 defer out.Close() 208 209 go io.Copy(in, out) 210 io.Copy(out, in) 211 }(conn) 212 } 213 }() 214 215 // Use the tls.Config and connect to the SSL proxy 216 d := &Dialer{ 217 TLS: config, 218 } 219 partitions, err := d.LookupPartitions(context.Background(), "tcp", l.Addr().String(), topic) 220 if err != nil { 221 t.Error(err) 222 return 223 } 224 225 // Verify returned partition data is what we expect 226 sort.Slice(partitions, func(i int, j int) bool { 227 return partitions[i].ID < partitions[j].ID 228 }) 229 230 want := []Partition{ 231 { 232 Topic: topic, 233 Leader: Broker{Host: "localhost", Port: 9092, ID: 1}, 234 Replicas: []Broker{{Host: "localhost", Port: 9092, ID: 1}}, 235 Isr: []Broker{{Host: "localhost", Port: 9092, ID: 1}}, 236 ID: 0, 237 }, 238 } 239 if !reflect.DeepEqual(partitions, want) { 240 t.Errorf("bad partitions:\ngot: %+v\nwant: %+v", partitions, want) 241 } 242 } 243 244 type MockConn struct { 245 net.Conn 246 done chan struct{} 247 partitions []Partition 248 } 249 250 func (m *MockConn) Read(b []byte) (n int, err error) { 251 select { 252 case <-time.After(time.Minute): 253 case <-m.done: 254 return 0, context.Canceled 255 } 256 257 return 0, io.EOF 258 } 259 260 func (m *MockConn) Write(b []byte) (n int, err error) { 261 select { 262 case <-time.After(time.Minute): 263 case <-m.done: 264 return 0, context.Canceled 265 } 266 267 return 0, io.EOF 268 } 269 270 func (m *MockConn) Close() error { 271 select { 272 case <-m.done: 273 default: 274 close(m.done) 275 } 276 return nil 277 } 278 279 func (m *MockConn) ReadPartitions(topics ...string) (partitions []Partition, err error) { 280 return m.partitions, err 281 } 282 283 func TestDialerConnectTLSHonorsContext(t *testing.T) { 284 config := tlsConfig(t) 285 d := &Dialer{ 286 TLS: config, 287 } 288 289 conn := &MockConn{ 290 done: make(chan struct{}), 291 } 292 293 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*25) 294 defer cancel() 295 296 _, err := d.connectTLS(ctx, conn, d.TLS) 297 if context.DeadlineExceeded != err { 298 t.Errorf("expected err to be %v; got %v", context.DeadlineExceeded, err) 299 t.FailNow() 300 } 301 } 302 303 func TestDialerResolver(t *testing.T) { 304 ctx := context.TODO() 305 306 tests := []struct { 307 scenario string 308 address string 309 resolver map[string][]string 310 }{ 311 { 312 scenario: "resolve domain to ip", 313 address: "example.com", 314 resolver: map[string][]string{ 315 "example.com": {"127.0.0.1"}, 316 }, 317 }, 318 { 319 scenario: "resolve domain to ip and port", 320 address: "example.com", 321 resolver: map[string][]string{ 322 "example.com": {"127.0.0.1:9092"}, 323 }, 324 }, 325 { 326 scenario: "resolve domain with port to ip", 327 address: "example.com:9092", 328 resolver: map[string][]string{ 329 "example.com": {"127.0.0.1:9092"}, 330 }, 331 }, 332 { 333 scenario: "resolve domain with port to ip with different port", 334 address: "example.com:9092", 335 resolver: map[string][]string{ 336 "example.com": {"127.0.0.1:80"}, 337 }, 338 }, 339 { 340 scenario: "resolve domain with port to ip", 341 address: "example.com:9092", 342 resolver: map[string][]string{ 343 "example.com": {"127.0.0.1"}, 344 }, 345 }, 346 } 347 348 for _, test := range tests { 349 t.Run(test.scenario, func(t *testing.T) { 350 topic := makeTopic() 351 createTopic(t, topic, 1) 352 defer deleteTopic(t, topic) 353 354 d := Dialer{ 355 Resolver: &mockResolver{addrs: test.resolver}, 356 } 357 358 // Write a message to ensure the partition gets created. 359 w := NewWriter(WriterConfig{ 360 Brokers: []string{"localhost:9092"}, 361 Topic: topic, 362 Dialer: &d, 363 }) 364 w.WriteMessages(context.Background(), Message{}) 365 w.Close() 366 367 partitions, err := d.LookupPartitions(ctx, "tcp", test.address, topic) 368 if err != nil { 369 t.Error(err) 370 return 371 } 372 373 sort.Slice(partitions, func(i int, j int) bool { 374 return partitions[i].ID < partitions[j].ID 375 }) 376 377 want := []Partition{ 378 { 379 Topic: topic, 380 Leader: Broker{Host: "localhost", Port: 9092, ID: 1}, 381 Replicas: []Broker{{Host: "localhost", Port: 9092, ID: 1}}, 382 Isr: []Broker{{Host: "localhost", Port: 9092, ID: 1}}, 383 ID: 0, 384 }, 385 } 386 if !reflect.DeepEqual(partitions, want) { 387 t.Errorf("bad partitions:\ngot: %+v\nwant: %+v", partitions, want) 388 } 389 }) 390 } 391 } 392 393 type mockResolver struct { 394 addrs map[string][]string 395 } 396 397 func (mr *mockResolver) LookupHost(ctx context.Context, host string) ([]string, error) { 398 if addrs, ok := mr.addrs[host]; !ok { 399 return nil, fmt.Errorf("unrecognized host %s", host) 400 } else { 401 return addrs, nil 402 } 403 }