packages feed

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

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

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

import Test.Hspec
import Test.QuickCheck

import Data.String

import Network.Mail.Assumpta.MonadSmtp as S
import Network.Mail.Assumpta.Mock as Mock

import Control.Monad.Writer
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import           Data.ByteString (ByteString) 
import qualified Data.ByteString.Char8 as BS (intercalate)


sampleMesgLines :: IsString s => [s]
sampleMesgLines = [
    "Date: Tue, 21 Jan 2020 02:28:37 +0800"
  , "To: Someone"
  , "From: Someone else"
  , "Subject: test message"
  , ""
  , "This is a test mailing"
  ]

-- | given an intercalate function, produce a message
-- from sampleMesgLines which does NOT end in a crlf.
joinMesg :: (IsString s) => (s -> [s] -> s) -> s
joinMesg f =
  f crlf sampleMesgLines

-- | sequences of commands in the SMTP monad,
-- and the bytestrings we expect to see sent by them.
-- We *don't* validate that inputs are sensible --
-- assumpta-core trusts the user to only pass
-- data that is valid according to the relevant RFCs.
--
-- tuple is: (description, test seq, expected response)
testSequences :: [(String, MockSmtp (), BS.ByteString)]
testSequences = [
    ( "trivial session"
    , ehlo "myhost.mydomain" >>
      quit
    , "EHLO myhost.mydomain\r\nQUIT\r\n"
    )

  , ( "expecting code sends nothing"
    , expectCode 250
    , ""
    )
 
  -- for message content not ending in CRLF: we must add a CRLF
  -- (plus the terminating ".\r\n"). 
  , let bs = joinMesg BS.intercalate
    in
    ( "short message not ending in CRLF, sent via sendRawMail"
    , 
      sendRawMail "sender@s.com" ["recipient@r.com"] bs
    , "MAIL FROM:<sender@s.com>\r\nRCPT TO:<recipient@r.com>\r\nDATA\r\n"
      <> bs <> "\r\n.\r\n"
    )

  -- for message content that DOES end in CRLF, we can only
  -- add the terminating  ".\r\n".
  , let bs = joinMesg BS.intercalate <> crlf
    in
    ( "short message ending in CRLF, sent via sendRawMail"
    , 
      sendRawMail "sender@s.com" ["recipient@r.com"] bs
    , "MAIL FROM:<sender@s.com>\r\nRCPT TO:<recipient@r.com>\r\nDATA\r\n"
      <> bs <> ".\r\n"
    ) 

  -- ** no-param commands
  , nullaryCommand noop "NOOP"
  , nullaryCommand quit "QUIT"
  , nullaryCommand rset "RSET"
  , nullaryCommand startTLS "STARTTLS"
  ]

-- | e.g. @nullaryCommand quit "QUIT"@
-- specifies that the result of executing the command-sequence
-- quit
-- is just that we send the string "QUIT", terminated with crlf.
nullaryCommand :: MockSmtp () -> (forall s . IsString s => s) ->
                    (String, MockSmtp (), ByteString)
nullaryCommand a str =
  (str <> " command", a, str <> (crlf :: ByteString))

-- | check that a sequence of MockSmtp commands sends the
-- expected ByteString.
evalTestSeq :: (String, MockSmtp (), BS.ByteString) -> Expectation
evalTestSeq (_, a, expectBS) =
  runMockSmtp a `shouldBe` expectBS


dataSpec :: SpecWith ()
dataSpec =
  describe "data_" $
    it "should pass on its parameter unaltered" $ property $ \xs ->
      let bs = BSC.pack xs
          output = runMockSmtp $ data_ bs
          expected_output = "DATA" <> crlf <> bs
      in  expected_output == output

sendRawMailSpec :: SpecWith ()
sendRawMailSpec =
  describe "sendRawMail" $ do
    context "when passed content that does NOT end in crlf" $
      it "will add a CRLF (and the '.CRLF' terminator)" $ property $ \xs ->
          let bs = BSC.pack $ xs <> "|" -- our input doesn't end in crlf
              output = runMockSmtp $ 
                          sendRawMail "sender@s.com" ["recipient@r.com"] bs
              expected_output =
                    "MAIL FROM:<sender@s.com>\r\n"
                    <> "RCPT TO:<recipient@r.com>\r\n"
                    <> "DATA\r\n"
                    <> bs <> "\r\n.\r\n"
          in  expected_output == output
    context "when passed content that DOES end in crlf" $
      it "will add only the '.CRLF' terminator" $ property $ \xs ->
        let bs = BSC.pack $ xs <> crlf -- our input ends in crlf
            output = runMockSmtp $ 
                        sendRawMail "sender@s.com" ["recipient@r.com"] bs
            expected_output =
                    "MAIL FROM:<sender@s.com>\r\n"
                    <> "RCPT TO:<recipient@r.com>\r\n"
                    <> "DATA\r\n"
                    <> bs <> ".\r\n"
        in  expected_output == output

-- `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 "executing sequences of MonadSmtp commands" $
    describe "should send expected ByteStrings" $
      forM_ testSequences $ \s@(descrip, _, _) ->
          it descrip $ evalTestSeq s
  dataSpec
  sendRawMailSpec