module Snap.Snaplet.Auth.Types.Tests ( tests ) where ------------------------------------------------------------------------------ import Control.Exception (SomeException, evaluate, try) import Control.Monad (liftM) import Data.Aeson (decode, eitherDecode, encode) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy.Char8 as BSL import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Time import Test.Framework (Test, testGroup) import Test.Framework.Providers.HUnit (testCase) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.HUnit hiding (Test) import qualified Test.QuickCheck as QC import qualified Test.QuickCheck.Monadic as QCM ------------------------------------------------------------------------------ import qualified Snap.Snaplet.Auth as A import Snap.TestCommon (eqTestCase, ordTestCase, readTestCase, showTestCase) ------------------------------------------------------------------------------ tests :: Test tests = testGroup "Auth type tests" [ testCase "Password serialization" dontSerializeClearText , testCase "Fill in [] roles" deserializeDefaultRoles , testCase "Fail deserialization" failDeserialize , testProperty "AuthFailure show instances" authFailureShows , testProperty "Encrypt agrees with password" encryptByteString , testCase "Reject clear encrypted pw check" rejectCheckClearText , testCase "Test Role Show instance" $ showTestCase (A.Role "a") , testCase "Test Role Read instance" $ readTestCase (A.Role "a") , testCase "Test Role Ord instance" $ ordTestCase (A.Role "a") (A.Role "b") , testCase "Test PW Show instance" $ showTestCase (A.ClearText "pw") , testCase "Test PW Read instance" $ readTestCase (A.ClearText "pw") , testCase "Test PW Ord instance" $ ordTestCase (A.ClearText "a") (A.ClearText "b") , testCase "Test AuthFailure Eq instance" $ eqTestCase A.BackendError A.DuplicateLogin --TODO better as property , testCase "Test AuthFailure Show instance" $ showTestCase A.BackendError -- , testCase "Test AuthFailure Read instance" $ -- readTestCase BackendError -- TODO/NOTE: show . read isn't id for , testCase "Test AuthFailure Ord instance" $ ordTestCase A.BackendError A.DuplicateLogin , testCase "Test UserId Show instance" $ showTestCase (A.UserId "1") , testCase "Test UserId Read instance" $ readTestCase (A.UserId "2") , testCase "Test AuthUser Show instance" $ showTestCase A.defAuthUser , testCase "Test AuthUser Eq instance" $ eqTestCase A.defAuthUser A.defAuthUser ] ------------------------------------------------------------------------------ dontSerializeClearText :: Assertion dontSerializeClearText = do let s = encode (A.ClearText "passwordisnthamster") -- Take the length of the ByteString to force it completely, rather than -- using deepseq; BSL.ByteString lacked an NFData instance until -- bytestring-0.10. r <- try $ evaluate (BSL.length s) >> return s case r of Left e -> (e :: SomeException) `seq` return () Right j -> assertFailure $ "Failed to reject ClearText password serialization: " ++ show j ------------------------------------------------------------------------------ sampleUserJson :: T.Text -> T.Text -> T.Text sampleUserJson reqPair optPair = T.intercalate "," [ "{\"uid\":\"1\"" , "\"login\":\"foo\"" , "\"email\":\"test@example.com\"" , "\"pw\":\"sha256|12|gz47sA0OvbVjos51OJRauQ==|Qe5aU2zAH0gIKHP68KrHJkvvwTvTAqA6UgA33BRpNEo=\"" , reqPair , "\"suspended_at\":null" , "\"remember_token\":\"81160620ef9b64865980c2ab760fcf7f14c06e057cbe1e723cba884a9be05547\"" , "\"login_count\":2" , "\"failed_login_count\":1" , "\"locked_until\":null" , "\"current_login_at\":\"2014-06-24T14:43:51.241Z\"" , "\"last_login_at\":null" , "\"current_ip\":\"127.0.0.1\"" , "\"last_ip\":null" , "\"created_at\":\"2014-06-24T14:43:51.236Z\"" , "\"updated_at\":\"2014-06-24T14:43:51.242Z\"" , "\"reset_token\":null" , "\"reset_requested_at\":null" , optPair , "\"meta\":{}}" ] ------------------------------------------------------------------------------ deserializeDefaultRoles :: Assertion deserializeDefaultRoles = either (\e -> assertFailure $ "Failed user deserialization: " ++ e) (\u -> assertEqual "Roles wasn't initialized to empty" [] (A.userRoles u)) (eitherDecode . BSL.fromChunks . (:[]) . encodeUtf8 $ sampleUserJson "\"activated_at\":null" "\"extra\":null") ------------------------------------------------------------------------------ failDeserialize :: Assertion failDeserialize = do case decode . BSL.fromChunks . (:[]) . encodeUtf8 $ t of Nothing -> return () Just a -> assertFailure $ "Expected deserialization failure, got authUser: " ++ show (a :: A.AuthUser) where t = T.replace "login" "loogin" $ sampleUserJson "\"extra\":null" "\"extra2\":null" ------------------------------------------------------------------------------ authFailureShows :: A.AuthFailure -> Bool authFailureShows ae = length (show ae) > 0 ------------------------------------------------------------------------------ instance QC.Arbitrary A.AuthFailure where arbitrary = do s <- (QC.arbitrary `QC.suchThat` (( > 0 ) . length)) tA <- QC.arbitrary tB <- QC.arbitrary let t = UTCTime (ModifiedJulianDay tA) (realToFrac (tB :: Double)) QC.oneof $ map return [A.AuthError s, A.BackendError ,A.DuplicateLogin, A.EncryptedPassword ,A.IncorrectPassword, A.LockedOut t ,A.PasswordMissing, A.UsernameMissing ,A.UserNotFound ] ------------------------------------------------------------------------------ encryptByteString :: QC.Property encryptByteString = QCM.monadicIO testStringEq where clearPw = BS.pack `liftM` (QC.arbitrary `QC.suchThat` ((>0) . length)) testStringEq = QCM.forAllM clearPw $ \s -> do ePW <- A.Encrypted `liftM` (QCM.run $ A.encrypt s) let cPW = A.ClearText s {- ePW' <- QCM.run $ encryptPassword (ClearText s) QCM.assert $ (checkPassword cPW ePW && checkPassword cPW cPW && checkPassword ePW ePW') --TODO/NOTe: This fails. Surpsising? Encrypt twice and get two different password hashes -} QCM.assert $ (A.checkPassword cPW ePW && A.checkPassword cPW (A.ClearText s)) ------------------------------------------------------------------------------ rejectCheckClearText :: Assertion rejectCheckClearText = do let b = A.checkPassword (A.Encrypted "") (A.ClearText "") r <- try $ b `seq` return b case r of Left e -> (e :: SomeException) `seq` return () Right _ -> assertFailure "checkPassword should not accept encripted-clear pair"