packages feed

assumpta-0.1.0.0: test-hspec/Network/Mail/Assumpta/ByteStringSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}

module Network.Mail.Assumpta.ByteStringSpec
  (main, spec)
  where

import Test.Hspec
import Test.QuickCheck
import Test.QuickCheck.IO (propertyIO)

import           Control.Concurrent
import           Control.Exception (bracket, evaluate)
import           Control.Monad

import qualified Data.ByteString.Char8 as BSC
import           Data.ByteString.Char8 ( ByteString )
import           Data.Monoid -- for early base

import           Network.Socket as N hiding (recv)
import           Network.Socket.ByteString as N (sendAll, recv)


import Network.Mail.Assumpta.ByteString

{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}

-- oops
deriving instance Eq ReplyLine
deriving instance Eq SmtpError

-- .. probably shouldn't churn through *quite* so many sockets
-- so fast - but I haven't run out, so a pool doesn't seem
-- necessary yet.
mkServerSoc :: IO (N.Socket, N.PortNumber)
mkServerSoc = do
    -- wildcard port
    let addr = N.SockAddrInet N.aNY_PORT N.iNADDR_ANY
    soc <- N.socket N.AF_INET N.Stream N.defaultProtocol
    N.bind soc addr
    N.listen soc 5
    -- find out what port we got
    port <- N.socketPort soc
    return (soc,port)

-- | @runSmtpWithServer a replies@
--
-- spin up a TCP server, run 'runSmtp' to send
-- commands to it, return result, plus requests received
-- by server. @replies@ are the canned responses
-- the server is to make.
--
-- The client needs to 'expect' something to start
-- with (i.e. the server goes first), else
-- things are likely to get out of sync.
runSmtpWithServer ::
  Smtp a -> [ByteString] -> IO (Either SmtpError a, ByteString)
runSmtpWithServer commands replies = do
    (soc, port) <- mkServerSoc
    mRequestsRecvd <- newEmptyMVar
    -- server
    void $ forkIO $
        let acquire = fst <$> N.accept soc
            release = N.close
        in bracket acquire release $ \soc' -> do
              requests <- forM replies $ \reply -> do
                N.sendAll soc' $ reply <> crlf
                N.recv soc' 4096
              -- ??? needed??
              requests <- evaluate requests
              --
              putMVar mRequestsRecvd $ mconcat requests
    clientRes <- runSmtp "localhost" (fromIntegral port) commands
    -- return results
    requestsRcvd <- readMVar mRequestsRecvd
    return (clientRes, requestsRcvd)

-- make a server test case out of description, cmds to run, etc
mkSmtpTestCase ::
  (Show b, Eq b) =>
    String
    -> Smtp b
    -> [ByteString]
    -> (Either SmtpError b, ByteString)
    -> SpecWith ()
mkSmtpTestCase testCaseName cmds serverReplies expectedResult =
  context ("when run with test case " <> show testCaseName) $
    it "should send expected bytes & return expected result" $
      runSmtpWithServer cmds serverReplies `shouldReturn` expectedResult

-- | when the client disconnects, the server should get a
-- final empty bytestring
smtpTestCases :: [SpecWith ()]
smtpTestCases = [
    mkSmtpTestCase
            "helo then quit"
            -- cmds
            (do expectGreeting
                helo "myhost"
                quit )
            -- responses
            [ "220 hi there"
            , "250 OK"
            , "221 Bye"
            ]
            -- expect
            (Right (), mconcat ["HELO myhost\r\n"
                                ,"QUIT\r\n"])

  , mkSmtpTestCase
            "improper greeting"
            -- cmds
            expectGreeting
            -- responses
            [ "250 OK" -- bad greeting
            ]
            -- expect
            (Left (UnexpectedResponse "220" [ReplyLine 250 "OK"]), "")

  , mkSmtpTestCase
            "using sendRawMail"
            -- cmds
            (do expectGreeting
                sendRawMail "somesender" ["somerecipt"] "some body text"
                quit )
            -- responses
            [ "220 hi there"
            , "250 OK"
            , "250 OK"
            , "354 End data with <CR><LF>.<CR><LF>"
            , "250 OK"
            , "221 Bye"
            ]
            -- expect
            (Right (), BSC.intercalate crlf [
                            "MAIL FROM:<somesender>"
                          , "RCPT TO:<somerecipt>"
                          , "DATA"
                          , "some body text"
                          , "."
                          , "QUIT\r\n"])

  ]

data_Spec :: SpecWith ()
data_Spec = 
  describe "data_ function" $
    it "should pass on its content unchanged" $ property $
      \(NonEmpty someData) -> ioProperty $ do
          let someData' = BSC.pack someData 
              cmds = do expectGreeting
                        data_ $ someData' <> crlf
              serverReplies = [
                  "220 hi there"
                , "354 End data with <CR><LF>.<CR><LF>"
                , "250 OK"
                ]
          res <- runSmtpWithServer cmds serverReplies
          return $ propertyIO $
              res `shouldBe` (Right (), "DATA\r\n" <> someData' <> "\r\n")


-- `main` is here so that this module can be run from GHCi on its own.  It is
-- not needed for automatic spec discovery.
main :: IO ()
main = hspec spec

spec :: Spec
spec = do
  describe "runSmtp" $ 
    sequence_ smtpTestCases
  data_Spec