packages feed

gemcap-0.1.0: test/Network/Gemini/Capsule/InternalSpec.hs

{-

gemcap

Cooyright (C) Jonathan Lamothe <jonathan@jlamothe.net>

This program 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
<https://www.gnu.org/licenses/>.

-}

{-# LANGUAGE OverloadedStrings, RecordWildCards #-}

module Network.Gemini.Capsule.InternalSpec (spec) where

import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.Char (ord)
import Data.Connection (Connection (..))
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.X509 (Certificate (..))
import System.IO.Streams (nullInput, unRead)
import Test.Hspec (
  Spec,
  context,
  describe,
  it,
  shouldBe,
  shouldReturn)

import Network.Gemini.Capsule.Types
import Network.Gemini.Capsule.Internal

spec :: Spec
spec = describe "Internal" $ do
  runConnectionSpec
  readURLSpec
  strFromConnSpec
  readMaxSpec
  stripCRLFSpec

runConnectionSpec :: Spec
runConnectionSpec = describe "runConnection" $ mapM_
  ( \(desc, ioConnRef, handler, mCert, expect) -> context desc $
    it ("should return " ++ show expect) $ do
      (conn, outRef) <- ioConnRef
      runConnection conn handler mCert
      readIORef outRef `shouldReturn` expect
  )

  --  description,            connection,  handler, certificate,     expectation
  [ ( "basic connection",     basicConn,   basicH,  Nothing,         basicExp    )
  , ( "no certificate",       basicConn,   certH,   Nothing,         noCertExp   )
  , ( "with certificate",     basicConn,   certH,   Just sampleCert, basicExp    )
  , ( "gibberish with CR/LF", gibConnCRLF, basicH,  Nothing,         gibExp      )
  , ( "gibberish w/o CR/LF",  gibConn,     basicH,  Nothing,         gibExp      )
  ]

  where
    basicConn   = mkIOConn ["gemini://example.com/\r\n"]
    gibConnCRLF = mkIOConn ["aosidjgfoeribjeworifj\r\n"]
    gibConn     = mkIOConn ["sodifjboije"]
    basicH _    = return newGemResponse { respBody = Just success }

    certH req = return $ case reqCert req of
      Nothing -> newGemResponse
        { respStatus = 60
        , respMeta   = "certificate required"
        }
      Just _ -> newGemResponse { respBody = Just success }

    basicExp  = ["20 text/gemini\r\nSuccess!\r\n"]
    noCertExp = ["60 certificate required\r\n"]
    gibExp    = ["59 bad request\r\n"]
    success   = "Success!\r\n"

readURLSpec :: Spec
readURLSpec = describe "readURL" $ mapM_
  ( \(desc, ioConn, expect) -> context desc $
    it ("should return " ++ show expect) $
      do
        conn <- ioConn
        readURL conn `shouldReturn` expect
  )

  --  description,       connection,  expected result
  [ ( "valid URL",       validConn,   Just validExp   )
  , ( "long URL",        longConn,    Just longExp    )
  , ( "too long URL",    tooLongConn, Nothing         )
  , ( "gibberish input", gibConn,     Nothing         )
  ]

  where
    validConn   = mkInConn ["gemini://example.com/\r\n"]
    longConn    = mkInConn [longBS]
    tooLongConn = mkInConn [tooLongBS]
    gibConn     = mkInConn ["aosidjfwoeinboijwefr"]
    longBS      = BS.pack (take 1024 bytes) <> "\r\n"
    tooLongBS   = BS.pack (take 1025 bytes) <> "\r\n"
    bytes       = BS.unpack prefix ++ repeat (fromIntegral $ ord 'A')
    validExp    = newGemURL "example.com"
    longExp     = validExp { gemPath = [longDir] }
    longDir     = replicate (1024 - BS.length prefix) 'A'
    prefix      = "gemini://example.com/"

strFromConnSpec :: Spec
strFromConnSpec = describe "strFromConn" $ mapM_
  ( \(desc, maxLen, ioConn, expect) -> context desc $
    it ("should return " ++ show expect) $ do
      conn <- ioConn
      strFromConn maxLen conn `shouldReturn` expect
  )

  --  description,    max size, connection,                expected
  [ ( "valid string", 100,      mkInConn ["foo\r\n"],      Just "foo"  )
  , ( "long string",  5,        mkInConn ["too long\r\n"], Nothing     )
  , ( "no CR/LF",     100,      mkInConn ["foo"],          Nothing     )
  , ( "bad UTF-8",    100,      mkInConn ["foo\xff\r\n"],  Nothing     )
  , ( "non-ASCII",    100,      mkInConn ["\xc3\xa9\r\n"], Just "\xe9" )
  ]

readMaxSpec :: Spec
readMaxSpec = describe "readMax" $ mapM_
  ( \(desc, maxLen, ioConn, expect) -> context desc $
    it ("should return " ++ show expect) $ do
      conn <- ioConn
      readMax maxLen conn `shouldReturn` expect
  )

  --  description,    max length,   connection,  expected
  [ ( "single input", 1024,         singleConn,  Just singleBS )
  , ( "multi input",  1024,         multiConn,   Just multiBS  )
  , ( "long input",   longLen,      longConn,    Just longBS   )
  , ( "too long",     pred longLen, longConn,    Nothing       )
  , ( "empty input",  1024,         mkInConn [], Just ""       )
  ]

  where
    singleConn = mkInConn ["foo"]
    multiConn  = mkInConn ["foo", "bar", "baz"]
    longConn   = mkInConn [longBS]
    longLen    = BS.length longBS
    singleBS   = "foo"
    multiBS    = "foobarbaz"
    longBS     = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

stripCRLFSpec :: Spec
stripCRLFSpec = describe "stripCRLF" $ mapM_
  ( \(input, expected) -> context (show input) $
    it ("should be" ++ show expected) $
      stripCRLF input `shouldBe` expected
  )

  --  input,     expectation
  [ ( "foo\r\n", Just "foo"  )
  , ( "foo\n",   Nothing     )
  , ( "foo",     Nothing     )
  , ( "\r\n",    Just ""     )
  ]

mkIOConn :: [BS.ByteString] -> IO (Connection (), IORef [BSL.ByteString])
mkIOConn input = do
  ref  <- newIORef []
  conn <-
    ( \c -> c { send = \bs -> modifyIORef' ref (++[bs]) }
    ) <$> mkInConn input
  return (conn, ref)

mkInConn :: [BS.ByteString] -> IO (Connection ())
mkInConn bss = do
  source <- nullInput
  mapM_ (`unRead` source) (reverse bss)
  let
    send          = const $ return ()
    close         = return ()
    connExtraInfo = ()
  return Connection {..}

sampleCert :: Certificate
sampleCert = Certificate
  { certVersion      = undefined
  , certSerial       = undefined
  , certSignatureAlg = undefined
  , certIssuerDN     = undefined
  , certValidity     = undefined
  , certSubjectDN    = undefined
  , certPubKey       = undefined
  , certExtensions   = undefined
  }

--jl