//go:build kafka_sasl // +build kafka_sasl package kafka_sasl import ( "context" "encoding/json" "fmt" "math/rand" "os" "strconv" "strings" "testing" "time" confluent "github.com/confluentinc/confluent-kafka-go/kafka" pilosaclient "github.com/featurebasedb/featurebase/v3/client" "github.com/featurebasedb/featurebase/v3/idk" "github.com/featurebasedb/featurebase/v3/idk/common" "github.com/featurebasedb/featurebase/v3/idk/idktest" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) var pilosaHost string var pilosaTLSHost string var pilosaGrpcHost string var kafkaHost string var certPath string var configMap *confluent.ConfigMap // configMap is used to connect to ssl port to create topics for testing var configMapSource idk.ConfluentCommand func init() { var ok bool if pilosaHost, ok = os.LookupEnv("IDK_TEST_PILOSA_HOST"); !ok { pilosaHost = "pilosa:10101" } if pilosaTLSHost, ok = os.LookupEnv("IDK_TEST_PILOSA_TLS_HOST"); !ok { pilosaTLSHost = "https://pilosa-tls:10111" } if pilosaGrpcHost, ok = os.LookupEnv("IDK_TEST_PILOSA_GRPC_HOST"); !ok { pilosaGrpcHost = "pilosa:20101" } if kafkaHost, ok = os.LookupEnv("IDK_TEST_KAFKA_HOST"); !ok { kafkaHost = "kafka:9094" } if certPath, ok = os.LookupEnv("IDK_TEST_CERT_PATH"); !ok { certPath = "/certs" } configMapSource = idk.ConfluentCommand{ KafkaBootstrapServers: []string{kafkaHost}, KafkaSaslUsername: "kafkaClient1", KafkaSaslPassword: "kafkaClient1pw", KafkaSaslMechanism: "PLAIN", KafkaSecurityProtocol: "SASL_SSL", KafkaSslKeyPassword: "123456", // for localhost test // SslCaLocation: "../docker-sasl/ssl_keys/ca-cert", // SslCertificateLocation: "../docker-sasl/ssl_keys/client_kafkaClient_client.pem", // SslKeyLocation: "../docker-sasl/ssl_keys/client_kafkaClient_client.key", // for circleci test KafkaSslCaLocation: "/ssl_keys/ca-cert", KafkaSslCertificateLocation: "/ssl_keys/client_kafkaClient_client.pem", KafkaSslKeyLocation: "/ssl_keys/client_kafkaClient_client.key", } configMap, _ = common.SetupConfluent(&configMapSource) } func configureTestFlags(main *Main) { main.PilosaHosts = []string{pilosaHost} main.PilosaGRPCHosts = []string{pilosaGrpcHost} main.ConfluentCommand = configMapSource main.KafkaBootstrapServers = []string{kafkaHost} // intentionally low timeout — if this gets triggered it shouldn't // have any negative effects main.Timeout = time.Millisecond * 20 main.Stats = "" _, main.Verbose = os.LookupEnv("IDK_TEST_VERBOSE") } func tCreateConfluentAdmin(t *testing.T) *confluent.AdminClient { t.Helper() adminClient, err := confluent.NewAdminClient(configMap) if err != nil { t.Fatalf("Failed to create Admin client: %s\n", err) } return adminClient } func tCreateConfluentTopic(t *testing.T, adminClient *confluent.AdminClient, topic string) { t.Helper() // Contexts are used to abort or limit the amount of time // the Admin call blocks waiting for a result. ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Create topics on cluster. // Set Admin options to wait for the operation to finish (or at most 60s) maxDuration, err := time.ParseDuration("60s") if err != nil { t.Fatalf("time.ParseDuration(60s)") } _, err = adminClient.CreateTopics( ctx, // Multiple topics can be created simultaneously // by providing more TopicSpecification structs here. []confluent.TopicSpecification{{ Topic: topic, NumPartitions: 64, ReplicationFactor: 1}}, // Admin options confluent.SetAdminOperationTimeout(maxDuration)) if err != nil { t.Fatalf("Problem during the topic creation: %v\n", err) } } func tCreateConfluentProducer(t *testing.T) *confluent.Producer { t.Helper() producer, err := confluent.NewProducer(configMap) if err != nil { t.Fatalf("Failed to create producer: %s\n", err) } return producer } func tWriteConfluentMessage(t *testing.T, producer *confluent.Producer, topic string, key string, value string) { t.Helper() err := producer.Produce( &confluent.Message{ TopicPartition: confluent.TopicPartition{ Topic: &topic, Offset: confluent.OffsetEnd, Partition: confluent.PartitionAny}, Key: []byte(key), Value: []byte(value)}, nil) if err != nil { t.Fatalf("err %v", err) } // Wait for delivery report e := <-producer.Events() message := e.(*confluent.Message) if message.TopicPartition.Error != nil { t.Fatalf("failed to deliver message: %v\n", message.TopicPartition) } else { t.Logf("%s:%s delivered to topic %s [%d] at offset %v\n", key, value, *message.TopicPartition.Topic, message.TopicPartition.Partition, message.TopicPartition.Offset) } } func makeRecordString(t *testing.T, fields []string, vals []interface{}) string { if len(fields) != len(vals) { t.Fatalf("have %d fields and %d vals", len(fields), len(vals)) } rec := make(map[string]interface{}) for i, field := range fields { rec[field] = vals[i] } ret, err := json.Marshal(rec) if err != nil { t.Fatalf("error marshaling record to json") } return string(ret) } func TestSaslFieldTypes(t *testing.T) { t.Parallel() fieldNames := []string{"i", "d", "t", "@s", "@st", "unixtime", "e"} records := [][]interface{}{ {100, 34.0404, "2021-02-01", "apple", "egg", 1617246530, "Lorem ipsum dolor sit amet"}, // Thu Apr 01 03:08:50 2021 UTC } sPilosaName := "s" rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // use admin to create topic adminClient := tCreateConfluentAdmin(t) tCreateConfluentTopic(t, adminClient, topic) defer adminClient.Close() // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } configureTestFlags(m) m.Index = fmt.Sprintf("sasl_cmd_test_index223ij%s", topic) m.AutoGenerate = true m.Header = "../kafka_static/testdata/TestFieldTypes.json" m.PackBools = "bools" m.BatchSize = 1 m.Topics = []string{topic} m.MaxMsgs = uint64(len(records)) m.PilosaHosts = []string{pilosaHost} m.Timeout = time.Minute m.LookupDBDSN = "postgresql://postgres:password@postgres:5432/postgres?sslmode=disable" m.LookupBatchSize = 1 // put records from all testcases into kafka producer := tCreateConfluentProducer(t) for _, vals := range records { rec := makeRecordString(t, fieldNames, vals) tWriteConfluentMessage(t, producer, topic, "akey", rec) } defer producer.Close() err = m.Run() if err != nil { t.Fatalf("running main: %v", err) } client := m.PilosaClient() schema, err := client.Schema() if err != nil { t.Fatalf("getting client: %v", err) } index := schema.Index(m.Index) defer func() { err := client.DeleteIndex(index) if err != nil { t.Logf("deleting index: %v", err) } }() if status, body, err := client.HTTPRequest("POST", "/recalculate-caches", nil, nil); err != nil { t.Fatalf("recalculating cache: status: %d, response: %s, error: %s", status, body, err) } // check data in Pilosa if !index.HasField(sPilosaName) { t.Fatalf("don't have field '%s'", sPilosaName) } fields := index.Field(sPilosaName) stPilosaName := "st" fieldSt := index.Field(stPilosaName) qr, err := client.Query(index.Count(fields.Row("apple"))) if err != nil { t.Errorf("querying: %v", err) } if qr.Result().Count() != 1 { t.Errorf("wrong count for field '%s', %d is not 1", sPilosaName, qr.Result().Count()) } qr, err = client.Query(index.Count(fieldSt.Row("egg"))) if err != nil { t.Fatalf("querying time range for egg: %v", err) } if qr.Result().Count() != 1 { t.Errorf("wrong count for field '%s', %d is not 1", stPilosaName, qr.Result().Count()) } qr, err = client.Query(index.Count(fieldSt.Range("egg", time.Unix(1617145530, 0), time.Unix(1617348530, 0)))) if err != nil { t.Fatalf("querying time range for egg: %v", err) } if qr.Result().Count() != 1 { t.Errorf("wrong count for field '%s' with time range, %d is not 1", stPilosaName, qr.Result().Count()) } if !index.HasField("i") { t.Fatal("don't have field 'i'") } fieldi := index.Field("i") qr, err = client.Query(index.Count(fieldi.Row(100))) if err != nil { t.Errorf("querying: %v", err) } if qr.Result().Count() != 1 { t.Errorf("wrong count for field 'i', %d is not 1", qr.Result().Count()) } if !index.HasField("d") { t.Fatal("don't have field 'd'") } fieldd := index.Field("d") qr, err = client.Query(index.Count(fieldd.Row(34.0404))) if err != nil { t.Errorf("querying: %v", err) } if qr.Result().Count() != 1 { t.Errorf("wrong count for field 'd', %d is not 1", qr.Result().Count()) } } func TestSaslLookupFieldIdNameDisallowed(t *testing.T) { t.Parallel() tcs := []lookupTestCase{ {name: "junk", text: "foo", expText: "foo"}, } rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // use admin to create topic adminClient := tCreateConfluentAdmin(t) tCreateConfluentTopic(t, adminClient, topic) defer adminClient.Close() // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } configureTestFlags(m) m.Index = fmt.Sprintf("sasl_cmd_test_index223ij%s", topic) m.AutoGenerate = true m.PackBools = "bools" m.BatchSize = 1 m.Topics = []string{topic} m.PilosaHosts = []string{pilosaHost} m.Timeout = time.Minute m.LookupDBDSN = "postgresql://postgres:password@postgres:5432/postgres?sslmode=disable" m.Header = "../kafka_static/testdata/LookupId.json" // lookupClient.Setup() can't get called until after initialFetch, so need to insert // some junk into kafka to test this at this level. m.MaxMsgs = uint64(len(tcs)) // make source wait for TimeOut after last message m.LookupBatchSize = 1 ingesterErrs := make(chan error, 1) go func() { err := m.Run() ingesterErrs <- err }() // put records from all testcases into kafka producer := tCreateConfluentProducer(t) for _, tc := range tcs { msg := messageStringFromTestcase(t, tc) tWriteConfluentMessage(t, producer, topic, "bkey", msg) } defer producer.Close() // Wait for ingestion to finish. err = <-ingesterErrs if !strings.Contains(err.Error(), "field name 'id' not allowed for LookupText fields") { t.Fatalf("invalid field name 'id' not detected: %s", err) } } // lookupTestCase consolidates definitions for: // - messages sent to Kafka // - record IDs generated by Pilosa and retrieved within a test // - values to check against Postgres // - values to check against Pilosa // name: testcase name // uniquePilosaVal: unqiue integer value sent to Pilosa. Corresponds to `int` field in Lookup.json // externalId: Pilosa record ID, allocated by Pilosa, looked up by test, used as Postgres ID as well // text: raw text sent to Postgres. Corresponds to `text` field in Lookup.josn // expText: text after retrieving from Postges (distinct from `text` due to escape characters) // missing: true if `text` should NOT be present in the Kafka message type lookupTestCase struct { name string uniquePilosaVal uint64 externalId uint64 text string expText string missing bool } // messageStringFromTestCase defines a Kafka json message string, to be // sent to Kafka. Matches Lookup.json. func messageStringFromTestcase(t *testing.T, tc lookupTestCase) string { if tc.missing { return fmt.Sprintf(`{"int":%d}`, tc.uniquePilosaVal) } else { return fmt.Sprintf(`{"int":%d,"text":"%s"}`, tc.uniquePilosaVal, tc.text) } } // TestLookupFieldWithExternalId checks that the external // lookup (postgres) feature works as expected, // - using ExternalGenerate to use pilosa to generate IDs // - with bad string input // - with missing data // This test is intended to match THR's use case. func TestSaslLookupFieldWithExternalId(t *testing.T) { t.Parallel() tcs := []lookupTestCase{ // When using ExternalGenerate (pilosa Nexter), then the only kafka setting that // makes sense is at-most-once delivery. // That means this postgres client/batcher should only be used in its current state // for this very specific use case. // Testing for duplicate records and overwriting behavior would be done here by defining // multiple testcases with overlapping ID values. // This is not done because that doesn't make sense when using ExternalGenerate: // - key is not present in Kafka message, so it is generated by Pilosa // - identifying duplicates is not possible without a primary key in the message {name: "missing-data", missing: true}, {name: "normal-write", text: "D", expText: "D"}, {name: "weirdstring-1", text: "Æ漢д ☮♬ ♞🜻💣", expText: "Æ漢д ☮♬ ♞🜻💣"}, {name: "weirdstring-2", text: "", expText: ""}, {name: "weirdstring-3", text: "'", expText: "'"}, {name: "doublequotes-1", text: `\"`, expText: `"`}, // is this sensible? {name: "doublequotes-2", text: `{\"log\": \"message\", 'with': 'whatever', ` + "`weird`: `syntax`}", expText: `{"log": "message", 'with': 'whatever', ` + "`weird`: `syntax`}"}, } // lookupTestCase.uniquePilosaVal is required to be unique, // so it can be used to correlate testcases with IDs allocated by Pilosa, // in retrieveTestCaseIds. Set it automatically here. lookupRecordCount := 0 for n := range tcs { tcs[n].uniquePilosaVal = uint64(100 * (n + 1)) if !tcs[n].missing { lookupRecordCount++ } } rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // put records from all testcases into kafka adminClient := tCreateConfluentAdmin(t) tCreateConfluentTopic(t, adminClient, topic) defer adminClient.Close() // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } configureTestFlags(m) m.Index = fmt.Sprintf("sasl_cmd_test_index223ij%s", topic) m.AutoGenerate = true m.PackBools = "bools" m.BatchSize = 1 m.Topics = []string{topic} m.PilosaHosts = []string{pilosaHost} m.Timeout = time.Minute m.Header = "../kafka_static/testdata/Lookup.json" pilosaFieldName := "int" // matches Lookup.json postgresColumnName := "text" // matches Lookup.json // lookup+external specific settings m.ExternalGenerate = true m.LookupDBDSN = "postgresql://postgres:password@postgres:5432/postgres?sslmode=disable" m.AllowMissingFields = true // needed for missing data testcases m.MaxMsgs = uint64(len(tcs)) // make source wait for TimeOut after last message m.LookupBatchSize = lookupRecordCount // put records from all testcases into kafka producer := tCreateConfluentProducer(t) for _, tc := range tcs { msg := messageStringFromTestcase(t, tc) tWriteConfluentMessage(t, producer, topic, "bkey", msg) } defer producer.Close() ingesterErrs := make(chan error, 1) go func() { err := m.Run() ingesterErrs <- err }() // Wait for ingestion to finish. if err := <-ingesterErrs; err != nil { t.Fatalf("running main: %v", err) } err = retrieveTestCaseIds(tcs, m.Index, pilosaFieldName) if err != nil { t.Fatalf("looking up ExternalIds: %v", err) } lookupClient, err := m.NewLookupClient() if err != nil { t.Fatal("creating lookup client") } defer lookupClient.Close() // check final postgres values for _, tc := range tcs { t.Run(tc.name+"-postgres", func(t *testing.T) { if tc.missing { if present, err := lookupClient.RowExists(tc.externalId); err != nil { t.Fatalf("querying postgres: %s", err) } else if present { t.Fatalf("present and shouldn't be") } } else { if got, err := lookupClient.ReadString(tc.externalId, postgresColumnName); err != nil { t.Fatalf("querying postgres: %s", err) } else if tc.expText != got { t.Errorf("wrong value from postgres, expected\n%s\n got\n%s\n", tc.expText, got) } } }) } // check postgres values via pilosa for _, tc := range tcs { t.Run(tc.name+"-pilosa", func(t *testing.T) { pilosaCount, pilosaVal, err := lookupViaPilosa(tc.externalId, postgresColumnName, m.Index) if err != nil { t.Fatalf("querying pilosa: %v", err) } if tc.missing { if pilosaCount > 0 { t.Errorf("present and shouldn't be") } } else { if pilosaVal != tc.expText { t.Errorf("wrong value from pilosa, expected\n%s\n got\n%s\n", tc.expText, pilosaVal) } } }) } // // delete data from pilosa // schema, err := m.PilosaClient().Schema() // if err != nil { // t.Errorf("getting client: %v", err) // } // index := schema.Index(m.Index) // err = m.PilosaClient().DeleteIndex(index) // if err != nil { // t.Errorf("deleting index: %v", err) // } // // delete data from postgres // err = lookupClient.DropTable() // if err != nil { // t.Errorf("dropping table: %v", err) // } } // lookupViaPilosa retrieves lookupText values from Postgres via the ExternalLookup // Pilosa query. // This was created as a helper function for TestLookupFieldWithExternalId. func lookupViaPilosa(id uint64, column, index string) (int, string, error) { pql := fmt.Sprintf(`ExternalLookup(ConstRow(columns=[%d]), query="select id, %s from %s where id = ANY($1)")`, id, "text", index) eResp, err := idktest.DoExtractQuery(pql, index) if err != nil { return 0, "", err } if eResp.Results[0].Columns == nil { // This itself is not an error condition; it is expected for the // missing-data testcase. return 0, "", nil } rows := eResp.Results[0].Columns[0].Rows count := len(rows) text := rows[0].(string) return count, text, nil } // retrieveTestCaseIds populates the externalId field of each testcase // by comparing the uniquePilosaValue in the testcase with the results of // an Extract query which correlates uniquePilosaValue with its // pilosa-allocated ID. // This was created as a helper function for TestLookupFieldWithExternalId. func retrieveTestCaseIds(tcs []lookupTestCase, index, field string) error { pql := fmt.Sprintf("Extract(All(), Rows(%s))", field) eResp, err := idktest.DoExtractQuery(pql, index) if err != nil { return err } if eResp.Results[0].Columns == nil { return errors.Errorf("no data in Extract response") } // Correlate PilosaVal to assign corresponding IDs. for n, tc := range tcs { for _, col := range eResp.Results[0].Columns { // ?? panic: interface conversion: interface {} is float64, not uint64 if uint64(col.Rows[0].(float64)) == tc.uniquePilosaVal { tcs[n].externalId = uint64(col.ColumnID) break } } if tcs[n].externalId == 0 { // NOTE This assumes an ID of 0 will not be used by the Nexter. return errors.Errorf("no externalID found for test case with uniquePilosaVal=%d", tc.uniquePilosaVal) } } return nil } func TestSaslDuplicateFieldNameDisallowed(t *testing.T) { t.Parallel() rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } configureTestFlags(m) m.Index = fmt.Sprintf("sasl_cmd_test_index223ij%s", topic) m.AutoGenerate = true m.PackBools = "bools" m.BatchSize = 1 m.Topics = []string{topic} m.PilosaHosts = []string{pilosaHost} m.Timeout = time.Minute m.LookupDBDSN = "postgresql://postgres:password@postgres:5432/postgres?sslmode=disable" m.Header = "../kafka_static/testdata/LookupDuplicate.json" m.MaxMsgs = uint64(0) err = m.Run() if !strings.Contains(err.Error(), "schema field 2 duplicates name of field 1 (text)") { t.Fatalf("duplicate field name not detected: %s", err) } } func TestSaslPrimaryKeyFieldsMissing(t *testing.T) { t.Parallel() fieldNames := []string{"i", "d", "t", "@s"} records := [][]interface{}{ {2, 5.5, "2021-02-01", nil}, } rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // use admin to create topic adminClient := tCreateConfluentAdmin(t) tCreateConfluentTopic(t, adminClient, topic) defer adminClient.Close() // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } configureTestFlags(m) m.Index = fmt.Sprintf("sasl_cmd_test_index223ij%s", topic) m.PrimaryKeyFields = []string{"s"} m.AllowMissingFields = true m.Header = "../kafka_static/testdata/TestFieldTypes.json" m.PackBools = "bools" m.BatchSize = 1 m.Topics = []string{topic} m.MaxMsgs = uint64(len(records)) m.PilosaHosts = []string{pilosaHost} m.Timeout = time.Minute m.Verbose = true // put records from all testcases into kafka producer := tCreateConfluentProducer(t) for _, vals := range records { msg := makeRecordString(t, fieldNames, vals) tWriteConfluentMessage(t, producer, topic, "akey", msg) } defer producer.Close() err = m.Run() if err == nil { t.Fatal("running main should have failed") } client := m.PilosaClient() schema, err := client.Schema() if err != nil { t.Fatalf("getting client: %v", err) } index := schema.Index(m.Index) err = client.DeleteIndex(index) if err != nil { t.Logf("deleting index: %v", err) } } func TestSaslIDFieldMissing(t *testing.T) { t.Parallel() fieldNames := []string{"i", "d", "t", "@s"} records := [][]interface{}{ {nil, 5.5, "2021-02-01", "apple"}, } rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // use admin to create topic adminClient := tCreateConfluentAdmin(t) tCreateConfluentTopic(t, adminClient, topic) defer adminClient.Close() // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } configureTestFlags(m) m.Index = fmt.Sprintf("sasl_cmd_test_index223ij%s", topic) m.IDField = "i" m.AllowMissingFields = true m.Header = "../kafka_static/testdata/TestFieldTypes.json" m.PackBools = "bools" m.BatchSize = 1 m.Topics = []string{topic} m.MaxMsgs = uint64(len(records)) m.PilosaHosts = []string{pilosaHost} m.Timeout = time.Minute m.Verbose = true // put records from all testcases into kafka producer := tCreateConfluentProducer(t) for _, vals := range records { msg := makeRecordString(t, fieldNames, vals) tWriteConfluentMessage(t, producer, topic, "akey", msg) } defer producer.Close() err = m.Run() if err == nil { t.Fatal("running main should have failed") } client := m.PilosaClient() schema, err := client.Schema() if err != nil { t.Fatalf("getting client: %v", err) } index := schema.Index(m.Index) err = client.DeleteIndex(index) if err != nil { t.Logf("deleting index: %v", err) } } func TestSaslCmdAutoID(t *testing.T) { t.Parallel() fieldNames := []string{"first"} records := [][]interface{}{ {"a"}, {"b"}, {"c"}, } rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // use admin to create topic adminClient := tCreateConfluentAdmin(t) tCreateConfluentTopic(t, adminClient, topic) defer adminClient.Close() // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } configureTestFlags(m) m.Index = fmt.Sprintf("sasl_cmd_test_auto_id223ij%s", topic) m.AutoGenerate = true m.ExternalGenerate = true m.Header = "../kafka_static/testdata/Flat.json" m.BatchSize = 1 m.Topics = []string{topic} m.MaxMsgs = uint64(len(records)) m.PilosaHosts = []string{pilosaHost} m.Timeout = time.Minute m.Verbose = true // put records from all testcases into kafka producer := tCreateConfluentProducer(t) for _, vals := range records { msg := makeRecordString(t, fieldNames, vals) tWriteConfluentMessage(t, producer, topic, "a", msg) } defer producer.Close() err = m.Run() if err != nil { t.Fatalf("running main: %v", err) } client := m.PilosaClient() schema, err := client.Schema() if err != nil { t.Fatalf("getting client: %v", err) } index := schema.Index(m.Index) defer func() { err := client.DeleteIndex(index) if err != nil { t.Logf("deleting index: %v", err) } }() qr, err := client.Query(index.Count(index.All())) if err != nil { t.Errorf("querying: %v", err) } if qr.Result().Count() != 3 { t.Errorf("wrong count for columns, %d is not 3", qr.Result().Count()) } qr, err = client.Query(pilosaclient.NewPQLBaseQuery(`Count(Distinct(All(), field="first"))`, index, nil)) if err != nil { t.Errorf("querying: %v", err) } if qr.Result().Count() != 3 { t.Errorf("wrong count for val, %d is not 3", qr.Result().Count()) } } func TestSaslSaslConfig(t *testing.T) { t.Parallel() rand.Seed(time.Now().UnixNano()) a := rand.Int() topic := "sasl_xyz" + strconv.Itoa(a) // create Main and run with MaxMsgs m, err := NewMain() if err != nil { t.Fatal(err) } m.Index = fmt.Sprintf("sasl_cmd_test_index223ij%s", topic) m.KafkaBootstrapServers = []string{kafkaHost} m.PilosaHosts = []string{pilosaHost} // m.LookupDBDSN = "postgresql://postgres:password@postgres:5432/postgres?sslmode=disable" m.AutoGenerate = true m.Topics = []string{topic} m.Header = "../kafka_static/testdata/LookupId.json" type testCase struct { name string config idk.ConfluentCommand expectedError string } tests := []testCase{ { name: "incorrect username", config: idk.ConfluentCommand{ KafkaSaslUsername: "wrong-username", KafkaSaslPassword: configMapSource.KafkaSaslPassword, KafkaSaslMechanism: configMapSource.KafkaSaslMechanism, KafkaSecurityProtocol: configMapSource.KafkaSecurityProtocol, KafkaSslKeyPassword: configMapSource.KafkaSslKeyPassword, KafkaSslCaLocation: configMapSource.KafkaSslCaLocation, KafkaSslCertificateLocation: configMapSource.KafkaSslCertificateLocation, KafkaSslKeyLocation: configMapSource.KafkaSslKeyLocation, }, expectedError: "Authentication failed: Invalid username or password", }, { name: "incorrect password", config: idk.ConfluentCommand{ KafkaSaslUsername: configMapSource.KafkaSaslUsername, KafkaSaslPassword: "wrong-password", KafkaSaslMechanism: configMapSource.KafkaSaslMechanism, KafkaSecurityProtocol: configMapSource.KafkaSecurityProtocol, KafkaSslKeyPassword: configMapSource.KafkaSslKeyPassword, KafkaSslCaLocation: configMapSource.KafkaSslCaLocation, KafkaSslCertificateLocation: configMapSource.KafkaSslCertificateLocation, KafkaSslKeyLocation: configMapSource.KafkaSslKeyLocation, }, expectedError: "Authentication failed: Invalid username or password", }, { name: "unsupported sasl mechanism", config: idk.ConfluentCommand{ KafkaSaslUsername: configMapSource.KafkaSaslUsername, KafkaSaslPassword: configMapSource.KafkaSaslPassword, KafkaSaslMechanism: "random-mechanism", KafkaSecurityProtocol: configMapSource.KafkaSecurityProtocol, KafkaSslKeyPassword: configMapSource.KafkaSslKeyPassword, KafkaSslCaLocation: configMapSource.KafkaSslCaLocation, KafkaSslCertificateLocation: configMapSource.KafkaSslCertificateLocation, KafkaSslKeyLocation: configMapSource.KafkaSslKeyLocation, }, expectedError: "Unsupported SASL mechanism", }, { name: "invalid value for security protocol", config: idk.ConfluentCommand{ KafkaSaslUsername: configMapSource.KafkaSaslUsername, KafkaSaslPassword: configMapSource.KafkaSaslPassword, KafkaSaslMechanism: configMapSource.KafkaSaslMechanism, KafkaSecurityProtocol: "random-protocol", KafkaSslKeyPassword: configMapSource.KafkaSslKeyPassword, KafkaSslCaLocation: configMapSource.KafkaSslCaLocation, KafkaSslCertificateLocation: configMapSource.KafkaSslCertificateLocation, KafkaSslKeyLocation: configMapSource.KafkaSslKeyLocation, }, expectedError: "Invalid value " + "\"random-protocol\"" + " for configuration property \"security.protocol\"", }, } runTest := func(t *testing.T, main *Main, config idk.ConfluentCommand, expectedError string) { main.CopyIn(config) err := m.Run() if err != nil { assert.Contains(t, err.Error(), expectedError) } } for _, test := range tests { t.Run(test.name, func(t *testing.T) { runTest(t, m, test.config, test.expectedError) }) } } func TestMaxMsgs(t *testing.T) { tests := []struct { rec []string count int }{ { rec: []string{`{"protocol": "TCP", "port": 22, "client": "127.0.0.1", "server": "127.0.0.1", "metadata": {"suspicious": true, "region": "North America"}}`}, count: 1, }, { rec: []string{`{"protocol": "TCP", "port": 22, "client": "127.0.0.1", "server": "127.0.0.1", "metadata": {"suspicious": true, "region": "North America"}}`, `{"protocol": "TCP", "port": 22, "client": "127.0.0.1", "server": "127.0.0.1", "metadata": {"suspicious": true, "region": "North America"}}`, `{"protocol": "SSH", "port": 22, "client": "127.0.0.1", "server": "127.0.0.1", "metadata": {"suspicious": false, "region": "South America"}}`}, count: 2, }, } collector := &collector{counter: 0} for i, test := range tests { t.Run(fmt.Sprintf("Test%d", i), func(t *testing.T) { topic := fmt.Sprintf("topic-max-msgs-%d", i) // create Main and run with MaxMsgs m, err := NewMain() m.Header = "testdata/Tree.json" if err != nil { t.Fatalf("opps %v", err) } m.NewSource = func() (idk.Source, error) { source := NewSource() source.Group = m.Group source.Topics = []string{topic} source.Log = m.Main.Log() source.Timeout = m.Timeout source.SkipOld = m.SkipOld source.Header = m.Header source.AllowMissingFields = m.AllowMissingFields cfg, err := common.SetupConfluent(&m.ConfluentCommand) if err != nil { return nil, err } source.ConfigMap = cfg err = source.Open() if err != nil { return nil, errors.Wrap(err, "opening source") } return NewWrapSource(source, collector), nil } configureTestFlags(m) m.Index = fmt.Sprintf("max-msgs-%d", i) m.AutoGenerate = true m.ExternalGenerate = true m.Header = "../kafka_static/testdata/Tree.json" m.BatchSize = 1 m.Topics = []string{fmt.Sprintf("test-topic-%d", i)} m.MaxMsgs = uint64(test.count) m.PilosaHosts = []string{pilosaHost} // use admin to create topic adminClient := tCreateConfluentAdmin(t) tCreateConfluentTopic(t, adminClient, topic) adminClient.Close() // put records from all testcases into kafka producer := tCreateConfluentProducer(t) for _, vals := range test.rec { tWriteConfluentMessage(t, producer, topic, "a", vals) } producer.Close() collector.reset() err = m.Run() if err != nil { t.Fatalf("running main: %v", err) } if collector.counter != uint64(test.count) { t.Fatalf("expected counter %d, got counter %d", test.count, collector.counter) } }) } } type wrapSource struct { source *Source collector *collector } type collector struct { counter uint64 } func (c *collector) reset() { c.counter = uint64(0) } func NewWrapSource(s *Source, c *collector) idk.Source { return &wrapSource{source: s, collector: c} } func (w *wrapSource) Record() (idk.Record, error) { a, err := w.source.Record() if err == nil { w.collector.counter++ } return a, err } func (w *wrapSource) Schema() []idk.Field { return w.source.Schema() } func (w *wrapSource) Close() error { return w.source.Close() }