mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-15 16:51:03 +00:00
Testing unicode is great, but we appear to have had a couple
of cases where we were using strings that weren't valid UTF8.
Weirdly, other instances of these strings work -- I think because
they're in raw quotes (backticks) rather than strings. Anyway,
this is what SonarCloud fusses about.
(cherry picked from commit ab543adbe3)
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()
|
|
}
|