http-test 0.1.7 → 0.2
raw patch · 5 files changed
+222/−286 lines, 5 filesdep +http-clientdep +lensdep +networkdep −curldep −safedep −stmdep ~aesondep ~mtldep ~textnew-component:exe:http-testnew-component:exe:test-http-test-on-bayeshive
Dependencies added: http-client, lens, network, tasty, tasty-hunit, wreq
Dependencies removed: curl, safe, stm
Dependency ranges changed: aeson, mtl, text
Files
- HttpTestExec.hs +22/−0
- Test/HTTP.hs +0/−265
- http-test.cabal +25/−17
- src/Test/HTTP.hs +170/−0
- test.hs +5/−4
+ HttpTestExec.hs view
@@ -0,0 +1,22 @@+import System.Environment+import qualified Network.Wreq +import qualified Network.Wreq+import Control.Lens+import Data.Aeson.Lens+import Control.Monad+import Network.URI+main = do+ args <- getArgs+ mapM_ dispatch $ filter notOption args++dispatch file = case parseURI file of + Nothing -> runFile file + Just _ -> runHttp file+++++isOption ('-':_) = True+isOption _ = False++notOption = not . isOption
− Test/HTTP.hs
@@ -1,265 +0,0 @@-module Test.HTTP (httpTest, session, get, getJSON, withJSON, post, postForm, postJSON, assert, assertEq, assertParse, failTest, debug, Program, Session, Url, curlGetString, curlPostString) where--import Network.Curl hiding (curlGetString)--import Control.Concurrent-import Control.Concurrent.STM-import Control.Exception hiding (assert)-import Control.Monad-import Control.Monad.Error-import Control.Monad.Reader-import qualified Control.Monad.State.Strict as State-import Control.Monad.Trans-import Data.Char-import Data.IORef-import Data.List-import Data.Maybe-import GHC.Conc-import qualified Data.Aeson as Ae-import Data.Aeson.Types (Parser, parseEither)-import Safe (readMay)-import System.Environment-import System.Exit-import qualified Data.Text as T-import Data.Text.Encoding (encodeUtf8, decodeUtf8)-import Data.ByteString.Lazy (fromStrict, toStrict)---type Program = ReaderT (TVar [Results]) IO--data SessionState = SessionState { sessionResults :: Results,- sessionBaseUrl :: String,- sessionCurl :: Curl }--type Session a = State.StateT SessionState (ErrorT String IO) a--type Results = [(String, Maybe String)]--type Url = String---- | Run one or more test sessions. httpTest will exit when done, with--- exit code 1 if there were failures-httpTest :: Program () -> IO ()-httpTest m = withCurlDo $ do- resTV <- newTVarIO []- runReaderT m resTV- finalRes <- fmap concat $ readTVarIO resTV-- mapM_ (putStrLn . ppRes) finalRes-- if any (isJust . snd) finalRes- then exitWith $ ExitFailure 1- else exitWith $ ExitSuccess--ppRes (nm, Nothing) = "Pass: "++nm-ppRes (nm, Just reason) = "FAIL: "++nm++"; "++reason----- | Define a single test session based on session name and base url-session :: String -- ^ Session name (used for logging failures)- -> Url -- ^ Base URL- -> Session () -- ^ the actions and assertions that define the session- -> Program ()-session sessionName baseURL m = do- c <- liftIO $ initialize- let state0 = SessionState [] baseURL c- liftIO $ setopts c [CurlCookieJar (sessionName++"_cookies"), - CurlFollowLocation True]- res <- liftIO $ runErrorT $ State.execStateT m state0- case res of - Right (SessionState res _ _) -> do- res_tv <- ask- liftIO $ atomically $ do - others <- readTVar res_tv- writeTVar res_tv $ others ++ [reverse res]- Left err -> do- res_tv <- ask- liftIO $ atomically $ do - others <- readTVar res_tv- writeTVar res_tv $ others ++ - [[(sessionName, - Just $ sessionName ++ " session failure:" ++err)]]---- | GET a web page as a String-get :: Url -- ^ URL- -> Session String-get url = do- (code, res) <- getRaw url- when (code /= CurlOK) $ - failTest ("GET "++url) (show code++"\nResponse:\n"++res)- return res--getRaw :: Url -> Session (CurlCode, String) -getRaw url = do- SessionState _ base c <- State.get- liftIO $ curlGetString c (base++url) [] ---- | GET a JSON value-getJSON :: Ae.FromJSON a => - Url -- ^ URL- -> Session a-getJSON url = do- str <- get url- case Ae.eitherDecode' $ fromStrict $ encodeUtf8 $ T.pack str of- Right x -> return x- Left err -> throwError $ "GET "++url ++ " JSON decoding failure: "++ err----- | perform an action with a JSON value from a GET-withJSON :: Ae.FromJSON a => - String -- ^ URL- -> (a -> Session ()) -- ^ action to perform on successfully decoded value- -> Session ()-withJSON url mu = do- str <- get url- case Ae.eitherDecode' $ fromStrict $ encodeUtf8 $ T.pack str of- Right x -> mu x- Left err -> do failTest ("GET "++url) $ "JSON decoding failure: "++ err- return ()---- | Post a form-postForm :: Url -- ^ URL- -> [(String,String)] -- ^ form fields- -> Session String-postForm url fields = post url $ map (\(x,y) -> x ++ ('=':y)) fields---- | Post a string body-post :: Url -> [String] -> Session String-post url body = do- SessionState _ base c <- State.get- (code, res) <- liftIO $ curlPostString c (base++url) [] body- when (code /= CurlOK) $ - failTest ("POST "++url) (show code++"\nResponse:\n"++res)- return res---- | Post a JSON value-postJSON :: (Ae.ToJSON a, Ae.FromJSON b) => Url -> a -> Session b-postJSON url x = do str <- post url [T.unpack $ decodeUtf8 $ toStrict $ Ae.encode x]- case Ae.eitherDecode' $ fromStrict $ encodeUtf8 $ T.pack str of- Right x -> return x- Left err -> throwError $ "POST "++url ++ " JSON decoding failure: "++ err------- | make an assertion-assert :: String -- ^ assertion name (used for reporting failures- -> Bool -- ^ Boolean of which we are asserting truth- -> Session ()-assert assName True = passTest assName-assert assName False = failTest assName "fail" ---- | assert equality, for better output messages-assertEq :: (Show a, Eq a) => String -- ^ assertion name (used for reporting failures- -> a -- ^ a value- -> a -- ^ what it is meant to be equal to- -> Session ()-assertEq assName x y | x == y = passTest assName- | otherwise = failTest assName $ "not equal: "++show x ++" /= "++show y---- | make an assertion in the Parser monad, for use with JSON value-assertParse :: String -- ^ assertion name (used for reporting failures- -> Parser Bool -- ^ Boolean of which we are asserting truth- -> Session ()-assertParse assName pb =- case parseEither (const pb) () of- Left err -> failTest assName $ "parse failure: "++err- Right True -> passTest assName- Right False -> failTest assName "fail" ----- | Output a string to stdout if @--verbose@ is in command line arguments-debug :: String -> Session ()-debug s = do- args <- liftIO $ getArgs- if "--verbose" `elem` args- then liftIO $ putStrLn s- else return ()-- --addTestResult p = - State.modify $ \s -> s { sessionResults = p : sessionResults s }--passTest tstNm = addTestResult (tstNm, Nothing)-failTest tstNm reason = addTestResult (tstNm, Just reason)---{-------------------------------------------------------- CURL AUXILIARY FUNCTIONS- -------------------------------------------------------}---- | Custom version of getString that uses an existing Curl to handle cookies-curlGetString :: Curl -> URLString- -> [CurlOption]- -> IO (CurlCode, String)-curlGetString h url opts = do- ref <- newIORef []- -- Note: later options may (and should, probably) override these defaults.- setopt h (CurlPostFields [])- setopt h (CurlPost False)- setopt h (CurlFailOnError True)- setDefaultSSLOpts h url- setopt h (CurlURL url)- setopt h (CurlWriteFunction (gatherOutput ref))- mapM_ (setopt h) opts- rc <- perform h- lss <- readIORef ref- return (rc, concat $ reverse lss)---- | Custom version of curlPost that uses an existing Curl to handle cookies--- and returns the result in a string.-curlPostString :: Curl -> URLString -> [CurlOption] -> [String] -> IO (CurlCode, String)-curlPostString h url opts body = do- ref <- newIORef []- -- Note: later options may (and should, probably) override these defaults.- setopt h (CurlFollowLocation True)- setopt h (CurlFailOnError True)- setopt h (CurlPost True)- setopt h (CurlPostFields body)- setDefaultSSLOpts h url- setopt h (CurlURL url)- setopt h (CurlWriteFunction (gatherOutput ref))- mapM_ (setopt h) opts- rc <- perform h- lss <- readIORef ref- return (rc, concat $ reverse lss)--{-------------------------------------------------------- ENCODING- -------------------------------------------------------}--- | FIXME (iperez): I don't know how to obtain all the possible--- transformations that can occur in the output. This should contain all HTML--- and Unicode representations that can be used by the server but were not in--- the original file.-decode :: String -> String-decode [] = []-decode ('\\':'u':a:b:c:d:xs)- | isHexDigit a && isHexDigit b && isHexDigit c && isHexDigit d- = chr (hexToInt [a,b,c,d]) : decode xs-decode ('\\':'n': xs) = '\n' : decode xs-decode ('\\':'"': xs) = '"' : decode xs-decode ('&':'q':'u':'o':'t':';':xs) = '"' : decode xs-decode ('&':'g':'t':';':xs) = '>' : decode xs-decode ('&':'l':'t':';':xs) = '<' : decode xs-decode (x : xs) = x : decode xs--hexToInt :: String -> Int-hexToInt [] = 0-hexToInt [n] = digitToInt n-hexToInt (n:ns) = digitToInt n * 16 + hexToInt ns--{-------------------------------------------------------- CONCURRENCY AUX- -------------------------------------------------------}---- | fork a thread and call the supplied function when the thread is about to--- terminate, with an exception or a returned value. The function is called--- with asynchronous exceptions masked. --- NOTE: This is taken from Control.Concurrency because it's not present in--- GHC 7.4-{-forkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId-forkFinally action and_then =- mask $ \restore ->- forkIO $ Control.Exception.try (restore action) >>= and_then--}
http-test.cabal view
@@ -1,5 +1,5 @@ Name: http-test-Version: 0.1.7+Version: 0.2 synopsis: Test framework for HTTP APIs Description: A simple framework for making assertions about the responses of HTTP servers.@@ -7,8 +7,7 @@ > import Test.HTTP > import Data.List (isInfixOf) >- > main = httpTest $ do- > session "BayesHive landing page" "https://bayeshive.com" $ do+ > main = defaultMain $ httpTestCase "BayesHive landing page" "https://bayeshive.com" $ do > landing <- get "/" > assert "Correct blog link" $ > "href=\"https://bayeshive.com/blog\"" `isInfixOf` landing@@ -33,23 +32,32 @@ Library Exposed-modules: Test.HTTP+ hs-source-dirs: src Build-depends: base >= 4 && < 5- , mtl >= 2.1 && < 2.2- , curl >= 1.3.8 && < 2- , aeson >= 0.6 && < 0.8- , safe >= 0.2 && < 0.4- , stm >= 2.4 && < 2.5- , text >= 0.10 && < 1.2+ , mtl + , aeson >= 0.7 && < 0.8+ , wreq+ , tasty+ , tasty-hunit+ , text >= 1.1 && < 1.2 , bytestring+ , lens+ , http-client -executable test-http-test+executable test-http-test-on-bayeshive main-is: test.hs Build-depends: base >= 4 && < 5- , mtl >= 2.1 && < 2.2- , curl >= 1.3.8 && < 2- , aeson- , safe >= 0.2 && < 0.4- , stm >= 2.4 && < 2.5- , text- , bytestring , http-test++executable http-test+ main-is: HttpTestExec.hs+ Buildable: False+ Build-depends: base >= 4 && < 5+ , lens+ , http-client+ , mtl + , aeson >= 0.7 && < 0.8+ , wreq+ , text >= 1.1 && < 1.2+ , bytestring+ , network
+ src/Test/HTTP.hs view
@@ -0,0 +1,170 @@+module Test.HTTP (httpTestCase, get, getJSON, withJSON, post, postJSON, postForm, assert, assertEq, assertParse, debug, Session, Url, Tasty.defaultMain) where++import Control.Monad+import Control.Monad.Error+import qualified Control.Monad.State.Strict as S+import Control.Monad.Trans+import Data.Char+import Data.IORef+import Data.List+import Data.Maybe+import qualified Data.Aeson as Ae+import Data.Aeson.Types (Parser, parseEither)+import System.Environment+import System.Exit+import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8, decodeUtf8)+import qualified Data.ByteString.Lazy as BL ++import qualified Test.Tasty.HUnit as HUnit+import Test.Tasty as Tasty+import qualified Network.Wreq as Wreq+import qualified Network.Wreq.Types as WreqT+import Control.Lens+import Data.Aeson.Lens++import qualified Network.HTTP.Client as HT ++import Data.ByteString.Lazy.Char8 (unpack, pack)++type Session = S.StateT HttpTest IO++data HttpTest = HttpTest { baseUrl :: String,+ cookieJar :: HT.CookieJar }++type Url = String+++httpTestCase :: String -- ^ Session name (used for logging failures)+ -> Url -- ^ Base URL+ -> Session () -- ^ the actions and assertions that define the session+ -> TestTree+httpTestCase sessionName sessBaseURL m = HUnit.testCase sessionName $ do+ S.evalStateT m $ HttpTest sessBaseURL (HT.createCookieJar [])+++withHT :: (HttpTest -> IO (HT.CookieJar, a)) -> Session a+withHT m = do + ht <- S.get + (cj, x) <- liftIO $ m ht+ S.modify $ \ht -> ht { cookieJar = cj }+ return x++-- | GET a web page as a String+get :: Url -- ^ URL+ -> Session String+get url = do+ (code, res) <- getRaw url+ when (code /= 200) $ + assertFailure $ "GET "++url++": "++show code -- ++"\nResponse:\n"++res+ return res++++getRaw :: Url -> Session (Int, String) +getRaw url = withHT $ \(HttpTest base cj) -> do+ r <- Wreq.getWith (Wreq.defaults & Wreq.cookies .~ cj) (base ++ url)+ return (r ^. Wreq.responseCookieJar, + (r ^. Wreq.responseStatus . Wreq.statusCode, + unpack $ r ^. Wreq.responseBody))+++-- | GET a JSON value+getJSON :: Ae.FromJSON a => + Url -- ^ URL+ -> Session a+getJSON url = do+ str <- get url+ case Ae.eitherDecode' $ BL.fromStrict $ encodeUtf8 $ T.pack str of+ Right x -> return x+ Left err -> throwError $ strMsg $ "GET "++url ++ " JSON decoding failure: "++ err+++-- | perform an action with a JSON value from a GET+withJSON :: Ae.FromJSON a => + String -- ^ URL+ -> (a -> Session ()) -- ^ action to perform on successfully decoded value+ -> Session ()+withJSON url mu = do+ str <- get url+ case Ae.eitherDecode' $ BL.fromStrict $ encodeUtf8 $ T.pack str of+ Right x -> mu x+ Left err -> do assertFailure $ "GET "++url++"JSON decoding failure: "++ err+ return ()+++-- | Post a string body+postRaw :: WreqT.Postable a => Url -> a -> Session String+postRaw url body = withHT $ \(HttpTest base cj) -> do+ r <- Wreq.postWith (Wreq.defaults & Wreq.cookies .~ cj) (base ++ url) body++ let code = r ^. Wreq.responseStatus . Wreq.statusCode++ when (code /= 200) $ + liftIO $ HUnit.assertFailure $ "POST "++url++": "++show code++"\nResponse:\n"++show r+ return $ (r ^. Wreq.responseCookieJar, unpack $ r ^. Wreq.responseBody)+++++-- | Post a form+postForm :: Url -- ^ URL+ -> [(String,String)] -- ^ form fields+ -> Session String+postForm url fields = postRaw url $ map (\(x,y) -> (BL.toStrict $ pack x, BL.toStrict $ pack y)) fields+++-- | Post a string body+post :: Url -> String -> Session String+post u s = postRaw u $ pack s+++-- | Post a JSON value+postJSON :: (Ae.ToJSON a, Ae.FromJSON b) => Url -> a -> Session b+postJSON url x = do str <- post url $ T.unpack $ decodeUtf8 $ BL.toStrict $ Ae.encode x+ case Ae.eitherDecode' $ BL.fromStrict $ encodeUtf8 $ T.pack str of+ Right x -> return x+ Left err -> throwError $ strMsg $ "POST "++url ++ " JSON decoding failure: "++ err+++-- | make an assertion+assert :: String -- ^ assertion name (used for reporting failures+ -> Bool -- ^ Boolean of which we are asserting truth+ -> Session ()+assert assName True = liftIO $ putStrLn $ "Pass: "++assName+assert assName False = liftIO $ HUnit.assertFailure assName++-- | assert an filure+assertFailure :: String -> Session ()+assertFailure nm = liftIO $ HUnit.assertFailure nm+++-- | assert equality, for better output messages+assertEq :: (Show a, Eq a) => String -- ^ assertion name (used for reporting failures+ -> a -- ^ a value+ -> a -- ^ what it is meant to be equal to+ -> Session ()+assertEq assName x y | x == y = liftIO $ putStrLn $ "Pass: "++assName+ | otherwise = liftIO $ HUnit.assertFailure $ assName++show x ++" /= "++show y++-- | make an assertion in the Parser monad, for use with JSON value+assertParse :: String -- ^ assertion name (used for reporting failures+ -> Parser Bool -- ^ Boolean of which we are asserting truth+ -> Session ()+assertParse assName pb =+ case parseEither (const pb) () of+ Left err -> assertFailure $ assName++" parse failure: "++err+ Right True -> liftIO $ putStrLn $ "Pass: "++assName+ Right False -> assertFailure $ assName++" JSON assertion false"+++-- | Output a string to stdout if @--verbose@ is in command line arguments+debug :: String -> Session ()+debug s = liftIO $ putStrLn s++{- args <- liftIO $ getArgs+ if "--verbose" `elem` args+ then + else return () -}++
test.hs view
@@ -1,11 +1,12 @@ import Test.HTTP import Data.List (isInfixOf) -main = httpTest $ do- session "BayesHive landing page" "https://bayeshive.com" $ do+main = defaultMain $ httpTestCase "BayesHive landing page" "https://bayeshive.com" $ do landing <- get "/" assert "Correct blog link" $ "href=\"https://bayeshive.com/blog\"" `isInfixOf` landing loginResult <- postForm "/auth/page/email/login" - [("email", "foo"), ("password", "bar")]- debug loginResult+ [("email", "footest@bar.com"), ("password", "secret")] + doclist <- getJSON "/doclist"+ debug $ show (doclist:: [String])+ return ()