{-# 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"