Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/run-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ jobs:
sam deploy --stack-name "${stackName}" --parameter-overrides "ParameterKey=SecretToken,ParameterValue=${{ secrets.SECRET_TOKEN }}" "ParameterKey=LambdaRole,ParameterValue=${{ secrets.AWS_LAMBDA_ROLE }}" --no-confirm-changeset --no-progressbar > disable_output
TEST_ENDPOINT=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="HelloApiEndpoint") | .OutputValue')
TENANT_ID_TEST_FUNCTION=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="TenantIdTestFunction") | .OutputValue')
W3C_TEST_FUNCTION=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="W3CTestFunction") | .OutputValue')
echo "TEST_ENDPOINT=$TEST_ENDPOINT" >> "$GITHUB_OUTPUT"
echo "TENANT_ID_TEST_FUNCTION=$TENANT_ID_TEST_FUNCTION" >> "$GITHUB_OUTPUT"
echo "W3C_TEST_FUNCTION=$W3C_TEST_FUNCTION" >> "$GITHUB_OUTPUT"
- name: run test
env:
SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
TEST_ENDPOINT: ${{ steps.deploy_stack.outputs.TEST_ENDPOINT }}
TENANT_ID_TEST_FUNCTION: ${{ steps.deploy_stack.outputs.TENANT_ID_TEST_FUNCTION }}
W3C_TEST_FUNCTION: ${{ steps.deploy_stack.outputs.W3C_TEST_FUNCTION }}
run: cd lambda-integration-tests && cargo test
- name: cleanup
if: always() && steps.deploy_stack.outputs.STACK_NAME
Expand Down
5 changes: 5 additions & 0 deletions lambda-integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ tracing = "0.1"

[dev-dependencies]
reqwest = { version = "0.13.1", features = ["blocking"] }
base64 = "0.22.1"

[features]
catch-all-fields = ["aws_lambda_events/catch-all-fields"]
Expand All @@ -36,3 +37,7 @@ path = "src/authorizer.rs"
[[bin]]
name = "tenant-id-test"
path = "src/tenant_id_test.rs"

[[bin]]
name = "w3c-test"
path = "src/w3c_test.rs"
34 changes: 34 additions & 0 deletions lambda-integration-tests/src/w3c_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use lambda_runtime::{service_fn, Error, LambdaEvent};
use serde_json::{json, Value};

async fn function_handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
let (_event, context) = event.into_parts();

let w3c_fields = context.w3c();
tracing::info!("w3c fields observed on context: {:?}", w3c_fields);

let client_context_has_custom = context
.client_context
.as_ref()
.map(|cc| !cc.custom.is_empty())
.unwrap_or(false);

let response = json!({
"statusCode": 200,
"body": json!({
"message": "W3C test successful",
"request_id": context.request_id,
"w3c": w3c_fields,
"has_client_context": context.client_context.is_some(),
"client_context_has_custom": client_context_has_custom,
}).to_string()
});

Ok(response)
}

#[tokio::main]
async fn main() -> Result<(), Error> {
lambda_runtime::tracing::init_default_subscriber();
lambda_runtime::run(service_fn(function_handler)).await
}
17 changes: 16 additions & 1 deletion lambda-integration-tests/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ Resources:
Runtime: provided.al2023
Role: !Ref LambdaRole

W3CTestFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: rust-cargolambda
BuildProperties:
Binary: w3c-test
Properties:
CodeUri: ./
Handler: bootstrap
Runtime: provided.al2023
Role: !Ref LambdaRole

AuthorizerFunction:
Type: AWS::Serverless::Function
Metadata:
Expand All @@ -74,4 +86,7 @@ Outputs:
Value: !Sub "https://${API}.execute-api.${AWS::Region}.amazonaws.com/integ-test/hello/"
TenantIdTestFunction:
Description: "Tenant ID test function name"
Value: !Ref TenantIdTestFunction
Value: !Ref TenantIdTestFunction
W3CTestFunction:
Description: "W3C trace-context test function name"
Value: !Ref W3CTestFunction
107 changes: 107 additions & 0 deletions lambda-integration-tests/tests/w3c_prod_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use base64::prelude::*;
use serde_json::json;

fn function_name() -> String {
std::env::var("W3C_TEST_FUNCTION").expect("W3C_TEST_FUNCTION environment variable not set")
}

/// Invoke the deployed Lambda function and return the parsed body JSON.
fn invoke(payload: &serde_json::Value, client_context_json: Option<&serde_json::Value>) -> serde_json::Value {
let response_path = format!(
"/tmp/w3c_response_{}.json",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);

let mut args: Vec<String> = vec![
"lambda".into(),
"invoke".into(),
"--function-name".into(),
function_name(),
"--payload".into(),
payload.to_string(),
"--cli-binary-format".into(),
"raw-in-base64-out".into(),
];

if let Some(cc) = client_context_json {
let encoded = BASE64_STANDARD.encode(cc.to_string());
args.push("--client-context".into());
args.push(encoded);
}

args.push(response_path.clone());

let output = std::process::Command::new("aws")
.args(&args)
.output()
.expect("Failed to invoke Lambda function");

assert!(
output.status.success(),
"Lambda invocation failed: {}",
String::from_utf8_lossy(&output.stderr)
);

let response = std::fs::read_to_string(&response_path).expect("Failed to read response file");
let response_json: serde_json::Value = serde_json::from_str(&response).expect("Failed to parse response JSON");

assert_eq!(
response_json["statusCode"],
200,
"handler returned non-200: {}",
serde_json::to_string_pretty(&response_json).unwrap()
);

let body: serde_json::Value =
serde_json::from_str(response_json["body"].as_str().expect("Body should be a string"))
.expect("Failed to parse body JSON");

let _ = std::fs::remove_file(&response_path);
body
}

#[test]
fn test_w3c_propagation_in_production() {
// 1. No client context — `context.w3c()` must be empty.
let body = invoke(&json!({ "test": "w3c_no_client_context" }), None);
assert_eq!(body["message"], "W3C test successful");
assert_eq!(body["has_client_context"], false);
assert_eq!(
body["w3c"],
json!({}),
"w3c should be empty when no clientContext header is present, got: {}",
body["w3c"]
);

// 2. Client context carrying `w3c` + a sibling `custom` field — `w3c()`
// must surface all three allowlisted fields, and the sibling `custom`
// must still be reachable on `context.client_context` (the `w3c` key
// was stripped during Context construction, not the whole object).
let client_context = json!({
"custom": { "source": "integ-test" },
"w3c": {
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
"tracestate": "rojo=00f067aa0ba902b7",
"baggage": "userId=alice"
}
});

let body = invoke(&json!({ "test": "w3c_with_client_context" }), Some(&client_context));
assert_eq!(body["message"], "W3C test successful");
assert_eq!(body["has_client_context"], true);
assert_eq!(
body["client_context_has_custom"], true,
"sibling clientContext fields must still be reachable after w3c is stripped"
);
assert_eq!(
body["w3c"]["traceparent"],
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
);
assert_eq!(body["w3c"]["tracestate"], "rojo=00f067aa0ba902b7");
assert_eq!(body["w3c"]["baggage"], "userId=alice");

println!("✅ W3C trace-context propagation test passed");
}
1 change: 1 addition & 0 deletions lambda-runtime/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ pub(crate) const LAMBDA_RUNTIME_CLIENT_CONTEXT: &str = "lambda-runtime-client-co
pub(crate) const LAMBDA_RUNTIME_COGNITO_IDENTITY: &str = "lambda-runtime-cognito-identity";
pub(crate) const LAMBDA_RUNTIME_TENANT_ID: &str = "lambda-runtime-aws-tenant-id";
pub(crate) const LAMBDA_RUNTIME_INVOCATION_ID: &str = "lambda-runtime-invocation-id";
pub(crate) const W3C_ALLOWED_FIELDS: &[&str] = &["traceparent", "tracestate", "baggage"];
Loading
Loading