packages feed

freckle-app-1.0.3.0: tests/Freckle/App/MemcachedSpec.hs

module Freckle.App.MemcachedSpec
  ( spec
  ) where

import Freckle.App.Prelude

import Control.Monad.IO.Unlift (MonadUnliftIO(..))
import Control.Monad.Logger
import Control.Monad.Reader
import qualified Data.List.NonEmpty as NE
import qualified Freckle.App.Env as Env
import Freckle.App.Memcached
import Freckle.App.Memcached.Client (MemcachedClient, newMemcachedClient)
import qualified Freckle.App.Memcached.Client as Memcached
import Freckle.App.Memcached.Servers
import Freckle.App.Test.Logging
import Test.Hspec

data ExampleValue
  = A
  | B
  | C
  deriving stock (Eq, Show)

instance Cachable ExampleValue where
  toCachable = \case
    A -> "A"
    B -> "Broken"
    C -> "C"

  fromCachable = \case
    "A" -> Right A
    "B" -> Right B
    "C" -> Right C
    x -> Left $ "invalid: " <> show x

newtype TestAppT m a = TestAppT
  { unTestAppT :: ReaderT MemcachedClient (LoggingT m) a
  }
  deriving newtype
    ( Functor
    , Applicative
    , Monad
    , MonadReader MemcachedClient
    , MonadIO
    , MonadLogger
    )

-- We could derive this in newer versions of unliftio-core, but defining it by
-- hand supports a few resolvers back, without CPP. This is just a copy of the
-- ReaderT instance,
--
-- https://hackage.haskell.org/package/unliftio-core-0.2.0.1/docs/src/Control.Monad.IO.Unlift.html#line-64
--
instance MonadUnliftIO m => MonadUnliftIO (TestAppT m) where
  {-# INLINE withRunInIO #-}
  withRunInIO inner = TestAppT $ withRunInIO $ \run -> inner (run . unTestAppT)

runTestAppT :: MonadUnliftIO m => TestAppT m a -> m (a, [Text])
runTestAppT f = do
  servers <- liftIO $ Env.parse $ Env.var
    (Env.eitherReader readMemcachedServers)
    "MEMCACHED_SERVERS"
    (Env.def defaultMemcachedServers)
  mc <- newMemcachedClient servers
  runCapturedLoggingT $ runReaderT (unTestAppT f) mc

spec :: Spec
spec = do
  describe "caching" $ do
    it "caches the given action by key using Cachable" $ example $ do
      void $ runTestAppT $ do
        key <- cacheKeyThrow "A"

        val <- caching key (cacheTTL 5) $ pure A
        mbs <- Memcached.get key

        liftIO $ val `shouldBe` A
        liftIO $ mbs `shouldBe` Just "A"

    it "logs, but doesn't fail, on deserialization errors" $ example $ do
      (_, msgs) <- runTestAppT $ do
        key <- cacheKeyThrow "B"

        val0 <- caching key (cacheTTL 5) $ pure B -- set
        val1 <- caching key (cacheTTL 5) $ pure B -- get will fail
        mbs <- Memcached.get key

        liftIO $ val0 `shouldBe` B
        liftIO $ val1 `shouldBe` B
        liftIO $ mbs `shouldBe` Just "Broken"

      fmap NE.last (NE.nonEmpty msgs)
        `shouldBe` Just "[Caching] error deserializing: invalid: \"Broken\""