diff --git a/docs/service_spec.md b/docs/service_spec.md index f3ce7b0..27f7311 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -94,6 +94,12 @@ v4 of the event schema originally required a `contextKeys` property on all featu This means that the SDK supports technology migrations, a feature which allows customers to migrate between data sources using well-defined migration stages. +#### Capability `"persistent-data-store"` + +This means the SDK is capable of interacting with external persistent data stores. The test harness must further identify which store types are supported through additional capabilities listed below. + +- `persistent-data-store-redis`: This means the SDK is capable of interacting with a Redis data store. + #### Capability `"polling-gzip"` This means the SDK is requesting gzip compression support on polling payloads. The SDK is expected to set the `Accept-Encoding` header to `gzip` in addition to enabling this capability. diff --git a/go.mod b/go.mod index 0f76bbb..997eac1 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/fatih/color v1.13.0 + github.com/go-redis/redis v6.15.9+incompatible github.com/gorilla/mux v1.8.0 github.com/launchdarkly/eventsource v1.6.2 github.com/launchdarkly/go-jsonstream/v3 v3.0.0 @@ -13,7 +14,7 @@ require ( github.com/redis/go-redis/v9 v9.6.1 github.com/stretchr/testify v1.7.0 golang.org/x/exp v0.0.0-20220823124025-807a23277127 - gopkg.in/yaml.v3 v3.0.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -25,7 +26,9 @@ require ( github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/gomega v1.34.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/sys v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 854ba58..02cf262 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,26 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -36,6 +54,17 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= @@ -48,17 +77,61 @@ github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdktests/server_side_persistence_base.go b/sdktests/server_side_persistence_base.go index dec4dde..0b2dac3 100644 --- a/sdktests/server_side_persistence_base.go +++ b/sdktests/server_side_persistence_base.go @@ -14,52 +14,60 @@ import ( ) func doServerSidePersistentTests(t *ldtest.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Password: "", // no password set - DB: 0, // use default DB - }) - - newServerSidePersistentTests(t, &RedisPersistentStore{redis: rdb}).Run(t) + if t.Capabilities().Has(servicedef.CapabilityPersistentDataStoreRedis) { + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + t.Run("redis", newServerSidePersistentTests(t, &RedisPersistentStore{redis: rdb}).Run) + } } type PersistentStore interface { DSN() string - WriteData(key string, data map[string]string) error + Get(key string) (string, error) + GetMap(key string) (map[string]string, error) + WriteMap(key string, data map[string]string) error + + Type() servicedef.SDKConfigPersistentType Reset() error } type ServerSidePersistentTests struct { + CommonStreamingTests persistentStore PersistentStore initialFlags map[string]string } func newServerSidePersistentTests(t *ldtest.T, persistentStore PersistentStore) *ServerSidePersistentTests { flagKeyBytes, err := - ldbuilders.NewFlagBuilder("flag-key").Version(1). - On(true).Variations(ldvalue.String("off"), ldvalue.String("match"), ldvalue.String("fallthrough")). - OffVariation(0). - FallthroughVariation(2). + ldbuilders.NewFlagBuilder("flag-key").Version(100). + On(true).Variations(ldvalue.String("fallthrough"), ldvalue.String("other")). + OffVariation(1). + FallthroughVariation(0). Build().MarshalJSON() require.NoError(t, err) initialFlags := map[string]string{"flag-key": string(flagKeyBytes)} uncachedFlagKeyBytes, err := - ldbuilders.NewFlagBuilder("uncached-flag-key").Version(1). - On(true).Variations(ldvalue.String("off"), ldvalue.String("match"), ldvalue.String("fallthrough")). - OffVariation(0). - FallthroughVariation(2). + ldbuilders.NewFlagBuilder("uncached-flag-key").Version(100). + On(true).Variations(ldvalue.String("fallthrough"), ldvalue.String("other")). + OffVariation(1). + FallthroughVariation(0). Build().MarshalJSON() require.NoError(t, err) initialFlags["uncached-flag-key"] = string(uncachedFlagKeyBytes) return &ServerSidePersistentTests{ - persistentStore: persistentStore, - initialFlags: initialFlags, + CommonStreamingTests: NewCommonStreamingTests(t, "serverSidePersistenceTests"), + persistentStore: persistentStore, + initialFlags: initialFlags, } } @@ -68,11 +76,12 @@ func (s *ServerSidePersistentTests) Run(t *ldtest.T) { t.Run("uses custom prefix", s.usesCustomPrefix) t.Run("daemon mode", s.doDaemonModeTests) + t.Run("read-write", s.doReadWriteTests) } func (s *ServerSidePersistentTests) usesDefaultPrefix(t *ldtest.T) { require.NoError(t, s.persistentStore.Reset()) - require.NoError(t, s.persistentStore.WriteData("launchdarkly:features", s.initialFlags)) + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) persistence := NewPersistence() persistence.SetStore(servicedef.SDKConfigPersistentStore{ @@ -80,7 +89,7 @@ func (s *ServerSidePersistentTests) usesDefaultPrefix(t *ldtest.T) { DSN: s.persistentStore.DSN(), }) persistence.SetCache(servicedef.SDKConfigPersistentCache{ - Mode: servicedef.Off, + Mode: servicedef.CacheModeOff, }) client := NewSDKClient(t, persistence) @@ -99,7 +108,7 @@ func (s *ServerSidePersistentTests) usesCustomPrefix(t *ldtest.T) { Prefix: customPrefix, }) persistence.SetCache(servicedef.SDKConfigPersistentCache{ - Mode: servicedef.Off, + Mode: servicedef.CacheModeOff, }) client := NewSDKClient(t, persistence) @@ -113,7 +122,7 @@ func (s *ServerSidePersistentTests) usesCustomPrefix(t *ldtest.T) { "flag value was updated, but it should not have been", ) - require.NoError(t, s.persistentStore.WriteData(customPrefix+":features", s.initialFlags)) + require.NoError(t, s.persistentStore.WriteMap(customPrefix+":features", s.initialFlags)) pollUntilFlagValueUpdated(t, client, "flag-key", ldcontext.New("user-key"), ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")) diff --git a/sdktests/server_side_persistence_daemon.go b/sdktests/server_side_persistence_daemon.go index 5d532bf..a4614ed 100644 --- a/sdktests/server_side_persistence_daemon.go +++ b/sdktests/server_side_persistence_daemon.go @@ -29,7 +29,7 @@ func (s *ServerSidePersistentTests) ignoresInitialization(t *ldtest.T) { DSN: s.persistentStore.DSN(), }) persistence.SetCache(servicedef.SDKConfigPersistentCache{ - Mode: servicedef.Off, + Mode: servicedef.CacheModeOff, }) context := ldcontext.New("user-key") @@ -49,14 +49,14 @@ func (s *ServerSidePersistentTests) ignoresInitialization(t *ldtest.T) { result.Reason.Value().GetErrorKind() == ldreason.EvalErrorFlagNotFound }, time.Second, time.Millisecond*20, "flag was found before it should have been") - require.NoError(t, s.persistentStore.WriteData("launchdarkly:features", s.initialFlags)) + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) pollUntilFlagValueUpdated(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")) } func (s *ServerSidePersistentTests) canDisableCache(t *ldtest.T) { require.NoError(t, s.persistentStore.Reset()) - require.NoError(t, s.persistentStore.WriteData("launchdarkly:features", s.initialFlags)) + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) persistence := NewPersistence() persistence.SetStore(servicedef.SDKConfigPersistentStore{ @@ -64,7 +64,7 @@ func (s *ServerSidePersistentTests) canDisableCache(t *ldtest.T) { DSN: s.persistentStore.DSN(), }) persistence.SetCache(servicedef.SDKConfigPersistentCache{ - Mode: servicedef.Off, + Mode: servicedef.CacheModeOff, }) context := ldcontext.New("user-key") @@ -89,7 +89,7 @@ func (s *ServerSidePersistentTests) cachesFlagForDuration(t *ldtest.T) { DSN: s.persistentStore.DSN(), }) persistence.SetCache(servicedef.SDKConfigPersistentCache{ - Mode: servicedef.TTL, + Mode: servicedef.CacheModeTTL, TTL: o.Some(1), }) context := ldcontext.New("user-key") @@ -98,7 +98,7 @@ func (s *ServerSidePersistentTests) cachesFlagForDuration(t *ldtest.T) { require.NoError(t, s.persistentStore.Reset()) client := NewSDKClient(t, persistence) - require.NoError(t, s.persistentStore.WriteData("launchdarkly:features", s.initialFlags)) + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) pollUntilFlagValueUpdated(t, client, "flag-key", context, ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")) @@ -132,7 +132,7 @@ func (s *ServerSidePersistentTests) cachesFlagForDuration(t *ldtest.T) { m.In(t).Assert(result.Value, m.Equal(ldvalue.String("default"))) m.In(t).Assert(result.Reason.Value().GetErrorKind(), m.Equal(ldreason.EvalErrorFlagNotFound)) - require.NoError(t, s.persistentStore.WriteData("launchdarkly:features", s.initialFlags)) + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) h.RequireNever(t, checkForUpdatedValue(t, client, "flag-key", context, @@ -153,12 +153,12 @@ func (s *ServerSidePersistentTests) cachesFlagForever(t *ldtest.T) { DSN: s.persistentStore.DSN(), }) persistence.SetCache(servicedef.SDKConfigPersistentCache{ - Mode: servicedef.Infinite, + Mode: servicedef.CacheModeInfinite, }) context := ldcontext.New("user-key") require.NoError(t, s.persistentStore.Reset()) - require.NoError(t, s.persistentStore.WriteData("launchdarkly:features", s.initialFlags)) + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) client := NewSDKClient(t, persistence) diff --git a/sdktests/server_side_persistence_read_write.go b/sdktests/server_side_persistence_read_write.go new file mode 100644 index 0000000..b11deb7 --- /dev/null +++ b/sdktests/server_side_persistence_read_write.go @@ -0,0 +1,417 @@ +package sdktests + +import ( + "fmt" + "time" + + "github.com/go-redis/redis" + "github.com/stretchr/testify/require" + + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + m "github.com/launchdarkly/go-test-helpers/v2/matchers" + h "github.com/launchdarkly/sdk-test-harness/v2/framework/helpers" + "github.com/launchdarkly/sdk-test-harness/v2/framework/ldtest" + o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt" + "github.com/launchdarkly/sdk-test-harness/v2/mockld" + "github.com/launchdarkly/sdk-test-harness/v2/servicedef" +) + +func (s *ServerSidePersistentTests) doReadWriteTests(t *ldtest.T) { + // No cache is enabled + t.Run("initializes store when data received", s.initializesStoreWhenDataReceived) + t.Run("applies updates to store", s.appliesUpdatesToStore) + + t.Run("data source updates respect versioning", s.dataSourceUpdatesRespectVersioning) + t.Run("data source deletions respect versioning", s.dataSourceDeletesRespectVersioning) + + cacheConfigs := []servicedef.SDKConfigPersistentCache{ + {Mode: servicedef.CacheModeInfinite}, + {Mode: servicedef.CacheModeTTL, TTL: o.Some(1)}, + } + + for _, cacheConfig := range cacheConfigs { + t.Run(fmt.Sprintf("cache mode %s", cacheConfig.Mode), func(t *ldtest.T) { + t.Run("does not cache flag miss", func(t *ldtest.T) { + s.doesNotCacheFlagMiss(t, cacheConfig) + }) + t.Run("sdk reflects data source updates even with cache", func(t *ldtest.T) { + s.sdkReflectsDataSourceUpdatesEvenWithCache(t, cacheConfig) + }) + t.Run("ignores direct database modifications", func(t *ldtest.T) { + s.ignoresDirectDatabaseModifications(t, cacheConfig) + }) + t.Run("ignores dropped flags", func(t *ldtest.T) { + s.ignoresFlagsBeingDiscardedFromStore(t, cacheConfig) + }) + }) + } +} + +func (s *ServerSidePersistentTests) initializesStoreWhenDataReceived(t *ldtest.T) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(servicedef.SDKConfigPersistentCache{ + Mode: servicedef.CacheModeOff, + }) + + sdkData := s.makeSDKDataWithFlag("flag-key", 1, ldvalue.String("value")) + _, configurers := s.setupDataSources(t, sdkData) + configurers = append(configurers, persistence) + + _, err := s.persistentStore.Get("launchdarkly:$inited") + require.Error(t, err) // should not exist + + _ = NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + s.eventuallyRequireDataStoreInit(t, "launchdarkly") +} + +func (s *ServerSidePersistentTests) appliesUpdatesToStore(t *ldtest.T) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(servicedef.SDKConfigPersistentCache{ + Mode: servicedef.CacheModeOff, + }) + + sdkData := s.makeSDKDataWithFlag("flag-key", 1, ldvalue.String("value")) + stream, configurers := s.setupDataSources(t, sdkData) + configurers = append(configurers, persistence) + + _, err := s.persistentStore.Get("launchdarkly:$inited") + if err != nil && err.Error() == string(redis.Nil) { + // as expected, inited key does not exist + } else { + require.Fail(t, "unexpected error failure", err) + } + + _ = NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + s.eventuallyRequireDataStoreInit(t, "launchdarkly") + s.eventuallyValidateFlagData(t, "launchdarkly", map[string]m.Matcher{ + "flag-key": basicFlagValidationMatcher("flag-key", 1, "value"), + }) + + updateData := s.makeFlagData("flag-key", 2, ldvalue.String("new-value")) + stream.StreamingService().PushUpdate("flags", "flag-key", updateData) + s.eventuallyValidateFlagData(t, "launchdarkly", map[string]m.Matcher{ + "flag-key": basicFlagValidationMatcher("flag-key", 2, "new-value"), + }) +} + +func (s *ServerSidePersistentTests) dataSourceUpdatesRespectVersioning(t *ldtest.T) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(servicedef.SDKConfigPersistentCache{ + Mode: servicedef.CacheModeOff, + }) + + sdkData := s.makeSDKDataWithFlag("flag-key", 1, ldvalue.String("value")) + stream, configurers := s.setupDataSources(t, sdkData) + configurers = append(configurers, persistence) + + _ = NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + s.eventuallyRequireDataStoreInit(t, "launchdarkly") + + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) + + // Lower versioned updates are ignored + updateData := s.makeFlagData("flag-key", 1, ldvalue.String("new-value")) + stream.StreamingService().PushUpdate("flags", "flag-key", updateData) + s.neverValidateFlagData(t, "launchdarkly", map[string]m.Matcher{ + "flag-key": basicFlagValidationMatcher("flag-key", 1, "new-value"), + "uncached-flag-key": basicFlagValidationMatcher("uncached-flag-key", 100, "value"), + }) + + // Same versioned updates are ignored + updateData = s.makeFlagData("flag-key", 100, ldvalue.String("new-value")) + stream.StreamingService().PushUpdate("flags", "flag-key", updateData) + s.neverValidateFlagData(t, "launchdarkly", map[string]m.Matcher{ + "flag-key": basicFlagValidationMatcher("flag-key", 1, "new-value"), + "uncached-flag-key": basicFlagValidationMatcher("uncached-flag-key", 100, "value"), + }) + + // Higher versioned updates are applied + updateData = s.makeFlagData("flag-key", 200, ldvalue.String("new-value")) + stream.StreamingService().PushUpdate("flags", "flag-key", updateData) + s.neverValidateFlagData(t, "launchdarkly", map[string]m.Matcher{ + "flag-key": basicFlagValidationMatcher("flag-key", 200, "new-value"), + "uncached-flag-key": basicFlagValidationMatcher("uncached-flag-key", 100, "value"), + }) +} + +func (s *ServerSidePersistentTests) dataSourceDeletesRespectVersioning(t *ldtest.T) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(servicedef.SDKConfigPersistentCache{ + Mode: servicedef.CacheModeOff, + }) + + sdkData := s.makeSDKDataWithFlag("flag-key", 100, ldvalue.String("value")) + stream, configurers := s.setupDataSources(t, sdkData) + configurers = append(configurers, persistence) + + _ = NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + s.eventuallyRequireDataStoreInit(t, "launchdarkly") + + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) + + // Lower versioned deletes are ignored + stream.StreamingService().PushDelete("flags", "flag-key", 1) + s.neverValidateFlagData(t, "launchdarkly", map[string]m.Matcher{ + "flag-key": basicDeletedFlagValidationMatcher(1), + "uncached-flag-key": basicFlagValidationMatcher("uncached-flag-key", 100, "fallthrough"), + }) + + // Higher versioned deletes are applied + stream.StreamingService().PushDelete("flags", "flag-key", 200) + s.eventuallyValidateFlagData(t, "launchdarkly", map[string]m.Matcher{ + "flag-key": basicDeletedFlagValidationMatcher(200), + "uncached-flag-key": basicFlagValidationMatcher("uncached-flag-key", 100, "fallthrough"), + }) +} + +func (s *ServerSidePersistentTests) ignoresDirectDatabaseModifications( + t *ldtest.T, cacheConfig servicedef.SDKConfigPersistentCache) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(cacheConfig) + + sdkData := s.makeSDKDataWithFlag("flag-key", 1, ldvalue.String("value")) + _, configurers := s.setupDataSources(t, sdkData) + configurers = append(configurers, persistence) + + client := NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + context := ldcontext.New("user-key") + s.eventuallyRequireDataStoreInit(t, "launchdarkly") + + pollUntilFlagValueUpdated(t, client, "flag-key", context, + ldvalue.String("default"), ldvalue.String("value"), ldvalue.String("default")) + + require.NoError(t, s.persistentStore.WriteMap("launchdarkly:features", s.initialFlags)) + + if cacheConfig.Mode == servicedef.CacheModeInfinite { + // This key was already cached, so it shouldn't see the change above. + h.RequireNever(t, + checkForUpdatedValue(t, client, "flag-key", context, + ldvalue.String("value"), ldvalue.String("new-value"), ldvalue.String("default")), + time.Millisecond*500, time.Millisecond*20, "flag-key was incorrectly updated") + + // But since we didn't evaluate this flag, this should actually be + // reflected by directly changing the database. + h.RequireEventually(t, + checkForUpdatedValue(t, client, "uncached-flag-key", context, + ldvalue.String("default"), ldvalue.String("fallthrough"), ldvalue.String("default")), + time.Millisecond*500, time.Millisecond*20, "uncached-flag-key was incorrectly cached") + } else if cacheConfig.Mode == servicedef.CacheModeTTL { + // This key was already cached, so it shouldn't see the change above. + h.RequireNever(t, + checkForUpdatedValue(t, client, "flag-key", context, + ldvalue.String("value"), ldvalue.String("new-value"), ldvalue.String("default")), + time.Duration(int(time.Second)*cacheConfig.TTL.Value()/2), time.Millisecond*20, "flag-key was incorrectly updated") + + // But eventually, it will expire and then we will fetch it from the database. + h.RequireEventually(t, + checkForUpdatedValue(t, client, "flag-key", context, + ldvalue.String("value"), ldvalue.String("fallthrough"), ldvalue.String("default")), + time.Duration(int(time.Second)*cacheConfig.TTL.Value()), time.Millisecond*20, "flag-key was incorrectly cached") + } +} + +func (s *ServerSidePersistentTests) ignoresFlagsBeingDiscardedFromStore( + t *ldtest.T, cacheConfig servicedef.SDKConfigPersistentCache) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(cacheConfig) + + sdkData := s.makeSDKDataWithFlag("flag-key", 1, ldvalue.String("value")) + _, configurers := s.setupDataSources(t, sdkData) + configurers = append(configurers, persistence) + + client := NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + context := ldcontext.New("user-key") + s.eventuallyRequireDataStoreInit(t, "launchdarkly") + + pollUntilFlagValueUpdated(t, client, "flag-key", context, + ldvalue.String("default"), ldvalue.String("value"), ldvalue.String("default")) + + require.NoError(t, s.persistentStore.Reset()) + + // This key was already cached, so it shouldn't see the change above. + h.RequireNever(t, + checkForUpdatedValue(t, client, "flag-key", context, + ldvalue.String("value"), ldvalue.String("default"), ldvalue.String("default")), + time.Millisecond*500, time.Millisecond*20, "flag was never updated") + + if cacheConfig.Mode == servicedef.CacheModeTTL { + // But eventually, it will expire and then we will fetch it from the database. + h.RequireEventually(t, + checkForUpdatedValue(t, client, "flag-key", context, + ldvalue.String("value"), ldvalue.String("default"), ldvalue.String("default")), + time.Second, time.Millisecond*20, "flag-key was incorrectly cached") + } +} + +func (s *ServerSidePersistentTests) doesNotCacheFlagMiss(t *ldtest.T, cacheConfig servicedef.SDKConfigPersistentCache) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(cacheConfig) + + stream, configurers := s.setupDataSources(t, mockld.NewServerSDKDataBuilder().Build()) + configurers = append(configurers, persistence) + + client := NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + context := ldcontext.New("user-key") + s.eventuallyRequireDataStoreInit(t, "launchdarkly") + + response := client.EvaluateFlag(t, servicedef.EvaluateFlagParams{ + FlagKey: "flag-key", + Context: o.Some(context), + ValueType: servicedef.ValueTypeAny, + DefaultValue: ldvalue.String("default"), + }) + + m.In(t).Assert(response.Value, m.Equal(ldvalue.String("default"))) + + updateData := s.makeFlagData("flag-key", 2, ldvalue.String("new-value")) + stream.StreamingService().PushUpdate("flags", "flag-key", updateData) + + h.RequireEventually(t, + checkForUpdatedValue(t, client, "flag-key", context, + ldvalue.String("default"), ldvalue.String("new-value"), ldvalue.String("default")), + time.Millisecond*500, time.Millisecond*20, "flag was never updated") +} + +func (s *ServerSidePersistentTests) sdkReflectsDataSourceUpdatesEvenWithCache( + t *ldtest.T, cacheConfig servicedef.SDKConfigPersistentCache) { + require.NoError(t, s.persistentStore.Reset()) + + persistence := NewPersistence() + persistence.SetStore(servicedef.SDKConfigPersistentStore{ + Type: s.persistentStore.Type(), + DSN: s.persistentStore.DSN(), + }) + persistence.SetCache(cacheConfig) + + sdkData := s.makeSDKDataWithFlag("flag-key", 1, ldvalue.String("value")) + stream, configurers := s.setupDataSources(t, sdkData) + configurers = append(configurers, persistence) + + client := NewSDKClient(t, s.baseSDKConfigurationPlus(configurers...)...) + context := ldcontext.New("user-key") + s.eventuallyRequireDataStoreInit(t, "launchdarkly") + + pollUntilFlagValueUpdated(t, client, "flag-key", context, + ldvalue.String("default"), ldvalue.String("value"), ldvalue.String("default")) + + updateData := s.makeFlagData("flag-key", 2, ldvalue.String("new-value")) + stream.StreamingService().PushUpdate("flags", "flag-key", updateData) + + // This change is reflected in less time than the cache TTL. This should + // prove it isn't caching that value. + h.RequireEventually(t, + checkForUpdatedValue(t, client, "flag-key", context, + ldvalue.String("value"), ldvalue.String("new-value"), ldvalue.String("default")), + time.Millisecond*500, time.Millisecond*20, "flag was updated") +} + +//nolint:unparam +func (s *ServerSidePersistentTests) eventuallyRequireDataStoreInit(t *ldtest.T, prefix string) { + h.RequireEventually(t, func() bool { + _, err := s.persistentStore.Get(prefix + ":$inited") + return err == nil + }, time.Second, time.Millisecond*20, prefix+":$inited key was not set") +} + +func (s *ServerSidePersistentTests) eventuallyValidateFlagData( + t *ldtest.T, prefix string, matchers map[string]m.Matcher) { + h.RequireEventually(t, func() bool { + data, err := s.persistentStore.GetMap(prefix + ":features") + if err != nil { + return false + } + + return validateFlagData(data, matchers) + }, time.Second, time.Millisecond*20, "flag data did not match") +} + +//nolint:unparam +func (s *ServerSidePersistentTests) neverValidateFlagData(t *ldtest.T, prefix string, matchers map[string]m.Matcher) { + h.RequireNever(t, func() bool { + data, err := s.persistentStore.GetMap(prefix + ":features") + if err != nil { + return false + } + + return validateFlagData(data, matchers) + }, time.Second, time.Millisecond*20, "flag data did not match") +} + +func basicFlagValidationMatcher(key string, version int, value string) m.Matcher { + return m.AllOf( + m.JSONProperty("key").Should(m.Equal(key)), + m.JSONProperty("version").Should(m.Equal(version)), + m.JSONProperty("variations").Should(m.Equal([]interface{}{value, "other"})), + ) +} + +func basicDeletedFlagValidationMatcher(version int) m.Matcher { + return m.AllOf( + m.JSONProperty("key").Should(m.Equal("$deleted")), + m.JSONProperty("version").Should(m.Equal(version)), + m.JSONProperty("deleted").Should(m.Equal(true)), + ) +} + +func validateFlagData(data map[string]string, matchers map[string]m.Matcher) bool { + if len(data) != len(matchers) { + return false + } + + for key, matcher := range matchers { + flag, ok := data[key] + if !ok { + return false + } + + result, _ := matcher.Test(flag) + if !result { + return false + } + } + + return true +} diff --git a/sdktests/server_side_persistence_redis.go b/sdktests/server_side_persistence_redis.go index 61f3cc0..f4db116 100644 --- a/sdktests/server_side_persistence_redis.go +++ b/sdktests/server_side_persistence_redis.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/redis/go-redis/v9" + + "github.com/launchdarkly/sdk-test-harness/v2/servicedef" ) type RedisPersistentStore struct { @@ -17,12 +19,26 @@ func (r RedisPersistentStore) DSN() string { return fmt.Sprintf("redis://%s", r.redis.Options().Addr) } +func (r *RedisPersistentStore) Type() servicedef.SDKConfigPersistentType { + return servicedef.Redis +} + func (r *RedisPersistentStore) Reset() error { var ctx = context.Background() return r.redis.FlushAll(ctx).Err() } -func (r *RedisPersistentStore) WriteData(key string, data map[string]string) error { +func (r *RedisPersistentStore) Get(key string) (string, error) { + var ctx = context.Background() + return r.redis.Get(ctx, key).Result() +} + +func (r *RedisPersistentStore) GetMap(key string) (map[string]string, error) { + var ctx = context.Background() + return r.redis.HGetAll(ctx, key).Result() +} + +func (r *RedisPersistentStore) WriteMap(key string, data map[string]string) error { var ctx = context.Background() _, err := r.redis.HSet(ctx, key, data).Result() return err diff --git a/sdktests/testsuite_entry_point.go b/sdktests/testsuite_entry_point.go index c7f5778..3500c04 100644 --- a/sdktests/testsuite_entry_point.go +++ b/sdktests/testsuite_entry_point.go @@ -90,9 +90,12 @@ func doAllServerSideTests(t *ldtest.T) { t.Run("secure mode hash", doServerSideSecureModeHashTests) t.Run("context type", doSDKContextTypeTests) t.Run("migrations", doServerSideMigrationTests) - t.Run("persistent data store", doServerSidePersistentTests) t.Run("hooks", doCommonHooksTests) t.Run("wrapper", doServerSideWrapperTests) + + if t.Capabilities().Has(servicedef.CapabilityPersistentDataStore) { + t.Run("persistent data store", doServerSidePersistentTests) + } } func doAllClientSideTests(t *ldtest.T) { diff --git a/servicedef/sdk_config.go b/servicedef/sdk_config.go index fdafe81..ad736d6 100644 --- a/servicedef/sdk_config.go +++ b/servicedef/sdk_config.go @@ -3,12 +3,12 @@ package servicedef import ( "encoding/json" + "github.com/launchdarkly/go-sdk-common/v3/ldtime" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" - "github.com/launchdarkly/go-sdk-common/v3/ldtime" ) type SDKConfigParams struct { @@ -123,9 +123,9 @@ type SDKConfigPersistentStore struct { type SDKConfigPersistentMode string const ( - Off = SDKConfigPersistentMode("off") - TTL = SDKConfigPersistentMode("ttl") - Infinite = SDKConfigPersistentMode("infinite") + CacheModeOff = SDKConfigPersistentMode("off") + CacheModeTTL = SDKConfigPersistentMode("ttl") + CacheModeInfinite = SDKConfigPersistentMode("infinite") ) type SDKConfigPersistentCache struct { diff --git a/servicedef/service_params.go b/servicedef/service_params.go index 09e8ccf..c17938d 100644 --- a/servicedef/service_params.go +++ b/servicedef/service_params.go @@ -18,26 +18,28 @@ const ( CapabilityAllFlagsClientSideOnly = "all-flags-client-side-only" CapabilityAllFlagsDetailsOnlyForTrackedFlags = "all-flags-details-only-for-tracked-flags" - CapabilityBigSegments = "big-segments" - CapabilityContextType = "context-type" - CapabilityContextComparison = "context-comparison" - CapabilitySecureModeHash = "secure-mode-hash" - CapabilityServerSidePolling = "server-side-polling" - CapabilityServiceEndpoints = "service-endpoints" - CapabilityTags = "tags" - CapabilityUserType = "user-type" - CapabilityFiltering = "filtering" - CapabilityFilteringStrict = "filtering-strict" - CapabilityAutoEnvAttributes = "auto-env-attributes" - CapabilityMigrations = "migrations" - CapabilityEventSampling = "event-sampling" - CapabilityEventGzip = "event-gzip" - CapabilityOptionalEventGzip = "optional-event-gzip" - CapabilityETagCaching = "etag-caching" - CapabilityInlineContext = "inline-context" - CapabilityAnonymousRedaction = "anonymous-redaction" - CapabilityPollingGzip = "polling-gzip" - CapabilityEvaluationHooks = "evaluation-hooks" + CapabilityBigSegments = "big-segments" + CapabilityContextType = "context-type" + CapabilityContextComparison = "context-comparison" + CapabilitySecureModeHash = "secure-mode-hash" + CapabilityServerSidePolling = "server-side-polling" + CapabilityServiceEndpoints = "service-endpoints" + CapabilityTags = "tags" + CapabilityUserType = "user-type" + CapabilityFiltering = "filtering" + CapabilityFilteringStrict = "filtering-strict" + CapabilityAutoEnvAttributes = "auto-env-attributes" + CapabilityMigrations = "migrations" + CapabilityEventSampling = "event-sampling" + CapabilityEventGzip = "event-gzip" + CapabilityOptionalEventGzip = "optional-event-gzip" + CapabilityETagCaching = "etag-caching" + CapabilityInlineContext = "inline-context" + CapabilityAnonymousRedaction = "anonymous-redaction" + CapabilityPollingGzip = "polling-gzip" + CapabilityEvaluationHooks = "evaluation-hooks" + CapabilityPersistentDataStore = "persistent-data-store" + CapabilityPersistentDataStoreRedis = "persistent-data-store-redis" // CapabilityTLSVerifyPeer means the SDK is capable of establishing a TLS session and verifying // its peer. This is generally a standard capability of all SDKs.