packages feed

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

{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

module Freckle.App.MemcachedSpec
  ( spec
  ) where

import Freckle.App.Test

import Blammo.Logging.LogSettings
import Blammo.Logging.Logger
import Control.Lens (lens, to, (^?))
import Data.Aeson (Value (..))
import Data.Aeson.Lens
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import qualified Freckle.App.Env as Env
import Freckle.App.Memcached
import Freckle.App.Memcached.Client (MemcachedClient, withMemcachedClient)
import qualified Freckle.App.Memcached.Client as Memcached
import Freckle.App.Memcached.Servers
import Freckle.App.OpenTelemetry

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

data App = App
  { appMemcachedClient :: MemcachedClient
  , appLogger :: Logger
  , appTracer :: Tracer
  }

instance HasMemcachedClient App where
  memcachedClientL =
    lens appMemcachedClient $ \x y -> x {appMemcachedClient = y}

instance HasLogger App where
  loggerL = lens appLogger $ \x y -> x {appLogger = y}

instance HasTracer App where
  tracerL = lens appTracer $ \x y -> x {appTracer = y}

loadApp :: (App -> IO a) -> IO a
loadApp f = do
  servers <-
    Env.parse id $
      Env.var
        (Env.eitherReader readMemcachedServers)
        "MEMCACHED_SERVERS"
        (Env.def defaultMemcachedServers)
  appLogger <- newTestLogger defaultLogSettings
  withTracerProvider $ \tp -> do
    let appTracer = makeTracer tp "freckle-app" tracerOptions
    withMemcachedClient servers $ \appMemcachedClient -> do
      f App {..}

spec :: Spec
spec = withApp loadApp $ do
  describe "caching" $ do
    it "caches the given action by key using Cachable" $ appExample $ do
      k <- cacheKeyThrow "A"

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

      val `shouldBe` A
      mbs `shouldBe` Just "A"

    it "logs, but doesn't fail, on deserialization errors" $ appExample $ do
      k <- cacheKeyThrow "B"

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

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

      msgs <- getLoggedMessagesLenient
      let Just LoggedMessage {..} = NE.last <$> NE.nonEmpty msgs
      Object loggedMessageMeta ^? key "error" . key "message" . _String
        `shouldBe` Just "Unable to deserialize: invalid: \"Broken\""

      -- This assertion is far too brittle, but can be useful to un-comment if
      -- you intend to work on this logic specifically
      -- Object loggedMessageMeta ^? key "error" . key "stack" . _String . to T.lines
      --   `shouldBe` Just
      --     [ "CallStack (from HasCallStack):"
      --     , "  throwM, called at library/Freckle/App/Memcached.hs:121:30 in freckle-app-1.10.8.0-1ebuZKUCQVI9sAWTLATGfO:Freckle.App.Memcached"
      --     , "  cachingAs, called at library/Freckle/App/Memcached.hs:92:11 in freckle-app-1.10.8.0-1ebuZKUCQVI9sAWTLATGfO:Freckle.App.Memcached"
      --     , "  caching, called at tests/Freckle/App/MemcachedSpec.hs:87:15 in main:Freckle.App.MemcachedSpec"
      --     ]
      Object loggedMessageMeta
        ^? key "error" . key "stack" . _String . to T.lines
        `shouldSatisfy` maybe False (not . null)