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  }