diff --git a/.github/infrastructure/docker-compose-ravendb.yml b/.github/infrastructure/docker-compose-ravendb.yml new file mode 100644 index 0000000000..cd00c9a58d --- /dev/null +++ b/.github/infrastructure/docker-compose-ravendb.yml @@ -0,0 +1,13 @@ +services: + ravendb: + image: ravendb/ravendb + container_name: RavenDb + ports: + - "8080:8080" + environment: + - RAVEN_LICENSE=${RAVENDBLICENSE} + - RAVEN_DATABASE=testdapr + - RAVEN_Setup_Mode=None + - RAVEN_License_Eula_Accepted=true + - RAVEN_Security_UnsecuredAccessAllowed=PrivateNetwork + restart: unless-stopped diff --git a/.github/scripts/test-info.mjs b/.github/scripts/test-info.mjs index 9a659db6f6..9afa921628 100644 --- a/.github/scripts/test-info.mjs +++ b/.github/scripts/test-info.mjs @@ -810,6 +810,12 @@ const components = { requireGCPCredentials: true, certificationSetup: 'certification-state.gcp.firestore-setup.sh', }, + 'state.ravendb': { + conformance: true, + certification: true, + conformanceSetup: 'docker-compose.sh ravendb', + requireRavenDBCredentials: true, + }, } /** @@ -822,6 +828,7 @@ const components = { * @property {boolean?} requireAWSCredentials If true, requires AWS credentials and makes the test "cloud-only" * @property {boolean?} requireGCPCredentials If true, requires GCP credentials and makes the test "cloud-only" * @property {boolean?} requireCloudflareCredentials If true, requires Cloudflare credentials and makes the test "cloud-only" + * @property {boolean?} requireRavenDBCredentials If true, requires RavenDB credentials * @property {boolean?} requireTerraform If true, requires Terraform * @property {boolean?} requireKind If true, requires KinD * @property {string?} conformanceSetup Setup script for conformance tests @@ -843,6 +850,7 @@ const components = { * @property {boolean?} require-aws-credentials Requires AWS credentials * @property {boolean?} require-gcp-credentials Requires GCP credentials * @property {boolean?} require-cloudflare-credentials Requires Cloudflare credentials + * @property {boolean?} require-ravendb-credentials Requires RavenDB credentials * @property {boolean?} require-terraform Requires Terraform * @property {boolean?} require-kind Requires KinD * @property {string?} setup-script Setup script @@ -914,6 +922,9 @@ function GenerateMatrix(testKind, enableCloudTests) { 'require-cloudflare-credentials': comp.requireCloudflareCredentials ? 'true' : undefined, + 'require-ravendb-credentials': comp.requireRavenDBCredentials + ? 'true' + : undefined, 'require-terraform': comp.requireTerraform ? 'true' : undefined, 'require-kind': comp.requireKind ? 'true' : undefined, 'setup-script': comp[testKind + 'Setup'] || undefined, diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index ed98baa84e..6afa7a5d9b 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -218,6 +218,12 @@ jobs: echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> $GITHUB_ENV echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }}" >> $GITHUB_ENV + - name: Set RavenDB env vars + if: matrix.require-ravendb-credentials == 'true' + run: | + echo "::add-mask::${{ secrets.RAVENDBLICENSE }}" + echo "RAVENDBLICENSE=${{ secrets.RAVENDBLICENSE }}" >> $GITHUB_ENV + - name: Configure AWS Credentials if: matrix.require-aws-credentials == 'true' uses: aws-actions/configure-aws-credentials@v1 diff --git a/go.mod b/go.mod index 8de5ed5036..e935398a97 100644 --- a/go.mod +++ b/go.mod @@ -110,6 +110,7 @@ require ( github.com/pkg/sftp v1.13.7 github.com/puzpuzpuz/xsync/v3 v3.0.0 github.com/rabbitmq/amqp091-go v1.9.0 + github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 github.com/redis/go-redis/v9 v9.6.3 github.com/riferrei/srclient v0.7.2 github.com/sendgrid/sendgrid-go v3.13.0+incompatible diff --git a/go.sum b/go.sum index ab720fb6a1..3a55f6c4f3 100644 --- a/go.sum +++ b/go.sum @@ -591,6 +591,7 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -920,6 +921,7 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -1132,6 +1134,7 @@ github.com/kitex-contrib/monitor-prometheus v0.0.0-20210817080809-024dd7bd51e1/g github.com/kitex-contrib/obs-opentelemetry v0.0.0-20220601144657-c60210e3c928/go.mod h1:VvMzPMfgL7iUG92eVZGuRybGVMKzuSrsfMvHHpL7/Ac= github.com/kitex-contrib/obs-opentelemetry/logging/logrus v0.0.0-20220601144657-c60210e3c928/go.mod h1:Eml/0Z+CqgGIPf9JXzLGu+N9NJoy2x5pqypN+hmKArE= github.com/kitex-contrib/tracer-opentracing v0.0.2/go.mod h1:mprt5pxqywFQxlHb7ugfiMdKbABTLI9YrBYs9WmlK5Q= +github.com/kjk/httplogproxy v0.0.0-20190214011443-6743ea9a2d3d/go.mod h1:kkVhzcC9maw+0jdT2UfGGikRmobjydsBiD6ElexuTLk= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -1162,6 +1165,7 @@ github.com/kubemq-io/kubemq-go v1.7.9 h1:dGTcs+cwmoLnnBX1H3xrKU2qd37JODNO/LHRk6V github.com/kubemq-io/kubemq-go v1.7.9/go.mod h1:f6n4qByudW/018Ymol/3s5sjJvt6flEN+ZgP1VVVv0U= github.com/kubemq-io/protobuf v1.3.1 h1:b4QcnpujV8U3go8pa2+FTESl6ygU6hY8APYibRtyemo= github.com/kubemq-io/protobuf v1.3.1/go.mod h1:mzbGBI05R+GhFLD520xweEIvDM+m4nI7ruJDhgEncas= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labd/commercetools-go-sdk v1.3.1 h1:EZnym91AutZXLZ+D1x52kZF35Wq51ZUEMewGCXdoje8= @@ -1516,6 +1520,8 @@ github.com/puzpuzpuz/xsync/v3 v3.0.0 h1:QwUcmah+dZZxy6va/QSU26M6O6Q422afP9jO8Jln github.com/puzpuzpuz/xsync/v3 v3.0.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 h1:hOnThDlsq0e4M7Sl3A3MnMlazYJsNuuDDqywa5mI7wQ= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427/go.mod h1:Zhu1DOotWGZcjom6CZH+8mJ2AD3fOx0QjVIrbpMxN04= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= diff --git a/state/ravendb/metadata.yaml b/state/ravendb/metadata.yaml new file mode 100644 index 0000000000..6134e47e5d --- /dev/null +++ b/state/ravendb/metadata.yaml @@ -0,0 +1,51 @@ +# yaml-language-server: $schema=../../component-metadata-schema.json +schemaVersion: v1 +type: state +name: ravendb +version: v1 +status: development-only +title: "RavenDB" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-ravendb/ +capabilities: + - crud +authenticationProfiles: + - title: "No authentication" + description: | + No authentication. Connect via connection string + metadata: + - name : connectionString + required: true + description: | + Connection string to ravenDB cluster + example: '"http://live-test.ravendb.net"' +metadata: + - name: DatabaseName + description: | + The name of the database to use. + default: '"daprStore"' + example: '"daprStore"' + - name: ServerURL + description: | + Url to ravendb cluster + default: '"127.0.0.1"' + example: '"http://live-test.ravendb.net"' + - name: CertPath + description: | + Path to the certificate for secure connection + example: "/path/to/cert" + - name: KeyPath + description: | + Path to the key for secure connection + example: "/path/to/key" + - name: EnableTTL + description: | + Boolean value that enables or disables RaveDB TTL functionality + example: "true" + default: "true" + - name: TTLFrequency + description: | + Sets RavenDB frequency on running the background expiration task and deleting records + example: "15" + default: "60" \ No newline at end of file diff --git a/state/ravendb/ravendb.go b/state/ravendb/ravendb.go new file mode 100644 index 0000000000..1e3e9fd01b --- /dev/null +++ b/state/ravendb/ravendb.go @@ -0,0 +1,519 @@ +/* +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 ravendb is an implementation of StateStore interface to perform operations on store + +package ravendb + +import ( + "context" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "net/http" + "reflect" + "strings" + "time" + + jsoniterator "github.com/json-iterator/go" + ravendb "github.com/ravendb/ravendb-go-client" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" + stateutils "github.com/dapr/components-contrib/state/utils" + "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" +) + +const ( + defaultDatabaseName = "daprStore" + databaseName = "databaseName" + serverURL = "serverUrl" + httpsPrefix = "https" + certPath = "certPath" + keyPath = "keyPath" + enableTTL = "enableTTL" + ttlFrequency = "ttlFrequency" + changeVector = "@change-vector" + expires = "@expires" + defaultEnableTTL = true + defaultTTLFrequency = int64(60) +) + +type RavenDB struct { + state.BulkStore + + documentStore *ravendb.DocumentStore + metadata RavenDBMetadata + + features []state.Feature + logger logger.Logger +} + +type RavenDBMetadata struct { + DatabaseName string + ServerURL string + CertPath string + KeyPath string + EnableTTL bool + TTLFrequency int64 +} + +type Item struct { + ID string + Value string + Etag string + TTL *time.Time +} + +func NewRavenDB(logger logger.Logger) state.Store { + store := &RavenDB{ + features: []state.Feature{ + state.FeatureETag, + state.FeatureTransactional, + state.FeatureTTL, + }, + logger: logger, + } + store.BulkStore = state.NewDefaultBulkStore(store) + return store +} + +func (r *RavenDB) Init(ctx context.Context, metadata state.Metadata) (err error) { + r.metadata, err = getRavenDBMetaData(metadata) + if err != nil { + return err + } + // TODO: Operation timeout? + store, err := r.getRavenDBStore(ctx) + if err != nil { + return errors.New("error in creating Raven DB Store") + } + + r.initTTL(store) + r.setupDatabase(store) + r.documentStore = store + + return nil +} + +// Features returns the features available in this state store. +func (r *RavenDB) Features() []state.Feature { + return r.features +} + +func (r *RavenDB) Delete(ctx context.Context, req *state.DeleteRequest) error { + session, err := r.documentStore.OpenSession("") + if err != nil { + return errors.New("error opening session while deleting") + } + defer session.Close() + + err = r.deleteInternal(ctx, req, session, false) + if err != nil { + return err + } + + err = session.SaveChanges() + if err != nil { + if isConcurrencyException(err) { + return state.NewETagError(state.ETagMismatch, err) + } + return errors.New("error saving changes") + } + + return nil +} + +func (r *RavenDB) Get(ctx context.Context, req *state.GetRequest) (*state.GetResponse, error) { + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return &state.GetResponse{}, fmt.Errorf("error opening session while storing data faild with error %s", err) + } + defer session.Close() + + var item *Item + err = session.Load(&item, req.Key) + if err != nil { + return &state.GetResponse{}, fmt.Errorf("error loading data %s", err) + } + if item == nil { + return &state.GetResponse{}, nil + } + ravenMeta, err := session.GetMetadataFor(item) + if err != nil { + return &state.GetResponse{}, fmt.Errorf("error getting metadata for %s", req.Key) + } + + var meta map[string]string + ttl, okTTL := ravenMeta.Get(expires) + if okTTL { + meta = map[string]string{ + state.GetRespMetaKeyTTLExpireTime: ttl.(string), + } + } + + var etagResp string + eTag, okETag := ravenMeta.Get(changeVector) + if okETag { + etagResp = eTag.(string) + } else { + etagResp = "" + } + + resp := &state.GetResponse{ + Data: []byte(item.Value), + ETag: &etagResp, + Metadata: meta, + } + return resp, nil +} + +func (r *RavenDB) Set(ctx context.Context, req *state.SetRequest) error { + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return fmt.Errorf("error opening session while storing data faild with error %s", err) + } + defer session.Close() + err = r.setInternal(ctx, req, session) + if err != nil { + return fmt.Errorf("error processing item %s", err) + } + + err = session.SaveChanges() + if err != nil { + if isConcurrencyException(err) { + return state.NewETagError(state.ETagMismatch, err) + } + return fmt.Errorf("error saving changes %s", err) + } + return nil +} + +func (r *RavenDB) Ping(ctx context.Context) error { + session, err := r.documentStore.OpenSession("") + if err != nil { + return fmt.Errorf("error opening session while storing data faild with error %s", err) + } + defer session.Close() + + return nil +} + +func (r *RavenDB) Multi(ctx context.Context, request *state.TransactionalStateRequest) error { + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return fmt.Errorf("error opening session while storing data faild with error %s", err) + } + defer session.Close() + for _, o := range request.Operations { + switch req := o.(type) { + case state.SetRequest: + err = r.setInternal(ctx, &req, session) + case state.DeleteRequest: + err = r.deleteInternal(ctx, &req, session, true) + } + + if err != nil { + return fmt.Errorf("error parsing requests: %w", err) + } + } + + err = session.SaveChanges() + if err != nil { + if isConcurrencyException(err) { + return state.NewETagError(state.ETagMismatch, err) + } + return fmt.Errorf("error during transaction, aborting the transaction: %w", err) + } + + return nil +} + +func (r *RavenDB) BulkGet(ctx context.Context, req []state.GetRequest, _ state.BulkGetOpts) ([]state.BulkGetResponse, error) { + // If nothing is being requested, short-circuit + if len(req) == 0 { + return nil, nil + } + keys := make([]string, len(req)) + for i, r := range req { + keys[i] = r.Key + } + session, err := r.documentStore.OpenSession(r.metadata.DatabaseName) + if err != nil { + return []state.BulkGetResponse{}, fmt.Errorf("error opening session while storing data faild with error %s", err) + } + defer session.Close() + + items := make(map[string]*Item, len(keys)) + err = session.LoadMulti(items, keys) + if err != nil { + return []state.BulkGetResponse{}, fmt.Errorf("faield bulk get with error: %s", err) + } + + resp := make([]state.BulkGetResponse, 0, len(items)) + + for ID, current := range items { + if current == nil { + convert := state.BulkGetResponse{ + Key: ID, + Data: nil, + ETag: nil, + Metadata: make(map[string]string), + } + resp = append(resp, convert) + } else { + ravenMeta, err := session.GetMetadataFor(current) + etagResp := "" + if err == nil { + eTag, okETag := ravenMeta.Get(changeVector) + if okETag { + etagResp = eTag.(string) + } + } + convert := state.BulkGetResponse{ + Key: current.ID, + Data: []byte(current.Value), + ETag: &etagResp, + Metadata: make(map[string]string), + } + resp = append(resp, convert) + } + } + + return resp, nil +} + +func (r *RavenDB) marshalToString(v interface{}) (string, error) { + if buf, ok := v.([]byte); ok { + return string(buf), nil + } + + return jsoniterator.ConfigFastest.MarshalToString(v) +} + +func (r *RavenDB) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := RavenDBMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) + return +} + +func (r *RavenDB) setInternal(ctx context.Context, req *state.SetRequest, session *ravendb.DocumentSession) error { + data, err := r.marshalToString(req.Value) + if err != nil { + return fmt.Errorf("ravendb error: failed to marshal value for key %s: %w", req.Key, err) + } + + item := &Item{ + ID: req.Key, + Value: data, + } + + if req.Options.Concurrency == state.FirstWrite { + // First write wins, we send empty change vector to check if exists + + // current SDK version of go doesn't let us to check concurency violation on items that are not in databse. + // we need to try to load, and do regullar save if item is not in DB (real first save) + // if we have item in DB we can try to override it with concurency check + var newItem *Item + err = session.Load(&newItem, req.Key) + if newItem == nil { + err = session.Store(item) + } else { + var eTag string + if req.HasETag() { + eTag = *req.ETag + } else { + eTag = RandStringRunes(5) + } + + if newItem.Value == item.Value { + return fmt.Errorf("error storing data: %s", err) + } + newItem.Value = item.Value + err = session.StoreWithChangeVectorAndID(newItem, eTag, req.Key) + } + if err != nil { + return fmt.Errorf("error storing data: %s", err) + } + } else { + // Last write wins + if req.HasETag() { + eTag := *req.ETag + err = session.StoreWithChangeVectorAndID(item, eTag, req.Key) + if err != nil { + return state.NewETagError(state.ETagMismatch, err) + } + } else { + err = session.Store(item) + } + + if err != nil { + return fmt.Errorf("error storing data: %s", err) + } + } + + reqTTL, err := stateutils.ParseTTL(req.Metadata) + if err != nil { + return fmt.Errorf("failed to parse TTL: %w", err) + } + + if reqTTL != nil { + metaData, err := session.Advanced().GetMetadataFor(item) + if err != nil { + return errors.New("failed to get metadata for item") + } + expiry := time.Now().Add(time.Second * time.Duration(*reqTTL)).UTC() + iso8601String := expiry.Format("2006-01-02T15:04:05.9999999Z07:00") + metaData.Put(expires, iso8601String) + } + return nil +} + +func (r *RavenDB) deleteInternal(ctx context.Context, req *state.DeleteRequest, session *ravendb.DocumentSession, fromTransaction bool) error { + var err error + if fromTransaction { + var itemToDelete *Item + err = session.Load(&itemToDelete, req.Key) + if err == nil && itemToDelete != nil { + err = session.Delete(itemToDelete) + } + } else { + if req.HasETag() { + err = session.DeleteByID(req.Key, *req.ETag) + } else { + // TODO: Fix after update to ravendb sdk + err = session.DeleteByID(req.Key, "") + } + } + + if err != nil { + return err + } + + return nil +} + +func (r *RavenDB) getRavenDBStore(ctx context.Context) (*ravendb.DocumentStore, error) { + serverNodes := []string{r.metadata.ServerURL} + store := ravendb.NewDocumentStore(serverNodes, r.metadata.DatabaseName) + if strings.HasPrefix(r.metadata.ServerURL, httpsPrefix) { + cer, err := tls.LoadX509KeyPair(r.metadata.CertPath, r.metadata.KeyPath) + if err != nil { + return nil, err + } + store.Certificate = &cer + x509cert, err := x509.ParseCertificate(cer.Certificate[0]) + if err != nil { + return nil, err + } + store.TrustStore = x509cert + if store.TrustStore == nil { + panic("nil trust store") + } + } + + if err := store.Initialize(); err != nil { + return nil, err + } + return store, nil +} + +func (r *RavenDB) Close() error { + if r.documentStore == nil { + return nil + } + + r.documentStore.Close() + return nil +} + +func (r *RavenDB) initTTL(store *ravendb.DocumentStore) { + configurationExppiration := ravendb.ExpirationConfiguration{ + Disabled: !r.metadata.EnableTTL, + DeleteFrequencyInSec: &r.metadata.TTLFrequency, + } + operation, err := ravendb.NewConfigureExpirationOperationWithConfiguration(&configurationExppiration) + if err != nil { + return + } + store.Maintenance().Send(operation) +} + +func (r *RavenDB) setupDatabase(store *ravendb.DocumentStore) { + operation := ravendb.NewGetDatabaseRecordOperation(r.metadata.DatabaseName) + err := store.Maintenance().Server().Send(operation) + if err == nil { + if operation.Command != nil && operation.Command.RavenCommandBase.StatusCode == http.StatusNotFound { + databaseRecord := ravendb.DatabaseRecord{ + DatabaseName: r.metadata.DatabaseName, + Disabled: false, + } + createOp := ravendb.NewCreateDatabaseOperation(&databaseRecord, 1) + err = store.Maintenance().Server().Send(createOp) + if err != nil { + return + } + } + } +} + +func getRavenDBMetaData(meta state.Metadata) (RavenDBMetadata, error) { + m := RavenDBMetadata{ + DatabaseName: defaultDatabaseName, + EnableTTL: defaultEnableTTL, + TTLFrequency: defaultTTLFrequency, + } + + err := kitmd.DecodeMetadata(meta.Properties, &m) + if err != nil { + return m, err + } + + if m.ServerURL == "" { + return m, errors.New("server url is required") + } + + if strings.HasPrefix(m.ServerURL, httpsPrefix) { + if m.CertPath == "" || m.KeyPath == "" { + return m, errors.New("certificate and key are required for secure connection") + } + } + + return m, nil +} + +func isConcurrencyException(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "Optimistic concurrency violation") +} + +func RandStringRunes(n int) string { + // Create a byte slice to hold the random bytes + bytes := make([]byte, n) + + // Fill the byte slice with random bytes + _, err := rand.Read(bytes) + if err != nil { + return "" + } + + // Encode the random bytes to a base64 string + // This will make it printable/usable as a string + return base64.URLEncoding.EncodeToString(bytes)[:n] +} diff --git a/state/ravendb/ravendb_test.go b/state/ravendb/ravendb_test.go new file mode 100644 index 0000000000..d097865b19 --- /dev/null +++ b/state/ravendb/ravendb_test.go @@ -0,0 +1,129 @@ +package ravendb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/state" +) + +func TestGetRavenDBMetadata(t *testing.T) { + t.Run("With default database name", func(t *testing.T) { + properties := map[string]string{ + serverURL: "127.0.0.1", + } + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, defaultDatabaseName, metadata.DatabaseName) + assert.Equal(t, defaultEnableTTL, metadata.EnableTTL) + assert.Equal(t, defaultTTLFrequency, metadata.TTLFrequency) + }) + + t.Run("With custom database name", func(t *testing.T) { + properties := map[string]string{ + serverURL: "127.0.0.1", + databaseName: "TestDB", + } + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, properties[databaseName], metadata.DatabaseName) + assert.Equal(t, defaultEnableTTL, metadata.EnableTTL) + assert.Equal(t, defaultTTLFrequency, metadata.TTLFrequency) + }) + + t.Run("With custom enable ttl values", func(t *testing.T) { + properties := map[string]string{ + serverURL: "127.0.0.1", + databaseName: "TestDB", + enableTTL: "false", + ttlFrequency: "15", + } + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, properties[databaseName], metadata.DatabaseName) + assert.False(t, metadata.EnableTTL) + assert.Equal(t, int64(15), metadata.TTLFrequency) + }) + + t.Run("with https without cert and key", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + _, err := getRavenDBMetaData(m) + require.Errorf(t, err, "certificate and key are required for secure connection") + }) + + t.Run("with https without key", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + certPath: "/path/to/cert", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + _, err := getRavenDBMetaData(m) + require.Errorf(t, err, "certificate and key are required for secure connection") + }) + + t.Run("with https without cert", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + keyPath: "/path/to/key", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + _, err := getRavenDBMetaData(m) + require.Errorf(t, err, "certificate and key are required for secure connection") + }) + + t.Run("with https", func(t *testing.T) { + properties := map[string]string{ + serverURL: "https://test.live.ravendb.com", + databaseName: "TestDB", + certPath: "/path/to/cert", + keyPath: "/path/to/key", + } + + m := state.Metadata{ + Base: metadata.Base{Properties: properties}, + } + + metadata, err := getRavenDBMetaData(m) + require.NoError(t, err) + assert.Equal(t, properties[serverURL], metadata.ServerURL) + assert.Equal(t, properties[databaseName], metadata.DatabaseName) + assert.Equal(t, properties[certPath], metadata.CertPath) + assert.Equal(t, properties[keyPath], metadata.KeyPath) + }) +} diff --git a/tests/certification/go.mod b/tests/certification/go.mod index 922874f4af..68872303f3 100644 --- a/tests/certification/go.mod +++ b/tests/certification/go.mod @@ -267,6 +267,7 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/statsd_exporter v0.22.7 // indirect github.com/puzpuzpuz/xsync/v3 v3.0.0 // indirect + github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/redis/go-redis/v9 v9.6.3 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect diff --git a/tests/certification/go.sum b/tests/certification/go.sum index 451a4e5634..1f1cda353b 100644 --- a/tests/certification/go.sum +++ b/tests/certification/go.sum @@ -491,6 +491,7 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -773,6 +774,7 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -966,6 +968,7 @@ github.com/kitex-contrib/monitor-prometheus v0.0.0-20210817080809-024dd7bd51e1/g github.com/kitex-contrib/obs-opentelemetry v0.0.0-20220601144657-c60210e3c928/go.mod h1:VvMzPMfgL7iUG92eVZGuRybGVMKzuSrsfMvHHpL7/Ac= github.com/kitex-contrib/obs-opentelemetry/logging/logrus v0.0.0-20220601144657-c60210e3c928/go.mod h1:Eml/0Z+CqgGIPf9JXzLGu+N9NJoy2x5pqypN+hmKArE= github.com/kitex-contrib/tracer-opentracing v0.0.2/go.mod h1:mprt5pxqywFQxlHb7ugfiMdKbABTLI9YrBYs9WmlK5Q= +github.com/kjk/httplogproxy v0.0.0-20190214011443-6743ea9a2d3d/go.mod h1:kkVhzcC9maw+0jdT2UfGGikRmobjydsBiD6ElexuTLk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -988,6 +991,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= @@ -1269,6 +1273,8 @@ github.com/puzpuzpuz/xsync/v3 v3.0.0 h1:QwUcmah+dZZxy6va/QSU26M6O6Q422afP9jO8Jln github.com/puzpuzpuz/xsync/v3 v3.0.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427 h1:hOnThDlsq0e4M7Sl3A3MnMlazYJsNuuDDqywa5mI7wQ= +github.com/ravendb/ravendb-go-client v0.0.0-20240723121956-2b87f37fe427/go.mod h1:Zhu1DOotWGZcjom6CZH+8mJ2AD3fOx0QjVIrbpMxN04= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= diff --git a/tests/certification/state/ravendb/README.md b/tests/certification/state/ravendb/README.md new file mode 100644 index 0000000000..972a41da7d --- /dev/null +++ b/tests/certification/state/ravendb/README.md @@ -0,0 +1,17 @@ +# RavenDB State Store certification testing + +This project aims to test the RavenDB State Store component under various conditions. + +## Test plan +Run: +go test -v -tags "unit certtests" -count=1 . + +## Basic Test for CRUD operations: +1. Able to create and test connection. +2. Able to do set, fetch, update and delete. +3. Negative test to fetch record with key, that is not present. + +## Component must reconnect when server or network errors are encountered + +## Infra test: +1. When RavenDB goes down and then comes back up - client is able to connect \ No newline at end of file diff --git a/tests/certification/state/ravendb/components/default/ravendb.yaml b/tests/certification/state/ravendb/components/default/ravendb.yaml new file mode 100644 index 0000000000..19cabb4e70 --- /dev/null +++ b/tests/certification/state/ravendb/components/default/ravendb.yaml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.ravendb + version: v1 + metadata: + - name: DatabaseName + value: "testdapr" + - name: ServerURL + value: "http://127.0.0.1:8080" + - name: TTLFrequency + value: "1" diff --git a/tests/certification/state/ravendb/config.yaml b/tests/certification/state/ravendb/config.yaml new file mode 100644 index 0000000000..fdfcd802dc --- /dev/null +++ b/tests/certification/state/ravendb/config.yaml @@ -0,0 +1,6 @@ +apiVersion: dapr.io/v1alpha1 +kind: Configuration +metadata: + name: keyvaultconfig +spec: + features: \ No newline at end of file diff --git a/tests/certification/state/ravendb/docker-compose.yml b/tests/certification/state/ravendb/docker-compose.yml new file mode 100644 index 0000000000..266101793f --- /dev/null +++ b/tests/certification/state/ravendb/docker-compose.yml @@ -0,0 +1,14 @@ +services: + ravendb: + image: ravendb/ravendb + container_name: RavenDb + ports: + - "8080:8080" + environment: + - RAVEN_LICENSE=${RavenDbLicense} + - RAVEN_DATABASE=testdapr + - RAVEN_Setup_Mode=None + - RAVEN_License_Eula_Accepted=true + - RAVEN_Security_UnsecuredAccessAllowed=PrivateNetwork + - RavenDbLicense + restart: unless-stopped \ No newline at end of file diff --git a/tests/certification/state/ravendb/ravendb_test.go b/tests/certification/state/ravendb/ravendb_test.go new file mode 100644 index 0000000000..e4ca48511c --- /dev/null +++ b/tests/certification/state/ravendb/ravendb_test.go @@ -0,0 +1,270 @@ +package ravendb_test + +import ( + "fmt" + "github.com/dapr/components-contrib/state" + stateRavenDB "github.com/dapr/components-contrib/state/ravendb" + "github.com/dapr/components-contrib/tests/certification/embedded" + "github.com/dapr/components-contrib/tests/certification/flow" + "github.com/dapr/components-contrib/tests/certification/flow/dockercompose" + "github.com/dapr/components-contrib/tests/certification/flow/network" + "github.com/dapr/components-contrib/tests/certification/flow/sidecar" + stateLoader "github.com/dapr/dapr/pkg/components/state" + daprTesting "github.com/dapr/dapr/pkg/testing" + daprClient "github.com/dapr/go-sdk/client" + "github.com/dapr/kit/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "strconv" + "testing" + "time" +) + +const ( + sidecarNamePrefix = "ravendb-sidecar-" + stateStoreName = "statestore" + certificationTestPrefix = "stable-certification-" + dockerComposeYAML = "docker-compose.yml" +) + +func TestRavenDB(t *testing.T) { + fmt.Printf("testing started:") + log := logger.NewLogger("dapr.components") + stateStore := stateRavenDB.NewRavenDB(log).(*stateRavenDB.RavenDB) + ports, err := daprTesting.GetFreePorts(2) + require.NoError(t, err) + + stateRegistry := stateLoader.NewRegistry() + stateRegistry.Logger = log + stateRegistry.RegisterComponent(func(l logger.Logger) state.Store { + return stateStore + }, "ravenDb") + + currentGrpcPort := ports[0] + currentHTTPPort := ports[1] + + basicTest := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", []byte("ravenCert1"), nil) + require.NoError(t, err) + + //this is set for the test after restart + err = client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key2", []byte("ravenCert2"), nil) + require.NoError(t, err) + + //this is set for the test after restart + err = client.SaveState(ctx, stateStoreName, "deleteInTransaction", []byte("ravenCert3"), nil) + require.NoError(t, err) + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, err) + assert.Equal(t, "ravenCert1", string(item.Value)) + + errUpdate := client.SaveState(ctx, stateStoreName, certificationTestPrefix+"key1", []byte("ravenCertUpdate"), nil) + require.NoError(t, errUpdate) + item, errUpdatedGet := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, errUpdatedGet) + assert.Equal(t, "ravenCertUpdate", string(item.Value)) + + // delete state + err = client.DeleteState(ctx, stateStoreName, certificationTestPrefix+"key1", nil) + require.NoError(t, err) + + return nil + } + + eTagTest := func() func(ctx flow.Context) error { + return func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + err = client.SaveState(ctx, stateStoreName, "k", []byte("v1"), nil) + require.NoError(t, err) + + resp1, err := client.GetState(ctx, stateStoreName, "k", nil) + require.NoError(t, err) + + err = client.SaveStateWithETag(ctx, stateStoreName, "k", []byte("v2"), resp1.Etag, nil) + require.NoError(t, err) + + resp2, err := client.GetState(ctx, stateStoreName, "k", nil) + require.NoError(t, err) + + err = client.SaveStateWithETag(ctx, stateStoreName, "k", []byte("v3"), "900invalid", nil) + require.Error(t, err) + + resp3, err := client.GetState(ctx, stateStoreName, "k", nil) + require.NoError(t, err) + assert.Equal(t, resp2.Etag, resp3.Etag) + assert.Equal(t, "v2", string(resp3.Value)) + + return nil + } + } + + timeToLiveTest := func() func(ctx flow.Context) error { + return func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprint(currentGrpcPort)) + require.NoError(t, err) + defer client.Close() + + assert.Error(t, client.SaveState(ctx, stateStoreName, certificationTestPrefix+"ttl1", []byte("revendbCert"), map[string]string{ + "ttlInSeconds": "mock value", + })) + require.NoError(t, client.SaveState(ctx, stateStoreName, certificationTestPrefix+"ttl2", []byte("revendbCert2"), map[string]string{ + "ttlInSeconds": "-1", + })) + require.NoError(t, client.SaveState(ctx, stateStoreName, certificationTestPrefix+"ttl3", []byte("revendbCert3"), map[string]string{ + "ttlInSeconds": "3", + })) + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"ttl3", nil) + require.NoError(t, err) + assert.Equal(t, "revendbCert3", string(item.Value)) + assert.Contains(t, item.Metadata, "ttlExpireTime") + expireTime, err := time.Parse(time.RFC3339, item.Metadata["ttlExpireTime"]) + require.NoError(t, err) + assert.InDelta(t, time.Now().Add(time.Second*3).Unix(), expireTime.Unix(), 2) + + assert.Eventually(t, func() bool { + item, err = client.GetState(ctx, stateStoreName, certificationTestPrefix+"ttl3", nil) + require.NoError(t, err) + return len(item.Value) == 0 + }, time.Second*10, time.Second*1) + + return nil + } + } + + transactionsTest := func() func(ctx flow.Context) error { + return func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(strconv.Itoa(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + err = client.ExecuteStateTransaction(ctx, stateStoreName, nil, []*daprClient.StateOperation{ + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey1", + Value: []byte("reqVal1"), + Metadata: map[string]string{ + "ttlInSeconds": "-1", + }, + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey2", + Value: []byte("reqVal2"), + Metadata: map[string]string{ + "ttlInSeconds": "222", + }, + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey3", + Value: []byte("reqVal3"), + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey4", + Value: []byte("reqVal101"), + Metadata: map[string]string{ + "ttlInSeconds": "50", + }, + }, + }, + { + Type: daprClient.StateOperationTypeUpsert, + Item: &daprClient.SetStateItem{ + Key: "reqKey5", + Value: []byte("reqVal103"), + Metadata: map[string]string{ + "ttlInSeconds": "50", + }, + }, + }, + { + Type: daprClient.StateOperationTypeDelete, + Item: &daprClient.SetStateItem{ + Key: "deleteInTransaction", + }, + }, + }) + require.NoError(t, err) + + resp1, err := client.GetState(ctx, stateStoreName, "reqKey1", nil) + require.NoError(t, err) + assert.Equal(t, "reqVal1", string(resp1.Value)) + + resp3, err := client.GetState(ctx, stateStoreName, "reqKey3", nil) + require.NoError(t, err) + assert.Equal(t, "reqVal3", string(resp3.Value)) + + resp4, err := client.GetState(ctx, stateStoreName, "deleteInTransaction", nil) + require.NoError(t, err) + assert.Nil(t, resp4.Value) + + return nil + } + } + + testGetAfterRavenDBRestart := func(ctx flow.Context) error { + client, err := daprClient.NewClientWithPort(fmt.Sprint(currentGrpcPort)) + if err != nil { + panic(err) + } + defer client.Close() + + // get state + item, err := client.GetState(ctx, stateStoreName, certificationTestPrefix+"key2", nil) + require.NoError(t, err) + assert.Equal(t, "ravenCert2", string(item.Value)) + + return nil + } + + flow.New(t, "Connecting RavenDB And Verifying majority of the tests."). + Step(dockercompose.Run("ravendb", dockerComposeYAML)). + Step("Waiting for component to start...", flow.Sleep(20*time.Second)). + Step(sidecar.Run(sidecarNamePrefix+"dockerClusterDefault", + embedded.WithoutApp(), + embedded.WithDaprGRPCPort(strconv.Itoa(currentGrpcPort)), + embedded.WithDaprHTTPPort(strconv.Itoa(currentHTTPPort)), + embedded.WithResourcesPath("components/default"), + embedded.WithStates(stateRegistry))). + Step("Waiting for component to load...", flow.Sleep(10*time.Second)). + Step("Run basic test", basicTest). + Step("Run Etag test", eTagTest()). + Step("Run transaction test", transactionsTest()). + Step("Run time to live test", timeToLiveTest()). + Step("Interrupt network", + network.InterruptNetwork(5*time.Second, nil, nil, "27017:27017")). + // Component should recover at this point. + Step("Wait", flow.Sleep(10*time.Second)). + Step("Run basic test again to verify reconnection occurred", basicTest). + Step("Stop RavenDB server", dockercompose.Stop("ravendb", dockerComposeYAML)). + Step("Start RavenDB server", dockercompose.Start("ravendb", dockerComposeYAML)). + Step("Waiting for component to start...", flow.Sleep(10*time.Second)). + Step("Get Values Saved Earlier And Not Expired, after RavenDB restart", testGetAfterRavenDBRestart). + Step("Wait to check documents", flow.Sleep(10*time.Second)). + Run() +} diff --git a/tests/config/state/ravendb/statestore.yaml b/tests/config/state/ravendb/statestore.yaml new file mode 100644 index 0000000000..2abd8333f3 --- /dev/null +++ b/tests/config/state/ravendb/statestore.yaml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.ravendb + version: v1 + metadata: + - name: DatabaseName + value: "testdapr" + - name: ServerURL + value: "http://127.0.0.1:8080" + - name: TTLFrequency + value: "1" \ No newline at end of file diff --git a/tests/config/state/tests.yml b/tests/config/state/tests.yml index ed0f60de09..b070953708 100644 --- a/tests/config/state/tests.yml +++ b/tests/config/state/tests.yml @@ -106,3 +106,5 @@ components: operations: [] - component: gcp.firestore.cloud operations: [] + - component: ravendb + operations: [ "first-write", "etag", "ttl", "transaction" ] diff --git a/tests/conformance/state_test.go b/tests/conformance/state_test.go index e102a68b27..1369ed2329 100644 --- a/tests/conformance/state_test.go +++ b/tests/conformance/state_test.go @@ -41,6 +41,7 @@ import ( s_oracledatabase "github.com/dapr/components-contrib/state/oracledatabase" s_postgresql_v1 "github.com/dapr/components-contrib/state/postgresql/v1" s_postgresql_v2 "github.com/dapr/components-contrib/state/postgresql/v2" + s_ravendb "github.com/dapr/components-contrib/state/ravendb" s_redis "github.com/dapr/components-contrib/state/redis" s_rethinkdb "github.com/dapr/components-contrib/state/rethinkdb" s_sqlite "github.com/dapr/components-contrib/state/sqlite" @@ -141,6 +142,8 @@ func loadStateStore(name string) state.Store { return s_gcpfirestore.NewFirestoreStateStore(testLogger) case "gcp.firestore.cloud": return s_gcpfirestore.NewFirestoreStateStore(testLogger) + case "ravendb": + return s_ravendb.NewRavenDB(testLogger) case "coherence": return s_coherence.NewCoherenceStateStore(testLogger) default: