components-contrib/tests/conformance/common.go

360 lines
11 KiB
Go

// ------------------------------------------------------------
// 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)
}