packages feed

mmzk-typeid-0.3.0.0: test/Spec.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}

import           Control.Monad
import           Data.Aeson
import qualified Data.ByteString.Lazy as BSL
import           Data.KindID
import           Data.KindID.Class
import           Data.Map (Map)
import qualified Data.Map as M
import           Data.String
import           Data.Text (Text)
import qualified Data.Text as T
import           Data.TypeID
import           Data.TypeID.Class
import           Data.TypeID.Error
import           Data.UUID.V7 (UUID)
import qualified Data.UUID.V7 as V7
import           GHC.Generics (Generic)
import           Test.Hspec

data TestData = TestData { name   :: String
                         , typeid :: String
                         , prefix :: Maybe Text
                         , uuid   :: Maybe String }
  deriving (Generic, FromJSON, ToJSON)

data TestDataUUID = TestDataUUID { name   :: String
                                 , typeid :: TypeID
                                 , prefix :: Text
                                 , uuid   :: UUID }
  deriving (Generic, FromJSON, ToJSON)

data Prefix = User | Post | Comment

instance ToPrefix 'User where
  type PrefixSymbol 'User = "user"

instance ToPrefix 'Post where
  type PrefixSymbol 'Post = "post"

instance ToPrefix 'Comment where
  type PrefixSymbol 'Comment = "comment"

anyTypeIDError :: Selector TypeIDError
anyTypeIDError = const True

withCheck :: HasCallStack => (IDConv a, IDGen a) => IO a -> IO a
withCheck action = do
  tid         <- action
  checkResult <- checkIDWithEnv tid
  case checkResult of
    Just _  -> do
      expectationFailure $ concat ["The ID ", id2String tid, " does not pass validity check"]
      undefined
    Nothing -> pure tid

withChecks :: HasCallStack => IDGen a => IO [a] -> IO [a]
withChecks action = do
  tids        <- action
  checkResult <- msum <$> mapM checkIDWithEnv tids
  case checkResult of
    Just _  -> expectationFailure "The IDs do not pass validity check" >> undefined
    Nothing -> pure tids

main :: IO ()
main = do
  invalid   <- BSL.readFile "test/invalid.json" >>= throwDecode :: IO [TestData]
  valid     <- BSL.readFile "test/valid.json" >>= throwDecode :: IO [TestData]
  validUUID <- BSL.readFile "test/valid.json" >>= throwDecode :: IO [TestDataUUID]

  hspec do
    describe "Generate TypeID" do
      it "can generate TypeID with prefix" do
        start <- V7.getEpochMilli
        tid   <- withCheck $ genID @TypeID "mmzk"
        end   <- V7.getEpochMilli
        getPrefix tid `shouldBe` "mmzk"
        getTime tid `shouldSatisfy` \t -> t >= start && t <= end
      it "can generate TypeID without prefix" do
        start <- V7.getEpochMilli
        tid   <- withCheck $ genID @TypeID ""
        end   <- V7.getEpochMilli
        getPrefix tid `shouldBe` ""
        getTime tid `shouldSatisfy` \t -> t >= start && t <= end
      it "can generate TypeID with stateless UUIDv7" do
        start <- V7.getEpochMilli
        tid   <- withCheck $ genID' @TypeID "mmzk"
        end   <- V7.getEpochMilli
        getPrefix tid `shouldBe` "mmzk"
        getTime tid `shouldSatisfy` \t -> t >= start && t <= end
      it "can generate in batch with same timestamp and in ascending order" do
        start <- V7.getEpochMilli
        tids  <- withChecks $ genIDs @TypeID "mmzk" 1526
        end   <- V7.getEpochMilli
        all ((== "mmzk") . getPrefix) tids `shouldBe` True
        let timestamp = getTime $ head tids
        all ((== timestamp) . getTime) tids `shouldBe` True
        all (uncurry (<)) (zip tids $ tail tids) `shouldBe` True
        timestamp `shouldSatisfy` \t -> t >= start && t <= end
      it "can parse TypeID from String" do
        case string2ID @TypeID "mmzk_00041061050r3gg28a1c60t3gf" of
          Left err  -> expectationFailure $ "Parse error: " ++ show err
          Right tid -> getPrefix tid `shouldBe` "mmzk"

    describe "Parse TypeID" do
      let invalidPrefixes = [ ("caps", "PREFIX")
                            , ("numeric", "12323")
                            , ("symbols", "pre.fix")
                            , ("spaces", "  ")
                            , ("long", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")
                            , ("ascii", "château") ]
      describe "can detect invalid prefix" do
        forM_ invalidPrefixes \(reason, prefix) -> it reason do
          genID @TypeID prefix `shouldThrow` anyTypeIDError
          case decorate @TypeID prefix V7.nil of
            Left _  -> pure ()
            Right _ -> expectationFailure "Should not be able to decorate with invalid prefix"
      let invalidSuffixes = [ ("spaces", " ")
                            , ("short", "01234")
                            , ("long", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789")
                            , ("caps", "00041061050R3GG28A1C60T3GF") -- Would be valid in lowercase
                            , ("hyphens", "00041061050-3gg28a1-60t3gf")
                            , ("crockford_ambiguous", "ooo41o61o5or3gg28a1c6ot3gi") -- Would be valid if we followed Crockford's substitution rules
                            , ("symbols", "00041061050.3gg28a1_60t3gf")
                            , ("wrong_alphabet", "ooooooiiiiiiuuuuuuulllllll") ]
      describe "can detect invalid suffix" do
        forM_ invalidSuffixes \(reason, suffix) -> it reason do
          case string2ID @TypeID suffix of
            Left _    -> pure ()
            Right tid -> expectationFailure $ "Parsed TypeID: " ++ show tid

    describe "Parse special values" do
      let specialValues = [ ("nil", "00000000000000000000000000", "00000000-0000-0000-0000-000000000000")
                          , ("one", "00000000000000000000000001", "00000000-0000-0000-0000-000000000001")
                          , ("ten", "0000000000000000000000000a", "00000000-0000-0000-0000-00000000000a")
                          , ("sixteen", "0000000000000000000000000g", "00000000-0000-0000-0000-000000000010")
                          , ("thirty-two", "00000000000000000000000010", "00000000-0000-0000-0000-000000000020") ]
      forM_ specialValues \(reason, tid, uuid) -> it reason do
        case string2ID @TypeID tid of
          Left err  -> expectationFailure $ "Parse error: " ++ show err
          Right tid -> show (getUUID tid) `shouldBe` uuid

    describe "TypeID valid JSON instances" do
      it "Decode and then encode should be identity" do
        tid  <- genID @TypeID "mmzk"
        tid' <- genID @TypeID "foo"
        let mapping = M.fromList [(tid, tid')]
        let json    = encode mapping
        decode json `shouldBe` Just mapping
        fmap encode (decode @(Map TypeID TypeID) json) `shouldBe` Just json
      describe "Valid JSON value" do
        forM_ valid \(TestData name tid (Just prefix) (Just uuid)) -> it name do
          case decode @TypeID (fromString $ show tid) of
            Nothing  -> expectationFailure "Parse JSON failed!"
            Just tid -> do
              getPrefix tid `shouldBe` prefix
              show (getUUID tid) `shouldBe` uuid
      describe "Valid JSON key" do
        forM_ valid \(TestData name tid (Just prefix) (Just uuid)) -> it name do
          case decode @(Map TypeID Int) (fromString $ "{" ++ show tid ++ ":" ++ "114514" ++ "}") of
            Nothing      -> expectationFailure "Parse JSON failed!"
            Just mapping -> do
              let (tid, _) = M.elemAt 0 mapping
              getPrefix tid `shouldBe` prefix
              show (getUUID tid) `shouldBe` uuid

    describe "TypeID invalid JSON instances" do
      describe "Invalid JSON value" do
        forM_ invalid \(TestData name tid _ _) -> it name do
          case decode @TypeID (fromString $ show tid) of
            Nothing  -> pure ()
            Just tid -> expectationFailure $ "Parsed TypeID: " ++ show tid
      describe "Invalid JSON key" do
        forM_ invalid \(TestData name tid _ _) -> it name do
          case decode @(Map TypeID Int) (fromString $ "{" ++ show tid ++ ":" ++ "114514" ++ "}") of
            Nothing  -> pure ()
            Just tid -> expectationFailure "Invalid TypeID key shouldn't be parsed!"

    describe "Test invalid.json" do
      forM_ invalid \(TestData name tid _ _) -> it name do
        case string2ID @TypeID tid of
          Left _    -> pure ()
          Right tid -> expectationFailure $ "Parsed TypeID: " ++ show tid

    describe "Test valid.json (TypeID as literal)" do
      forM_ valid \(TestData name tid (Just prefix) (Just uuid)) -> it name do
        case string2ID @TypeID tid of
          Left err  -> expectationFailure $ "Parse error: " ++ show err
          Right tid -> do
            getPrefix tid `shouldBe` prefix
            show (getUUID tid) `shouldBe` uuid

    describe "Test valid.json (TypeID as JSON)" do
      forM_ validUUID \(TestDataUUID name tid prefix uuid) -> it name do
        getPrefix tid `shouldBe` prefix
        getUUID tid `shouldBe` uuid

    describe "Generate type-level TypeID (KindID) with 'Symbol' prefixes" do
      it "can generate KindID with prefix" do
        start <- V7.getEpochMilli
        kid   <- withCheck $ genID @(KindID "mmzk")
        end   <- V7.getEpochMilli
        getPrefix kid `shouldBe` "mmzk"
        getTime kid `shouldSatisfy` \t -> start <= t && t <= end
      it "can generate KindID without prefix" do
        start <- V7.getEpochMilli
        kid   <- withCheck $ genID @(KindID "")
        end   <- V7.getEpochMilli
        getPrefix kid `shouldBe` ""
        getTime kid `shouldSatisfy` \t -> start <= t && t <= end
      it "can generate KindID with stateless UUID v7" do
        start <- V7.getEpochMilli
        kid   <- withCheck $ genID' @(KindID "mmzk")
        end   <- V7.getEpochMilli
        getPrefix kid `shouldBe` "mmzk"
      it "can generate in batch with same timestamp and in ascending order" do
        start <- V7.getEpochMilli
        kids  <- withChecks $ genIDs @(KindID "mmzk") 1526
        end   <- V7.getEpochMilli
        all ((== "mmzk") . getPrefix) kids `shouldBe` True
        let timestamp = getTime $ head kids
        all ((== timestamp) . getTime) kids `shouldBe` True
        all (uncurry (<)) (zip kids $ tail kids) `shouldBe` True
        timestamp `shouldSatisfy` \t -> start <= t && t <= end
      it "can parse KindID from String" do
        case string2ID @(KindID "mmzk") "mmzk_00041061050r3gg28a1c60t3gf" of
          Left err  -> expectationFailure $ "Parse error: " ++ show err
          Right kid -> getPrefix kid `shouldBe` "mmzk"
      it "cannot parse KindID into wrong prefix" do
        case string2ID @(KindID "foo") "mmzk_00041061050r3gg28a1c60t3gf" of
          Left err  -> pure ()
          Right kid -> expectationFailure $ "Parsed TypeID: " ++ show kid

    describe "Generate type-level TypeID with custom data kind prefixes" do
      it "can generate TypeID with prefix" do
          kid <- withCheck $ genID @(KindID 'Post)
          getPrefix kid `shouldBe` "post"
      it "can parse TypeID from String" do
        case string2ID @(KindID User) "user_00041061050r3gg28a1c60t3gf" of
          Left err  -> expectationFailure $ "Parse error: " ++ show err
          Right kid -> getPrefix kid `shouldBe` "user"
      it "cannot parse TypeID into wrong prefix" do
        case string2ID @(KindID Comment) "user_00041061050r3gg28a1c60t3gf" of
          Left err  -> pure ()
          Right kid -> expectationFailure $ "Parsed TypeID: " ++ show kid
      it "can generate in batch with same timestamp and in ascending order" do
        kids <- withChecks $ genIDs @(KindID 'Comment) 1526
        all ((== "comment") . getPrefix) kids `shouldBe` True
        let timestamp = getTime $ head kids
        all ((== timestamp) . getTime) kids `shouldBe` True
        all (uncurry (<)) (zip kids $ tail kids) `shouldBe` True