// ------------------------------------------------------------ // Copyright (c) Microsoft Corporation and Dapr Contributors. // Licensed under the MIT License. // ------------------------------------------------------------ package conformance import ( "errors" "fmt" "io/ioutil" "os" "path/filepath" "strings" "testing" "fortio.org/fortio/log" "github.com/dapr/components-contrib/bindings" b_azure_blobstorage "github.com/dapr/components-contrib/bindings/azure/blobstorage" b_azure_eventgrid "github.com/dapr/components-contrib/bindings/azure/eventgrid" 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_kafka "github.com/dapr/components-contrib/bindings/kafka" b_redis "github.com/dapr/components-contrib/bindings/redis" "github.com/dapr/components-contrib/pubsub" p_servicebus "github.com/dapr/components-contrib/pubsub/azure/servicebus" p_kafka "github.com/dapr/components-contrib/pubsub/kafka" p_redis "github.com/dapr/components-contrib/pubsub/redis" "github.com/dapr/components-contrib/secretstores" ss_azure "github.com/dapr/components-contrib/secretstores/azure/keyvault" ss_local_env "github.com/dapr/components-contrib/secretstores/local/env" ss_local_file "github.com/dapr/components-contrib/secretstores/local/file" "github.com/dapr/components-contrib/state" s_cosmosdb "github.com/dapr/components-contrib/state/azure/cosmosdb" s_mongodb "github.com/dapr/components-contrib/state/mongodb" s_redis "github.com/dapr/components-contrib/state/redis" 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" "github.com/dapr/dapr/pkg/apis/components/v1alpha1" "github.com/dapr/dapr/pkg/components" config "github.com/dapr/dapr/pkg/config/modes" "github.com/google/uuid" "github.com/dapr/dapr/pkg/logger" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" ) const ( redis = "redis" kafka = "kafka" 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"` AllOperations bool `yaml:"allOperations,omitempty"` Operations []string `yaml:"operations,omitempty"` Config map[string]string `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.Warnf("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) ([]v1alpha1.Component, error) { cfg := config.StandaloneConfig{ ComponentsPath: componentPath, } standaloneComps := components.NewStandaloneComponents(cfg) 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]string) { for k, v := range configMap { val := v if strings.EqualFold(val, generateUUID) { // check if generate uuid is specified val = uuid.New().String() t.Logf("Generated UUID %s", val) } configMap[k] = val } } func ConvertMetadataToProperties(items []v1alpha1.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.Warnf("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 string) string { if strings.Contains(componentName, ".") { return strings.Join(strings.Split(componentName, "."), "/") } return componentName } 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 { t.Run(comp.Component, func(t *testing.T) { // Parse and generate any keys ParseConfigurationMap(t, comp.Config) componentConfigPath := convertComponentNameToPath(comp.Component) 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 := conf_pubsub.NewTestConfig(comp.Component, comp.AllOperations, comp.Operations, comp.Config) 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 := conf_bindings.NewTestConfig(comp.Component, comp.AllOperations, comp.Operations, comp.Config) 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 "azure.servicebus": pubsub = p_servicebus.NewAzureServiceBus(testLogger) case kafka: pubsub = p_kafka.NewKafka(testLogger) default: return nil } return pubsub } func loadSecretStore(tc TestComponent) secretstores.SecretStore { var store secretstores.SecretStore switch tc.Component { case "localfile": store = ss_local_file.NewLocalSecretStore(testLogger) case "localenv": store = ss_local_env.NewEnvSecretStore(testLogger) case "azure.keyvault": store = ss_azure.NewAzureKeyvaultSecretStore(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 "cosmosdb": store = s_cosmosdb.NewCosmosDBStateStore(testLogger) case "mongodb": store = s_mongodb.NewMongoDB(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 kafka: binding = b_kafka.NewKafka(testLogger) case "http": binding = b_http.NewHTTP(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 kafka: binding = b_kafka.NewKafka(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) }