packages feed

hs-opentelemetry-api-1.0.0.0: test/OpenTelemetry/PropagatorSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.PropagatorSpec where

import Data.List (sort)
import qualified OpenTelemetry.Context as Ctx
import OpenTelemetry.Propagator
import Test.Hspec


spec :: Spec
spec = describe "Propagator" $ do
  -- Propagators API §TextMap Propagator: carrier get/set keys
  -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/
  describe "TextMap" $ do
    it "emptyTextMap has no keys" $ do
      textMapKeys emptyTextMap `shouldBe` []

    it "textMapInsert then textMapLookup retrieves value" $ do
      let tm = textMapInsert "foo" "bar" emptyTextMap
      textMapLookup "foo" tm `shouldBe` Just "bar"

    it "is case-insensitive on keys" $ do
      let tm = textMapInsert "Content-Type" "text/plain" emptyTextMap
      textMapLookup "content-type" tm `shouldBe` Just "text/plain"
      textMapLookup "CONTENT-TYPE" tm `shouldBe` Just "text/plain"
      textMapKeys tm `shouldMatchList` ["Content-Type"]
      textMapToList tm `shouldMatchList` [("Content-Type", "text/plain")]

    it "textMapDelete removes entry" $ do
      let tm = textMapInsert "k" "v" emptyTextMap
          tm' = textMapDelete "k" tm
      textMapLookup "k" tm' `shouldBe` Nothing

    it "textMapDelete is case-insensitive" $ do
      let tm = textMapInsert "X-Custom" "val" emptyTextMap
          tm' = textMapDelete "x-custom" tm
      textMapLookup "x-custom" tm' `shouldBe` Nothing

    it "textMapKeys returns all keys" $ do
      let tm = textMapInsert "b" "2" $ textMapInsert "a" "1" emptyTextMap
      sort (textMapKeys tm) `shouldBe` ["a", "b"]

    it "textMapToList returns all pairs" $ do
      let tm = textMapFromList [("X", "1"), ("Y", "2")]
          pairs = textMapToList tm
      length pairs `shouldBe` 2

    it "textMapFromList preserves key casing" $ do
      let tm = textMapFromList [("FOO", "bar")]
      textMapLookup "foo" tm `shouldBe` Just "bar"
      textMapToList tm `shouldMatchList` [("FOO", "bar")]

    it "textMapInsert overwrites existing key" $ do
      let tm = textMapInsert "k" "v1" $ textMapInsert "k" "v2" emptyTextMap
      textMapLookup "k" tm `shouldBe` Just "v1"

  -- Propagators API §Composite Propagator: fields, extract, inject
  -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/
  describe "Propagator composition" $ do
    it "mempty propagator fields are empty" $ do
      let p = mempty :: TextMapPropagator
      propagatorFields p `shouldBe` []

    it "composed propagator merges fields" $ do
      let p1 = Propagator {propagatorFields = ["traceparent"], extractor = \_ c -> pure c, injector = \_ o -> pure o}
          p2 = Propagator {propagatorFields = ["baggage"], extractor = \_ c -> pure c, injector = \_ o -> pure o}
          combined = p1 <> p2 :: TextMapPropagator
      propagatorFields combined `shouldBe` ["traceparent", "baggage"]

    it "extract chains extractors" $ do
      let p1 = Propagator {propagatorFields = [], extractor = \_ c -> pure c, injector = \_ o -> pure o}
          p2 = Propagator {propagatorFields = [], extractor = \_ c -> pure c, injector = \_ o -> pure o}
          combined = p1 <> p2 :: TextMapPropagator
      _ <- extract combined emptyTextMap Ctx.empty :: IO Ctx.Context
      pure ()

    it "inject chains injectors left to right" $ do
      let p1 = Propagator {propagatorFields = ["a"], extractor = \_ c -> pure c, injector = \_ tm -> pure (textMapInsert "a" "1" tm)}
          p2 = Propagator {propagatorFields = ["b"], extractor = \_ c -> pure c, injector = \_ tm -> pure (textMapInsert "b" "2" tm)}
          combined = p1 <> p2 :: TextMapPropagator
      result <- inject combined Ctx.empty emptyTextMap
      textMapLookup "a" result `shouldBe` Just "1"
      textMapLookup "b" result `shouldBe` Just "2"

  -- Implementation-specific: global TextMap propagator registry slot
  -- https://opentelemetry.io/docs/specs/otel/context/api-propagators/
  describe "Global TextMapPropagator" $ do
    it "setGlobalTextMapPropagator and getGlobalTextMapPropagator roundtrip" $ do
      let p = Propagator {propagatorFields = ["test-field"], extractor = \_ c -> pure c, injector = \_ o -> pure o} :: TextMapPropagator
      setGlobalTextMapPropagator p
      p' <- getGlobalTextMapPropagator
      propagatorFields p' `shouldBe` ["test-field"]
      setGlobalTextMapPropagator (mempty :: TextMapPropagator)