diff --git a/tests/certification/bindings/azure/cosmosdb/README.md b/tests/certification/bindings/azure/cosmosdb/README.md index c1e6f62648..adc00b7b6d 100644 --- a/tests/certification/bindings/azure/cosmosdb/README.md +++ b/tests/certification/bindings/azure/cosmosdb/README.md @@ -10,11 +10,10 @@ This project aims to test the Azure CosmosDB binding component under various con * Authenticate with Master Key ### Other tests -- TODO: Verify data sent to output binding is written to Cosmos DB -- TODO: Expected failure for invalid partition key specified (Component Metadata Partition Key does not match Cosmos DB container) -- TODO: Expected failure for partition key missing from document -- TODO: Expected failure for `id` missing from document -- TODO: Graceful handling of connection resets / interruption (client connection only, not during Invoke/Create operation itself) +- Verify data sent to output binding is written to Cosmos DB +- Expected failure for invalid partition key specified (Component Metadata Partition Key does not match Cosmos DB container) +- Expected failure for partition key missing from document +- Expected failure for `id` missing from document ### Running the tests diff --git a/tests/certification/bindings/azure/cosmosdb/components/wrongPartitionKey/cosmosdb.yaml b/tests/certification/bindings/azure/cosmosdb/components/wrongPartitionKey/cosmosdb.yaml new file mode 100644 index 0000000000..0ef29a7705 --- /dev/null +++ b/tests/certification/bindings/azure/cosmosdb/components/wrongPartitionKey/cosmosdb.yaml @@ -0,0 +1,30 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: azure-cosmosdb-binding + namespace: default +spec: + type: bindings.azure.cosmosdb + version: v1 + metadata: + - name: url + secretKeyRef: + name: AzureCosmosDBUrl + key: AzureCosmosDBUrl + - name: database + secretKeyRef: + name: AzureCosmosDB + key: AzureCosmosDB + - name: collection + secretKeyRef: + name: AzureCosmosDBCollection + key: AzureCosmosDBCollection + - name: partitionKey + value: wrongPartitionKey + - name: masterKey + secretKeyRef: + name: AzureCosmosDBMasterKey + key: AzureCosmosDBMasterKey + +auth: + secretStore: envvar-secret-store diff --git a/tests/certification/bindings/azure/cosmosdb/components/wrongPartitionKey/localsecrets.yaml b/tests/certification/bindings/azure/cosmosdb/components/wrongPartitionKey/localsecrets.yaml new file mode 100644 index 0000000000..94bb7a2643 --- /dev/null +++ b/tests/certification/bindings/azure/cosmosdb/components/wrongPartitionKey/localsecrets.yaml @@ -0,0 +1,9 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store + namespace: default +spec: + type: secretstores.local.env + version: v1 + metadata: diff --git a/tests/certification/bindings/azure/cosmosdb/config.yaml b/tests/certification/bindings/azure/cosmosdb/config.yaml index 6c95e632ff..345edb50ff 100644 --- a/tests/certification/bindings/azure/cosmosdb/config.yaml +++ b/tests/certification/bindings/azure/cosmosdb/config.yaml @@ -1,6 +1,6 @@ apiVersion: dapr.io/v1alpha1 kind: Configuration metadata: - name: keyvaultconfig + name: cosmosdbbindingconfig spec: features: diff --git a/tests/certification/bindings/azure/cosmosdb/cosmosdb_test.go b/tests/certification/bindings/azure/cosmosdb/cosmosdb_test.go index b985142efe..fc549d9bd5 100644 --- a/tests/certification/bindings/azure/cosmosdb/cosmosdb_test.go +++ b/tests/certification/bindings/azure/cosmosdb/cosmosdb_test.go @@ -3,11 +3,16 @@ // Licensed under the MIT License. // ------------------------------------------------------------ -package keyvault_test +package cosmosdbbinding_test import ( + "encoding/json" + "fmt" + "os" "testing" + "time" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/dapr/components-contrib/bindings" @@ -18,32 +23,167 @@ import ( secretstores_loader "github.com/dapr/dapr/pkg/components/secretstores" "github.com/dapr/dapr/pkg/runtime" dapr_testing "github.com/dapr/dapr/pkg/testing" + daprsdk "github.com/dapr/go-sdk/client" "github.com/dapr/kit/logger" "github.com/dapr/components-contrib/tests/certification/embedded" "github.com/dapr/components-contrib/tests/certification/flow" "github.com/dapr/components-contrib/tests/certification/flow/sidecar" + + "github.com/a8m/documentdb" ) const ( sidecarName = "cosmosdb-sidecar" ) -func TestKeyVault(t *testing.T) { +func createDocument(generateID bool, includePK bool) map[string]interface{} { + document := map[string]interface{}{ + "orderid": "123abc456def", + "nestedproperty": map[string]interface{}{ + "subproperty": "something of value for testing", + }, + "description": "certification test item", + } + if generateID { + randomID := uuid.New().String() + document["id"] = randomID + } + if includePK { + document["partitionKey"] = "partitioniningOnThisValue" + } + + return document +} + +func TestCosmosDBBinding(t *testing.T) { ports, err := dapr_testing.GetFreePorts(2) assert.NoError(t, err) - currentGrpcPort := ports[0] - currentHttpPort := ports[1] + currentGRPCPort := ports[0] + currentHTTPPort := ports[1] log := logger.NewLogger("dapr.components") + invokeCreateWithDocument := func(ctx flow.Context, document map[string]interface{}) error { + client, clientErr := daprsdk.NewClientWithPort(fmt.Sprint(currentGRPCPort)) + if clientErr != nil { + panic(clientErr) + } + defer client.Close() + + bytesDoc, marshalErr := json.Marshal(document) + if marshalErr != nil { + return marshalErr + } + + invokeRequest := &daprsdk.InvokeBindingRequest{ + Name: "azure-cosmosdb-binding", + Operation: "create", + Data: bytesDoc, + Metadata: nil, + } + + err = client.InvokeOutputBinding(ctx, invokeRequest) + return err + } + + testInvokeCreateAndVerify := func(ctx flow.Context) error { + document := createDocument(true, true) + invokeErr := invokeCreateWithDocument(ctx, document) + assert.NoError(t, invokeErr) + + // sleep to avoid metdata request rate limit before initializing new client + flow.Sleep(3 * time.Second) + + // all environment variables loaded here are also loaded in the component definition YAML files + // these are generated by the setup-azure-conf-test.sh script and injected by the GitHub Workflow, or by + // locally sourcing the generated .rc file + config := documentdb.NewConfig(&documentdb.Key{ + Key: os.Getenv("AzureCosmosDBMasterKey"), + }) + config.IdentificationHydrator = nil + dbclient := documentdb.New(os.Getenv("AzureCosmosDBUrl"), config) + + dbs, queryDBErr := dbclient.QueryDatabases(&documentdb.Query{ + Query: "SELECT * FROM ROOT r WHERE r.id=@id", + Parameters: []documentdb.Parameter{ + {Name: "@id", Value: os.Getenv("AzureCosmosDB")}, + }, + }) + assert.NoError(t, queryDBErr) + db := &dbs[0] + colls, queryCollErr := dbclient.QueryCollections(db.Self, &documentdb.Query{ + Query: "SELECT * FROM ROOT r WHERE r.id=@id", + Parameters: []documentdb.Parameter{ + {Name: "@id", Value: os.Getenv("AzureCosmosDBCollection")}, + }, + }) + assert.NoError(t, queryCollErr) + collection := &colls[0] + + var items []map[string]interface{} + _, queryErr := dbclient.QueryDocuments( + collection.Self, + documentdb.NewQuery("SELECT * FROM ROOT r WHERE r.id=@id", documentdb.P{Name: "@id", Value: document["id"].(string)}), + &items, + documentdb.CrossPartition(), + ) + + assert.NoError(t, queryErr) + + result := items[0] + // verify the item retrieved from the database matches the item we inserted + assert.Equal(t, document["id"], result["id"]) + assert.Equal(t, document["orderid"], result["orderid"]) + assert.Equal(t, document["partitionKey"], result["partitionKey"]) + assert.Equal(t, document["nestedproperty"].(map[string]interface{})["subproperty"], + result["nestedproperty"].(map[string]interface{})["subproperty"]) + + // cleanup + _, err = dbclient.DeleteDocument(result["_self"].(string), documentdb.PartitionKey(result["partitionKey"].(string))) + assert.NoError(t, err) + + return nil + } + + testInvokeCreateWithoutPartitionKey := func(ctx flow.Context) error { + document := createDocument(true, false) + invokeErr := invokeCreateWithDocument(ctx, document) + + assert.Error(t, invokeErr) + assert.Contains(t, invokeErr.Error(), "missing partitionKey field") + + return nil + } + + testInvokeCreateWithoutID := func(ctx flow.Context) error { + document := createDocument(false, true) + invokeErr := invokeCreateWithDocument(ctx, document) + + assert.Error(t, invokeErr) + assert.Contains(t, invokeErr.Error(), "the required properties - 'id; ' - are missing") + + return nil + } + + testInvokeCreateWithWrongPartitionKey := func(ctx flow.Context) error { + document := createDocument(true, false) + document["wrongPartitionKey"] = "somepkvalue" + invokeErr := invokeCreateWithDocument(ctx, document) + + assert.Error(t, invokeErr) + assert.Contains(t, invokeErr.Error(), "PartitionKey extracted from document doesn't match the one specified in the header") + + return nil + } + flow.New(t, "cosmosdb binding authentication using service principal"). Step(sidecar.Run(sidecarName, embedded.WithoutApp(), embedded.WithComponentsPath("./components/serviceprincipal"), - embedded.WithDaprGRPCPort(currentGrpcPort), - embedded.WithDaprHTTPPort(currentHttpPort), + embedded.WithDaprGRPCPort(currentGRPCPort), + embedded.WithDaprHTTPPort(currentHTTPPort), runtime.WithSecretStores( secretstores_loader.New("local.env", func() secretstores.SecretStore { return secretstore_env.NewEnvSecretStore(log) @@ -59,15 +199,42 @@ func TestKeyVault(t *testing.T) { ports, err = dapr_testing.GetFreePorts(2) assert.NoError(t, err) - currentGrpcPort = ports[0] - currentHttpPort = ports[1] + currentGRPCPort = ports[0] + currentHTTPPort = ports[1] flow.New(t, "cosmosdb binding authentication using master key"). Step(sidecar.Run(sidecarName, embedded.WithoutApp(), embedded.WithComponentsPath("./components/masterkey"), - embedded.WithDaprGRPCPort(currentGrpcPort), - embedded.WithDaprHTTPPort(currentHttpPort), + embedded.WithDaprGRPCPort(currentGRPCPort), + embedded.WithDaprHTTPPort(currentHTTPPort), + runtime.WithSecretStores( + secretstores_loader.New("local.env", func() secretstores.SecretStore { + return secretstore_env.NewEnvSecretStore(log) + }), + ), + runtime.WithOutputBindings( + bindings_loader.NewOutput("azure.cosmosdb", func() bindings.OutputBinding { + return cosmosdbbinding.NewCosmosDB(log) + }), + ))). + Step("verify data sent to output binding is written to Cosmos DB", testInvokeCreateAndVerify). + Step("expect error if id is missing from document", testInvokeCreateWithoutID). + Step("expect error if partition key is missing from document", testInvokeCreateWithoutPartitionKey). + Run() + + ports, err = dapr_testing.GetFreePorts(2) + assert.NoError(t, err) + + currentGRPCPort = ports[0] + currentHTTPPort = ports[1] + + flow.New(t, "cosmosdb binding with wrong partition key specified"). + Step(sidecar.Run(sidecarName, + embedded.WithoutApp(), + embedded.WithComponentsPath("./components/wrongPartitionKey"), + embedded.WithDaprGRPCPort(currentGRPCPort), + embedded.WithDaprHTTPPort(currentHTTPPort), runtime.WithSecretStores( secretstores_loader.New("local.env", func() secretstores.SecretStore { return secretstore_env.NewEnvSecretStore(log) @@ -78,5 +245,6 @@ func TestKeyVault(t *testing.T) { return cosmosdbbinding.NewCosmosDB(log) }), ))). + Step("verify error when wrong partition key used", testInvokeCreateWithWrongPartitionKey). Run() } diff --git a/tests/certification/bindings/azure/cosmosdb/go.mod b/tests/certification/bindings/azure/cosmosdb/go.mod index 2471ba615c..fd8d8a6d41 100644 --- a/tests/certification/bindings/azure/cosmosdb/go.mod +++ b/tests/certification/bindings/azure/cosmosdb/go.mod @@ -3,10 +3,13 @@ module github.com/dapr/components-contrib/tests/certification/bindings/azure/cos go 1.17 require ( + github.com/a8m/documentdb v1.3.1-0.20211026005403-13c3593b3c3a github.com/dapr/components-contrib v1.5.1-rc.1 github.com/dapr/components-contrib/tests/certification v0.0.0-20211130185200-4918900c09e1 github.com/dapr/dapr v1.5.2-0.20211207220041-6296ceb58dec + github.com/dapr/go-sdk v1.3.0 github.com/dapr/kit v0.0.2-0.20210614175626-b9074b64d233 + github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.7.0 ) @@ -30,7 +33,6 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/a8m/documentdb v1.3.1-0.20211026005403-13c3593b3c3a // indirect github.com/andybalholm/brotli v1.0.2 // indirect github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 // indirect @@ -38,7 +40,6 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.1.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/dapr/go-sdk v1.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/fasthttp/router v1.3.8 // indirect @@ -52,7 +53,6 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/cel-go v0.7.3 // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gnostic v0.5.1 // indirect github.com/grandcat/zeroconf v0.0.0-20190424104450-85eadb44205c // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 // indirect