module Spec.Evaluate (allTests) where import Test.HUnit import Data.Aeson import Data.Aeson.Types (Value(..)) import qualified Data.HashMap.Strict as HM import Data.HashMap.Strict (HashMap) import Data.Function ((&)) import Data.Generics.Product (getField) import LaunchDarkly.Server.Store import LaunchDarkly.Server.Store.Internal import LaunchDarkly.Server.Client import LaunchDarkly.Server.Client.Internal import LaunchDarkly.Server.User import LaunchDarkly.Server.User.Internal import LaunchDarkly.Server.Features import LaunchDarkly.Server.Operators import LaunchDarkly.Server.Details import LaunchDarkly.Server.Store.Internal import LaunchDarkly.Server.Evaluate import LaunchDarkly.Server.Config import Util.Features makeEmptyStore :: IO (StoreHandle IO) makeEmptyStore = do handle <- makeStoreIO Nothing 0 initializeStore handle mempty mempty pure handle testFlagReturnsOffVariationIfFlagIsOff :: Test testFlagReturnsOffVariationIfFlagIsOff = TestCase $ do store <- makeEmptyStore x <- evaluateDetail flag user store assertEqual "test" expected x where expected = (EvaluationDetail { value = String "off" , variationIndex = pure 1 , reason = EvaluationReasonOff }, []) user = unwrapUser $ makeUser "x" flag = Flag { key = "feature" , version = 1 , on = False , trackEvents = False , trackEventsFallthrough = False , deleted = False , prerequisites = [] , salt = "" , sel = "" , targets = [] , rules = [] , fallthrough = VariationOrRollout { variation = Just 0 , rollout = Nothing } , offVariation = Just 1 , variations = [String "fall", String "off", String "on"] , debugEventsUntilDate = Nothing } testFlagReturnsFallthroughIfFlagIsOnAndThereAreNoRules :: Test testFlagReturnsFallthroughIfFlagIsOnAndThereAreNoRules = TestCase $ do store <- makeEmptyStore x <- evaluateDetail flag user store assertEqual "test" expected x where expected = (EvaluationDetail { value = String "fall" , variationIndex = pure 0 , reason = EvaluationReasonFallthrough }, []) user = unwrapUser $ makeUser "x" flag = Flag { key = "feature" , version = 1 , on = True , trackEvents = False , trackEventsFallthrough = False , deleted = False , prerequisites = [] , salt = "" , sel = "" , targets = [] , rules = [] , fallthrough = VariationOrRollout { variation = Just 0 , rollout = Nothing } , offVariation = Just 1 , variations = [String "fall", String "off", String "on"] , debugEventsUntilDate = Nothing } testFlagReturnsErrorIfFallthroughHasTooHighVariation :: Test testFlagReturnsErrorIfFallthroughHasTooHighVariation = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag >>= (pure () @=?) stringVariationDetail client "a" (makeUser "b") "default" >>= (expected @=?) where flag = (makeTestFlag "a" 52) { on = True , offVariation = Nothing , fallthrough = VariationOrRollout { variation = Just 999 , rollout = Nothing } , variations = [ String "abc" , String "123" , String "456" ] } expected = EvaluationDetail { value = "default" , variationIndex = Nothing , reason = EvaluationReasonError EvalErrorKindMalformedFlag } testFlagReturnsErrorIfFallthroughHasNeitherVariationNorRollout :: Test testFlagReturnsErrorIfFallthroughHasNeitherVariationNorRollout = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag >>= (pure () @=?) stringVariationDetail client "a" (makeUser "b") "default" >>= (expected @=?) where flag = (makeTestFlag "a" 52) { on = True , offVariation = Nothing , fallthrough = VariationOrRollout { variation = Nothing , rollout = Nothing } , variations = [String "abc"] } expected = EvaluationDetail { value = "default" , variationIndex = Nothing , reason = EvaluationReasonError EvalErrorKindMalformedFlag } testFlagReturnsErrorIfFallthroughHasEmptyRolloutVariationList :: Test testFlagReturnsErrorIfFallthroughHasEmptyRolloutVariationList = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag >>= (pure () @=?) stringVariationDetail client "a" (makeUser "b") "default" >>= (expected @=?) where flag = (makeTestFlag "a" 52) { on = True , offVariation = Nothing , fallthrough = VariationOrRollout { variation = Nothing , rollout = pure Rollout { variations = [] , bucketBy = pure "key" } } , variations = [String "abc"] } expected = EvaluationDetail { value = "default" , variationIndex = Nothing , reason = EvaluationReasonError EvalErrorKindMalformedFlag } testFlagReturnsOffVariationIfPrerequisiteIsNotFound :: Test testFlagReturnsOffVariationIfPrerequisiteIsNotFound = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag >>= (pure () @=?) stringVariationDetail client "a" (makeUser "b") "default" >>= (expected @=?) where flag = (makeTestFlag "a" 52) { on = True , offVariation = pure 1 , fallthrough = VariationOrRollout { variation = pure 0 , rollout = Nothing } , variations = [String "fall", String "off", String "on"] , prerequisites = [ Prerequisite { key = "feature1" , variation = 1 } ] } expected = EvaluationDetail { value = "off" , variationIndex = pure 1 , reason = EvaluationReasonPrerequisiteFailed "feature1" } testFlagReturnsOffVariationIfPrerequisiteIsOff :: Test testFlagReturnsOffVariationIfPrerequisiteIsOff = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag0 >>= (pure () @=?) insertFlag (getField @"store" clientI) flag1 >>= (pure () @=?) stringVariationDetail client "feature0" (makeUser "b") "default" >>= (expected @=?) where flag0 = (makeTestFlag "feature0" 52) { on = True , offVariation = pure 1 , fallthrough = VariationOrRollout { variation = pure 0 , rollout = Nothing } , variations = [String "fall", String "off", String "on"] , prerequisites = [ Prerequisite { key = "feature1" , variation = 1 } ] } flag1 = (makeTestFlag "feature1" 52) { on = False , offVariation = pure 1 , fallthrough = VariationOrRollout { variation = pure 0 , rollout = Nothing } , variations = [String "nogo", String "go"] } expected = EvaluationDetail { value = "off" , variationIndex = pure 1 , reason = EvaluationReasonPrerequisiteFailed "feature1" } testFlagReturnsOffVariationIfPrerequisiteIsNotMet :: Test testFlagReturnsOffVariationIfPrerequisiteIsNotMet = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag0 >>= (pure () @=?) insertFlag (getField @"store" clientI) flag1 >>= (pure () @=?) stringVariationDetail client "feature0" (makeUser "b") "default" >>= (expected @=?) where flag0 = (makeTestFlag "feature0" 52) { on = True , offVariation = pure 1 , fallthrough = VariationOrRollout { variation = pure 0 , rollout = Nothing } , variations = [String "fall", String "off", String "on"] , prerequisites = [ Prerequisite { key = "feature1" , variation = 1 } ] } flag1 = (makeTestFlag "feature1" 52) { on = True , offVariation = pure 1 , fallthrough = VariationOrRollout { variation = pure 0 , rollout = Nothing } , variations = [String "nogo", String "go"] } expected = EvaluationDetail { value = "off" , variationIndex = pure 1 , reason = EvaluationReasonPrerequisiteFailed "feature1" } testFlagReturnsFallthroughVariationIfPrerequisiteIsMetAndThereAreNoRules :: Test testFlagReturnsFallthroughVariationIfPrerequisiteIsMetAndThereAreNoRules = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag0 >>= (pure () @=?) insertFlag (getField @"store" clientI) flag1 >>= (pure () @=?) stringVariationDetail client "feature0" (makeUser "b") "default" >>= (expected @=?) where flag0 = (makeTestFlag "feature0" 52) { on = True , offVariation = pure 1 , fallthrough = VariationOrRollout { variation = pure 0 , rollout = Nothing } , variations = [String "fall", String "off", String "on"] , prerequisites = [ Prerequisite { key = "feature1" , variation = 1 } ] } flag1 = (makeTestFlag "feature1" 52) { on = True , offVariation = pure 1 , fallthrough = VariationOrRollout { variation = pure 1 , rollout = Nothing } , variations = [String "nogo", String "go"] } expected = EvaluationDetail { value = "fall" , variationIndex = pure 0 , reason = EvaluationReasonFallthrough } testClauseCanMatchCustomAttribute :: Test testClauseCanMatchCustomAttribute = TestCase $ do store <- makeStoreIO Nothing 0 x <- evaluateDetail flag user store assertEqual "test" expected x where expected = (EvaluationDetail { value = Bool True , variationIndex = pure 1 , reason = EvaluationReasonRuleMatch { ruleIndex = 0 , ruleId = "clause" } }, []) user = unwrapUser $ (makeUser "x") & userSetName (pure "bob") & userSetCustom (HM.fromList [("legs", Number 4)]) flag = Flag { key = "feature" , version = 1 , on = True , trackEvents = False , trackEventsFallthrough = False , deleted = False , prerequisites = [] , salt = "" , sel = "" , targets = [] , rules = [ Rule { clauses = [ Clause { attribute = "name" , op = OpIn , values = [String "bob"] , negate = False } ] , variationOrRollout = VariationOrRollout { variation = Just 1 , rollout = Nothing } , id = "clause" , trackEvents = False } ] , fallthrough = VariationOrRollout { variation = Just 0 , rollout = Nothing } , offVariation = Just 0 , variations = [Bool False, Bool True] , debugEventsUntilDate = Nothing } makeTestClient :: IO Client makeTestClient = do (Client client) <- makeClient $ (makeConfig "") & configSetOffline True initializeStore (getField @"store" client) mempty mempty pure (Client client) testEvaluatingUnknownFlagReturnsDefault :: Test testEvaluatingUnknownFlagReturnsDefault = TestCase $ do client <- makeTestClient boolVariation client "a" (makeUser "b") False >>= (False @=?) testEvaluatingUnknownFlagReturnsDefaultWithDetail :: Test testEvaluatingUnknownFlagReturnsDefaultWithDetail = TestCase $ do client <- makeTestClient boolVariationDetail client "a" (makeUser "b") False >>= (expected @=?) where expected = EvaluationDetail { value = False , variationIndex = Nothing , reason = EvaluationReasonError EvalErrorFlagNotFound } testDefaultIsReturnedIfFlagEvaluatesToNil :: Test testDefaultIsReturnedIfFlagEvaluatesToNil = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag >>= (pure () @=?) boolVariation client "a" (makeUser "b") False >>= (False @=?) where flag = (makeTestFlag "a" 52) { on = False , offVariation = Nothing } testDefaultIsReturnedIfFlagEvaluatesToNilWithDetail :: Test testDefaultIsReturnedIfFlagEvaluatesToNilWithDetail = TestCase $ do client@(Client clientI) <- makeTestClient insertFlag (getField @"store" clientI) flag >>= (pure () @=?) boolVariationDetail client "a" (makeUser "b") False >>= (expected @=?) where flag = (makeTestFlag "a" 52) { on = False , offVariation = Nothing } expected = EvaluationDetail { value = False , variationIndex = Nothing , reason = EvaluationReasonOff } allTests :: Test allTests = TestList [ testFlagReturnsOffVariationIfFlagIsOff , testFlagReturnsFallthroughIfFlagIsOnAndThereAreNoRules , testFlagReturnsErrorIfFallthroughHasTooHighVariation , testFlagReturnsErrorIfFallthroughHasNeitherVariationNorRollout , testFlagReturnsErrorIfFallthroughHasEmptyRolloutVariationList , testFlagReturnsOffVariationIfPrerequisiteIsNotFound , testFlagReturnsOffVariationIfPrerequisiteIsOff , testFlagReturnsOffVariationIfPrerequisiteIsNotMet , testFlagReturnsFallthroughVariationIfPrerequisiteIsMetAndThereAreNoRules , testClauseCanMatchCustomAttribute , testEvaluatingUnknownFlagReturnsDefault , testEvaluatingUnknownFlagReturnsDefaultWithDetail , testDefaultIsReturnedIfFlagEvaluatesToNil , testDefaultIsReturnedIfFlagEvaluatesToNilWithDetail ]