packages feed

hw-kafka-streamly-0.1.0.0: test/Kafka/Streamly/SourceTest.hs

module Kafka.Streamly.SourceTest (tests) where

import Kafka.Consumer (KafkaError (..), RdKafkaRespErrT (..))
import Kafka.Streamly.Source (
    isFatal,
    isPartitionEOF,
    isPollTimeout,
    skipNonFatal,
    skipNonFatalExcept,
 )
import Streamly.Data.Fold qualified as Fold
import Streamly.Data.Stream (Stream)
import Streamly.Data.Stream qualified as Stream
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, testCase, (@?=))

tests :: TestTree
tests =
    testGroup
        "Source"
        [ testGroup "isFatal" isFatalTests
        , testGroup "isPollTimeout" isPollTimeoutTests
        , testGroup "isPartitionEOF" isPartitionEOFTests
        , testGroup "skipNonFatal" skipNonFatalTests
        , testGroup "skipNonFatalExcept" skipNonFatalExceptTests
        ]

-------------------------------------------------------------------------------
-- isFatal
-------------------------------------------------------------------------------

fatalErrors :: [(String, KafkaError)]
fatalErrors =
    [ ("KafkaUnknownConfigurationKey", KafkaUnknownConfigurationKey "")
    , ("KafkaInvalidConfigurationValue", KafkaInvalidConfigurationValue "")
    , ("KafkaBadConfiguration", KafkaBadConfiguration)
    , ("KafkaBadSpecification", KafkaBadSpecification "")
    , ("RdKafkaRespErrDestroy", KafkaResponseError RdKafkaRespErrDestroy)
    , ("RdKafkaRespErrFail", KafkaResponseError RdKafkaRespErrFail)
    , ("RdKafkaRespErrInvalidArg", KafkaResponseError RdKafkaRespErrInvalidArg)
    , ("RdKafkaRespErrSsl", KafkaResponseError RdKafkaRespErrSsl)
    , ("RdKafkaRespErrUnknownProtocol", KafkaResponseError RdKafkaRespErrUnknownProtocol)
    , ("RdKafkaRespErrNotImplemented", KafkaResponseError RdKafkaRespErrNotImplemented)
    , ("RdKafkaRespErrAuthentication", KafkaResponseError RdKafkaRespErrAuthentication)
    , ("RdKafkaRespErrInconsistentGroupProtocol", KafkaResponseError RdKafkaRespErrInconsistentGroupProtocol)
    , ("RdKafkaRespErrTopicAuthorizationFailed", KafkaResponseError RdKafkaRespErrTopicAuthorizationFailed)
    , ("RdKafkaRespErrGroupAuthorizationFailed", KafkaResponseError RdKafkaRespErrGroupAuthorizationFailed)
    , ("RdKafkaRespErrClusterAuthorizationFailed", KafkaResponseError RdKafkaRespErrClusterAuthorizationFailed)
    , ("RdKafkaRespErrUnsupportedSaslMechanism", KafkaResponseError RdKafkaRespErrUnsupportedSaslMechanism)
    , ("RdKafkaRespErrIllegalSaslState", KafkaResponseError RdKafkaRespErrIllegalSaslState)
    , ("RdKafkaRespErrUnsupportedVersion", KafkaResponseError RdKafkaRespErrUnsupportedVersion)
    ]

nonFatalErrors :: [(String, KafkaError)]
nonFatalErrors =
    [ ("RdKafkaRespErrTimedOut", KafkaResponseError RdKafkaRespErrTimedOut)
    , ("RdKafkaRespErrPartitionEof", KafkaResponseError RdKafkaRespErrPartitionEof)
    , ("RdKafkaRespErrNoError", KafkaResponseError RdKafkaRespErrNoError)
    ]

isFatalTests :: [TestTree]
isFatalTests =
    [ testCase ("fatal: " <> label) (isFatal err @?= True)
    | (label, err) <- fatalErrors
    ]
        <> [ testCase ("non-fatal: " <> label) (isFatal err @?= False)
           | (label, err) <- nonFatalErrors
           ]

-------------------------------------------------------------------------------
-- isPollTimeout
-------------------------------------------------------------------------------

isPollTimeoutTests :: [TestTree]
isPollTimeoutTests =
    [ testCase "matches RdKafkaRespErrTimedOut" $
        isPollTimeout (KafkaResponseError RdKafkaRespErrTimedOut) @?= True
    , testCase "rejects RdKafkaRespErrPartitionEof" $
        isPollTimeout (KafkaResponseError RdKafkaRespErrPartitionEof) @?= False
    , testCase "rejects RdKafkaRespErrAuthentication" $
        isPollTimeout (KafkaResponseError RdKafkaRespErrAuthentication) @?= False
    , testCase "rejects KafkaBadConfiguration" $
        isPollTimeout KafkaBadConfiguration @?= False
    ]

-------------------------------------------------------------------------------
-- isPartitionEOF
-------------------------------------------------------------------------------

isPartitionEOFTests :: [TestTree]
isPartitionEOFTests =
    [ testCase "matches RdKafkaRespErrPartitionEof" $
        isPartitionEOF (KafkaResponseError RdKafkaRespErrPartitionEof) @?= True
    , testCase "rejects RdKafkaRespErrTimedOut" $
        isPartitionEOF (KafkaResponseError RdKafkaRespErrTimedOut) @?= False
    , testCase "rejects RdKafkaRespErrAuthentication" $
        isPartitionEOF (KafkaResponseError RdKafkaRespErrAuthentication) @?= False
    , testCase "rejects KafkaBadConfiguration" $
        isPartitionEOF KafkaBadConfiguration @?= False
    ]

-------------------------------------------------------------------------------
-- skipNonFatal / skipNonFatalExcept
-------------------------------------------------------------------------------

runStream ::
    (Stream IO a -> Stream IO b) ->
    [a] ->
    IO [b]
runStream f xs = Stream.fold Fold.toList (f (Stream.fromList xs))

timedOut, partitionEof, authentication :: KafkaError
timedOut = KafkaResponseError RdKafkaRespErrTimedOut
partitionEof = KafkaResponseError RdKafkaRespErrPartitionEof
authentication = KafkaResponseError RdKafkaRespErrAuthentication

skipNonFatalTests :: [TestTree]
skipNonFatalTests =
    [ testCase "drops poll-timeout, keeps authentication, keeps all Rights" dropsAndKeeps
    , testCase "passes all Rights through unchanged" passesAllRights
    , testCase "drops non-fatal Lefts even with empty extension list" dropsNonFatal
    , testCase "drops partition-EOF (non-fatal)" dropsPartitionEof
    ]
  where
    dropsAndKeeps :: Assertion
    dropsAndKeeps = do
        let input =
                [ Right (1 :: Int)
                , Left timedOut
                , Right 2
                , Left authentication
                , Right 3
                ]
        result <- runStream skipNonFatal input
        result @?= [Right 1, Right 2, Left authentication, Right 3]

    passesAllRights :: Assertion
    passesAllRights = do
        let input = [Right (n :: Int) | n <- [1 .. 5]]
        result <- runStream skipNonFatal input
        result @?= input

    dropsNonFatal :: Assertion
    dropsNonFatal = do
        let input :: [Either KafkaError Int]
            input =
                [ Left timedOut
                , Left partitionEof
                , Left (KafkaResponseError RdKafkaRespErrNoError)
                ]
        result <- runStream skipNonFatal input
        result @?= []

    dropsPartitionEof :: Assertion
    dropsPartitionEof = do
        let input =
                [ Right (1 :: Int)
                , Left partitionEof
                , Right 2
                ]
        result <- runStream skipNonFatal input
        result @?= [Right 1, Right 2]

skipNonFatalExceptTests :: [TestTree]
skipNonFatalExceptTests =
    [ testCase "keeps poll-timeout when isPollTimeout is in the list" keepsTimeouts
    , testCase "keeps partition-EOF when isPartitionEOF is in the list" keepsPartitionEof
    , testCase "empty predicate list behaves like skipNonFatal" emptyListEquivalent
    ]
  where
    keepsTimeouts :: Assertion
    keepsTimeouts = do
        let input =
                [ Right (1 :: Int)
                , Left timedOut
                , Right 2
                , Left authentication
                , Right 3
                ]
        result <- runStream (skipNonFatalExcept [isPollTimeout]) input
        result @?= [Right 1, Left timedOut, Right 2, Left authentication, Right 3]

    keepsPartitionEof :: Assertion
    keepsPartitionEof = do
        let input =
                [ Right (1 :: Int)
                , Left partitionEof
                , Left timedOut
                , Right 2
                ]
        result <- runStream (skipNonFatalExcept [isPartitionEOF]) input
        result @?= [Right 1, Left partitionEof, Right 2]

    emptyListEquivalent :: Assertion
    emptyListEquivalent = do
        let input =
                [ Right (1 :: Int)
                , Left timedOut
                , Left authentication
                , Right 2
                ]
        viaExcept <- runStream (skipNonFatalExcept []) input
        viaPlain <- runStream skipNonFatal input
        viaExcept @?= viaPlain