packages feed

purebred-email-0.5: tests/Parser.hs

-- This file is part of purebred-email
-- Copyright (C) 2019, 2021  Fraser Tweedale
--
-- purebred-email is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-# LANGUAGE OverloadedStrings #-}

module Parser where

import Data.Either (isLeft)

import Data.Attoparsec.ByteString.Lazy as AL
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as Char8
import qualified Data.ByteString.Lazy as L
import Data.Time (ZonedTime(ZonedTime), timeZoneMinutes)
import Test.QuickCheck.Instances ()
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck

import Data.IMF
import Data.IMF.Syntax (takeTillString)

tests :: TestTree
tests = testGroup "Parser tests"
  [ testProperty "takeTillString" $ \h l c r ->
      let
        s = h <> l <> c <> r
        s' = L.fromChunks $ B.foldr ((:) . B.singleton) [] s
      in
        not (c `B.isInfixOf` l) ==>
          case AL.parse (AL.string h *> takeTillString c) s' of
            AL.Done r' l' -> r' == L.fromStrict r && l' == l
            _ -> False
  , testBodyParsing
  , testDateTimeParsing
  ]


data Antibody = Antibody
  deriving (Eq, Show)

instance EqMessage Antibody

data MaybeBody = NotPresent | Present B.ByteString
  deriving (Eq, Show)

instance EqMessage MaybeBody


testBodyParsing :: TestTree
testBodyParsing = testGroup "BodyHandler tests"
  [ testCase "empty body" $
      parseOnly (message (const $ NoBody Antibody) <* endOfInput) msg
      @?= Right (Message hdrs Antibody)
  , testCase "optional body (present, parses)" $
      parseOnly (message opt <* endOfInput) (msg <> "\r\nyeah")
      @?= Right (Message hdrs (Present "yeah"))
  , testCase "optional body (present, no parse)" $
      isLeft (parseOnly (message opt <* endOfInput) (msg <> "\r\nnah"))
      @?= True
  , testCase "optional body (present, empty)" $
      isLeft (parseOnly (message opt <* endOfInput) (msg <> "\r\n"))
      @?= True
  , testCase "optional body (absent)" $
      parseOnly (message opt <* endOfInput) msg
      @?= Right (Message hdrs NotPresent)
  ]
  where
    msg = "From: alice@example.net\r\n"
    hdrs = Headers [("From", "alice@example.net")]
    opt _ = OptionalBody (Present <$> AL.string "yeah", NotPresent)

testDateTimeParsing :: TestTree
testDateTimeParsing = testGroup "dateTime parsing" $
  let
    now = explode $ read "2021-01-03 20:16:00 +1000"
    go = fmap explode . parseOnly (dateTime <* endOfInput)
    explode (ZonedTime lt z) = (lt, timeZoneMinutes z)
  in
    [ testCase "good (full)" $
        go "Sun, 03 Jan 2021 20:16:00 +1000"
        @?= Right now
    , testCase "good (comment)" $
        go "Sun, 03 Jan 2021 20:16:00 +1000 (wat)"
        @?= Right now
    , testCase "good (extra whitespace)" $
        go "  Sun,  03  Jan  2021  20:16:00  +1000  "
        @?= Right now
    , testCase "good (no seconds)" $
        go "Sun, 03 Jan 2021 20:16 +1000"
        @?= Right now
    , testCase "good (no day-of-week)" $
        go "03 Jan 2021 20:16:00 +1000"
        @?= Right now
    , testCase "good (single-digit date)" $
        go "Sun, 3 Jan 2021 20:16:00 +1000"
        @?= Right now
    , testCase "good (leap second)" $
        go "Sun, 03 Jan 2021 20:16:60 +1000"
        @?= Right (explode $ read "2021-01-03 20:16:60 +1000")
    , testCase "good (5-digit year)" $
        go "Sun, 03 Jan 12021 20:16:00 +1000"
        @?= Right (explode $ read "12021-01-03 20:16:00 +1000")
    , testCase "bad (invalid day-of-week)" $
        isLeft (go "Moo, 03 Jan 2021 20:16:00 +1000")
        @?= True
    , testCase "bad (inconsistent day-of-week)" $
        isLeft (go "Mon, 03 Jan 2021 20:16:00 +1000")
        @?= True
    , testCase "bad (day < 1)" $
        isLeft (go "Thu, 01 Jan 2021 20:16:00 +1000")
        @?= True
    , testCase "bad (out of range day)" $
        isLeft (go "Tue, 33 Jan 2021 20:16:00 +1000")
        @?= True
    , testCase "bad (inconsistent day of month)" $
        isLeft (go "Mon, 29 Feb 2021 20:16:00 +1000")
        @?= True
    , testCase "bad (invalid month)" $
        isLeft (go "Sun, 03 Joo 2021 20:16:00 +1000")
        @?= True
    , testCase "bad (year insufficient digits)" $
        isLeft (go "03 Jan 9 20:16:00 +1000")
        @?= True
    , testCase "bad (year < 1900)" $
        isLeft (go "Sun, 03 Jan 1899 20:16:00 +1000")
        @?= True
    , testCase "bad (hour > 23)" $
        isLeft (go "Sun, 03 Jan 2021 24:16:00 +1000")
        @?= True
    , testCase "bad (minute > 59)" $
        isLeft (go "Sun, 03 Jan 2021 20:60:00 +1000")
        @?= True
    , testCase "bad (second > 60)" $
        isLeft (go "Sun, 03 Jan 2021 20:16:61 +1000")
        @?= True
    , testCase "bad (no timezone)" $
        isLeft (go "Sun, 03 Jan 2021 20:16:00")
        @?= True
    , testCase "bad (invalid tz seconds)" $
        isLeft (go "Sun, 03 Jan 2021 20:16:00 +0960")
        @?= True
    , testCase "good (obs-zone grammar)" $
        (fmap (fmap snd) . traverse go . fmap ("Sun, 03 Jan 2021 20:16:00 " <>))
          (
            ["UT", "GMT", "EST", "EDT", "CST", "CDT", "MST", "MDT", "PST", "PDT"]
            <> fmap Char8.singleton (['A'..'I'] <> ['K'..'Z'] <> ['a'..'i'] <> ['k'..'z'])
          )
        @?= Right (
          let mil = [1..12] <> fmap negate [1..12] <> [0]
          in fmap (* 60) ([0, 0, -5, -4, -6, -5, -7, -6, -8, -7] <> mil <> mil)
        )
    , testCase "bad (obs-zone grammar)" $
        all (isLeft . go) (("Sun, 03 Jan 2021 20:16:00 " <>) <$> ["UTC", "CET", "AEST", "J"])
        @?= True
    , testCase "good (obs-year 2 digit)" $
        go "Sun, 03 Jan 21 20:16:00 +1000"
        @?= Right now
    , testCase "good (obs-year 3 digit)" $
        go "Sun, 03 Jan 121 20:16:00 +1000"
        @?= Right now
    , testCase "good (obs-year 2 digit 2049)" $
        go "Sun, 03 Jan 49 20:16:60 +1000"
        @?= Right (explode $ read "2049-01-03 20:16:60 +1000")
    , testCase "good (obs-year 2 digit 1950)" $
        go "Tue, 03 Jan 50 20:16:60 +1000"
        @?= Right (explode $ read "1950-01-03 20:16:60 +1000")
    ]