freckle-app 1.9.4.0 → 1.9.5.0
raw patch · 4 files changed
+328/−5 lines, 4 filesdep +bcp47dep +cookiedep +yesod-testdep ~base
Dependencies added: bcp47, cookie, yesod-test
Dependency ranges changed: base
Files
- CHANGELOG.md +5/−1
- freckle-app.cabal +7/−3
- library/Freckle/App/Test/Yesod.hs +311/−0
- package.yaml +5/−1
CHANGELOG.md view
@@ -1,4 +1,8 @@-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.9.4.0...main)+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.9.5.0...main)++## [v1.9.5.0](https://github.com/freckle/freckle-app/compare/v1.9.4.0...v1.9.5.0)++- Add module `Freckle.App.Test.Yesod` ## [v1.9.4.0](https://github.com/freckle/freckle-app/compare/v1.9.3.0...v1.9.4.0)
freckle-app.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: freckle-app-version: 1.9.4.0+version: 1.9.5.0 license: MIT license-file: LICENSE maintainer: Freckle Education@@ -55,6 +55,7 @@ Freckle.App.Test.Hspec.Runner Freckle.App.Test.Properties.JSON Freckle.App.Test.Properties.PathPiece+ Freckle.App.Test.Yesod Freckle.App.Wai Freckle.App.Yesod Freckle.App.Yesod.Routes@@ -88,7 +89,8 @@ aeson, aws-xray-client-persistent, aws-xray-client-wai,- base >=4.14.3.0 && <5,+ base,+ bcp47, bugsnag, bytestring, case-insensitive,@@ -96,6 +98,7 @@ conduit, conduit-extra, containers,+ cookie, datadog, doctest, dotenv,@@ -147,7 +150,8 @@ wai, wai-extra, yaml,- yesod-core+ yesod-core,+ yesod-test if impl(ghc >=9.2) ghc-options: -Wno-missing-kind-signatures
+ library/Freckle/App/Test/Yesod.hs view
@@ -0,0 +1,311 @@+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Similar to "Yesod.Test" from the yesod-test package+--+-- Actions in the 'YesodExample' monad are generalized to a+-- 'MonadYesodExample' constraint, allowing tests to be written+-- in custom monads more easily.+module Freckle.App.Test.Yesod+ ( -- * Monad class+ MonadYesodExample (..)++ -- * Making requests++ -- ** Via RequestBuilder+ , request+ , RequestBuilder+ , setMethod+ , setUrl+ , setRequestBody+ , addGetParam+ , addPostParam+ , addRequestHeader+ , addJsonHeaders+ , setLanguage+ , addAcceptLanguage+ , addFile++ -- ** Other ways+ , get+ , post+ , followRedirect++ -- * Inspecting the response++ -- ** Getting the body+ , getRawBody+ , getCsvBody+ , getJsonBody++ -- ** Dealing with the response+ , getResponse+ , withResponse+ , SResponse (..)++ -- * Assertions++ -- ** Status+ , statusIs++ -- ** Header fields+ , assertHeader+ , assertHeaderContains+ , assertHeaderSatisfies++ -- ** Body+ , bodyContains++ -- * Cookies+ , getRequestCookies+ , testSetCookie+ , testDeleteCookie+ , testClearCookies++ -- * Foundational details+ , SIO+ , TestApp+ , YesodExample+ , YesodExampleData (..)+ , getTestYesod+ )+where++import Freckle.App.Prelude++import Blammo.Logging (LoggingT)+import Control.Monad.Except (ExceptT)+import Control.Monad.State (StateT)+import Control.Monad.Trans.Maybe (MaybeT)+import Control.Monad.Trans.Resource (ResourceT)+import Control.Monad.Validate (ValidateT)+import Data.Aeson (FromJSON, eitherDecode)+import Data.BCP47 (BCP47)+import qualified Data.BCP47 as BCP47+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL+import Data.CaseInsensitive (CI)+import Data.Csv (FromNamedRecord, decodeByName)+import qualified Data.Text as T+import qualified Data.Vector as V+import Network.HTTP.Types.Header (hAccept, hAcceptLanguage, hContentType)+import Network.Wai.Test (SResponse (..))+import Test.Hspec.Expectations.Lifted (expectationFailure)+import UnliftIO.Exception (throwString)+import Web.Cookie (SetCookie)+import Yesod.Core (RedirectUrl, Yesod)+import Yesod.Test+ ( RequestBuilder+ , SIO+ , TestApp+ , YesodExample+ , YesodExampleData (..)+ , addFile+ , addGetParam+ , addPostParam+ , addRequestHeader+ , getRequestCookies+ , setMethod+ , setRequestBody+ , setUrl+ , withResponse+ )+import qualified Yesod.Test++class (MonadIO m, Yesod site) => MonadYesodExample site m | m -> site where+ liftYesodExample :: YesodExample site a -> m a++instance Yesod site => MonadYesodExample site (YesodExample site) where+ liftYesodExample = id++instance MonadYesodExample site m => MonadYesodExample site (StateT s m) where+ liftYesodExample = lift . liftYesodExample++instance MonadYesodExample site m => MonadYesodExample site (ReaderT r m) where+ liftYesodExample = lift . liftYesodExample++instance MonadYesodExample site m => MonadYesodExample site (ValidateT e m) where+ liftYesodExample = lift . liftYesodExample++instance MonadYesodExample site m => MonadYesodExample site (MaybeT m) where+ liftYesodExample = lift . liftYesodExample++instance MonadYesodExample site m => MonadYesodExample site (ExceptT e m) where+ liftYesodExample = lift . liftYesodExample++instance MonadYesodExample site m => MonadYesodExample site (ResourceT m) where+ liftYesodExample = lift . liftYesodExample++instance MonadYesodExample site m => MonadYesodExample site (LoggingT m) where+ liftYesodExample = lift . liftYesodExample++-- | Assert the last response has the given text+--+-- The check is performed using the response body in full text form.+bodyContains :: forall m site. MonadYesodExample site m => String -> m ()+bodyContains = liftYesodExample . Yesod.Test.bodyContains++-- | Clears the current cookies+testClearCookies :: forall m site. MonadYesodExample site m => m ()+testClearCookies = liftYesodExample $ Yesod.Test.testClearCookies++-- | Deletes the cookie of the given name+testDeleteCookie+ :: forall m site. MonadYesodExample site m => ByteString -> m ()+testDeleteCookie = liftYesodExample . Yesod.Test.testDeleteCookie++-- | Sets a cookie+testSetCookie :: forall m site. MonadYesodExample site m => SetCookie -> m ()+testSetCookie = liftYesodExample . Yesod.Test.testSetCookie++-- | Get the body of the most recent response and decode it as JSON+getJsonBody :: forall a m site. (MonadYesodExample site m, FromJSON a) => m a+getJsonBody = either err pure . eitherDecode =<< getRawBody+ where+ err e = throwString $ "Error decoding JSON response body: " <> e++-- | Get the body of the most recent response and decode it as CSV+getCsvBody+ :: forall a m site. (MonadYesodExample site m, FromNamedRecord a) => m [a]+getCsvBody = either err (pure . V.toList . snd) . decodeByName =<< getRawBody+ where+ err e = throwString $ "Error decoding CSV response body: " <> e++-- | Get the body of the most recent response as a byte string+getRawBody :: forall m site. MonadYesodExample site m => m BSL.ByteString+getRawBody =+ fmap simpleBody . maybe (throwString "Test response had no body") pure+ =<< getResponse++-- | Get the most recently provided response value, if available+getResponse :: forall m site. MonadYesodExample site m => m (Maybe SResponse)+getResponse = liftYesodExample getResponse++-- | The general interface for performing requests+--+-- 'request' takes a 'RequestBuilder', constructs a request, and executes it.+--+-- The 'RequestBuilder' allows you to build up attributes of the request,+-- like the headers, parameters, and URL of the request.+request+ :: forall m site. MonadYesodExample site m => RequestBuilder site () -> m ()+request = liftYesodExample . Yesod.Test.request++-- | Set a language for the test Request+--+-- This uses a @_LANG@ query parameter since it's a singleton case, just to+-- exercise that machinery.+setLanguage :: BCP47 -> RequestBuilder site ()+setLanguage = addGetParam "_LANG" . BCP47.toText++-- | Set the @Accept-Language@ header to a list of raw values+--+-- This allows testing with actual quality-factors, etc.+addAcceptLanguage :: [Text] -> RequestBuilder site ()+addAcceptLanguage values =+ addRequestHeader (hAcceptLanguage, encodeUtf8 $ T.intercalate "," values)++-- | Sets both @Content-Type@ and @Accept@ fields to @application/json@+addJsonHeaders :: RequestBuilder site ()+addJsonHeaders = do+ addRequestHeader (hContentType, "application/json")+ addRequestHeader (hAccept, "application/json")++-- | Assert the last response status is as expected+--+-- If the status code doesn't match, a portion of the body is also+-- printed to aid in debugging.+statusIs+ :: forall m site. (HasCallStack, MonadYesodExample site m) => Int -> m ()+statusIs = liftYesodExample . Yesod.Test.statusIs++-- | Assert that the given header field's value satisfied some predicate+assertHeaderSatisfies+ :: forall m site+ . MonadYesodExample site m+ => CI ByteString+ -- ^ Field name+ -> String+ -- ^ Some description of the predicate; this is included+ -- in the error message if the assertion fails+ -> (ByteString -> Bool)+ -- ^ Predicate applied to the field value which is expected+ -- to return 'True'+ -> m ()+assertHeaderSatisfies header predicateDesc predicate = liftYesodExample $ withResponse $ \res ->+ case lookup header $ simpleHeaders res of+ Just value | predicate value -> pure ()+ Just value ->+ expectationFailure $+ concat+ [ "Expected header "+ , show header+ , " "+ , predicateDesc+ , ", but received "+ , show value+ ]+ Nothing ->+ expectationFailure $+ concat+ [ "Expected header "+ , show header+ , predicateDesc+ , ", but it was not present"+ ]++-- | Assert that the given header field's value contains+-- some particular byte string within it+assertHeaderContains+ :: MonadYesodExample site m+ => CI ByteString+ -- ^ Field name+ -> ByteString+ -- ^ Substring that we expect to find anywhere within the field value+ -> m ()+assertHeaderContains header substring =+ assertHeaderSatisfies+ header+ ("to contain " <> show substring)+ (substring `BS.isInfixOf`)++-- | Assert the given header key/value pair was returned+assertHeader+ :: forall m site+ . MonadYesodExample site m+ => CI ByteString+ -- ^ Field name+ -> ByteString+ -- ^ Expected field value+ -> m ()+assertHeader k v = liftYesodExample $ Yesod.Test.assertHeader k v++-- | Follow a redirect, if the last response was a redirect+followRedirect+ :: forall m site+ . MonadYesodExample site m+ => m (Either Text Text)+ -- ^ Left with an error message if not a redirect,+ -- Right with the redirected URL if it was+followRedirect = liftYesodExample Yesod.Test.followRedirect++-- | Perform a GET request to url+get+ :: forall url m site+ . (MonadYesodExample site m, RedirectUrl site url)+ => url+ -> m ()+get = liftYesodExample . Yesod.Test.get++-- | Perform a POST request to url+post+ :: forall url m site+ . (MonadYesodExample site m, RedirectUrl site url)+ => url+ -> m ()+post = liftYesodExample . Yesod.Test.post++-- | Get the foundation value used for the current test+getTestYesod :: forall m site. MonadYesodExample site m => m site+getTestYesod = liftYesodExample Yesod.Test.getTestYesod
package.yaml view
@@ -1,5 +1,5 @@ name: freckle-app-version: 1.9.4.0+version: 1.9.5.0 maintainer: Freckle Education category: Utils github: freckle/freckle-app@@ -74,6 +74,8 @@ - aeson - aws-xray-client-persistent - aws-xray-client-wai+ - base+ - bcp47 - bugsnag - bytestring - case-insensitive@@ -81,6 +83,7 @@ - conduit - conduit-extra - containers+ - cookie - datadog - doctest - dotenv@@ -133,6 +136,7 @@ - wai-extra - yaml - yesod-core+ - yesod-test tests: spec: