packages feed

gamgee-1.0.0: test/Gamgee/Test/Property.hs

{-# LANGUAGE DeriveAnyClass             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE NoImplicitPrelude          #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE PolyKinds                  #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeApplications           #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

-- | Property based tests
module Gamgee.Test.Property
  ( propertyTests
  ) where

import           Control.Monad.ST          (ST)
import qualified Crypto.Random             as CR
import qualified Data.ByteArray.Encoding   as BE
import qualified Data.ByteString           as BS
import qualified Data.Text                 as Text
import qualified Gamgee.Effects            as Eff
import qualified Gamgee.Test.Operation     as Op
import qualified Gamgee.Token              as Token
import           Relude
import qualified Relude.Extra.Map          as Map
import           Test.QuickCheck           (allProperties)
import           Test.QuickCheck.Instances ()
import qualified Test.QuickCheck.Monadic   as T
import qualified Test.Tasty                as T
import qualified Test.Tasty.QuickCheck     as T


deriving newtype instance T.Arbitrary Token.TokenIdentifier
deriving newtype instance T.Arbitrary Token.TokenLabel
deriving newtype instance T.Arbitrary Token.TokenIssuer

instance T.Arbitrary Token.TokenSecret where
  arbitrary = Token.TokenSecretPlainText . decodeUtf8 <$> base32String
    where
      base32String :: T.Gen ByteString
      base32String = BE.convertToBase BE.Base32 <$> T.arbitrary `T.suchThat` (\s -> BS.length s `mod` 10 == 0)

instance T.Arbitrary Token.TokenAlgorithm where
  arbitrary = T.oneof [ pure Token.AlgorithmSHA1
                      , pure Token.AlgorithmSHA256
                      , pure Token.AlgorithmSHA512
                      ]

instance T.Arbitrary Token.TokenDigits where
  arbitrary = T.oneof [ pure Token.Digits6
                      , pure Token.Digits8
                      ]

instance T.Arbitrary Token.TokenPeriod where
  arbitrary = Token.TokenPeriod <$> T.choose (10, 120)

instance T.Arbitrary Token.TokenSpec where
  arbitrary = Token.TokenSpec
              <$> pure Token.TOTP
              <*> T.arbitrary
              <*> T.arbitrary
              <*> T.arbitrary
              <*> T.arbitrary
              <*> T.arbitrary
              <*> T.arbitrary

-- | Ensures that an effect succeeded, fail the ST monad otherwise
ensureSuccess :: ST s (Either Eff.EffError a) -> ST s a
ensureSuccess action = do
  res <- action
  either (fail . show) return res

withSystemDRG :: (T.Testable a) => (CR.SystemDRG -> T.PropertyM IO a) -> T.Property
withSystemDRG f = T.monadicIO $ do
  drg <- liftIO CR.getSystemDRG
  f drg

prop_addThenRemove :: Token.TokenSpec -> T.Property
prop_addThenRemove spec = withSystemDRG $ \drg -> T.assert (addThenRemove drg == Right [])
  where
    addThenRemove :: CR.SystemDRG -> Either Eff.EffError [Op.OutputMessage]
    addThenRemove drg = Op.runTest Nothing $ \store -> do
      ensureSuccess $ Op.addToken drg store [""] spec
      ensureSuccess $ Op.deleteToken store (Token.getIdentifier spec)
      Op.listTokens store

prop_addThenList :: [Token.TokenSpec] -> T.Property
prop_addThenList specList = withSystemDRG $ \drg -> T.assert (addThenList drg == Right (Token.unTokenIdentifier <$> Map.keys specs))
  where
    specs :: HashMap Token.TokenIdentifier Token.TokenSpec
    specs = fromList $ map (\spec -> (Token.getIdentifier spec, spec)) specList

    addThenList :: CR.SystemDRG -> Either Eff.EffError [Op.OutputMessage]
    addThenList drg = Op.runTest Nothing $ \store -> do
      mapM_ (ensureSuccess . Op.addToken drg store [""]) $ Map.elems specs
      Op.listTokens store

prop_addThenGetOTP :: Token.TokenSpec -> T.Property
prop_addThenGetOTP spec = withSystemDRG $ \drg ->
  forM_ (addThenGetOTP drg) $ \case
    Left err  -> fail $ "Expected an OTP but found " <> show err
    Right otp -> case Token.tokenDigits spec of
                   Token.Digits6 -> T.assert $ (Text.length <$> otp) == [6]
                   Token.Digits8 -> T.assert $ (Text.length <$> otp) == [8]

  where
    ident :: Token.TokenIdentifier
    ident = Token.getIdentifier spec

    addThenGetOTP :: CR.SystemDRG -> [Either Eff.EffError [Op.OutputMessage]]
    addThenGetOTP drg = Op.runTest Nothing $ \store -> do
      ensureSuccess $ Op.addToken drg store [""] spec
      mapM (Op.getOTP drg store [""] ident . fromInteger) [100 + i*30 | i <- [0..4]]

prop_doubleAddFails :: Token.TokenSpec -> T.Property
prop_doubleAddFails spec = withSystemDRG $
  \drg -> T.assert (doubleAdd drg == Left (Eff.AlreadyExists $ Token.getIdentifier spec))

  where
    doubleAdd :: CR.SystemDRG -> Either Eff.EffError ()
    doubleAdd drg = Op.runTest Nothing $ \store -> do
      ensureSuccess $ Op.addToken drg store [""] spec
      Op.addToken drg store [""] spec

prop_doubleDeleteFails :: Token.TokenSpec -> T.Property
prop_doubleDeleteFails spec = withSystemDRG $
  \drg -> T.assert (doubleDelete drg == Left (Eff.NoSuchToken ident))

  where
    ident :: Token.TokenIdentifier
    ident = Token.getIdentifier spec

    doubleDelete :: CR.SystemDRG -> Either Eff.EffError ()
    doubleDelete drg = Op.runTest Nothing $ \store -> do
      ensureSuccess $ Op.addToken drg store [""] spec
      ensureSuccess $ Op.deleteToken store ident
      Op.deleteToken store ident

prop_deleteOnEmptyFails :: Token.TokenIdentifier -> T.Property
prop_deleteOnEmptyFails ident = T.monadicIO $
  T.assert (deleteOnEmpty == Left (Eff.NoSuchToken ident))

  where
    deleteOnEmpty :: Either Eff.EffError ()
    deleteOnEmpty = Op.runTest Nothing $ \store -> Op.deleteToken store ident


-- Template Haskell hack to make the following $allProperties work
return []

propertyTests :: T.TestTree
propertyTests = T.testProperties "Property Tests" $allProperties