packages feed

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

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.Propagator.W3CTraceContextSpec (spec) where

import Data.Attoparsec.ByteString (parseOnly)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C8
import Data.Either (isLeft)
import qualified Data.Text as T
import OpenTelemetry.Propagator.W3CTraceContext
import OpenTelemetry.Trace.TraceState (Key (..), Value (..), empty, fromList, insert, toList)
import Test.Hspec


spec :: Spec
spec = describe "W3C TraceContext TraceState" $ do
  describe "tracestateParser" $ do
    it "parses empty tracestate" $ do
      parseOnly tracestateParser "" `shouldBe` Right empty

    it "parses single key=value pair" $ do
      let result = parseOnly tracestateParser "vendor1=value1"
      result `shouldBe` Right (insert (Key "vendor1") (Value "value1") empty)

    it "parses multiple key=value pairs" $ do
      let result = parseOnly tracestateParser "vendor1=value1,vendor2=value2"
      case result of
        Right ts -> do
          let pairs = toList ts
          pairs `shouldContain` [(Key "vendor1", Value "value1")]
          pairs `shouldContain` [(Key "vendor2", Value "value2")]
        Left err -> expectationFailure $ "Parse failed: " ++ err

    it "parses key=value pairs with spaces" $ do
      let result = parseOnly tracestateParser " vendor1=value1 , vendor2=value2 "
      case result of
        Right ts -> do
          let pairs = toList ts
          pairs `shouldContain` [(Key "vendor1", Value "value1")]
          pairs `shouldContain` [(Key "vendor2", Value "value2")]
        Left err -> expectationFailure $ "Parse failed: " ++ err

    it "handles multi-tenant keys" $ do
      let result = parseOnly tracestateParser "tenant@vendor=value1"
      result `shouldBe` Right (insert (Key "tenant@vendor") (Value "value1") empty)

    it "handles special characters in keys" $ do
      let result = parseOnly tracestateParser "vendor-1_2*3/4@tenant=value1"
      result `shouldBe` Right (insert (Key "vendor-1_2*3/4@tenant") (Value "value1") empty)

    it "handles special characters in values" $ do
      let result = parseOnly tracestateParser "vendor1=value-with_special*chars/and@symbols"
      result `shouldBe` Right (insert (Key "vendor1") (Value "value-with_special*chars/and@symbols") empty)

    it "limits to 32 entries" $ do
      let pairs = [C8.pack $ "key" ++ show i ++ "=value" ++ show i | i <- [1 .. 40]]
          input = C8.intercalate "," pairs
          result = parseOnly tracestateParser input
      case result of
        Right ts -> length (toList ts) `shouldBe` 32
        Left err -> expectationFailure $ "Parse failed: " ++ err

    it "rejects invalid key starting with uppercase" $ do
      let result = parseOnly tracestateParser "VENDOR=value1"
      result `shouldSatisfy` isLeft

    it "rejects invalid key with invalid characters" $ do
      let result = parseOnly tracestateParser "vendor$=value1"
      result `shouldSatisfy` isLeft

    it "rejects keys that are too long" $ do
      let longKey = T.replicate 257 "a"
          input = C8.pack $ T.unpack longKey ++ "=value1"
          result = parseOnly tracestateParser input
      result `shouldSatisfy` isLeft

    it "rejects values that are too long" $ do
      let longValue = T.replicate 257 "a"
          input = C8.pack $ "vendor1=" ++ T.unpack longValue
          result = parseOnly tracestateParser input
      result `shouldSatisfy` isLeft

    it "rejects invalid value with comma" $ do
      let result = parseOnly tracestateParser "vendor1=value,with,comma"
      result `shouldSatisfy` isLeft

    it "rejects invalid value with equals" $ do
      let result = parseOnly tracestateParser "vendor1=value=with=equals"
      result `shouldSatisfy` isLeft

  describe "encodeTraceState" $ do
    it "encodes empty tracestate" $ do
      encodeTraceState empty `shouldBe` ""

    it "encodes single key=value pair" $ do
      let ts = insert (Key "vendor1") (Value "value1") empty
      encodeTraceState ts `shouldBe` "vendor1=value1"

    it "encodes multiple key=value pairs" $ do
      let ts = fromList [(Key "vendor1", Value "value1"), (Key "vendor2", Value "value2")]
      let encoded = encodeTraceState ts
      -- Order might vary, so check both possibilities
      encoded `shouldSatisfy` \s ->
        s == "vendor1=value1,vendor2=value2"
          || s == "vendor2=value2,vendor1=value1"

    it "limits to 32 entries when encoding" $ do
      let buildTS n acc
            | n > 40 = acc
            | otherwise =
                buildTS (n + 1) $
                  insert
                    (Key $ T.pack $ "key" ++ show n)
                    (Value $ T.pack $ "value" ++ show n)
                    acc
          ts = buildTS 1 empty
          encoded = encodeTraceState ts
          entryCount = length $ filter (== ',') $ C8.unpack encoded
      entryCount `shouldBe` 31 -- 32 entries = 31 commas
    it "handles special characters in encoding" $ do
      let ts = insert (Key "vendor-1_2*3/4@tenant") (Value "value-with_special*chars/and@symbols") empty
      encodeTraceState ts `shouldBe` "vendor-1_2*3/4@tenant=value-with_special*chars/and@symbols"

  describe "encodeTraceStateFull" $ do
    it "encodes empty tracestate" $ do
      encodeTraceStateFull empty `shouldBe` ""

    it "encodes single key=value pair" $ do
      let ts = insert (Key "vendor1") (Value "value1") empty
      encodeTraceStateFull ts `shouldBe` "vendor1=value1"

    it "preserves all entries beyond 32 limit" $ do
      let pairs = [(Key $ T.pack $ "key" ++ show i, Value $ T.pack $ "value" ++ show i) | i <- [1 .. 40]]
          ts = fromList pairs
          encoded = encodeTraceStateFull ts
          entryCount = length $ filter (== '=') $ C8.unpack encoded
      entryCount `shouldBe` 40 -- Should preserve all 40 entries
    it "does not filter oversized entries" $ do
      let longValue = T.replicate 200 "x" -- Much longer than 128 char limit
          ts = insert (Key "longentry") (Value longValue) empty
          encoded = encodeTraceStateFull ts
      encoded `shouldSatisfy` C8.isInfixOf "longentry="
      C8.length encoded `shouldSatisfy` (> 200)

    it "matches encodeTraceState for small tracestates" $ do
      let ts = fromList [(Key "vendor1", Value "value1"), (Key "vendor2", Value "value2")]
      encodeTraceStateFull ts `shouldBe` encodeTraceState ts

  describe "round-trip property" $ do
    it "round-trips simple valid tracestate" $ do
      let validPairs = [("vendor1", "value1"), ("vendor2", "value2")]
          ts = fromList [(Key $ T.pack k, Value $ T.pack v) | (k, v) <- validPairs]
          encoded = encodeTraceState ts
          parsed = parseOnly tracestateParser encoded
      case parsed of
        Right ts' -> toList ts' `shouldBe` toList ts
        Left err -> expectationFailure $ "Round-trip failed: " ++ err

    it "round-trips complex valid tracestate" $ do
      let validPairs = [("tenant@vendor", "complex-value_with*chars"), ("simple123", "value")]
          ts = fromList [(Key $ T.pack k, Value $ T.pack v) | (k, v) <- validPairs]
          encoded = encodeTraceState ts
          parsed = parseOnly tracestateParser encoded
      case parsed of
        Right ts' -> toList ts' `shouldBe` toList ts
        Left err -> expectationFailure $ "Round-trip failed: " ++ err