hask-redis-mux-0.1.0.0: test/ClusterSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import qualified Data.ByteString.Char8 as BS8
import Data.Time.Clock (getCurrentTime)
import Database.Redis.Cluster
import Database.Redis.Resp (RespData (..))
import Test.Hspec
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
describe "Hash tag extraction" $ do
it "extracts hash tag from valid key" $ do
extractHashTag "{user}:profile" `shouldBe` "user"
extractHashTag "{user}:settings" `shouldBe` "user"
it "returns full key when no hash tag present" $ do
extractHashTag "simple-key" `shouldBe` "simple-key"
extractHashTag "key:with:colons" `shouldBe` "key:with:colons"
it "handles edge cases correctly" $ do
-- Empty tag
extractHashTag "{}" `shouldBe` "{}"
-- No closing brace
extractHashTag "{user" `shouldBe` "{user"
-- No opening brace
extractHashTag "user}" `shouldBe` "user}"
-- Multiple braces - uses first valid pair
extractHashTag "{first}{second}" `shouldBe` "first"
-- Braces at end
extractHashTag "key{tag}" `shouldBe` "key{tag}"
it "handles special characters in hash tags" $ do
extractHashTag "{user:1}" `shouldBe` "user:1"
extractHashTag "{a-b}" `shouldBe` "a-b"
extractHashTag "{tag.with.dots}" `shouldBe` "tag.with.dots"
describe "Slot calculation" $ do
it "calculates slot within valid range" $ do
let slot = calculateSlot "test-key"
slot `shouldSatisfy` (< 16384)
it "calculates same slot for keys with same hash tag" $ do
let slot1 = calculateSlot "{user}:profile"
slot2 = calculateSlot "{user}:settings"
slot1 `shouldBe` slot2
it "calculates different slots for different keys (usually)" $ do
let slot1 = calculateSlot "key1"
slot2 = calculateSlot "key2"
-- Note: This could theoretically fail if both keys hash to same slot,
-- but probability is very low
slot1 `shouldNotBe` slot2
it "handles empty key" $ do
let slot = calculateSlot ""
slot `shouldSatisfy` (< 16384)
it "handles very long keys" $ do
let longKey = BS8.replicate 1000 'x'
slot = calculateSlot longKey
slot `shouldSatisfy` (< 16384)
describe "Topology parsing" $ do
it "parses simple CLUSTER SLOTS response" $ do
currentTime <- getCurrentTime
let response =
RespArray
[ RespArray
[ RespInteger 0,
RespInteger 5460,
RespArray
[ RespBulkString "127.0.0.1",
RespInteger 7000,
RespBulkString "node-id-1"
]
]
]
case parseClusterSlots response currentTime of
Left err -> expectationFailure $ "Parsing failed: " ++ err
Right topology -> do
-- Check that slots are assigned
case findNodeForSlot topology 0 of
Nothing -> expectationFailure "Slot 0 should be assigned"
Just nodeId -> nodeId `shouldNotBe` ""
it "handles invalid responses" $ do
currentTime <- getCurrentTime
let invalidResponse = RespBulkString "invalid"
case parseClusterSlots invalidResponse currentTime of
Left _ -> return () -- Expected
Right _ -> expectationFailure "Should fail on invalid response"
it "parses response with replicas" $ do
currentTime <- getCurrentTime
let response =
RespArray
[ RespArray
[ RespInteger 0,
RespInteger 5460,
RespArray
[ RespBulkString "127.0.0.1",
RespInteger 7000,
RespBulkString "master-1"
],
RespArray
[ RespBulkString "127.0.0.1",
RespInteger 7003,
RespBulkString "replica-1"
]
]
]
case parseClusterSlots response currentTime of
Left err -> expectationFailure $ "Parsing failed: " ++ err
Right topology -> do
-- Check that master is assigned
case findNodeForSlot topology 0 of
Nothing -> expectationFailure "Slot 0 should be assigned"
Just nodeId -> nodeId `shouldNotBe` ""
describe "Node lookup" $ do
it "finds correct node for slot" $ do
currentTime <- getCurrentTime
let response =
RespArray
[ RespArray
[ RespInteger 0,
RespInteger 100,
RespArray
[ RespBulkString "127.0.0.1",
RespInteger 7000,
RespBulkString "node1"
]
],
RespArray
[ RespInteger 101,
RespInteger 200,
RespArray
[ RespBulkString "127.0.0.1",
RespInteger 7001,
RespBulkString "node2"
]
]
]
case parseClusterSlots response currentTime of
Left err -> expectationFailure $ "Parsing failed: " ++ err
Right topology -> do
findNodeForSlot topology 50 `shouldSatisfy` (/= Nothing)
findNodeForSlot topology 150 `shouldSatisfy` (/= Nothing)
-- Out of range
findNodeForSlot topology 16384 `shouldBe` Nothing
it "returns empty string for slots not covered by any node" $ do
currentTime <- getCurrentTime
let response =
RespArray
[ RespArray
[ RespInteger 0,
RespInteger 100,
RespArray
[ RespBulkString "127.0.0.1",
RespInteger 7000,
RespBulkString "node1"
]
]
]
case parseClusterSlots response currentTime of
Left err -> expectationFailure $ "Parsing failed: " ++ err
Right topology -> do
findNodeForSlot topology 50 `shouldSatisfy` (/= Nothing)
findNodeForSlot topology 200 `shouldBe` Just ""