mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
1068 lines
31 KiB
Go
1068 lines
31 KiB
Go
//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()
|
||
}
|