diff --git a/.autover/changes/f13d1778-21ba-4438-b4d5-aecbc524e11c.json b/.autover/changes/f13d1778-21ba-4438-b4d5-aecbc524e11c.json new file mode 100644 index 000000000..c5e429339 --- /dev/null +++ b/.autover/changes/f13d1778-21ba-4438-b4d5-aecbc524e11c.json @@ -0,0 +1,32 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Core", + "Type": "Minor", + "ChangelogMessages": [ + "Add ILambdaContext.W3C to expose the W3C trace-context fields (traceparent, tracestate, baggage) carried on clientContext.w3c at invoke time." + ] + }, + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Minor", + "ChangelogMessages": [ + "Implement ILambdaContext.W3C in LambdaContext, surfacing the allowlisted W3C trace-context fields (traceparent, tracestate, baggage) parsed from the client context header." + ] + }, + { + "Name": "Amazon.Lambda.TestUtilities", + "Type": "Minor", + "ChangelogMessages": [ + "Add W3C trace-context support to TestLambdaContext via the settable W3CFields property and the W3C property." + ] + }, + { + "Name": "Amazon.Lambda.AspNetCoreServer", + "Type": "Minor", + "ChangelogMessages": [ + "Implement ILambdaContext.W3C on the SnapStart empty context, returning an empty trace-context during initialization." + ] + } + ] +} diff --git a/.github/workflows/dockerized-test.yml b/.github/workflows/dockerized-test.yml new file mode 100644 index 000000000..30a6af3f8 --- /dev/null +++ b/.github/workflows/dockerized-test.yml @@ -0,0 +1,46 @@ +name: dockerized-test + +permissions: + contents: read + +on: + push: + branches: [master] + pull_request: + branches: ['*'] + workflow_dispatch: + +jobs: + dockerized-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up .NET + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: | + 10.0.x + + - name: Publish W3CTestHandler + run: | + dotnet publish test/dockerized/W3CTestHandler/W3CTestHandler.csproj \ + -c Release \ + -r linux-x64 \ + --self-contained \ + -o test/dockerized/tasks + chmod +x test/dockerized/tasks/bootstrap + + - name: Build the test image + run: | + docker build . \ + -t local/test \ + -f Dockerfile.test \ + --build-arg BASE_IMAGE=public.ecr.aws/lambda/provided:al2023 + + - name: Run dockerized suites + uses: aws/containerized-test-runner-for-aws-lambda@0863dd17b5fc19585250a2405c0f939a77b4f397 # main + with: + suiteFileArray: '["./test/dockerized/suites/*.json"]' + dockerImageName: 'local/test' + taskFolder: './test/dockerized/tasks' diff --git a/.gitignore b/.gitignore index fa7937bf4..1feb44e78 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ global.json # JetBrains Rider per-project cache **/*.lscache + +test/dockerized/tasks/ diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 000000000..beec40f2b --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,16 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +ARG BASE_IMAGE=public.ecr.aws/lambda/provided:al2023 +FROM ${BASE_IMAGE} + + +ARG RIE_URL=https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie + +RUN curl -fsSL -o /usr/local/bin/aws-lambda-rie "${RIE_URL}" \ + && chmod +x /usr/local/bin/aws-lambda-rie + +COPY test/dockerized/tasks /var/task +RUN chmod +x /var/task/bootstrap + +ENTRYPOINT ["/usr/local/bin/aws-lambda-rie", "/var/task/bootstrap"] diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs index acd7c1ad5..eec65b007 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/SnapStartEmptyLambdaContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using Amazon.Lambda.Core; namespace Amazon.Lambda.AspNetCoreServer.Internal; @@ -64,4 +65,8 @@ public SnapStartEmptyLambdaContext() public IClientApplication Client { get; } public IDictionary Custom { get; } = new Dictionary(); public string TenantId { get; } + + private static readonly IReadOnlyDictionary EmptyW3C = + new ReadOnlyDictionary(new Dictionary()); + public IReadOnlyDictionary W3C => EmptyW3C; } diff --git a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs index bcdce39df..b81372a54 100644 --- a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs @@ -1,6 +1,7 @@ namespace Amazon.Lambda.Core { using System; + using System.Collections.Generic; /// /// Object that allows you to access useful information available within @@ -107,6 +108,15 @@ public interface ILambdaContext /// handlers). /// ILambdaSerializer Serializer { get { return null; } } + + /// + /// The W3C trace-context fields (traceparent, tracestate, + /// baggage) carried on the invoke's clientContext.w3c payload. + /// + IReadOnlyDictionary W3C + { + get { return new System.Collections.ObjectModel.ReadOnlyDictionary(new Dictionary()); } + } #endif } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs index 943a61d91..f91c80a81 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs @@ -15,6 +15,7 @@ using Amazon.Lambda.Core; using System; +using System.Collections.Generic; using Amazon.Lambda.RuntimeSupport.Helpers; namespace Amazon.Lambda.RuntimeSupport @@ -30,6 +31,7 @@ internal class LambdaContext : ILambdaContext private readonly int _memoryLimitInMB; private readonly Lazy _cognitoIdentityLazy; private readonly Lazy _cognitoClientContextLazy; + private readonly Lazy> _w3cLazy; private readonly IConsoleLoggerWriter _consoleLogger; public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lambdaEnvironment, IConsoleLoggerWriter consoleLogger) @@ -49,6 +51,7 @@ public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lamb long.TryParse(_runtimeApiHeaders.DeadlineMs, out _deadlineMs); _cognitoIdentityLazy = new Lazy(() => CognitoIdentity.FromJson(runtimeApiHeaders.CognitoIdentityJson)); _cognitoClientContextLazy = new Lazy(() => CognitoClientContext.FromJson(runtimeApiHeaders.ClientContextJson)); + _w3cLazy = new Lazy>(() => W3CTraceContext.FromClientContextJson(runtimeApiHeaders.ClientContextJson)); } public string TraceId => _runtimeApiHeaders.TraceId; @@ -77,6 +80,8 @@ public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lamb public string TenantId => _runtimeApiHeaders.TenantId; + public IReadOnlyDictionary W3C => _w3cLazy.Value; + /// /// The serializer the Lambda function registered with the runtime, surfaced via /// . Assigned per-invocation by diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/W3CTraceContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/W3CTraceContext.cs new file mode 100644 index 000000000..172b89dd4 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/W3CTraceContext.cs @@ -0,0 +1,77 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text.Json; + +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// Extracts the W3C trace-context fields (traceparent, tracestate, baggage) + /// carried on clientContext.w3c at invoke time. Any other key on + /// w3c is ignored, and any allowlisted key whose value is not a + /// string is dropped. + /// + internal static class W3CTraceContext + { + internal static readonly string[] AllowedFields = { "traceparent", "tracestate", "baggage" }; + + private static readonly IReadOnlyDictionary Empty = + new ReadOnlyDictionary(new Dictionary()); + + internal static IReadOnlyDictionary FromClientContextJson(string clientContextJson) + { + if (string.IsNullOrWhiteSpace(clientContextJson)) + { + return Empty; + } + + JsonDocument doc; + try + { + doc = JsonDocument.Parse(clientContextJson); + } + catch (JsonException) + { + return Empty; + } + + using (doc) + { + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object) + { + return Empty; + } + + if (!root.TryGetProperty("w3c", out var w3c)) + { + return Empty; + } + + if (w3c.ValueKind != JsonValueKind.Object) + { + return Empty; + } + + var fields = new Dictionary(AllowedFields.Length); + foreach (var key in AllowedFields) + { + if (!w3c.TryGetProperty(key, out var value)) + { + continue; + } + // Allowlisted keys whose value is not a JSON string are dropped. + if (value.ValueKind != JsonValueKind.String) + { + continue; + } + fields[key] = value.GetString(); + } + + return new ReadOnlyDictionary(fields); + } + } + } +} diff --git a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs index b472403e0..cbd3fd106 100644 --- a/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.TestUtilities/TestLambdaContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -87,5 +88,29 @@ public class TestLambdaContext : ILambdaContext /// in production. /// public ILambdaSerializer Serializer { get; set; } + + /// + /// The W3C trace-context fields (traceparent, tracestate, + /// baggage) a test wants to pretend were carried on + /// clientContext.w3c at invoke time. + /// + public IDictionary W3CFields { get; set; } + + /// + /// The W3C trace-context fields configured via , + /// wrapped in a read-only dictionary. Returns an empty read-only dictionary + /// when no fields were set. + /// + public IReadOnlyDictionary W3C + { + get + { + if (W3CFields == null) + { + return new ReadOnlyDictionary(new Dictionary()); + } + return new ReadOnlyDictionary(new Dictionary(W3CFields)); + } + } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/W3CContextTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/W3CContextTests.cs new file mode 100644 index 000000000..cb9b8e1c4 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/W3CContextTests.cs @@ -0,0 +1,198 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Globalization; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + + public class W3CContextTests + { + private readonly TestEnvironmentVariables _environmentVariables = new TestEnvironmentVariables(); + + private LambdaContext BuildContext(string clientContextJson) + { + var deadlineMs = DateTimeOffset.UtcNow.AddHours(1) + .ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture); + var headers = new Dictionary> + { + ["Lambda-Runtime-Aws-Request-Id"] = new[] { Guid.NewGuid().ToString() }, + ["Lambda-Runtime-Deadline-Ms"] = new[] { deadlineMs } + }; + if (clientContextJson != null) + { + headers["Lambda-Runtime-Client-Context"] = new[] { clientContextJson }; + } + + var runtimeApiHeaders = new RuntimeApiHeaders(headers); + var env = new LambdaEnvironment(_environmentVariables); + return new LambdaContext(runtimeApiHeaders, env, + new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())); + } + + [Fact] + public void W3C_ReturnsEmpty_WhenClientContextHeaderAbsent() + { + var context = BuildContext(clientContextJson: null); + Assert.Empty(context.W3C); + } + + [Fact] + public void W3C_ReturnsEmpty_WhenClientContextHasNoW3CKey() + { + var context = BuildContext("{\"custom\":{\"value\":\"test\"}}"); + Assert.Empty(context.W3C); + // ClientContext is still populated — w3c is just not there. + Assert.NotNull(context.ClientContext); + Assert.Equal("test", context.ClientContext.Custom["value"]); + } + + [Fact] + public void W3C_ReturnsBaggageOnly() + { + var context = BuildContext("{\"w3c\":{\"baggage\":\"userId=alice\"}}"); + var w3c = context.W3C; + Assert.Single(w3c); + Assert.Equal("userId=alice", w3c["baggage"]); + } + + [Fact] + public void W3C_ReturnsAllThreeAllowlistedFields() + { + const string json = @"{ + ""custom"": { ""value"": ""test"" }, + ""w3c"": { + ""traceparent"": ""00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"", + ""tracestate"": ""rojo=00f067aa0ba902b7"", + ""baggage"": ""userId=alice"" + } + }"; + var context = BuildContext(json); + var w3c = context.W3C; + Assert.Equal(3, w3c.Count); + Assert.Equal("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", w3c["traceparent"]); + Assert.Equal("rojo=00f067aa0ba902b7", w3c["tracestate"]); + Assert.Equal("userId=alice", w3c["baggage"]); + } + + [Fact] + public void W3C_StripsSource_WhenReadFromClientContext() + { + const string json = @"{ + ""custom"": { ""value"": ""test"" }, + ""w3c"": { + ""traceparent"": ""00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"", + ""baggage"": ""userId=alice"" + } + }"; + var context = BuildContext(json); + + // W3C fields are surfaced through W3C + Assert.Equal(2, context.W3C.Count); + // but IClientContext has no way to read them back + Assert.NotNull(context.ClientContext); + Assert.Null(context.ClientContext.Environment); + Assert.Null(context.ClientContext.Client); + Assert.NotNull(context.ClientContext.Custom); + Assert.Equal("test", context.ClientContext.Custom["value"]); + Assert.False(context.ClientContext.Custom.ContainsKey("w3c")); + Assert.False(context.ClientContext.Custom.ContainsKey("traceparent")); + } + + [Fact] + public void W3C_DropsAllowlistedFields_WithNonStringValues() + { + const string json = @"{ + ""w3c"": { + ""baggage"": ""abc"", + ""traceparent"": 42, + ""tracestate"": null + } + }"; + var context = BuildContext(json); + var w3c = context.W3C; + Assert.Single(w3c); + Assert.Equal("abc", w3c["baggage"]); + } + + [Fact] + public void W3C_TreatsNonObjectValueAsEmpty() + { + var context = BuildContext("{\"w3c\":\"not-an-object\"}"); + Assert.Empty(context.W3C); + } + + [Fact] + public void W3C_TreatsArrayValueAsEmpty() + { + var context = BuildContext("{\"w3c\":[\"baggage=abc\"]}"); + Assert.Empty(context.W3C); + } + + [Fact] + public void W3C_ResultIsReadOnly() + { + var context = BuildContext("{\"w3c\":{\"baggage\":\"abc\"}}"); + var w3c = context.W3C; + + Assert.IsType>(w3c); + + var mutable = (ICollection>)w3c; + Assert.True(mutable.IsReadOnly); + Assert.Throws(() => + mutable.Add(new KeyValuePair("baggage", "tampered"))); + + Assert.Equal("abc", context.W3C["baggage"]); + } + + [Fact] + public void W3C_AllowlistDrops_NonAllowlistedKeys_EvenWhenValueIsString() + { + const string json = @"{ + ""w3c"": { + ""baggage"": ""keep=me"", + ""unknownField"": ""should-not-appear"", + ""x-custom-trace"": ""should-not-appear"" + } + }"; + var context = BuildContext(json); + var w3c = context.W3C; + Assert.Single(w3c); + Assert.Equal("keep=me", w3c["baggage"]); + Assert.False(w3c.ContainsKey("unknownField")); + } + + [Fact] + public void W3C_OmitsAbsentAllowlistedKeys() + { + var context = BuildContext("{\"w3c\":{\"baggage\":\"abc\"}}"); + var w3c = context.W3C; + Assert.True(w3c.ContainsKey("baggage")); + Assert.False(w3c.ContainsKey("traceparent")); + Assert.False(w3c.ContainsKey("tracestate")); + } + + [Fact] + public void W3C_ReturnsEmpty_WhenAllAllowlistedValuesAreNonStrings() + { + const string json = @"{ + ""w3c"": { + ""traceparent"": 42, + ""tracestate"": null, + ""baggage"": { ""nested"": ""no"" } + } + }"; + var context = BuildContext(json); + Assert.Empty(context.W3C); + } + + [Fact] + public void W3C_ReturnsEmpty_WhenClientContextJsonIsMalformed() + { + Assert.Empty(W3CTraceContext.FromClientContextJson("{ not valid json")); + } + } +} diff --git a/test/dockerized/W3CTestHandler/Program.cs b/test/dockerized/W3CTestHandler/Program.cs new file mode 100644 index 000000000..623dd0444 --- /dev/null +++ b/test/dockerized/W3CTestHandler/Program.cs @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; + +namespace W3CTestHandler +{ + public static class Program + { + private static readonly DefaultLambdaJsonSerializer JsonSerializer = new DefaultLambdaJsonSerializer(); + + public static async Task Main(string[] args) + { + // The RIE and the Lambda runtime API both pass the configured + // handler through the _HANDLER environment variable. + var handler = Environment.GetEnvironmentVariable("_HANDLER") ?? string.Empty; + + Func dispatch = handler switch + { + "w3c.get_w3c" => GetW3c, + "w3c.get_w3c_and_source" => GetW3cAndSource, + "w3c.echo_client_context" => EchoClientContext, + "w3c.w3c_is_callable" => W3CIsCallable, + _ => throw new Exception($"Handler '{handler}' is not supported."), + }; + + using var wrapper = HandlerWrapper.GetHandlerWrapper( + (Func)dispatch, + JsonSerializer); + using var bootstrap = new LambdaBootstrap(wrapper); + await bootstrap.RunAsync(); + } + + /// Returns the raw W3C dictionary — the primary assertion target. + public static object GetW3c(JsonNode input, ILambdaContext context) + { + // Returning the IReadOnlyDictionary directly serializes as a JSON object. + return context.W3C; + } + + public static object GetW3cAndSource(JsonNode input, ILambdaContext context) + { + var clientContext = context.ClientContext; + return new Dictionary + { + ["w3c"] = context.W3C, + ["clientContextIsDefined"] = clientContext != null, + // IClientContext has no "w3c" accessor at all on .NET — the + // strip-the-source step is naturally met by the type system. + ["clientContextHasW3c"] = false, + ["clientContext"] = SerializeClientContext(clientContext), + }; + } + + public static object EchoClientContext(JsonNode input, ILambdaContext context) + { + return SerializeClientContext(context.ClientContext); + } + + public static object W3CIsCallable(JsonNode input, ILambdaContext context) + { + var property = context.GetType().GetProperty(nameof(ILambdaContext.W3C)); + return new Dictionary + { + ["isCallable"] = property != null, + }; + } + + private static object SerializeClientContext(IClientContext clientContext) + { + if (clientContext == null) + { + return null; + } + var result = new Dictionary(); + if (clientContext.Custom != null && clientContext.Custom.Count > 0) + { + result["custom"] = clientContext.Custom.ToDictionary(kv => kv.Key, kv => (object)kv.Value); + } + if (clientContext.Environment != null && clientContext.Environment.Count > 0) + { + result["env"] = clientContext.Environment.ToDictionary(kv => kv.Key, kv => (object)kv.Value); + } + return result.Count == 0 ? null : result; + } + } +} diff --git a/test/dockerized/W3CTestHandler/W3CTestHandler.csproj b/test/dockerized/W3CTestHandler/W3CTestHandler.csproj new file mode 100644 index 000000000..028ddcac5 --- /dev/null +++ b/test/dockerized/W3CTestHandler/W3CTestHandler.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + bootstrap + linux-x64 + true + false + true + IDE0060 + + + + + + + + + diff --git a/test/dockerized/suites/ctx.json b/test/dockerized/suites/ctx.json new file mode 100644 index 000000000..9f93fdb1e --- /dev/null +++ b/test/dockerized/suites/ctx.json @@ -0,0 +1,31 @@ +{ + "name": "ctx", + "tests": [ + { + "name": "client_context_is_echoed_when_no_w3c_key", + "handler": "w3c.echo_client_context", + "request": {}, + "clientContext": { + "custom": { "value": "hello" }, + "env": { "stage": "beta" } + }, + "assertions": [ + { + "response": { + "custom": { "value": "hello" }, + "env": { "stage": "beta" } + } + } + ] + }, + + { + "name": "client_context_is_null_when_header_absent", + "handler": "w3c.echo_client_context", + "request": {}, + "assertions": [ + { "response": null } + ] + } + ] +} diff --git a/test/dockerized/suites/w3c.json b/test/dockerized/suites/w3c.json new file mode 100644 index 000000000..f907b8732 --- /dev/null +++ b/test/dockerized/suites/w3c.json @@ -0,0 +1,150 @@ +{ + "name": "w3c", + "tests": [ + { + "name": "w3c_is_callable_on_context", + "handler": "w3c.w3c_is_callable", + "request": {}, + "assertions": [ + { "response": { "isCallable": true } } + ] + }, + + { + "name": "w3c_returns_empty_when_no_client_context_header", + "handler": "w3c.get_w3c", + "request": {}, + "assertions": [ + { "response": {} } + ] + }, + + { + "name": "w3c_returns_empty_when_client_context_has_no_w3c_key", + "handler": "w3c.get_w3c", + "request": {}, + "clientContext": { + "custom": { "value": "test" } + }, + "assertions": [ + { "response": {} } + ] + }, + + { + "name": "w3c_returns_baggage_only", + "handler": "w3c.get_w3c", + "request": {}, + "clientContext": { + "w3c": { "baggage": "userId=alice" } + }, + "assertions": [ + { "response": { "baggage": "userId=alice" } } + ] + }, + + { + "name": "w3c_returns_all_three_allowlisted_fields", + "handler": "w3c.get_w3c", + "request": {}, + "clientContext": { + "w3c": { + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + "tracestate": "rojo=00f067aa0ba902b7", + "baggage": "userId=alice" + } + }, + "assertions": [ + { + "response": { + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + "tracestate": "rojo=00f067aa0ba902b7", + "baggage": "userId=alice" + } + } + ] + }, + + { + "name": "w3c_allowlist_drops_non_allowlisted_keys", + "handler": "w3c.get_w3c", + "request": {}, + "clientContext": { + "w3c": { + "baggage": "keep=me", + "unknownField": "should-not-appear", + "x-custom-trace": "should-not-appear" + } + }, + "assertions": [ + { "response": { "baggage": "keep=me" } } + ] + }, + + { + "name": "w3c_drops_allowlisted_fields_with_non_string_values", + "handler": "w3c.get_w3c", + "request": {}, + "clientContext": { + "w3c": { + "traceparent": 42, + "tracestate": null, + "baggage": { "nested": "no" } + } + }, + "assertions": [ + { "response": {} } + ] + }, + + { + "name": "w3c_treats_non_object_as_empty", + "handler": "w3c.get_w3c", + "request": {}, + "clientContext": { + "w3c": "not-an-object" + }, + "assertions": [ + { "response": {} } + ] + }, + + { + "name": "w3c_treats_array_as_empty", + "handler": "w3c.get_w3c", + "request": {}, + "clientContext": { + "w3c": ["baggage=abc"] + }, + "assertions": [ + { "response": {} } + ] + }, + + { + "name": "w3c_strips_source_client_context_w3c_after_construction", + "handler": "w3c.get_w3c_and_source", + "request": {}, + "clientContext": { + "custom": { "value": "test" }, + "w3c": { + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + "baggage": "userId=alice" + } + }, + "assertions": [ + { + "response": { + "w3c": { + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + "baggage": "userId=alice" + }, + "clientContextIsDefined": true, + "clientContextHasW3c": false, + "clientContext": { "custom": { "value": "test" } } + } + } + ] + } + ] +}