packages feed

apache-md5-0.6.1.0: test/unit-tests-main.hs

{-# LANGUAGE NoImplicitPrelude #-}
module Main (main)
    where

import Prelude (Show(show), error)

import Control.Arrow (Arrow(second))
import Control.Monad
    ( Monad((>>=), return)
    , replicateM
    , replicateM_
    , void
    , when
    )
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Random
import qualified Data.ByteString.Char8 as C8
import Data.Eq (Eq((==), (/=)))
import Data.Function ((.), ($), on)
import Data.Int (Int)
import Data.List ((++), break, concat, drop, dropWhile, takeWhile, unlines)
import Data.String (String)
import System.Exit (ExitCode(..))
import System.IO (IO)
import System.Process (readProcessWithExitCode)
import Test.Framework
import Test.Framework.Providers.HUnit (testCase)
import Test.HUnit (Assertion, assertEqual)

import Data.Digest.ApacheMD5 (apacheMD5)
import Data.Digest.ApacheMD5.Internal (Salt(Salt))


-- {{{ Settings ---------------------------------------------------------------

numberOfTests :: Int
numberOfTests = 1000

maxPasswordLength :: Int
maxPasswordLength = 255

-- }}} Settings ---------------------------------------------------------------

-- | Execute @htpasswd@ command to generate salt and Apache MD5 hash for given
-- password.
runHtpasswd
    :: String
    -- ^ Password
    -> IO (String, String)
    -- ^ (Salt, Hash)
runHtpasswd password = do
    (ec,out,err) <- readProcessWithExitCode "htpasswd"
        ["-nbm", "foo", password] ""
        --       ^^^^^ Username is irelevant
    when (ec /= ExitSuccess)
        -- Missing command, ...
        . error $ unlines
            [ "Command htpasswd returned: "
            , err
            , show ec
            ]
    return $ parse out
    where
        -- Split "<username>:$apr1$<salt>$<hash>\n" to pair (Salt, Hash)
        parse :: String -> (String, String)
        parse = second (drop 1)      -- ("<salt>","<hash>")
            . break (== '$')         -- ("<salt>","$<hash>")
            . drop 7                 -- "<salt>$<hash>"
            . dropWhile (/= ':')     -- ":$apr1$<salt>$<hash>"
            . takeWhile (/= '\n')    -- "<username>:$apr1$<salt>$<hash>"

-- | Generate random password, run @htpasswd@ to obtain salt and reference
-- Apache MD5, then ran provided function with password and salt as arguments
-- and compare its output to what @htpasswd@ generated.
testApacheMD5 :: (String -> String -> String) -> Int -> Assertion
testApacheMD5 toTest n =
    void $ getStdGen >>= runRandT (replicateM_ n testApacheMD5')
  where
    testApacheMD5' :: RandT StdGen IO ()
    testApacheMD5' = do
        password <- genPassword
        (salt, hash) <- liftIO $ runHtpasswd password

        liftIO $ assertEqual (msg salt password) hash
            $ toTest password salt

    msg :: String -> String -> String
    msg s p = concat
        [ "Hash does not match for salt = "
        , show s
        , " and password "
        , show p
        ]

    -- Generate password of length between 0 and maxPasswordLength.
    genPassword :: RandT StdGen IO String
    genPassword = getRandomR (0, maxPasswordLength)
        >>= (`replicateM` getRandomR ('!', '~'))

main :: IO ()
main = defaultMain
    [ testCase ("apacheMD5 (" ++ show numberOfTests ++ " tests)")
        $ testApacheMD5 apacheMD5' numberOfTests
    ]
  where
    -- Wrap tested function so that it takes Strings and produces String.
    apacheMD5' :: String -> String -> String
    apacheMD5' =
        (C8.unpack .) . ((\password -> apacheMD5 password . Salt) `on` C8.pack)