components-contrib/tests/conformance/common.go

502 lines
16 KiB
Go

/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package conformance
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/components-contrib/pubsub"
"github.com/dapr/components-contrib/secretstores"
"github.com/dapr/components-contrib/state"
"github.com/dapr/kit/logger"
b_azure_blobstorage "github.com/dapr/components-contrib/bindings/azure/blobstorage"
b_azure_cosmosdb "github.com/dapr/components-contrib/bindings/azure/cosmosdb"
b_azure_eventgrid "github.com/dapr/components-contrib/bindings/azure/eventgrid"
b_azure_eventhubs "github.com/dapr/components-contrib/bindings/azure/eventhubs"
b_azure_servicebusqueues "github.com/dapr/components-contrib/bindings/azure/servicebusqueues"
b_azure_storagequeues "github.com/dapr/components-contrib/bindings/azure/storagequeues"
b_http "github.com/dapr/components-contrib/bindings/http"
b_influx "github.com/dapr/components-contrib/bindings/influx"
b_kafka "github.com/dapr/components-contrib/bindings/kafka"
b_mqtt "github.com/dapr/components-contrib/bindings/mqtt"
b_redis "github.com/dapr/components-contrib/bindings/redis"
p_snssqs "github.com/dapr/components-contrib/pubsub/aws/snssqs"
p_eventhubs "github.com/dapr/components-contrib/pubsub/azure/eventhubs"
p_servicebus "github.com/dapr/components-contrib/pubsub/azure/servicebus"
p_hazelcast "github.com/dapr/components-contrib/pubsub/hazelcast"
p_inmemory "github.com/dapr/components-contrib/pubsub/in-memory"
p_jetstream "github.com/dapr/components-contrib/pubsub/jetstream"
p_kafka "github.com/dapr/components-contrib/pubsub/kafka"
p_mqtt "github.com/dapr/components-contrib/pubsub/mqtt"
p_natsstreaming "github.com/dapr/components-contrib/pubsub/natsstreaming"
p_pulsar "github.com/dapr/components-contrib/pubsub/pulsar"
p_rabbitmq "github.com/dapr/components-contrib/pubsub/rabbitmq"
p_redis "github.com/dapr/components-contrib/pubsub/redis"
ss_azure "github.com/dapr/components-contrib/secretstores/azure/keyvault"
ss_kubernetes "github.com/dapr/components-contrib/secretstores/kubernetes"
ss_local_env "github.com/dapr/components-contrib/secretstores/local/env"
ss_local_file "github.com/dapr/components-contrib/secretstores/local/file"
s_cosmosdb "github.com/dapr/components-contrib/state/azure/cosmosdb"
s_azuretablestorage "github.com/dapr/components-contrib/state/azure/tablestorage"
s_cassandra "github.com/dapr/components-contrib/state/cassandra"
s_cockroachdb "github.com/dapr/components-contrib/state/cockroachdb"
s_mongodb "github.com/dapr/components-contrib/state/mongodb"
s_mysql "github.com/dapr/components-contrib/state/mysql"
s_postgresql "github.com/dapr/components-contrib/state/postgresql"
s_redis "github.com/dapr/components-contrib/state/redis"
s_sqlserver "github.com/dapr/components-contrib/state/sqlserver"
conf_bindings "github.com/dapr/components-contrib/tests/conformance/bindings"
conf_pubsub "github.com/dapr/components-contrib/tests/conformance/pubsub"
conf_secret "github.com/dapr/components-contrib/tests/conformance/secretstores"
conf_state "github.com/dapr/components-contrib/tests/conformance/state"
)
const (
eventhubs = "azure.eventhubs"
redis = "redis"
kafka = "kafka"
mqtt = "mqtt"
generateUUID = "$((uuid))"
)
// nolint:gochecknoglobals
var testLogger = logger.NewLogger("testLogger")
type TestConfiguration struct {
ComponentType string `yaml:"componentType,omitempty"`
Components []TestComponent `yaml:"components,omitempty"`
}
type TestComponent struct {
Component string `yaml:"component,omitempty"`
Profile string `yaml:"profile,omitempty"`
AllOperations bool `yaml:"allOperations,omitempty"`
Operations []string `yaml:"operations,omitempty"`
Config map[string]interface{} `yaml:"config,omitempty"`
}
// NewTestConfiguration reads the tests.yml and loads the TestConfiguration.
func NewTestConfiguration(configFilepath string) (*TestConfiguration, error) {
if isYaml(configFilepath) {
b, err := readTestConfiguration(configFilepath)
if err != nil {
log.Printf("error reading file %s : %s", configFilepath, err)
return nil, err
}
tc, err := decodeYaml(b)
return &tc, err
}
return nil, errors.New("no test configuration file tests.yml found")
}
func LoadComponents(componentPath string) ([]Component, error) {
standaloneComps := NewStandaloneComponents(componentPath)
components, err := standaloneComps.LoadComponents()
if err != nil {
return nil, err
}
return components, nil
}
func LookUpEnv(key string) string {
if val, ok := os.LookupEnv(key); ok {
return val
}
return ""
}
func ParseConfigurationMap(t *testing.T, configMap map[string]interface{}) {
for k, v := range configMap {
switch val := v.(type) {
case string:
if strings.EqualFold(val, generateUUID) {
// check if generate uuid is specified
val = uuid.New().String()
t.Logf("Generated UUID %s", val)
configMap[k] = val
} else {
jsonMap := make(map[string]interface{})
err := json.Unmarshal([]byte(val), &jsonMap)
if err == nil {
ParseConfigurationMap(t, jsonMap)
mapBytes, err := json.Marshal(jsonMap)
if err == nil {
configMap[k] = string(mapBytes)
}
}
}
case map[string]interface{}:
ParseConfigurationMap(t, val)
case map[interface{}]interface{}:
parseConfigurationInterfaceMap(t, val)
}
}
}
func parseConfigurationInterfaceMap(t *testing.T, configMap map[interface{}]interface{}) {
for k, v := range configMap {
switch val := v.(type) {
case string:
if strings.EqualFold(val, generateUUID) {
// check if generate uuid is specified
val = uuid.New().String()
t.Logf("Generated UUID %s", val)
configMap[k] = val
} else {
jsonMap := make(map[string]interface{})
err := json.Unmarshal([]byte(val), &jsonMap)
if err == nil {
ParseConfigurationMap(t, jsonMap)
mapBytes, err := json.Marshal(jsonMap)
if err == nil {
configMap[k] = string(mapBytes)
}
}
}
case map[string]interface{}:
ParseConfigurationMap(t, val)
case map[interface{}]interface{}:
parseConfigurationInterfaceMap(t, val)
}
}
}
func ConvertMetadataToProperties(items []MetadataItem) (map[string]string, error) {
properties := map[string]string{}
for _, c := range items {
val := c.Value.String()
if strings.HasPrefix(c.Value.String(), "${{") {
// look up env var with that name. remove ${{}} and space
k := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(val, "${{"), "}}"))
v := LookUpEnv(k)
if v == "" {
return map[string]string{}, fmt.Errorf("required env var is not set %s", k)
}
val = v
}
properties[c.Name] = val
}
return properties, nil
}
// isYaml checks whether the file is yaml or not.
func isYaml(fileName string) bool {
extension := strings.ToLower(filepath.Ext(fileName))
if extension == ".yaml" || extension == ".yml" {
return true
}
return false
}
func readTestConfiguration(filePath string) ([]byte, error) {
b, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("error reading file %s", filePath)
}
return b, nil
}
func decodeYaml(b []byte) (TestConfiguration, error) {
var testConfig TestConfiguration
err := yaml.Unmarshal(b, &testConfig)
if err != nil {
log.Printf("error parsing string as yaml %s", err)
return TestConfiguration{}, err
}
return testConfig, nil
}
func (tc *TestConfiguration) loadComponentsAndProperties(t *testing.T, filepath string) (map[string]string, error) {
comps, err := LoadComponents(filepath)
assert.Nil(t, err)
assert.Equal(t, 1, len(comps)) // We only expect a single component per file
c := comps[0]
props, err := ConvertMetadataToProperties(c.Spec.Metadata)
return props, err
}
func convertComponentNameToPath(componentName, componentProfile string) string {
pathName := componentName
if strings.Contains(componentName, ".") {
pathName = strings.Join(strings.Split(componentName, "."), "/")
}
if componentProfile != "" {
pathName = path.Join(pathName, componentProfile)
}
return pathName
}
func (tc *TestConfiguration) Run(t *testing.T) {
// Increase verbosity of tests to allow troubleshooting of runs.
testLogger.SetOutputLevel(logger.DebugLevel)
// For each component in the tests file run the conformance test
for _, comp := range tc.Components {
testName := comp.Component
if comp.Profile != "" {
testName += "-" + comp.Profile
}
t.Run(testName, func(t *testing.T) {
// Parse and generate any keys
ParseConfigurationMap(t, comp.Config)
componentConfigPath := convertComponentNameToPath(comp.Component, comp.Profile)
switch tc.ComponentType {
case "state":
filepath := fmt.Sprintf("../config/state/%s", componentConfigPath)
props, err := tc.loadComponentsAndProperties(t, filepath)
if err != nil {
t.Errorf("error running conformance test for %s: %s", comp.Component, err)
break
}
store := loadStateStore(comp)
assert.NotNil(t, store)
storeConfig := conf_state.NewTestConfig(comp.Component, comp.AllOperations, comp.Operations, comp.Config)
conf_state.ConformanceTests(t, props, store, storeConfig)
case "secretstores":
filepath := fmt.Sprintf("../config/secretstores/%s", componentConfigPath)
props, err := tc.loadComponentsAndProperties(t, filepath)
if err != nil {
t.Errorf("error running conformance test for %s: %s", comp.Component, err)
break
}
store := loadSecretStore(comp)
assert.NotNil(t, store)
storeConfig := conf_secret.NewTestConfig(comp.Component, comp.AllOperations, comp.Operations)
conf_secret.ConformanceTests(t, props, store, storeConfig)
case "pubsub":
filepath := fmt.Sprintf("../config/pubsub/%s", componentConfigPath)
props, err := tc.loadComponentsAndProperties(t, filepath)
if err != nil {
t.Errorf("error running conformance test for %s: %s", comp.Component, err)
break
}
pubsub := loadPubSub(comp)
assert.NotNil(t, pubsub)
pubsubConfig, err := conf_pubsub.NewTestConfig(comp.Component, comp.AllOperations, comp.Operations, comp.Config)
if err != nil {
t.Errorf("error running conformance test for %s: %s", comp.Component, err)
break
}
conf_pubsub.ConformanceTests(t, props, pubsub, pubsubConfig)
case "bindings":
filepath := fmt.Sprintf("../config/bindings/%s", componentConfigPath)
props, err := tc.loadComponentsAndProperties(t, filepath)
if err != nil {
t.Errorf("error running conformance test for %s: %s", comp.Component, err)
break
}
inputBinding := loadInputBindings(comp)
outputBinding := loadOutputBindings(comp)
atLeastOne(t, func(item interface{}) bool {
return item != nil
}, inputBinding, outputBinding)
bindingsConfig, err := conf_bindings.NewTestConfig(comp.Component, comp.AllOperations, comp.Operations, comp.Config)
if err != nil {
t.Errorf("error running conformance test for %s: %s", comp.Component, err)
break
}
conf_bindings.ConformanceTests(t, props, inputBinding, outputBinding, bindingsConfig)
default:
t.Errorf("unknown component type %s", tc.ComponentType)
}
})
}
}
func loadPubSub(tc TestComponent) pubsub.PubSub {
var pubsub pubsub.PubSub
switch tc.Component {
case redis:
pubsub = p_redis.NewRedisStreams(testLogger)
case eventhubs:
pubsub = p_eventhubs.NewAzureEventHubs(testLogger)
case "azure.servicebus":
pubsub = p_servicebus.NewAzureServiceBus(testLogger)
case "natsstreaming":
pubsub = p_natsstreaming.NewNATSStreamingPubSub(testLogger)
case "jetstream":
pubsub = p_jetstream.NewJetStream(testLogger)
case kafka:
pubsub = p_kafka.NewKafka(testLogger)
case "pulsar":
pubsub = p_pulsar.NewPulsar(testLogger)
case mqtt:
pubsub = p_mqtt.NewMQTTPubSub(testLogger)
case "hazelcast":
pubsub = p_hazelcast.NewHazelcastPubSub(testLogger)
case "rabbitmq":
pubsub = p_rabbitmq.NewRabbitMQ(testLogger)
case "in-memory":
pubsub = p_inmemory.New(testLogger)
case "aws.snssqs":
pubsub = p_snssqs.NewSnsSqs(testLogger)
default:
return nil
}
return pubsub
}
func loadSecretStore(tc TestComponent) secretstores.SecretStore {
var store secretstores.SecretStore
switch tc.Component {
case "azure.keyvault.certificate":
store = ss_azure.NewAzureKeyvaultSecretStore(testLogger)
case "azure.keyvault.serviceprincipal":
store = ss_azure.NewAzureKeyvaultSecretStore(testLogger)
case "kubernetes":
store = ss_kubernetes.NewKubernetesSecretStore(testLogger)
case "localenv":
store = ss_local_env.NewEnvSecretStore(testLogger)
case "localfile":
store = ss_local_file.NewLocalSecretStore(testLogger)
default:
return nil
}
return store
}
func loadStateStore(tc TestComponent) state.Store {
var store state.Store
switch tc.Component {
case redis:
store = s_redis.NewRedisStateStore(testLogger)
case "azure.cosmosdb":
store = s_cosmosdb.NewCosmosDBStateStore(testLogger)
case "mongodb":
store = s_mongodb.NewMongoDB(testLogger)
case "azure.sql":
fallthrough
case "sqlserver":
store = s_sqlserver.NewSQLServerStateStore(testLogger)
case "postgresql":
store = s_postgresql.NewPostgreSQLStateStore(testLogger)
case "mysql":
store = s_mysql.NewMySQLStateStore(testLogger)
case "azure.tablestorage.storage":
store = s_azuretablestorage.NewAzureTablesStateStore(testLogger)
case "azure.tablestorage.cosmosdb":
store = s_azuretablestorage.NewAzureTablesStateStore(testLogger)
case "cassandra":
store = s_cassandra.NewCassandraStateStore(testLogger)
case "cockroachdb":
store = s_cockroachdb.New(testLogger)
default:
return nil
}
return store
}
func loadOutputBindings(tc TestComponent) bindings.OutputBinding {
var binding bindings.OutputBinding
switch tc.Component {
case redis:
binding = b_redis.NewRedis(testLogger)
case "azure.blobstorage":
binding = b_azure_blobstorage.NewAzureBlobStorage(testLogger)
case "azure.storagequeues":
binding = b_azure_storagequeues.NewAzureStorageQueues(testLogger)
case "azure.servicebusqueues":
binding = b_azure_servicebusqueues.NewAzureServiceBusQueues(testLogger)
case "azure.eventgrid":
binding = b_azure_eventgrid.NewAzureEventGrid(testLogger)
case eventhubs:
binding = b_azure_eventhubs.NewAzureEventHubs(testLogger)
case "azure.cosmosdb":
binding = b_azure_cosmosdb.NewCosmosDB(testLogger)
case kafka:
binding = b_kafka.NewKafka(testLogger)
case "http":
binding = b_http.NewHTTP(testLogger)
case "influx":
binding = b_influx.NewInflux(testLogger)
case mqtt:
binding = b_mqtt.NewMQTT(testLogger)
default:
return nil
}
return binding
}
func loadInputBindings(tc TestComponent) bindings.InputBinding {
var binding bindings.InputBinding
switch tc.Component {
case "azure.servicebusqueues":
binding = b_azure_servicebusqueues.NewAzureServiceBusQueues(testLogger)
case "azure.storagequeues":
binding = b_azure_storagequeues.NewAzureStorageQueues(testLogger)
case "azure.eventgrid":
binding = b_azure_eventgrid.NewAzureEventGrid(testLogger)
case eventhubs:
binding = b_azure_eventhubs.NewAzureEventHubs(testLogger)
case kafka:
binding = b_kafka.NewKafka(testLogger)
case mqtt:
binding = b_mqtt.NewMQTT(testLogger)
default:
return nil
}
return binding
}
func atLeastOne(t *testing.T, predicate func(interface{}) bool, items ...interface{}) {
met := false
for _, item := range items {
met = met || predicate(item)
}
assert.True(t, met)
}