packages feed

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

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.Propagator.W3CIntegrationSpec (spec) where

import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C8
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import OpenTelemetry.Common (TraceFlags (..))
import OpenTelemetry.Propagator.W3CTraceContext
import OpenTelemetry.Trace.Core
import OpenTelemetry.Trace.Id
import OpenTelemetry.Trace.TraceState (Key (..), Value (..), empty, fromList, insert, toList)
import Test.Hspec


spec :: Spec
spec = describe "W3C TraceContext Integration" $ do
  describe "decodeSpanContext" $ do
    it "decodes traceparent and empty tracestate" $ do
      let traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
          tracestate = Nothing
          result = decodeSpanContext traceparent tracestate
      case result of
        Just spanCtx -> do
          OpenTelemetry.Trace.Core.traceFlags spanCtx `shouldBe` TraceFlags 1
          traceState spanCtx `shouldBe` empty
        Nothing -> expectationFailure "Failed to decode span context"

    it "decodes traceparent and tracestate with single entry" $ do
      let traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
          tracestate = Just "vendor1=value1"
          result = decodeSpanContext traceparent tracestate
      case result of
        Just spanCtx -> do
          OpenTelemetry.Trace.Core.traceFlags spanCtx `shouldBe` TraceFlags 1
          traceState spanCtx `shouldBe` insert (Key "vendor1") (Value "value1") empty
        Nothing -> expectationFailure "Failed to decode span context"

    it "decodes traceparent and tracestate with multiple entries" $ do
      let traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
          tracestate = Just "vendor1=value1,vendor2=value2,vendor3=value3"
          result = decodeSpanContext traceparent tracestate
      case result of
        Just spanCtx -> do
          OpenTelemetry.Trace.Core.traceFlags spanCtx `shouldBe` TraceFlags 1
          let decodedPairs = toList (traceState spanCtx)
          decodedPairs `shouldContain` [(Key "vendor1", Value "value1")]
          decodedPairs `shouldContain` [(Key "vendor2", Value "value2")]
          decodedPairs `shouldContain` [(Key "vendor3", Value "value3")]
          length decodedPairs `shouldBe` 3
        Nothing -> expectationFailure "Failed to decode span context"

    it "handles invalid tracestate gracefully" $ do
      let traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
          tracestate = Just "INVALID$KEY=value1" -- Invalid key format
          result = decodeSpanContext traceparent tracestate
      case result of
        Just spanCtx -> do
          OpenTelemetry.Trace.Core.traceFlags spanCtx `shouldBe` TraceFlags 1
          traceState spanCtx `shouldBe` empty -- Should fall back to empty
        Nothing -> expectationFailure "Failed to decode span context"

    it "returns Nothing for invalid traceparent" $ do
      let traceparent = Just "invalid-traceparent"
          tracestate = Just "vendor1=value1"
          result = decodeSpanContext traceparent tracestate
      result `shouldBe` Nothing

    it "returns Nothing for missing traceparent" $ do
      let traceparent = Nothing
          tracestate = Just "vendor1=value1"
          result = decodeSpanContext traceparent tracestate
      result `shouldBe` Nothing

  describe "encodeTraceState integration" $ do
    it "encodes complex tracestate correctly" $ do
      let complexState =
            fromList
              [ (Key "tenant@vendor", Value "complex-value_with*chars")
              , (Key "simple", Value "value")
              , (Key "numeric123", Value "123-456")
              ]
          encoded = encodeTraceState complexState
      -- Verify it can be round-tripped
      let traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
          result = decodeSpanContext traceparent (Just encoded)
      case result of
        Just spanCtx -> do
          let decodedState = traceState spanCtx
          decodedState `shouldBe` complexState
        Nothing -> expectationFailure "Failed to round-trip complex tracestate"

    it "handles empty tracestate in round-trip" $ do
      let encoded = encodeTraceState empty
      encoded `shouldBe` ""
      let traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
          result = decodeSpanContext traceparent (if encoded == "" then Nothing else Just encoded)
      case result of
        Just spanCtx -> traceState spanCtx `shouldBe` empty
        Nothing -> expectationFailure "Failed to handle empty tracestate"

  describe "W3C specification compliance" $ do
    it "respects 32 entry limit in parsing" $ do
      let entries = ["key" ++ show i ++ "=value" ++ show i | i <- [1 .. 40]]
          longTracestate = C8.intercalate "," $ map C8.pack entries
          traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
          result = decodeSpanContext traceparent (Just longTracestate)
      case result of
        Just spanCtx -> do
          let stateEntries = traceState spanCtx
          length (toList stateEntries) `shouldBe` 32
        Nothing -> expectationFailure "Failed to parse long tracestate"

    it "validates key format according to spec" $ do
      let testCases =
            [ ("validkey", True)
            , ("valid123", True)
            , ("valid_key", True)
            , ("valid-key", True)
            , ("valid*key", True)
            , ("valid/key", True)
            , ("tenant@vendor", True)
            , ("123numeric", True)
            , ("INVALIDKEY", False) -- Must start with lowercase
            , ("invalid$key", False) -- Invalid character
            , ("", False) -- Empty key
            ]
      mapM_
        ( \(key, shouldSucceed) -> do
            let tracestate = C8.pack $ T.unpack key ++ "=value"
                traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
                result = decodeSpanContext traceparent (Just tracestate)
            if shouldSucceed
              then
                result
                  `shouldSatisfy` ( \case
                                      Just spanCtx -> not $ null $ toList $ traceState spanCtx
                                      Nothing -> False
                                  )
              else
                result
                  `shouldSatisfy` ( \case
                                      Just spanCtx -> null $ toList $ traceState spanCtx
                                      Nothing -> True
                                  )
        )
        testCases

    it "validates value format according to spec" $ do
      let testCases =
            [ ("validvalue", True)
            , ("valid value with spaces", True)
            , ("valid-value_with*special/chars", True)
            , ("valid!value#with$symbols%", True)
            , ("value,with,comma", False) -- Comma not allowed
            , ("value=with=equals", False) -- Equals not allowed in continuation
            ]
      mapM_
        ( \(value, shouldSucceed) -> do
            let tracestate = C8.pack $ "validkey=" ++ T.unpack value
                traceparent = Just "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
                result = decodeSpanContext traceparent (Just tracestate)
            if shouldSucceed
              then
                result
                  `shouldSatisfy` ( \case
                                      Just spanCtx -> not $ null $ toList $ traceState spanCtx
                                      Nothing -> False
                                  )
              else
                result
                  `shouldSatisfy` ( \case
                                      Just spanCtx -> null $ toList $ traceState spanCtx
                                      Nothing -> True
                                  )
        )
        testCases