packages feed

launchdarkly-server-sdk-redis-hedis (empty) → 1.0.0

raw patch · 12 files changed

+554/−0 lines, 12 filesdep +HUnitdep +aesondep +basesetup-changed

Dependencies added: HUnit, aeson, base, bytestring, exceptions, generic-lens, hedis, launchdarkly-server-sdk, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Change log++All notable changes to the LaunchDarkly SDK for Haskell - Redis Integration will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org).++
+ LICENSE view
@@ -0,0 +1,13 @@+Copyright 2023 Catamorphic, Co.++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.
+ README.md view
@@ -0,0 +1,45 @@+# LaunchDarkly Server SDK for Haskell - Redis Integration++[![Hackage](https://img.shields.io/hackage/v/launchdarkly-server-sdk-redis-hedis)](https://hackage.haskell.org/package/launchdarkly-server-sdk-redis-hedis)+[![CircleCI](https://circleci.com/gh/launchdarkly/haskell-server-sdk-redis-hedis.svg?style=shield)](https://circleci.com/gh/launchdarkly/haskell-server-sdk-redis-hedis)+[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/haskell-server-sdk-redis-hedis)++## LaunchDarkly overview++[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!++[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly)++## Supported Haskell versions++This version of the LaunchDarkly SDK has been tested with Haskell v4.++## Getting started++Download a release archive from the [GitHub Releases](https://github.com/launchdarkly/haskell-server-sdk-redis-hedis/releases) for use in your project. Refer to the [SDK Redis documentation](https://docs.launchdarkly.com/sdk/features/storing-data/redis#haskell) for complete instructions on getting started with using the SDK with Redis.++## Learn more++Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/haskell) or our [code-generated API documentation](https://launchdarkly.github.io/haskell-server-sdk/).++## Testing++We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly.++## Contributing++We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK.++## About LaunchDarkly++* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard.  With LaunchDarkly, you can:+    * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.+    * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).+    * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.+    * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.+* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list.+* Explore LaunchDarkly+    * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information+    * [docs.launchdarkly.com](https://docs.launchdarkly.com/  "LaunchDarkly Documentation") for our documentation and SDK reference guides+    * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/  "LaunchDarkly API Documentation") for our API documentation+    * [launchdarkly.com/blog](https://launchdarkly.com/blog/  "LaunchDarkly Blog Documentation") for the latest product updates
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ launchdarkly-server-sdk-redis-hedis.cabal view
@@ -0,0 +1,88 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.35.1.+--+-- see: https://github.com/sol/hpack++name:           launchdarkly-server-sdk-redis-hedis+version:        1.0.0+synopsis:       LaunchDarkly Server-Side SDK - Redis Integration+description:    Redis integration for the LaunchDarkly SDK using hedis+category:       Web+homepage:       https://github.com/launchdarkly/haskell-server-sdk-redis-hedis#readme+bug-reports:    https://github.com/launchdarkly/haskell-server-sdk-redis-hedis/issues+author:         LaunchDarkly+maintainer:     dev@launchdarkly.com+copyright:      2023 Catamorphic, Co+license:        Apache-2.0+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md+    LICENSE++source-repository head+  type: git+  location: https://github.com/launchdarkly/haskell-server-sdk-redis-hedis++library+  exposed-modules:+      LaunchDarkly.Server.Store.Redis+  other-modules:+      LaunchDarkly.Server.Store.Redis.Internal+      Paths_launchdarkly_server_sdk_redis_hedis+  hs-source-dirs:+      src+  default-extensions:+      DataKinds+      DeriveAnyClass+      LambdaCase+      OverloadedStrings+      ScopedTypeVariables+      TypeApplications+  ghc-options: -fwarn-unused-imports -Wall -Wno-name-shadowing+  build-depends:+      aeson >=1.4.7.1 && <1.6 || >=2.0.1.0 && <2.2+    , base >=4.13.0.0 && <5+    , bytestring >=0.10.10.1 && <0.12+    , exceptions >=0.10.4 && <0.11+    , generic-lens >=2.0.0.0 && <2.3+    , hedis >=0.12.15 && <0.16+    , launchdarkly-server-sdk >=4.0 && <5+    , text >=1.2.4.0 && <2.1+  default-language: Haskell2010++test-suite haskell-server-sdk-redis-hedis-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Spec.Get+      Spec.Initialization+      Spec.Upsert+      Util+      LaunchDarkly.Server.Store.Redis+      LaunchDarkly.Server.Store.Redis.Internal+      Paths_launchdarkly_server_sdk_redis_hedis+  hs-source-dirs:+      test+      src+  default-extensions:+      DataKinds+      DeriveAnyClass+      LambdaCase+      OverloadedStrings+      ScopedTypeVariables+      TypeApplications+  ghc-options: -rtsopts -threaded -with-rtsopts=-N -Wno-name-shadowing -fwarn-unused-imports -Wall+  build-depends:+      HUnit+    , aeson >=1.4.7.1 && <1.6 || >=2.0.1.0 && <2.2+    , base >=4.13.0.0 && <5+    , bytestring >=0.10.10.1 && <0.12+    , exceptions >=0.10.4 && <0.11+    , generic-lens >=2.0.0.0 && <2.3+    , hedis >=0.12.15 && <0.16+    , launchdarkly-server-sdk+    , text >=1.2.4.0 && <2.1+  default-language: Haskell2010
+ src/LaunchDarkly/Server/Store/Redis.hs view
@@ -0,0 +1,9 @@+-- | The public interface for the LaunchDarkly Haskell Redis integration+module LaunchDarkly.Server.Store.Redis+    ( RedisStoreConfig+    , makeRedisStoreConfig+    , redisConfigSetNamespace+    , makeRedisStore+    ) where++import LaunchDarkly.Server.Store.Redis.Internal
+ src/LaunchDarkly/Server/Store/Redis/Internal.hs view
@@ -0,0 +1,143 @@+module LaunchDarkly.Server.Store.Redis.Internal+    ( RedisStoreConfig+    , makeRedisStoreConfig+    , redisConfigSetNamespace+    , makeRedisStore+    ) where++import Control.Exception (throwIO)+import Control.Monad (forM_, void)+import Control.Monad.Catch (Exception, Handler (..), catches)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Bifunctor (first)+import Data.ByteString (ByteString)+import Data.Functor ((<&>))+import Data.Generics.Product (getField)+import Data.Maybe (isJust)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Data.Typeable (Typeable)+import Database.Redis+    ( Connection+    , ConnectionLostException+    , Redis+    , Reply+    , TxResult (..)+    , del+    , get+    , hget+    , hgetall+    , hset+    , multiExec+    , runRedis+    , set+    , watch+    )++import LaunchDarkly.AesonCompat (KeyMap, fromList, mapValues, objectKeys, toList)+import LaunchDarkly.Server.Store (PersistentDataStore (..), SerializedItemDescriptor (..), StoreResult, byteStringToVersionedData, serializeWithPlaceholder)++-- | Opaque type used to configure the Redis store integration.+data RedisStoreConfig = RedisStoreConfig+    { namespace :: Text+    , connection :: Connection+    }++-- | Create a default config from a given connection pool.+makeRedisStoreConfig :: Connection -> RedisStoreConfig+makeRedisStoreConfig con =+    RedisStoreConfig+        { namespace = "launchdarkly"+        , connection = con+        }++-- |+-- Configure the Redis key prefix. All keys are prefixed by default before+-- being inserted into Redis. The default prefix is "launchdarkly".+redisConfigSetNamespace :: Text -> RedisStoreConfig -> RedisStoreConfig+redisConfigSetNamespace namespace' config = config {namespace = namespace'}++-- |+-- Construct a `PersistentDataStore` that can then be used during SDK+-- configuration.+makeRedisStore :: RedisStoreConfig -> IO PersistentDataStore+makeRedisStore config =+    pure+        PersistentDataStore+            { persistentDataStoreUpsertFeature = redisUpsert config+            , persistentDataStoreGetFeature = redisGetFeature config+            , persistentDataStoreInitialize = redisInitialize config+            , persistentDataStoreIsInitialized = redisIsInitialized config+            , persistentDataStoreAllFeatures = redisGetAll config+            }++newtype RedisError = RedisError Text deriving (Typeable, Show, Exception)++makeKey :: RedisStoreConfig -> Text -> ByteString+makeKey config key = encodeUtf8 $ T.concat [namespace config, ":", key]++exceptOnReply :: (MonadIO m) => Either Reply a -> m a+exceptOnReply = \case+    Left err -> liftIO $ throwIO $ RedisError $ T.pack $ show err+    Right x -> pure x++run :: RedisStoreConfig -> Redis a -> StoreResult a+run config action =+    catches+        (runRedis (connection config) action <&> pure)+        [ Handler $ \(e :: ConnectionLostException) -> pure $ Left $ T.pack $ show e+        , Handler $ \(RedisError err) -> pure $ Left err+        ]++createSerializedItemDescriptor :: ByteString -> SerializedItemDescriptor+createSerializedItemDescriptor byteString = SerializedItemDescriptor (Just byteString) 0 False++redisInitialize :: RedisStoreConfig -> KeyMap (KeyMap SerializedItemDescriptor) -> StoreResult ()+redisInitialize config values = run config $ do+    del (map (makeKey config) $ objectKeys values) >>= void . exceptOnReply+    forM_ (toList values) $ \(kind, features) -> forM_ (toList features) $ \(key, feature) ->+        hset (makeKey config kind) (encodeUtf8 key) (serializeWithPlaceholder feature) >>= void . exceptOnReply+    set (makeKey config "$inited") "" >>= void . exceptOnReply++redisUpsert :: RedisStoreConfig -> Text -> Text -> SerializedItemDescriptor -> StoreResult Bool+redisUpsert = redisUpsertInternal (pure ())++redisUpsertInternal :: IO () -> RedisStoreConfig -> Text -> Text -> SerializedItemDescriptor -> StoreResult Bool+redisUpsertInternal hook config kind key opaque = run config tryUpsert+  where+    tryUpsert =+        watch [space]+            >>= void . exceptOnReply+            >> hget space (encodeUtf8 key)+            >>= exceptOnReply+            >>= \x ->+                liftIO hook >> case x of+                    Nothing -> doInsert+                    (Just byteString) -> case byteStringToVersionedData byteString of+                        Nothing -> pure True+                        Just decodedVersion ->+                            if getField @"version" decodedVersion >= getField @"version" opaque+                                then pure False+                                else doInsert+    space = makeKey config kind+    doInsert =+        multiExec (hset space (encodeUtf8 key) (serializeWithPlaceholder opaque)) >>= \case+            TxSuccess _ -> pure True+            TxError err -> liftIO $ throwIO $ RedisError $ T.pack $ show err+            TxAborted -> tryUpsert++redisGetFeature :: RedisStoreConfig -> Text -> Text -> StoreResult (Maybe SerializedItemDescriptor)+redisGetFeature config kind key =+    run config $+        hget (makeKey config kind) (encodeUtf8 key)+            >>= exceptOnReply+            >>= \result -> pure $ createSerializedItemDescriptor <$> result++redisIsInitialized :: RedisStoreConfig -> StoreResult Bool+redisIsInitialized config =+    run config $ (get (makeKey config "$inited") >>= exceptOnReply) <&> isJust++redisGetAll :: RedisStoreConfig -> Text -> StoreResult (KeyMap SerializedItemDescriptor)+redisGetAll config kind =+    run config $ (hgetall (makeKey config kind) >>= exceptOnReply) <&> (mapValues createSerializedItemDescriptor . fromList . map (first decodeUtf8))
+ test/Spec.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE RecordWildCards #-}++module Main where++import Control.Monad (when)+import Test.HUnit (Counts (..), Test (TestLabel, TestList), runTestTT)++import qualified Spec.Get+import qualified Spec.Initialization+import qualified Spec.Upsert++import System.Exit (ExitCode (ExitFailure), exitWith)++main :: IO ()+main = do+    Counts {..} <-+        runTestTT $+            TestList+                [ TestLabel "Get" Spec.Get.allTests+                , TestLabel "Initialization" Spec.Initialization.allTests+                , TestLabel "Upsert" Spec.Upsert.allTests+                ]+    when (errors + failures > 0) $ exitWith (ExitFailure 1)
+ test/Spec/Get.hs view
@@ -0,0 +1,43 @@+module Spec.Get (allTests) where++import Data.Maybe (fromJust)+import LaunchDarkly.AesonCompat (lookupKey, mapValues)+import LaunchDarkly.Server.Store (PersistentDataStore (..), SerializedItemDescriptor (..))+import Test.HUnit+import Util (defaultPrefix, initialData, initialFlag, makeDefaultRedisBackend)++testCanExistingItem :: Test+testCanExistingItem = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    _ <- persistentDataStoreInitialize backend initialData+    flag <- persistentDataStoreGetFeature backend "features" "first-flag"++    assertEqual "" (Right $ Just expectedFlag) flag+  where+    expectedFlag = initialFlag {version = 0, deleted = False}++testHandlesNonExistentData :: Test+testHandlesNonExistentData = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    _ <- persistentDataStoreInitialize backend initialData+    flag <- persistentDataStoreGetFeature backend "features" "does-not-exist"++    assertEqual "" (Right Nothing) flag++testAllFlagsReturnsEveryFlag :: Test+testAllFlagsReturnsEveryFlag = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    _ <- persistentDataStoreInitialize backend initialData+    flags <- persistentDataStoreAllFeatures backend "features"++    assertEqual "" (Right expectedFlags) flags+  where+    expectedFlags = mapValues (\flag -> flag {version = 0, deleted = False}) $ fromJust $ lookupKey "features" initialData++allTests :: Test+allTests =+    TestList+        [ testCanExistingItem+        , testHandlesNonExistentData+        , testAllFlagsReturnsEveryFlag+        ]
+ test/Spec/Initialization.hs view
@@ -0,0 +1,71 @@+module Spec.Initialization (allTests) where++import LaunchDarkly.Server.Store (PersistentDataStore (..))+import Test.HUnit+import Util (defaultPrefix, emptyData, initialData, makeDefaultRedisBackend)++testClientIsNotInitializedByDefault :: Test+testClientIsNotInitializedByDefault = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    isInitialized <- persistentDataStoreIsInitialized backend++    assertEqual "" (Right False) isInitialized++testClientCanBeMarkedAsInitialized :: Test+testClientCanBeMarkedAsInitialized = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    initResult <- persistentDataStoreInitialize backend initialData+    isInitialized <- persistentDataStoreIsInitialized backend++    assertEqual "" (Right ()) initResult+    assertEqual "" (Right True) isInitialized++testClientCanSeeInitializiationFromAnotherClient :: Test+testClientCanSeeInitializiationFromAnotherClient = TestCase $ do+    activeBackend <- makeDefaultRedisBackend defaultPrefix+    passiveBackend <- makeDefaultRedisBackend defaultPrefix+    _ <- persistentDataStoreInitialize activeBackend initialData+    isActiveBackendInitialized <- persistentDataStoreIsInitialized activeBackend+    isPassiveBackendInitialized <- persistentDataStoreIsInitialized passiveBackend++    assertEqual "" (Right True) isActiveBackendInitialized+    assertEqual "" (Right True) isPassiveBackendInitialized++testDifferentPrefixesAreIndependent :: Test+testDifferentPrefixesAreIndependent = TestCase $ do+    initializedBackend <- makeDefaultRedisBackend "initialized"+    uninitializedBackend <- makeDefaultRedisBackend "uninitialized"+    _ <- persistentDataStoreInitialize initializedBackend initialData+    isInitializedBackendInitialized <- persistentDataStoreIsInitialized initializedBackend+    isUninitializedBackendInitialized <- persistentDataStoreIsInitialized uninitializedBackend++    assertEqual "" (Right True) isInitializedBackendInitialized+    assertEqual "" (Right False) isUninitializedBackendInitialized++testInitializationReplacesAllPreviousData :: Test+testInitializationReplacesAllPreviousData = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    _ <- persistentDataStoreInitialize backend initialData+    _ <- persistentDataStoreIsInitialized backend+    flagResult <- persistentDataStoreGetFeature backend "features" "first-flag"+    segmentResult <- persistentDataStoreGetFeature backend "segments" "first-segment"++    case flagResult of Right (Just _) -> pure (); _ -> assertFailure "first-flag was not present"+    case segmentResult of Right (Just _) -> pure (); _ -> assertFailure "first-segment was not present"++    _ <- persistentDataStoreInitialize backend emptyData+    flagResult' <- persistentDataStoreGetFeature backend "features" "first-flag"+    segmentResult' <- persistentDataStoreGetFeature backend "segments" "first-segment"++    case flagResult' of Right Nothing -> pure (); _ -> assertFailure "first-flag was not present"+    case segmentResult' of Right Nothing -> pure (); _ -> assertFailure "first-segment was not present"++allTests :: Test+allTests =+    TestList+        [ testClientIsNotInitializedByDefault+        , testClientCanBeMarkedAsInitialized+        , testClientCanSeeInitializiationFromAnotherClient+        , testDifferentPrefixesAreIndependent+        , testInitializationReplacesAllPreviousData+        ]
+ test/Spec/Upsert.hs view
@@ -0,0 +1,75 @@+module Spec.Upsert (allTests) where++import Data.ByteString (isInfixOf)+import Data.ByteString.Char8 (pack)+import Data.Char (toLower)+import GHC.Natural (Natural)+import qualified LaunchDarkly.Server.Store as Store+import Test.HUnit+import Text.Printf+import Util (defaultPrefix, emptyData, makeDefaultRedisBackend)++createTestFlag :: Natural -> Bool -> Store.SerializedItemDescriptor+createTestFlag version deleted =+    Store.SerializedItemDescriptor+        { Store.item = Just $ pack $ printf "{\"deleted\":%s,\"version\":%d}" (map toLower $ show deleted) version+        , Store.version = version+        , Store.deleted = deleted+        }++testHandlesVersionsAppropriately :: Natural -> Natural -> Natural -> Test+testHandlesVersionsAppropriately initialVersion secondVersion expectedVersion = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    _ <- Store.persistentDataStoreInitialize backend emptyData+    _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag initialVersion False+    _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag secondVersion False+    flag <- Store.persistentDataStoreGetFeature backend "features" "first-flag"++    assertEqual "" (Right $ Just expectedFlag) flag+  where+    expectedFlag = (createTestFlag expectedVersion False) {Store.version = 0, Store.deleted = False}++testUpsertionAfterDeletionHandlesVersionsAppropriately :: Natural -> Natural -> Natural -> Bool -> Test+testUpsertionAfterDeletionHandlesVersionsAppropriately initialVersion secondVersion expectedVersion expectedDeletionStatus = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    _ <- Store.persistentDataStoreInitialize backend emptyData+    _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag initialVersion True+    _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag secondVersion False+    flag <- Store.persistentDataStoreGetFeature backend "features" "first-flag"++    case flag of+        Right (Just Store.SerializedItemDescriptor {Store.item = Just item, Store.version = 0, Store.deleted = False}) -> do+            assertBool "" (expectedVersionString `isInfixOf` item)+            assertBool "" (expectedDeletionString `isInfixOf` item)+        _ -> assertFailure "failed to restore deleted file"+  where+    expectedVersionString = pack $ printf "\"version\":%d" expectedVersion+    expectedDeletionString = pack $ printf "\"deleted\":%s" (map toLower $ show expectedDeletionStatus)++testUpsertionToADeletionHandlesVersionsAppropriately :: Natural -> Natural -> Natural -> Bool -> Test+testUpsertionToADeletionHandlesVersionsAppropriately initialVersion secondVersion expectedVersion expectedDeletionStatus = TestCase $ do+    backend <- makeDefaultRedisBackend defaultPrefix+    _ <- Store.persistentDataStoreInitialize backend emptyData+    _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag initialVersion False+    _ <- Store.persistentDataStoreUpsertFeature backend "features" "first-flag" $ createTestFlag secondVersion True+    flag <- Store.persistentDataStoreGetFeature backend "features" "first-flag"++    case flag of+        Right (Just Store.SerializedItemDescriptor {Store.item = Just item, Store.version = 0, Store.deleted = False}) -> do+            assertBool "" (expectedVersionString `isInfixOf` item)+            assertBool "" (expectedDeletionString `isInfixOf` item)+        _ -> assertFailure "failed to restore deleted file"+  where+    expectedVersionString = pack $ printf "\"version\":%d" expectedVersion+    expectedDeletionString = pack $ printf "\"deleted\":%s" (map toLower $ show expectedDeletionStatus)++allTests :: Test+allTests =+    TestList+        [ TestLabel "Newer versions replace older" $ testHandlesVersionsAppropriately 1 2 2+        , TestLabel "Older versions do not replace newer" $ testHandlesVersionsAppropriately 2 1 2+        , TestLabel "Newer versions replace older" $ testUpsertionAfterDeletionHandlesVersionsAppropriately 1 2 2 False+        , TestLabel "Older versions do not replace newer" $ testUpsertionAfterDeletionHandlesVersionsAppropriately 2 1 2 True+        , TestLabel "Newer versions replace older" $ testUpsertionToADeletionHandlesVersionsAppropriately 1 2 2 True+        , TestLabel "Older versions do not replace newer" $ testUpsertionToADeletionHandlesVersionsAppropriately 2 1 2 False+        ]
+ test/Util.hs view
@@ -0,0 +1,37 @@+module Util where++import Control.Monad (void)+import Data.Function ((&))+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Database.Redis as R+import LaunchDarkly.AesonCompat (KeyMap, emptyObject, insertKey, singleton)+import LaunchDarkly.Server.Store (PersistentDataStore (..), SerializedItemDescriptor (..))+import LaunchDarkly.Server.Store.Redis (makeRedisStore, makeRedisStoreConfig, redisConfigSetNamespace)++defaultPrefix :: T.Text+defaultPrefix = "launchdarkly-test-prefix"++emptyData :: KeyMap (KeyMap SerializedItemDescriptor)+emptyData =+    emptyObject+        & insertKey "features" emptyObject+        & insertKey "segments" emptyObject++initialFlag :: SerializedItemDescriptor+initialFlag = SerializedItemDescriptor {version = 1, deleted = False, item = Just $ T.encodeUtf8 "flag"}++initialSegment :: SerializedItemDescriptor+initialSegment = SerializedItemDescriptor {version = 1, deleted = False, item = Just $ T.encodeUtf8 "segment"}++initialData :: KeyMap (KeyMap SerializedItemDescriptor)+initialData =+    emptyObject+        & insertKey "features" (singleton "first-flag" initialFlag)+        & insertKey "segments" (singleton "first-segment" initialSegment)++makeDefaultRedisBackend :: T.Text -> IO PersistentDataStore+makeDefaultRedisBackend prefix = do+    con <- R.checkedConnect R.defaultConnectInfo+    void $ R.runRedis con R.flushall+    makeRedisStore $ redisConfigSetNamespace prefix $ makeRedisStoreConfig con