packages feed

nri-redis-0.4.1.1: test/Spec/Redis.hs

{-# LANGUAGE QuasiQuotes #-}

module Spec.Redis (tests) where

import qualified Control.Concurrent.MVar as MVar
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Dict
import qualified Expect
import Helpers
import qualified NonEmptyDict
import qualified Platform
import qualified Redis
import qualified Redis.Counter
import qualified Redis.Hash
import qualified Redis.Internal as Internal
import qualified Redis.List
import qualified Redis.SortedSet
import qualified Set
import qualified Task
import qualified Test
import qualified Prelude

-- put this at the top of the file so that adding tests doesn't push
-- the line number of the source location of this file down, which would
-- change golden test results
spanForTask :: (Show e) => Task e a -> Expect.Expectation' Platform.TracingSpan
spanForTask task =
  Expect.fromIO <| do
    spanVar <- MVar.newEmptyMVar
    res <-
      Platform.rootTracingSpanIO
        "test-request"
        Platform.silentTrack
        (MVar.putMVar spanVar)
        "test-root"
        (\log -> Task.attempt log task)
    case res of
      Err err -> Prelude.fail <| Text.toList (Debug.toString err)
      Ok _ ->
        MVar.takeMVar spanVar
          |> map constantValuesForVariableFields

spanForFailingTask :: Task e () -> Expect.Expectation' Platform.TracingSpan
spanForFailingTask task =
  Expect.fromIO <| do
    spanVar <- MVar.newEmptyMVar
    res <-
      Platform.rootTracingSpanIO
        "test-request"
        Platform.silentTrack
        (MVar.putMVar spanVar)
        "test-root"
        (\log -> Task.attempt log task)
    case res of
      Err _ ->
        MVar.takeMVar spanVar
          |> map constantValuesForVariableFields
      Ok _ ->
        Prelude.fail "Expected task to fail"

tests :: TestHandlers -> Test.Test
tests TestHandlers {handler, autoExtendExpireHandler, noNamespaceHandler} =
  Test.describe
    "Redis Library"
    [ Test.describe "query tests using handler" (queryTests handler),
      Test.describe "query tests using auto extend expire handler" (queryTests autoExtendExpireHandler),
      Test.describe "observability tests" (observabilityTests handler),
      Test.describe "ttl tests" (ttlTests handler autoExtendExpireHandler),
      Test.describe "no-namespace handler tests" (noNamespaceTests noNamespaceHandler handler)
    ]

-- We want to test all of our potential makeApi alternatives because it's easy
-- to break. Right now they all share code but if we change that, we would
-- break the observability usability without noticing.
--
-- All the `srcLocFile` fields in the golden result files should contain the
-- value "test/Main.hs". If it points to one of the src files of the redis
-- library it means stack frames for redis query in bugsnag, newrelic, etc will
-- not point to the application code making the query!
observabilityTests :: Redis.Handler' x -> List Test.Test
observabilityTests handler =
  [ Test.test "Redis.query reports the span data we expect" <| \() -> do
      span <-
        Redis.query handler (Redis.ping api)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-query"),
    Test.test "Redis.transaction reports the span data we expect" <| \() -> do
      span <-
        Redis.transaction handler (Redis.ping api)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-transaction"),
    Test.test "Redis.Hash.query reports the span data we expect" <| \() -> do
      span <-
        Redis.Hash.query handler (Redis.Hash.ping hashApi)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-hash-query"),
    Test.test "Redis.Hash.transaction reports the span data we expect" <| \() -> do
      span <-
        Redis.Hash.transaction handler (Redis.Hash.ping hashApi)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-hash-transaction"),
    Test.test "Redis.List.query reports the span data we expect" <| \() -> do
      span <-
        Redis.List.query handler (Redis.List.ping listApi)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-list-query"),
    Test.test "Redis.List.transaction reports the span data we expect" <| \() -> do
      span <-
        Redis.List.transaction handler (Redis.List.ping listApi)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-list-transaction"),
    Test.test "Redis.Counter.query reports the span data we expect" <| \() -> do
      span <-
        Redis.Counter.query handler (Redis.Counter.ping counterApi)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-counter-query"),
    Test.test "Redis.Counter.transaction reports the span data we expect" <| \() -> do
      span <-
        Redis.Counter.transaction handler (Redis.Counter.ping counterApi)
          |> spanForTask
      span
        |> Debug.toString
        |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-counter-transaction"),
    Test.describe
      "with 0 ms timeout"
      [ Test.test "Redis.query reports the span data we expect" <| \() -> do
          handlerThatExpiresImmediately <- Expect.succeeds (Redis.withQueryTimeoutMilliseconds 0 handler)
          span <-
            Redis.query handlerThatExpiresImmediately (Redis.ping api)
              |> spanForFailingTask
          span
            |> Debug.toString
            |> Expect.all
              [ Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-redis-query-timeout"),
                \spanText -> Expect.true (Text.contains "Redis Query" spanText)
              ],
        Test.test "Redis.withQueryTimeoutMilliseconds reports the span data we expect" <| \() -> do
          span <-
            Redis.withQueryTimeoutMilliseconds 0 handler
              |> spanForTask
          span
            |> Debug.toString
            |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-with-query-timout"),
        Test.test "Redis.withoutQueryTimeout reports the span data we expect" <| \() -> do
          spanSettingTimeout <-
            Redis.withoutQueryTimeout handler
              |> spanForTask
          spanSettingTimeout
            |> Debug.toString
            |> Expect.equalToContentsOf (goldenResultsDir ++ "/observability-spec-reporting-without-query-timout")
      ]
  ]

queryTests :: Redis.Handler' x -> List Test.Test
queryTests redisHandler =
  [ Test.test "get and set" <| \() -> do
      Redis.set api "bob" "hello!" |> Redis.query testNS |> Expect.succeeds
      result <- Redis.get api "bob" |> Redis.query testNS |> Expect.succeeds
      Expect.equal result (Just "hello!"),
    Test.test "namespaces namespace" <| \() -> do
      let nsHandler1 = addNamespace "NS1" redisHandler
      let nsHandler2 = addNamespace "NS2" redisHandler
      Redis.set api "bob" "hello!" |> Redis.query nsHandler1 |> Expect.succeeds
      Redis.set api "bob" "goodbye" |> Redis.query nsHandler2 |> Expect.succeeds
      result1 <- Redis.get api "bob" |> Redis.query nsHandler1 |> Expect.succeeds
      result2 <- Redis.get api "bob" |> Redis.query nsHandler2 |> Expect.succeeds
      Expect.all
        [ \() -> Expect.notEqual result1 result2,
          \() -> Expect.equal (Just "hello!") result1,
          \() -> Expect.equal (Just "goodbye") result2
        ]
        (),
    Test.test "getset" <| \() -> do
      Redis.set api "getset" "1" |> Redis.query testNS |> Expect.succeeds
      result1 <- Redis.getset api "getset" "2" |> Redis.query testNS |> Expect.succeeds
      result2 <- Redis.get api "getset" |> Redis.query testNS |> Expect.succeeds
      Expect.all
        [ \() -> Expect.equal (Just "1") result1,
          \() -> Expect.equal (Just "2") result2
        ]
        (),
    Test.test "del dels" <| \() -> do
      Redis.set api "del" "mistake..." |> Redis.query testNS |> Expect.succeeds
      _ <- Redis.del api ("del" :| []) |> Redis.query testNS |> Expect.succeeds
      result <- Redis.get api "del" |> Redis.query testNS |> Expect.succeeds
      Expect.equal Nothing result,
    Test.test "del counts" <| \() -> do
      Redis.set api "delCount" "A thing" |> Redis.query testNS |> Expect.succeeds
      result <- Redis.del api ("delCount" :| ["key that doesn't exist"]) |> Redis.query testNS |> Expect.succeeds
      Expect.equal 1 result,
    Test.test "json roundtrip" <| \() -> do
      let testData :: [Int] = [1, 2, 3]
      Redis.set jsonApi' "JSON list" testData |> Redis.query testNS |> Expect.succeeds
      result <- Redis.get jsonApi' "JSON list" |> Redis.query testNS |> Expect.succeeds
      Expect.equal (Just testData) result,
    Test.test "mget retrieves a mapping of the requested keys and their corresponding values" <| \() -> do
      Redis.set api "mgetTest::key1" "value 1" |> Redis.query testNS |> Expect.succeeds
      Redis.set api "mgetTest::key3" "value 3" |> Redis.query testNS |> Expect.succeeds
      result <-
        Redis.mget api ("mgetTest::key1" :| ["mgetTest::key2", "mgetTest::key3"])
          |> Redis.query testNS
          |> Expect.succeeds
      Expect.equal
        (Dict.toList result)
        [("mgetTest::key1", "value 1"), ("mgetTest::key3", "value 3")],
    Test.test "mget json roundtrip" <| \() -> do
      Redis.set jsonApi' "Json.mgetTest::key1" ([1, 2] :: [Int]) |> Redis.query testNS |> Expect.succeeds
      Redis.set jsonApi' "Json.mgetTest::key2" ([3, 4] :: [Int]) |> Redis.query testNS |> Expect.succeeds
      result <-
        Redis.mget jsonApi' ("Json.mgetTest::key1" :| ["Json.mgetTest::key2"])
          |> Redis.query testNS
          |> Expect.succeeds
      Expect.equal
        (Dict.toList result)
        [ ("Json.mgetTest::key1", [1, 2]),
          ("Json.mgetTest::key2", [3, 4])
        ],
    Test.test "mset allows setting multiple values at once" <| \() -> do
      let firstKey = "msetTest::key1"
      let firstValue = "value 1"
      let nonEmptyDict = NonEmptyDict.init firstKey firstValue (Dict.fromList [("msetTest::key2", "value 2")])
      let dict = NonEmptyDict.toDict nonEmptyDict
      Redis.mset api nonEmptyDict |> Redis.query testNS |> Expect.succeeds
      result <- Redis.mget api (firstKey :| Dict.keys dict) |> Redis.query testNS |> Expect.succeeds
      Expect.equal result dict,
    Test.test "Json.mset allows setting multiple JSON values at once" <| \() -> do
      let firstKey = "Json.msetTest::key1"
      let firstValue = [1, 2]
      let nonEmptyDict = NonEmptyDict.init firstKey firstValue (Dict.fromList [("Json.msetTest::key2", [3, 4] :: [Int])])
      let dict = NonEmptyDict.toDict nonEmptyDict
      Redis.mset jsonApi' nonEmptyDict |> Redis.query testNS |> Expect.succeeds
      result <- Redis.mget jsonApi' (firstKey :| Dict.keys dict) |> Redis.query testNS |> Expect.succeeds
      Expect.equal result dict,
    Test.test "transaction preserves order" <| \() -> do
      [ Redis.List.del listApi ("order" :| []),
        Redis.List.rpush listApi "order" ("1" :| []),
        Redis.List.rpush listApi "order" ("2" :| []),
        Redis.List.rpush listApi "order" ("3" :| [])
        ]
        |> Redis.sequence
        |> map (\_ -> ())
        |> Redis.transaction testNS
        |> Expect.succeeds
      result <- Redis.List.lrange listApi "order" 0 (-1) |> Redis.query testNS |> Expect.succeeds
      Expect.equal result ["1", "2", "3"],
    Test.test "sequence is happy doing nothing" <| \() -> do
      _ <-
        Redis.sequence []
          |> Redis.transaction testNS
          |> Expect.succeeds
      Redis.sequence []
        |> Redis.query testNS
        |> Expect.succeeds
        |> map (\_ -> ()),
    Test.test "hmset inserts at least one field" <| \() -> do
      Redis.Hash.hmset hashApi "hmset-insert-test" (NonEmptyDict.init "field" "val" Dict.empty)
        |> Redis.query redisHandler
        |> Expect.succeeds
      response <-
        Redis.Hash.hget hashApi "hmset-insert-test" "field"
          |> Redis.query redisHandler
          |> Expect.succeeds
      response
        |> Expect.equal (Just "val"),
    Test.test "hmset overwrites at least existing field" <| \() -> do
      Redis.Hash.hset hashApi "hmset-overwrite-test" "field" "old-val"
        |> Redis.query redisHandler
        |> Expect.succeeds
      Redis.Hash.hmset hashApi "hmset-overwrite-test" (NonEmptyDict.init "field" "val" Dict.empty)
        |> Redis.query redisHandler
        |> Expect.succeeds
      response <-
        Redis.Hash.hget hashApi "hmset-overwrite-test" "field"
          |> Redis.query redisHandler
          |> Expect.succeeds
      response
        |> Expect.equal (Just "val"),
    Test.test "hmset inserts multiple fields" <| \() -> do
      NonEmptyDict.init
        "field"
        "val"
        (Dict.fromList [("field2", "val2")])
        |> Redis.Hash.hmset hashApi "hmset-insert-multiple-test"
        |> Redis.query redisHandler
        |> Expect.succeeds
      field <-
        Redis.Hash.hget hashApi "hmset-insert-multiple-test" "field"
          |> Redis.query redisHandler
          |> Expect.succeeds
      field
        |> Expect.equal (Just "val")
      field2 <-
        Redis.Hash.hget hashApi "hmset-insert-multiple-test" "field2"
          |> Redis.query redisHandler
          |> Expect.succeeds
      field2
        |> Expect.equal (Just "val2"),
    Test.test "zadd returns count of stored values" <| \() -> do
      _ <-
        Redis.SortedSet.del sortedSetApi ("zadd-returns-count-of-stored-values" :| [])
          |> Redis.query redisHandler
          |> Expect.succeeds
      NonEmptyDict.init "foo" 1 Dict.empty
        |> Redis.SortedSet.zadd sortedSetApi "zadd-returns-count-of-stored-values"
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal 1),
    Test.test "zrange works as expected" <| \() -> do
      _ <-
        Redis.SortedSet.del sortedSetApi ("zrange-works" :| [])
          |> Redis.query redisHandler
          |> Expect.succeeds
      _ <-
        NonEmptyDict.init "one" 1 (Dict.fromList [("two", 2), ("three", 3)])
          |> Redis.SortedSet.zadd sortedSetApi "zrange-works"
          |> Redis.query redisHandler
          |> Expect.succeeds
      Redis.SortedSet.zrange sortedSetApi "zrange-works" 0 (-1)
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal ["one", "two", "three"])
      Redis.SortedSet.zrange sortedSetApi "zrange-works" 2 3
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal ["three"])
      Redis.SortedSet.zrange sortedSetApi "zrange-works" (-2) (-1)
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal ["two", "three"]),
    Test.test "zrank works as expected" <| \() -> do
      _ <-
        Redis.SortedSet.del sortedSetApi ("zrank-works" :| [])
          |> Redis.query redisHandler
          |> Expect.succeeds
      _ <-
        NonEmptyDict.init "one" 1 (Dict.fromList [("two", 2), ("twobis", 2), ("three", 3)])
          |> Redis.SortedSet.zadd sortedSetApi "zrank-works"
          |> Redis.query redisHandler
          |> Expect.succeeds
      Redis.SortedSet.zrank sortedSetApi "zrank-works" "one"
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal (Just 0))
      Redis.SortedSet.zrank sortedSetApi "zrank-works" "two"
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal (Just 1))
      Redis.SortedSet.zrank sortedSetApi "zrank-works" "twobis"
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal (Just 2))
      Redis.SortedSet.zrank sortedSetApi "zrank-works" "three"
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal (Just 3))
      Redis.SortedSet.zrank sortedSetApi "zrank-works" "foobar"
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal Nothing)
      Redis.SortedSet.zrank sortedSetApi "zrank-works-nothing-stored" "foobar"
        |> Redis.query redisHandler
        |> Expect.andCheck (Expect.equal Nothing),
    Test.test "scan iterates over all matching keys in batches" <| \() -> do
      let firstKey = "scanTest::key1"
      let firstValue = "value 1"
      let nonEmptyDict =
            NonEmptyDict.init firstKey firstValue <|
              Dict.fromList
                [ ("scanTest::key2", "value 2"),
                  ("scanTest::key3", "value 3"),
                  ("scanTest::key4", "value 4")
                ]
      let expectedKeys =
            NonEmptyDict.toDict nonEmptyDict
              |> Dict.keys
      Redis.mset api nonEmptyDict
        |> Redis.query testNS
        |> Expect.succeeds
      let processBatch = \batchKeys acc ->
            Task.succeed (List.foldl Set.insert acc batchKeys)
      keySet <-
        Redis.foldWithScan testNS (Just "scanTest::*") (Just 2) processBatch Set.empty
          |> Expect.succeeds
      keySet
        |> Set.toList
        |> Expect.equal expectedKeys,
    Test.test "scan works correctly when deleting keys" <| \() -> do
      let firstKey = "scanDeleteTest::key1"
      let firstValue = "value 1"
      let nonEmptyDict =
            NonEmptyDict.init firstKey firstValue <|
              Dict.fromList
                [ ("scanDeleteTest::key2", "value 2"),
                  ("scanDeleteTest::key3", "value 3"),
                  ("scanDeleteTest::key4", "value 4")
                ]
      let expectedKeys =
            NonEmptyDict.toDict nonEmptyDict
              |> Dict.keys
      Redis.mset api nonEmptyDict
        |> Redis.query testNS
        |> Expect.succeeds
      let processBatch = \batchKeys (accDeleted, accKeys) ->
            case batchKeys of
              [] -> Task.succeed (accDeleted, accKeys)
              first : rest -> do
                nDel <-
                  Redis.del api (first :| rest)
                    |> Redis.query testNS
                Task.succeed (accDeleted + nDel, List.foldl Set.insert accKeys batchKeys)
      (totalDeleted, keySet) <-
        Redis.foldWithScan testNS (Just "scanDeleteTest::*") (Just 2) processBatch (0, Set.empty)
          |> Expect.succeeds
      totalDeleted
        |> Expect.equal (List.length expectedKeys)
      keySet
        |> Set.toList
        |> Expect.equal expectedKeys,
    Test.test "eval runs and returns something" <| \() -> do
      let script = [Redis.script|return 1|]
      result <- Redis.eval testNS script |> Expect.succeeds
      Expect.equal result 1,
    Test.test "eval returns Int" <| \() -> do
      let script = [Redis.script|return 1|]
      (result :: Int) <- Redis.eval testNS script |> Expect.succeeds
      Expect.equal result 1,
    Test.test "eval returns ()" <| \() -> do
      let script = [Redis.script|redis.call("ECHO", "hi")|]
      Redis.eval testNS script |> Expect.succeeds,
    Test.test "eval returns List Int" <| \() -> do
      let script = [Redis.script|return {1,2}|]
      (result :: List Int) <- Redis.eval testNS script |> Expect.succeeds
      Expect.equal result [1, 2],
    Test.test "eval with arguments runs and returns something" <| \() -> do
      let script =
            [Redis.script|
      local a = ${Redis.Key "hi"}
      local b = ${Redis.Literal "hello"}
      return 1|]
      result <- Redis.eval testNS script |> Expect.succeeds
      Expect.equal result 1,
    Test.test "eval with arguments returns argument" <| \() -> do
      let script =
            [Redis.script|
      local a = ${Redis.Key 2}
      local b = ${Redis.Literal 3}
      local c = ${Redis.Literal 4}
      local d = ${Redis.Literal 5}
      return {b, c, d}|]
      result <- Redis.eval testNS script |> Expect.succeeds
      Expect.equal result [3, 4, 5],
    Test.test "eval with arguments namespaces key" <| \() -> do
      let script = [Redis.script|return ${Redis.Key "hi"}|]
      (result :: Text) <- Redis.eval testNS script |> Expect.succeeds
      Expect.true
        ( List.member
            result
            -- All tests here run twice:
            -- - once with the auto-extend-expire handler
            -- - once with the normal handler
            -- each run generates a different namespace
            [ "tests-auto-extend-expire:testNamespace:hi",
              "tests:testNamespace:hi"
            ]
        )
  ]
  where
    testNS = addNamespace "testNamespace" redisHandler

ttlTests :: Redis.Handler -> Redis.HandlerAutoExtendExpire -> List Test.Test
ttlTests handler autoExtendExpireHandler =
  let testNS = addNamespace "ttlTestNamespace" handler
      testNSWithExpiry = addNamespace "ttlTestNamespace" autoExtendExpireHandler
   in [ Test.test "ttl reads key w/o ttl" <| \() -> do
          Redis.set api "no-expiry" "value" |> Redis.query testNS |> Expect.succeeds
          result <- Redis.ttl api "no-expiry" |> Redis.query testNS |> Expect.succeeds
          Expect.equal result Redis.NeverExpires,
        Test.test "ttl reads key w/ ttl" <| \() -> do
          Redis.set api "expires" "value" |> Redis.query testNSWithExpiry |> Expect.succeeds
          result <- Redis.ttl api "expires" |> Redis.query testNSWithExpiry |> Expect.succeeds
          Expect.equal result (Redis.ExpiresInSeconds 1),
        Test.test "ttl reports missing key" <| \() -> do
          result <- Redis.ttl api "not-found" |> Redis.query testNSWithExpiry |> Expect.succeeds
          Expect.equal result Redis.TTLKeyNotFound
      ]

-- | Tests that pin down the contract of `handlerWithoutNamespace`: keys are
-- neither prefixed on the way in nor stripped on the way out. The
-- `nsHandler` argument is the regular namespaced `handler` (namespace
-- `"tests"`); we use it to confirm prefixing happens on the namespaced side
-- but not on the no-namespace side.
noNamespaceTests :: Redis.Handler -> Redis.Handler -> List Test.Test
noNamespaceTests noNs nsHandler =
  [ Test.test "set via no-namespace handler stores the key under its literal name" <| \() -> do
      -- Write at literal key "noNs::literalKey" using the no-namespace handler.
      Redis.set api "noNs::literalKey" "value-no-ns" |> Redis.query noNs |> Expect.succeeds
      -- The namespaced handler would look at "tests:noNs::literalKey", which
      -- nothing has written to, so it should see Nothing.
      result <- Redis.get api "noNs::literalKey" |> Redis.query nsHandler |> Expect.succeeds
      Expect.equal result Nothing,
    Test.test "get via no-namespace handler reads the literal key (no prefix added)" <| \() -> do
      -- Namespaced handler writes "noNs::roundTrip" → redis sees "tests:noNs::roundTrip".
      Redis.set api "noNs::roundTrip" "via-namespace" |> Redis.query nsHandler |> Expect.succeeds
      -- The no-namespace handler can reach that same value by spelling out
      -- the full prefixed key, since it adds no prefix of its own.
      result <- Redis.get api "tests:noNs::roundTrip" |> Redis.query noNs |> Expect.succeeds
      Expect.equal result (Just "via-namespace"),
    Test.test "eval via no-namespace handler does not prefix script keys" <| \() -> do
      let script = [Redis.script|return ${Redis.Key "noNs::evalKey"}|]
      (result :: Text) <- Redis.eval noNs script |> Expect.succeeds
      Expect.equal result "noNs::evalKey",
    Test.test "foldWithScan via no-namespace handler returns keys verbatim" <| \() -> do
      let scanPrefix = "noNs::scanTest::"
      let firstKey = scanPrefix ++ "k1"
      let nonEmptyDict =
            NonEmptyDict.init firstKey "v1"
              <| Dict.fromList [(scanPrefix ++ "k2", "v2")]
      let expectedKeys =
            NonEmptyDict.toDict nonEmptyDict
              |> Dict.keys
      Redis.mset api nonEmptyDict |> Redis.query noNs |> Expect.succeeds
      let processBatch = \batchKeys acc ->
            Task.succeed (List.foldl Set.insert acc batchKeys)
      keySet <-
        Redis.foldWithScan noNs (Just (scanPrefix ++ "*")) (Just 10) processBatch Set.empty
          |> Expect.succeeds
      keySet
        |> Set.toList
        |> Expect.equal expectedKeys
  ]

addNamespace :: Text -> Redis.Handler' x -> Redis.Handler' x
addNamespace namespace handler' =
  let combined = case Internal.namespace handler' of
        Just existing -> existing ++ ":" ++ namespace
        Nothing -> namespace
   in handler' {Internal.namespace = Just combined}

api :: Redis.Api Text Text
api = Redis.textApi identity

hashApi :: Redis.Hash.Api Text Text Text
hashApi = Redis.Hash.textApi identity identity Just

listApi :: Redis.List.Api Text Text
listApi = Redis.List.textApi identity

counterApi :: Redis.Counter.Api Text
counterApi = Redis.Counter.makeApi identity

sortedSetApi :: Redis.SortedSet.Api Text Text
sortedSetApi = Redis.SortedSet.textApi identity

jsonApi' :: Redis.Api Text [Int]
jsonApi' = Redis.jsonApi identity

-- | Timestamps recorded in spans would make each test result different from the
-- last. This helper sets all timestamps to zero to prevent this.
--
-- Similarly the db URI changes in each test, because we create temporary test
-- database. To prevent this from failing tests we set the URI to a standard
-- value.
constantValuesForVariableFields :: Platform.TracingSpan -> Platform.TracingSpan
constantValuesForVariableFields span =
  span
    { Platform.started = 0,
      Platform.finished = 0,
      Platform.allocated = 0,
      Platform.children = map constantValuesForVariableFields (Platform.children span)
    }