Skip to content
Merged
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
5 changes: 4 additions & 1 deletion crates/fakecloud-apigateway/src/service_usage_plans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ impl ApiGatewayService {
.get_mut(&id)
.ok_or_else(|| not_found("UsagePlan not found"))?;
apply_patch_operations(req, |op, path, value| {
if op != "replace" && op != "add" {
// `remove` must fall through too — the /apiStages/<id>:<stage>
// detach arm below is a remove op (previously dead code because
// this guard dropped every non-replace/add op).
if op != "replace" && op != "add" && op != "remove" {
return;
}
match path {
Expand Down
24 changes: 19 additions & 5 deletions crates/fakecloud-apigatewayv2/src/extras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1492,7 +1492,25 @@ impl ApiGatewayV2Service {
} else {
&mut state.route_responses
};
let mut value = entry.clone();
let bucket = store.entry(api).or_default();
if !is_create && !bucket.contains_key(&key) {
return Err(not_found("Response", &id));
}
// Update is a partial patch: merge the incoming fields onto the
// existing record so unspecified members persist. Previously the
// record was replaced wholesale, wiping every field the caller
// didn't resend.
let mut value = if is_create {
entry.clone()
} else {
let mut existing = bucket.get(&key).cloned().unwrap_or_else(|| entry.clone());
if let (Some(dst), Some(src)) = (existing.as_object_mut(), entry.as_object()) {
for (k, v) in src {
dst.insert(k.clone(), v.clone());
}
}
existing
};
if is_integration {
value["IntegrationResponseId"] = json!(id);
// IntegrationResponseKey is required on the Smithy response shape.
Expand All @@ -1513,10 +1531,6 @@ impl ApiGatewayV2Service {
value["RouteResponseKey"] = json!("$default");
}
}
let bucket = store.entry(api).or_default();
if !is_create && !bucket.contains_key(&key) {
return Err(not_found("Response", &id));
}
bucket.insert(key, value.clone());
ok(value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ impl ResourceProvisioner {
.unwrap_or(false),
client_secrets: Vec::new(),
refresh_token_rotation: None,
analytics_configuration: props
.get("AnalyticsConfiguration")
.filter(|v| !v.is_null())
.cloned(),
};

state.user_pool_clients.insert(client_id.clone(), client);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,10 @@ impl ResourceProvisioner {
capacity_provider_strategy,
availability_zone_rebalancing,
volume_configurations: Vec::new(),
service_connect_configuration: props
.get("ServiceConnectConfiguration")
.filter(|v| !v.is_null())
.cloned(),
};
state.services.insert(key.clone(), service);
if let Some(c) = state.clusters.get_mut(&cluster_name) {
Expand Down
3 changes: 3 additions & 0 deletions crates/fakecloud-cognito/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,9 @@ fn user_pool_client_to_json(client: &UserPoolClient) -> Value {
}
obj["RefreshTokenRotation"] = r;
}
if let Some(ref analytics) = client.analytics_configuration {
obj["AnalyticsConfiguration"] = analytics.clone();
}

obj
}
Expand Down
7 changes: 7 additions & 0 deletions crates/fakecloud-cognito/src/service/user_pools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,10 @@ impl CognitoService {
.unwrap_or(false),
client_secrets: Vec::new(),
refresh_token_rotation: parse_refresh_token_rotation(&body["RefreshTokenRotation"]),
analytics_configuration: body
.get("AnalyticsConfiguration")
.filter(|v| !v.is_null())
.cloned(),
};

let response = user_pool_client_to_json(&client);
Expand Down Expand Up @@ -775,6 +779,9 @@ impl CognitoService {
client.refresh_token_rotation =
parse_refresh_token_rotation(&body["RefreshTokenRotation"]);
}
if let Some(analytics) = body.get("AnalyticsConfiguration").filter(|v| !v.is_null()) {
client.analytics_configuration = Some(analytics.clone());
}

client.last_modified_date = Utc::now();

Expand Down
5 changes: 5 additions & 0 deletions crates/fakecloud-cognito/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,11 @@ pub struct UserPoolClient {
/// (old token invalidated). Default "DISABLED".
#[serde(default)]
pub refresh_token_rotation: Option<RefreshTokenRotationConfig>,
/// Pinpoint AnalyticsConfiguration block, stored verbatim so
/// Create/Update round-trip it through DescribeUserPoolClient. Defaults
/// to absent for pre-existing snapshots.
#[serde(default)]
pub analytics_configuration: Option<serde_json::Value>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down
55 changes: 55 additions & 0 deletions crates/fakecloud-e2e/tests/apigateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,61 @@ async fn api_keys_and_usage_plans() {
.expect("delete_api_key");
}

#[tokio::test]
async fn usage_plan_api_stage_add_then_remove() {
use aws_sdk_apigateway::types::{Op, PatchOperation};
let server = TestServer::start().await;
let client = server.apigateway_client().await;

let plan_id = client
.create_usage_plan()
.name("stage-plan")
.send()
.await
.expect("create_usage_plan")
.id()
.expect("plan id")
.to_string();

// Attach an API stage the way Terraform/CDK does: {op:add, /apiStages}.
let added = client
.update_usage_plan()
.usage_plan_id(&plan_id)
.patch_operations(
PatchOperation::builder()
.op(Op::Add)
.path("/apiStages")
.value("abc123:prod")
.build(),
)
.send()
.await
.expect("add api stage");
assert_eq!(added.api_stages().len(), 1);
assert_eq!(added.api_stages()[0].api_id(), Some("abc123"));
assert_eq!(added.api_stages()[0].stage(), Some("prod"));

// Remove it via {op:remove, /apiStages/<apiId>:<stage>} — the arm that was
// previously dead because the op guard rejected "remove".
let removed = client
.update_usage_plan()
.usage_plan_id(&plan_id)
.patch_operations(
PatchOperation::builder()
.op(Op::Remove)
.path("/apiStages/abc123:prod")
.build(),
)
.send()
.await
.expect("remove api stage");
assert!(
removed.api_stages().is_empty(),
"api stage should be removed, got {:?}",
removed.api_stages()
);
}

#[tokio::test]
async fn get_export_returns_openapi_with_real_paths() {
let server = TestServer::start().await;
Expand Down
72 changes: 72 additions & 0 deletions crates/fakecloud-e2e/tests/apigatewayv2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1610,3 +1610,75 @@ async fn update_api_changes_route_selection_expression() {
let got = client.get_api().api_id(&api_id).send().await.unwrap();
assert_eq!(got.route_selection_expression(), Some("$request.body.kind"));
}

#[tokio::test]
async fn update_route_response_merges_fields() {
let server = TestServer::start().await;
let client = server.apigatewayv2_client().await;

let api_id = client
.create_api()
.name("rr-api")
.protocol_type(aws_sdk_apigatewayv2::types::ProtocolType::Websocket)
.route_selection_expression("$request.body.action")
.send()
.await
.unwrap()
.api_id()
.unwrap()
.to_string();

let route_id = client
.create_route()
.api_id(&api_id)
.route_key("$default")
.send()
.await
.unwrap()
.route_id()
.unwrap()
.to_string();

// Create a route response carrying two independent fields.
let created = client
.create_route_response()
.api_id(&api_id)
.route_id(&route_id)
.route_response_key("$default")
.model_selection_expression("$request.body.kind")
.send()
.await
.expect("create_route_response");
let rr_id = created.route_response_id().unwrap().to_string();
assert_eq!(
created.model_selection_expression(),
Some("$request.body.kind")
);

// Update ONLY the route response key. The previously-set
// modelSelectionExpression must survive the merge (before the fix, update
// wholesale-replaced the record and dropped it).
client
.update_route_response()
.api_id(&api_id)
.route_id(&route_id)
.route_response_id(&rr_id)
.route_response_key("$default")
.send()
.await
.expect("update_route_response");

let got = client
.get_route_response()
.api_id(&api_id)
.route_id(&route_id)
.route_response_id(&rr_id)
.send()
.await
.expect("get_route_response");
assert_eq!(
got.model_selection_expression(),
Some("$request.body.kind"),
"modelSelectionExpression must persist across a partial update"
);
}
63 changes: 63 additions & 0 deletions crates/fakecloud-e2e/tests/cognito.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,69 @@ async fn cognito_create_describe_user_pool_client() {
assert_eq!(described.user_pool_id().unwrap(), pool_id);
}

#[tokio::test]
async fn cognito_user_pool_client_analytics_configuration_round_trips() {
use aws_sdk_cognitoidentityprovider::types::AnalyticsConfigurationType;
let server = TestServer::start().await;
let client = server.cognito_client().await;

let pool_id = client
.create_user_pool()
.pool_name("analytics-pool")
.send()
.await
.expect("create user pool")
.user_pool()
.unwrap()
.id()
.unwrap()
.to_string();

let analytics = AnalyticsConfigurationType::builder()
.application_id("app-1234567890abcdef")
.role_arn("arn:aws:iam::123456789012:role/pinpoint-role")
.external_id("ext-42")
.user_data_shared(true)
.build();

let created = client
.create_user_pool_client()
.user_pool_id(&pool_id)
.client_name("analytics-client")
.analytics_configuration(analytics)
.send()
.await
.expect("create client with analytics");
let client_id = created
.user_pool_client()
.unwrap()
.client_id()
.unwrap()
.to_string();
let created_ac = created
.user_pool_client()
.unwrap()
.analytics_configuration()
.expect("analytics config echoed on create");
assert_eq!(created_ac.application_id(), Some("app-1234567890abcdef"));

// Persisted: describe reflects it, not the default.
let described = client
.describe_user_pool_client()
.user_pool_id(&pool_id)
.client_id(&client_id)
.send()
.await
.expect("describe client");
let ac = described
.user_pool_client()
.unwrap()
.analytics_configuration()
.expect("analytics config persisted");
assert_eq!(ac.external_id(), Some("ext-42"));
assert!(ac.user_data_shared());
}

#[tokio::test]
async fn cognito_create_client_with_secret() {
let server = TestServer::start().await;
Expand Down
Loading
Loading