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