packages feed

effectful-tracing-0.1.0.0: test/Effectful/Tracing/BaggageSpec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Effectful.Tracing.BaggageSpec
-- Description : Tests for the baggage value, the ambient effect, and the codec.
--
-- Three areas: the pure 'Baggage' operations (insert / lookup / delete / list),
-- the ambient 'BaggageContext' effect (reading the in-scope baggage and the
-- lexical scoping of 'withBaggageEntry' / 'localBaggage'), and the W3C @baggage@
-- header codec (render / parse, percent-encoding, metadata, whitespace, the
-- entry cap, and skipping malformed members), including 'injectBaggage' /
-- 'extractBaggage' round-trips.
module Effectful.Tracing.BaggageSpec
  ( tests
  ) where

import Data.Text qualified as T

import Effectful (Eff, runPureEff)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

import Effectful.Tracing.Baggage
  ( BaggageContext
  , BaggageEntry (BaggageEntry)
  , baggageFromList
  , baggageSize
  , baggageToList
  , deleteBaggage
  , emptyBaggage
  , getBaggage
  , insertBaggage
  , lookupBaggage
  , lookupBaggageValue
  , localBaggage
  , nullBaggage
  , runBaggage
  , runBaggageWith
  , withBaggageEntry
  )
import Effectful.Tracing.Propagation.Baggage
  ( extractBaggage
  , injectBaggage
  , maxBaggageEntries
  , parseBaggage
  , renderBaggage
  )

tests :: TestTree
tests =
  testGroup
    "Baggage"
    [ testGroup "pure operations" pureOps
    , testGroup "ambient effect" effectOps
    , testGroup "codec" codecOps
    ]

pureOps :: [TestTree]
pureOps =
  [ testCase "empty has no entries" $ do
      nullBaggage emptyBaggage @?= True
      baggageSize emptyBaggage @?= 0
  , testCase "insert then lookup" $ do
      let b = insertBaggage "k" "v" emptyBaggage
      lookupBaggageValue "k" b @?= Just "v"
      lookupBaggage "k" b @?= Just (BaggageEntry "v" Nothing)
      lookupBaggageValue "absent" b @?= Nothing
  , testCase "insert replaces an existing key" $ do
      let b = insertBaggage "k" "second" (insertBaggage "k" "first" emptyBaggage)
      lookupBaggageValue "k" b @?= Just "second"
      baggageSize b @?= 1
  , testCase "delete removes a key" $ do
      let b = deleteBaggage "k" (insertBaggage "k" "v" emptyBaggage)
      lookupBaggageValue "k" b @?= Nothing
      nullBaggage b @?= True
  , testCase "toList is ordered by key" $ do
      let b = insertBaggage "b" "2" (insertBaggage "a" "1" (insertBaggage "c" "3" emptyBaggage))
      map fst (baggageToList b) @?= ["a", "b", "c"]
  ]

effectOps :: [TestTree]
effectOps =
  [ testCase "runBaggage starts empty" $
      runPure (nullBaggage <$> getBaggage) @?= True
  , testCase "runBaggageWith seeds the ambient baggage" $
      runPureEff (runBaggageWith seeded (lookupBaggageValue "tenant" <$> getBaggage))
        @?= Just "acme"
  , testCase "withBaggageEntry is lexically scoped" $ do
      let result = runPure $ do
            before <- nullBaggage <$> getBaggage
            inside <- withBaggageEntry "x" "1" (lookupBaggageValue "x" <$> getBaggage)
            after <- lookupBaggageValue "x" <$> getBaggage
            pure (before, inside, after)
      result @?= (True, Just "1", Nothing)
  , testCase "localBaggage nests" $ do
      let result = runPure $
            withBaggageEntry "a" "1" $
              withBaggageEntry "b" "2" $ do
                b <- getBaggage
                pure (lookupBaggageValue "a" b, lookupBaggageValue "b" b)
      result @?= (Just "1", Just "2")
  , testCase "localBaggage can delete for a scope" $ do
      let result = runPure $
            withBaggageEntry "a" "1" $ do
              dropped <- localBaggage (deleteBaggage "a") (lookupBaggageValue "a" <$> getBaggage)
              restored <- lookupBaggageValue "a" <$> getBaggage
              pure (dropped, restored)
      result @?= (Nothing, Just "1")
  ]
  where
    seeded = baggageFromList [("tenant", BaggageEntry "acme" Nothing)]

codecOps :: [TestTree]
codecOps =
  [ testCase "render joins entries ordered by key" $
      renderBaggage (insertBaggage "b" "2" (insertBaggage "a" "1" emptyBaggage))
        @?= "a=1,b=2"
  , testCase "render percent-encodes values" $
      renderBaggage (insertBaggage "city" "New York" emptyBaggage)
        @?= "city=New%20York"
  , testCase "render appends metadata verbatim" $
      renderBaggage (baggageFromList [("k", BaggageEntry "v" (Just "ttl=30"))])
        @?= "k=v;ttl=30"
  , testCase "render of empty is the empty string" $
      renderBaggage emptyBaggage @?= ""
  , testCase "parse reads multiple entries" $ do
      let b = parseBaggage "k1=v1,k2=v2"
      lookupBaggageValue "k1" b @?= Just "v1"
      lookupBaggageValue "k2" b @?= Just "v2"
  , testCase "parse trims surrounding whitespace" $ do
      let b = parseBaggage " k1 = v1 , k2 = v2 "
      lookupBaggageValue "k1" b @?= Just "v1"
      lookupBaggageValue "k2" b @?= Just "v2"
  , testCase "parse percent-decodes values" $
      lookupBaggageValue "city" (parseBaggage "city=New%20York") @?= Just "New York"
  , testCase "parse keeps metadata as opaque text" $
      lookupBaggage "k" (parseBaggage "k=v;ttl=30;public")
        @?= Just (BaggageEntry "v" (Just "ttl=30;public"))
  , testCase "parse skips malformed members" $ do
      let b = parseBaggage "k1=v1,noequals,=novalue,k3=v3"
      map fst (baggageToList b) @?= ["k1", "k3"]
  , testCase "parse caps the entry count" $ do
      let header = T.intercalate "," ["k" <> T.pack (show i) <> "=v" | i <- [1 .. 200 :: Int]]
      baggageSize (parseBaggage header) @?= maxBaggageEntries
  , testCase "round-trip preserves values needing encoding" $ do
      let b = insertBaggage "k" "a b,c;d" emptyBaggage
      parseBaggage (renderBaggage b) @?= b
  , testCase "injectBaggage emits the header from ambient baggage" $
      runPure (withBaggageEntry "k" "v" injectBaggage)
        @?= [("baggage", "k=v")]
  , testCase "injectBaggage emits nothing when empty" $
      runPure injectBaggage @?= []
  , testCase "extractBaggage reads the header" $
      lookupBaggageValue "k" (extractBaggage [("baggage", "k=v")]) @?= Just "v"
  , testCase "extractBaggage on a missing header is empty" $
      nullBaggage (extractBaggage [("other", "x")]) @?= True
  ]

-- | Run a 'BaggageContext' computation that needs no other effects, starting
-- from empty baggage.
runPure :: Eff '[BaggageContext] a -> a
runPure = runPureEff . runBaggage