packages feed

hs-opentelemetry-propagator-w3c-0.1.0.0: test/OpenTelemetry/Propagator/W3CMultiHeaderSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.Propagator.W3CMultiHeaderSpec (spec) where

import qualified Data.ByteString.Char8 as C8
import Data.List (intercalate)
import qualified Data.Text as T
import OpenTelemetry.Propagator.W3CTraceContext
import OpenTelemetry.Trace.TraceState (Key (..), Value (..), empty, fromList, toList)
import Test.Hspec


spec :: Spec
spec = describe "W3C TraceContext Multi-Header Support" $ do
  describe "encodeTraceStateMultiple" $ do
    it "returns single header for small tracestate" $ do
      let ts = fromList [(Key "vendor1", Value "value1"), (Key "vendor2", Value "value2")]
          result = encodeTraceStateMultiple 512 ts
      result `shouldBe` ["vendor1=value1,vendor2=value2"]

    it "splits into multiple headers when size limit exceeded" $ do
      let mediumValue = T.replicate 50 "x" -- Use smaller values that won't be filtered
          ts =
            fromList
              [ (Key "vendor1", Value mediumValue)
              , (Key "vendor2", Value mediumValue)
              , (Key "vendor3", Value mediumValue)
              , (Key "vendor4", Value mediumValue)
              ]
          result = encodeTraceStateMultiple 150 ts -- Small limit to force splitting
      length result `shouldSatisfy` (> 1)
      -- Each header should be under the size limit
      all (\h -> C8.length h <= 150) result `shouldBe` True

    it "removes entries larger than 128 characters" $ do
      let longValue = T.replicate 200 "x" -- This entry will be > 128 chars total
          shortValue = "short"
          ts =
            fromList
              [ (Key "long", Value longValue)
              , (Key "vendor1", Value shortValue)
              , (Key "vendor2", Value shortValue)
              ]
          result = encodeTraceStateMultiple 512 ts
          combinedResult = C8.intercalate "," result
      -- The long entry should be filtered out
      combinedResult `shouldNotSatisfy` C8.isInfixOf "long="
      combinedResult `shouldSatisfy` C8.isInfixOf "vendor1=short"
      combinedResult `shouldSatisfy` C8.isInfixOf "vendor2=short"

    it "respects 32 entry limit before splitting" $ do
      let pairs = [(Key (T.pack $ "key" ++ show i), Value (T.pack $ "val" ++ show i)) | i <- [1 .. 40]]
          ts = fromList pairs
          result = encodeTraceStateMultiple 512 ts
          combinedResult = C8.intercalate "," result
          entryCount = length $ filter (== '=') $ C8.unpack combinedResult
      entryCount `shouldBe` 32

    it "returns empty list for empty tracestate" $ do
      let result = encodeTraceStateMultiple 512 empty
      result `shouldBe` []

    it "handles single very large header correctly" $ do
      let pairs = [(Key (T.pack $ "k" ++ show i), Value (T.pack $ "v" ++ show i)) | i <- [1 .. 20]]
          ts = fromList pairs
          result = encodeTraceStateMultiple 50 ts -- Very small limit
      length result `shouldSatisfy` (> 1)
      -- Should not exceed the limit
      all (\h -> C8.length h <= 50) result `shouldBe` True

  describe "decodeTraceStateMultiple" $ do
    it "combines single header correctly" $ do
      let headers = ["vendor1=value1,vendor2=value2"]
          result = decodeTraceStateMultiple headers
          pairs = toList result
      pairs `shouldContain` [(Key "vendor1", Value "value1")]
      pairs `shouldContain` [(Key "vendor2", Value "value2")]

    it "combines multiple headers in order" $ do
      let headers = ["vendor1=value1,vendor2=value2", "vendor3=value3,vendor4=value4"]
          result = decodeTraceStateMultiple headers
          pairs = toList result
      length pairs `shouldBe` 4
      pairs `shouldContain` [(Key "vendor1", Value "value1")]
      pairs `shouldContain` [(Key "vendor2", Value "value2")]
      pairs `shouldContain` [(Key "vendor3", Value "value3")]
      pairs `shouldContain` [(Key "vendor4", Value "value4")]

    it "handles empty headers gracefully" $ do
      let headers = ["vendor1=value1", "", "vendor2=value2"]
          result = decodeTraceStateMultiple headers
          pairs = toList result
      pairs `shouldContain` [(Key "vendor1", Value "value1")]
      pairs `shouldContain` [(Key "vendor2", Value "value2")]

    it "returns empty tracestate for invalid headers" $ do
      let headers = ["invalid$key=value", "another=bad,value"]
          result = decodeTraceStateMultiple headers
      result `shouldBe` empty

    it "returns empty tracestate for empty header list" $ do
      let result = decodeTraceStateMultiple []
      result `shouldBe` empty

    it "handles whitespace-only headers" $ do
      let headers = ["vendor1=value1", "   ", "vendor2=value2"]
          result = decodeTraceStateMultiple headers
          pairs = toList result
      pairs `shouldContain` [(Key "vendor1", Value "value1")]
      pairs `shouldContain` [(Key "vendor2", Value "value2")]

  describe "round-trip multi-header property" $ do
    it "round-trips through multiple headers" $ do
      let originalPairs =
            [ (Key "vendor1", Value "value1")
            , (Key "vendor2", Value "value2")
            , (Key "vendor3", Value "value3")
            ]
          ts = fromList originalPairs
          encoded = encodeTraceStateMultiple 512 ts
          decoded = decodeTraceStateMultiple encoded
          decodedPairs = toList decoded
      decodedPairs `shouldBe` originalPairs

    it "round-trips with size-constrained splitting" $ do
      let originalPairs =
            [ (Key "vendor1", Value "val1")
            , (Key "vendor2", Value "val2")
            , (Key "vendor3", Value "val3")
            , (Key "vendor4", Value "val4")
            ]
          ts = fromList originalPairs
          encoded = encodeTraceStateMultiple 25 ts -- Force splitting
          decoded = decodeTraceStateMultiple encoded
          decodedPairs = toList decoded
      -- Should contain all original entries (order may vary due to splitting)
      length decodedPairs `shouldBe` 4
      all (`elem` decodedPairs) originalPairs `shouldBe` True

  describe "RFC7230 compliance" $ do
    it "maintains header order when combining" $ do
      let headers = ["first=1", "second=2", "third=3"]
          result = decodeTraceStateMultiple headers
      -- The combined header should be processed in order
      result `shouldSatisfy` (/= empty)

    it "handles comma-separated values correctly" $ do
      let headers = ["a=1,b=2", "c=3,d=4"]
          result = decodeTraceStateMultiple headers
          pairs = toList result
      length pairs `shouldBe` 4
      pairs `shouldContain` [(Key "a", Value "1")]
      pairs `shouldContain` [(Key "b", Value "2")]
      pairs `shouldContain` [(Key "c", Value "3")]
      pairs `shouldContain` [(Key "d", Value "4")]

  describe "W3C specification compliance" $ do
    it "supports at least 512 character recommendation" $ do
      let longValuePairs = [(Key (T.pack $ "vendor" ++ show i), Value (T.pack $ replicate 30 'x')) | i <- [1 .. 10]]
          ts = fromList longValuePairs
          encoded = encodeTraceStateMultiple 512 ts
          totalSize = sum $ map C8.length encoded
      -- Should handle at least 512 characters worth of data
      totalSize `shouldSatisfy` (>= 300) -- Conservative check given filtering
    it "removes oversized entries as per spec" $ do
      let oversizedEntry = (Key "big", Value (T.replicate 200 "x"))
          normalEntry = (Key "small", Value "value")
          ts = fromList [oversizedEntry, normalEntry]
          encoded = encodeTraceStateMultiple 512 ts
          combined = C8.intercalate "," encoded
      -- Oversized entry should be filtered out
      combined `shouldNotSatisfy` C8.isInfixOf "big="
      combined `shouldSatisfy` C8.isInfixOf "small=value"