packages feed

web-cookiejar-0.1.1.0: test/Cookie/JarSpec.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Cookie.JarSpec
Copyright   : (c) 2023 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3
-}
module Cookie.JarSpec (spec) where

import Data.ByteString (ByteString)
import Data.ByteString.Builder (Builder, toLazyByteString)
import qualified Data.ByteString.Lazy as L
import Data.List (sortBy)
import Data.String (fromString)
import Data.Time (UTCTime)
import Data.Time.Clock.POSIX
  ( posixSecondsToUTCTime
  )
import Data.Word (Word16)
import Network.HTTP.Client
  ( Cookie (..)
  , CookieJar
  , compareCookies
  , createCookieJar
  , destroyCookieJar
  , equalCookie
  )
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec
import Test.QuickCheck
  ( Arbitrary (arbitrary)
  , Gen
  , Property
  , chooseInteger
  , forAll
  , listOf1
  , suchThat
  )
import Test.QuickCheck.Monadic (assert, monadicIO, pick, run)
import Web.Cookie.Jar


spec :: Spec
spec = describe "Module Web.Cookie.Jar" $ do
  context "parsing with cookieParser" $ do
    context "and building with cookieBuilder" $ do
      it "should almost roundtrip" prop_almostRoundtripCookie

  context "parsing with cookieJarParser" $ do
    context "and building with jarBuilder" $ do
      it "should almost roundtrip" (prop_almostRoundtripCookieJar jarBuilder)

    context "and building netscapeJarBuilder" $ do
      it "should almost roundtrip" (prop_almostRoundtripCookieJar netscapeJarBuilder)

  context "when accessing persisted jars" $ around useTmp $ do
    context "writeJar then readJar" $ do
      it "should almost roundtrip" (prop_almostRoundtripSavedJars writeJar)
    context "writeNetscapeJar then readJar" $ do
      it "should almost roundtrip" (prop_almostRoundtripSavedJars writeNetscapeJar)


useTmp :: (FilePath -> IO a) -> IO a
useTmp = withSystemTempDirectory "web-cookiejar"


genJarWithPath :: Gen (FilePath, CookieJar)
genJarWithPath = do
  let mkPath i = "cookie-jar-" ++ i ++ ".txt"
  index <- mkPath . show <$> genWord16
  (,) index <$> genCookieJar


prop_almostRoundtripSavedJars :: (FilePath -> CookieJar -> IO ()) -> FilePath -> Property
prop_almostRoundtripSavedJars writer root = monadicIO $ do
  (jarBase, jar) <- pick genJarWithPath
  let jarPath = root ++ "/" ++ jarBase

  -- this match is incomplete, that's ok, the test fails if it produces a Left
  Right jar' <- run $ do
    writer jarPath jar
    readJar jarPath
  assert $ almostEqJar jar jar'


prop_almostRoundtripCookieJar :: (CookieJar -> Builder) -> Property
prop_almostRoundtripCookieJar toBuilder =
  forAll (cookieJarWithX toBuilder <$> genCookieJar) $ \(j, _txt, j') ->
    either (const False) (almostEqJar j) j'


cookieJarWithX
  :: (CookieJar -> Builder)
  -> CookieJar
  -> (CookieJar, ByteString, Either String CookieJar)
cookieJarWithX toBuilder j =
  let txt = asByteString $ toBuilder j
   in (j, txt, parseCookieJar txt)


genCookieJar :: Gen CookieJar
genCookieJar = createCookieJar <$> listOf1 genCookie


almostEqJar :: CookieJar -> CookieJar -> Bool
almostEqJar jar1 jar2 =
  let
    cookiesOf = sortBy compareCookies . map fixup . destroyCookieJar
   in
    and $ zipWith equalCookie (cookiesOf jar1) (cookiesOf jar2)


prop_almostRoundtripCookie :: Property
prop_almostRoundtripCookie =
  forAll (cookieWithLine <$> genCookie) $ \(c, line) ->
    either (const False) (almostEq c) $ parseOnly cookieParser line


asByteString :: Builder -> ByteString
asByteString = L.toStrict . toLazyByteString


cookieWithLine :: Cookie -> (Cookie, ByteString)
cookieWithLine c = (c, asByteString $ cookieBuilder c)


genCookie :: Gen Cookie
genCookie = do
  (creation, expiry) <- genCreationAndExpiry
  Cookie
    <$> genWithSuffix "name_"
    <*> genWithSuffix "value_"
    <*> pure expiry
    <*> genWithSuffix "domain_"
    <*> genWithSuffix "path_"
    <*> pure creation
    <*> pure creation
    <*> arbitrary
    <*> arbitrary
    <*> arbitrary
    <*> arbitrary


genCreationAndExpiry :: Gen (UTCTime, UTCTime)
genCreationAndExpiry = do
  creation <- genUTCTime
  expiry <- genUTCTime `suchThat` (> creation)
  pure (creation, expiry)


almostEq :: Cookie -> Cookie -> Bool
almostEq c1 c2 = fixup c1 `equalCookie` fixup c2


fixup :: Cookie -> Cookie
fixup c =
  let epoch = posixSecondsToUTCTime 0
   in c
        { cookie_persistent = True
        , cookie_last_access_time = epoch
        , cookie_creation_time = epoch
        }


genWord16 :: Gen Word16
genWord16 = arbitrary


genUTCTime :: Gen UTCTime
genUTCTime = posixSecondsToUTCTime . fromInteger <$> chooseInteger (1, 365 * 86400 * 20)


genWithSuffix :: ByteString -> Gen ByteString
genWithSuffix bs = (bs <>) . fromString . show <$> genWord16