use std::collections::BTreeMap;
use std::sync::Arc;
use async_trait::async_trait;
use base64::Engine;
use chrono::Utc;
use http::StatusCode;
use parking_lot::RwLock;
use serde_json::{json, Value};
use uuid::Uuid;
use fakecloud_aws::arn::Arn;
use fakecloud_core::pagination::paginate;
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
use crate::evaluator::RateLimiter;
use crate::state::{
AccountState, ApiKey, IpSet, RegexPatternSet, RuleGroup, SharedWafv2State, Wafv2Accounts,
WebAcl,
};
const SUPPORTED_ACTIONS: &[&str] = &[
"AssociateWebACL",
"CheckCapacity",
"CreateAPIKey",
"CreateIPSet",
"CreateRegexPatternSet",
"CreateRuleGroup",
"CreateWebACL",
"DeleteAPIKey",
"DeleteFirewallManagerRuleGroups",
"DeleteIPSet",
"DeleteLoggingConfiguration",
"DeletePermissionPolicy",
"DeleteRegexPatternSet",
"DeleteRuleGroup",
"DeleteWebACL",
"DescribeAllManagedProducts",
"DescribeManagedProductsByVendor",
"DescribeManagedRuleGroup",
"DisassociateWebACL",
"GenerateMobileSdkReleaseUrl",
"GetDecryptedAPIKey",
"GetIPSet",
"GetLoggingConfiguration",
"GetManagedRuleSet",
"GetMobileSdkRelease",
"GetPermissionPolicy",
"GetRateBasedStatementManagedKeys",
"GetRegexPatternSet",
"GetRuleGroup",
"GetSampledRequests",
"GetTopPathStatisticsByTraffic",
"GetWebACL",
"GetWebACLForResource",
"ListAPIKeys",
"ListAvailableManagedRuleGroups",
"ListAvailableManagedRuleGroupVersions",
"ListIPSets",
"ListLoggingConfigurations",
"ListManagedRuleSets",
"ListMobileSdkReleases",
"ListRegexPatternSets",
"ListResourcesForWebACL",
"ListRuleGroups",
"ListTagsForResource",
"ListWebACLs",
"PutLoggingConfiguration",
"PutManagedRuleSetVersions",
"PutPermissionPolicy",
"TagResource",
"UntagResource",
"UpdateIPSet",
"UpdateManagedRuleSetVersionExpiryDate",
"UpdateRegexPatternSet",
"UpdateRuleGroup",
"UpdateWebACL",
];
pub struct Wafv2Service {
state: SharedWafv2State,
rate_limiter: Arc<RateLimiter>,
}
mod api_keys;
mod capacity;
mod ip_sets;
mod logging;
mod mobile_sdk;
mod permission_policy;
mod regex_pattern_sets;
mod rule_groups;
mod sampled_requests;
mod tags;
mod web_acls;
impl Wafv2Service {
pub fn new(state: SharedWafv2State) -> Self {
Self::with_rate_limiter(state, Arc::new(RateLimiter::new()))
}
pub fn with_rate_limiter(state: SharedWafv2State, rate_limiter: Arc<RateLimiter>) -> Self {
Self {
state,
rate_limiter,
}
}
pub fn shared_state(&self) -> SharedWafv2State {
Arc::clone(&self.state)
}
pub fn rate_limiter(&self) -> Arc<RateLimiter> {
Arc::clone(&self.rate_limiter)
}
}
impl Default for Wafv2Service {
fn default() -> Self {
Self::new(Arc::new(RwLock::new(Wafv2Accounts::new())))
}
}
#[async_trait]
impl AwsService for Wafv2Service {
fn service_name(&self) -> &str {
"wafv2"
}
fn supported_actions(&self) -> &[&str] {
SUPPORTED_ACTIONS
}
async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
match req.action.as_str() {
"CreateWebACL" => self.create_web_acl(&req),
"GetWebACL" => self.get_web_acl(&req),
"ListWebACLs" => self.list_web_acls(&req),
"UpdateWebACL" => self.update_web_acl(&req),
"DeleteWebACL" => self.delete_web_acl(&req),
"CreateRuleGroup" => self.create_rule_group(&req),
"GetRuleGroup" => self.get_rule_group(&req),
"ListRuleGroups" => self.list_rule_groups(&req),
"UpdateRuleGroup" => self.update_rule_group(&req),
"DeleteRuleGroup" => self.delete_rule_group(&req),
"CreateIPSet" => self.create_ip_set(&req),
"GetIPSet" => self.get_ip_set(&req),
"ListIPSets" => self.list_ip_sets(&req),
"UpdateIPSet" => self.update_ip_set(&req),
"DeleteIPSet" => self.delete_ip_set(&req),
"CreateRegexPatternSet" => self.create_regex_pattern_set(&req),
"GetRegexPatternSet" => self.get_regex_pattern_set(&req),
"ListRegexPatternSets" => self.list_regex_pattern_sets(&req),
"UpdateRegexPatternSet" => self.update_regex_pattern_set(&req),
"DeleteRegexPatternSet" => self.delete_regex_pattern_set(&req),
"AssociateWebACL" => self.associate_web_acl(&req),
"DisassociateWebACL" => self.disassociate_web_acl(&req),
"GetWebACLForResource" => self.get_web_acl_for_resource(&req),
"ListResourcesForWebACL" => self.list_resources_for_web_acl(&req),
"PutLoggingConfiguration" => self.put_logging_configuration(&req),
"GetLoggingConfiguration" => self.get_logging_configuration(&req),
"DeleteLoggingConfiguration" => self.delete_logging_configuration(&req),
"ListLoggingConfigurations" => self.list_logging_configurations(&req),
"PutPermissionPolicy" => self.put_permission_policy(&req),
"GetPermissionPolicy" => self.get_permission_policy(&req),
"DeletePermissionPolicy" => self.delete_permission_policy(&req),
"TagResource" => self.tag_resource(&req),
"UntagResource" => self.untag_resource(&req),
"ListTagsForResource" => self.list_tags_for_resource(&req),
"CreateAPIKey" => self.create_api_key(&req),
"DeleteAPIKey" => self.delete_api_key(&req),
"GetDecryptedAPIKey" => self.get_decrypted_api_key(&req),
"ListAPIKeys" => self.list_api_keys(&req),
"DescribeAllManagedProducts" => self.describe_all_managed_products(&req),
"DescribeManagedProductsByVendor" => self.describe_managed_products_by_vendor(&req),
"DescribeManagedRuleGroup" => self.describe_managed_rule_group(&req),
"GetManagedRuleSet" => self.get_managed_rule_set(&req),
"ListAvailableManagedRuleGroups" => self.list_available_managed_rule_groups(&req),
"ListAvailableManagedRuleGroupVersions" => {
self.list_available_managed_rule_group_versions(&req)
}
"ListManagedRuleSets" => self.list_managed_rule_sets(&req),
"PutManagedRuleSetVersions" => self.put_managed_rule_set_versions(&req),
"UpdateManagedRuleSetVersionExpiryDate" => {
self.update_managed_rule_set_version_expiry_date(&req)
}
"GenerateMobileSdkReleaseUrl" => self.generate_mobile_sdk_release_url(&req),
"GetMobileSdkRelease" => self.get_mobile_sdk_release(&req),
"ListMobileSdkReleases" => self.list_mobile_sdk_releases(&req),
"CheckCapacity" => self.check_capacity(&req),
"GetSampledRequests" => self.get_sampled_requests(&req),
"GetTopPathStatisticsByTraffic" => self.get_top_path_statistics_by_traffic(&req),
"GetRateBasedStatementManagedKeys" => self.get_rate_based_statement_managed_keys(&req),
"DeleteFirewallManagerRuleGroups" => self.delete_firewall_manager_rule_groups(&req),
other => Err(AwsServiceError::action_not_implemented("wafv2", other)),
}
}
}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {}
impl Wafv2Service {
fn get_top_path_statistics_by_traffic(
&self,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body = req.json_body();
let _web_acl_arn = require_str_len(&body, "WebAclArn", 20, 2048)?;
let _scope = require_scope(&body)?;
body.get("TimeWindow")
.ok_or_else(|| invalid_param("TimeWindow is required"))?;
let _limit = require_int_range(&body, "Limit", 1, 100)?;
let _bots_per_path = require_int_range(&body, "NumberOfTopTrafficBotsPerPath", 1, 10)?;
opt_str_len(&body, "UriPathPrefix", 1, 512)?;
opt_str_len(&body, "BotCategory", 1, 256)?;
opt_str_len(&body, "BotName", 1, 256)?;
opt_str_len(&body, "BotOrganization", 1, 256)?;
validate_opt_next_marker(&body)?;
Ok(AwsResponse::ok_json(json!({
"PathStatistics": [],
"TopCategories": [],
"TotalRequestCount": 0_u64,
})))
}
}
fn account_mut<'a>(state: &'a mut Wafv2Accounts, account_id: &str) -> &'a mut AccountState {
state.accounts.entry(account_id.to_string()).or_default()
}
fn require_str(body: &Value, field: &str) -> Result<String, AwsServiceError> {
body.get(field)
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or_else(|| invalid_param(format!("{field} is required")))
}
fn require_scope(body: &Value) -> Result<String, AwsServiceError> {
let scope = require_str(body, "Scope")?;
validate_enum(&scope, &["REGIONAL", "CLOUDFRONT"], "Scope")?;
Ok(scope)
}
fn validate_str_len(
value: &str,
min: usize,
max: usize,
field: &str,
) -> Result<(), AwsServiceError> {
if value.len() < min || value.len() > max {
return Err(invalid_param(format!(
"{field} must be between {min} and {max} characters"
)));
}
Ok(())
}
fn validate_int_range(value: i64, min: i64, max: i64, field: &str) -> Result<(), AwsServiceError> {
if value < min || value > max {
return Err(invalid_param(format!(
"{field} must be between {min} and {max}"
)));
}
Ok(())
}
fn validate_enum(value: &str, allowed: &[&str], field: &str) -> Result<(), AwsServiceError> {
if !allowed.contains(&value) {
return Err(invalid_param(format!("Invalid {field}: {value}")));
}
Ok(())
}
fn opt_str_len(
body: &Value,
field: &str,
min: usize,
max: usize,
) -> Result<Option<String>, AwsServiceError> {
match body.get(field).and_then(Value::as_str) {
Some(s) => {
validate_str_len(s, min, max, field)?;
Ok(Some(s.to_owned()))
}
None => Ok(None),
}
}
fn require_str_len(
body: &Value,
field: &str,
min: usize,
max: usize,
) -> Result<String, AwsServiceError> {
let s = require_str(body, field)?;
validate_str_len(&s, min, max, field)?;
Ok(s)
}
fn opt_int_range(
body: &Value,
field: &str,
min: i64,
max: i64,
) -> Result<Option<i64>, AwsServiceError> {
match body.get(field) {
Some(v) => {
let n = v
.as_i64()
.ok_or_else(|| invalid_param(format!("{field} must be an integer")))?;
validate_int_range(n, min, max, field)?;
Ok(Some(n))
}
None => Ok(None),
}
}
fn require_int_range(
body: &Value,
field: &str,
min: i64,
max: i64,
) -> Result<i64, AwsServiceError> {
let v = body
.get(field)
.ok_or_else(|| invalid_param(format!("{field} is required")))?;
let n = v
.as_i64()
.ok_or_else(|| invalid_param(format!("{field} must be an integer")))?;
validate_int_range(n, min, max, field)?;
Ok(n)
}
fn validate_opt_limit(body: &Value) -> Result<(), AwsServiceError> {
opt_int_range(body, "Limit", 1, 100)?;
Ok(())
}
fn validate_opt_next_marker(body: &Value) -> Result<(), AwsServiceError> {
opt_str_len(body, "NextMarker", 1, 256)?;
Ok(())
}
fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "WAFInvalidParameterException", msg)
}
fn normalize_resource_arn(arn: &str) -> String {
if let Some(rest) = arn.strip_prefix("arn:aws:elasticloadbalancing:") {
if let Some((before, after)) = rest.split_once(":listener/") {
let mut parts = after.splitn(4, '/');
let ty = parts.next();
let name = parts.next();
let lb_suffix = parts.next();
if let (Some(ty), Some(name), Some(lb_suffix)) = (ty, name, lb_suffix) {
return format!(
"arn:aws:elasticloadbalancing:{before}:loadbalancer/{ty}/{name}/{lb_suffix}"
);
}
}
}
arn.to_string()
}
fn not_found(resource: &str) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"WAFNonexistentItemException",
format!("{resource} not found"),
)
}
fn already_exists(msg: &str) -> AwsServiceError {
AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "WAFDuplicateItemException", msg)
}
fn stale_lock_token() -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"WAFOptimisticLockException",
"LockToken does not match the current value; refresh and retry",
)
}
fn synth_uuid() -> String {
Uuid::new_v4().to_string()
}
fn synth_arn(
account_id: &str,
region: &str,
scope: &str,
kind: &str,
name: &str,
id: &str,
) -> String {
let region = if region.is_empty() {
"us-east-1"
} else {
region
};
let (region_in_arn, scope_seg) = if scope == "CLOUDFRONT" {
("us-east-1", "global")
} else {
(region, region)
};
Arn::new(
"wafv2",
region_in_arn,
account_id,
&format!("{scope_seg}/{kind}/{name}/{id}"),
)
.to_string()
}
fn parse_string_list(value: Option<&Value>) -> Vec<String> {
value
.and_then(Value::as_array)
.map(|v| {
v.iter()
.filter_map(|s| s.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
}
fn parse_tags(value: Option<&Value>) -> Result<BTreeMap<String, String>, AwsServiceError> {
let mut out = BTreeMap::new();
let Some(arr) = value.and_then(Value::as_array) else {
return Ok(out);
};
for tag in arr {
let key = tag
.get("Key")
.and_then(Value::as_str)
.ok_or_else(|| invalid_param("Tag.Key is required"))?
.to_string();
let value = tag
.get("Value")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
out.insert(key, value);
}
Ok(out)
}
fn parse_custom_response_bodies(value: Option<&Value>) -> BTreeMap<String, Value> {
value
.and_then(Value::as_object)
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
fn resource_exists(account: &AccountState, arn: &str) -> bool {
account.web_acls.values().any(|w| w.arn == arn)
|| account.rule_groups.values().any(|r| r.arn == arn)
|| account.ip_sets.values().any(|s| s.arn == arn)
|| account.regex_pattern_sets.values().any(|s| s.arn == arn)
}
fn compute_capacity(rules: &[Value]) -> i64 {
rules
.iter()
.map(|r| r.get("Statement").map(count_statement_leaves).unwrap_or(1) as i64)
.sum()
}
fn count_statement_leaves(stmt: &Value) -> u32 {
let Some(obj) = stmt.as_object() else {
return 1;
};
let mut total = 0u32;
for (k, v) in obj {
match k.as_str() {
"AndStatement" | "OrStatement" => {
if let Some(arr) = v.get("Statements").and_then(Value::as_array) {
for s in arr {
total += count_statement_leaves(s);
}
}
}
"NotStatement" => {
if let Some(s) = v.get("Statement") {
total += count_statement_leaves(s);
}
}
_ => {
total += 1;
}
}
}
total.max(1)
}
fn managed_products() -> Vec<Value> {
vec![
json!({
"VendorName": "AWS",
"ManagedRuleSetName": "AWSManagedRulesCommonRuleSet",
"ProductId": "prod-aws-common",
"ProductLink": "https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html",
"ProductTitle": "Core rule set",
"ProductDescription": "OWASP Top 10 baseline rules",
"SnsTopicArn": "arn:aws:sns:us-east-1::aws-managed-common-notifications",
"IsVersioningSupported": true,
"IsAdvancedManagedRuleSet": false,
}),
json!({
"VendorName": "AWS",
"ManagedRuleSetName": "AWSManagedRulesSQLiRuleSet",
"ProductId": "prod-aws-sqli",
"ProductLink": "https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html",
"ProductTitle": "SQL injection rule set",
"ProductDescription": "Rules that block SQL injection patterns",
"SnsTopicArn": "arn:aws:sns:us-east-1::aws-managed-sqli-notifications",
"IsVersioningSupported": true,
"IsAdvancedManagedRuleSet": false,
}),
]
}
fn managed_rule_summaries(_vendor: &str, _name: &str) -> Vec<Value> {
vec![json!({
"Name": "RuleA",
"Action": {"Block": {}},
})]
}
fn web_acl_summary_json(
id: &str,
name: &str,
arn: &str,
description: Option<&str>,
lock_token: &str,
) -> Value {
let mut obj = json!({
"Id": id,
"Name": name,
"ARN": arn,
"LockToken": lock_token,
});
if let Some(d) = description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.to_string()));
}
obj
}
fn web_acl_detail_json(acl: &WebAcl) -> Value {
let mut obj = json!({
"Id": acl.id,
"Name": acl.name,
"ARN": acl.arn,
"DefaultAction": acl.default_action,
"Rules": acl.rules,
"VisibilityConfig": acl.visibility_config,
"Capacity": acl.capacity,
"ManagedByFirewallManager": acl.managed_by_firewall_manager,
"RetrofittedByFirewallManager": acl.retrofitted_by_firewall_manager,
"LabelNamespace": acl.label_namespace,
"TokenDomains": acl.token_domains,
"PreProcessFirewallManagerRuleGroups": acl.pre_process_firewall_manager_rule_groups,
"PostProcessFirewallManagerRuleGroups": acl.post_process_firewall_manager_rule_groups,
});
if let Some(d) = &acl.description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.clone()));
}
if !acl.custom_response_bodies.is_empty() {
obj.as_object_mut().unwrap().insert(
"CustomResponseBodies".to_string(),
json!(acl.custom_response_bodies),
);
}
if let Some(c) = &acl.captcha_config {
obj.as_object_mut()
.unwrap()
.insert("CaptchaConfig".to_string(), c.clone());
}
if let Some(c) = &acl.challenge_config {
obj.as_object_mut()
.unwrap()
.insert("ChallengeConfig".to_string(), c.clone());
}
if let Some(c) = &acl.association_config {
obj.as_object_mut()
.unwrap()
.insert("AssociationConfig".to_string(), c.clone());
}
if let Some(c) = &acl.data_protection_config {
obj.as_object_mut()
.unwrap()
.insert("DataProtectionConfig".to_string(), c.clone());
}
if let Some(c) = &acl.on_source_d_do_s_protection_config {
obj.as_object_mut()
.unwrap()
.insert("OnSourceDDoSProtectionConfig".to_string(), c.clone());
}
if let Some(c) = &acl.application_config {
obj.as_object_mut()
.unwrap()
.insert("ApplicationConfig".to_string(), c.clone());
}
obj
}
fn rule_group_summary_json(
id: &str,
name: &str,
arn: &str,
description: Option<&str>,
lock_token: &str,
) -> Value {
let mut obj = json!({
"Id": id,
"Name": name,
"ARN": arn,
"LockToken": lock_token,
});
if let Some(d) = description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.to_string()));
}
obj
}
fn rule_group_detail_json(rg: &RuleGroup) -> Value {
let mut obj = json!({
"Id": rg.id,
"Name": rg.name,
"ARN": rg.arn,
"Capacity": rg.capacity,
"Rules": rg.rules,
"VisibilityConfig": rg.visibility_config,
"LabelNamespace": rg.label_namespace,
"AvailableLabels": rg.available_labels,
"ConsumedLabels": rg.consumed_labels,
});
if let Some(d) = &rg.description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.clone()));
}
if !rg.custom_response_bodies.is_empty() {
obj.as_object_mut().unwrap().insert(
"CustomResponseBodies".to_string(),
json!(rg.custom_response_bodies),
);
}
obj
}
fn ip_set_summary_json(
id: &str,
name: &str,
arn: &str,
description: Option<&str>,
lock_token: &str,
) -> Value {
let mut obj = json!({
"Id": id,
"Name": name,
"ARN": arn,
"LockToken": lock_token,
});
if let Some(d) = description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.to_string()));
}
obj
}
fn ip_set_detail_json(set: &IpSet) -> Value {
let mut obj = json!({
"Id": set.id,
"Name": set.name,
"ARN": set.arn,
"IPAddressVersion": set.ip_address_version,
"Addresses": set.addresses,
});
if let Some(d) = &set.description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.clone()));
}
obj
}
fn regex_set_summary_json(
id: &str,
name: &str,
arn: &str,
description: Option<&str>,
lock_token: &str,
) -> Value {
let mut obj = json!({
"Id": id,
"Name": name,
"ARN": arn,
"LockToken": lock_token,
});
if let Some(d) = description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.to_string()));
}
obj
}
fn regex_set_detail_json(set: &RegexPatternSet) -> Value {
let mut obj = json!({
"Id": set.id,
"Name": set.name,
"ARN": set.arn,
"RegularExpressionList": set.regular_expressions,
});
if let Some(d) = &set.description {
obj.as_object_mut()
.unwrap()
.insert("Description".to_string(), Value::String(d.clone()));
}
obj
}
#[cfg(test)]
mod arn_norm_tests {
use super::normalize_resource_arn;
#[test]
fn elb_listener_arn_collapses_to_load_balancer_arn() {
let listener =
"arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/web/abc/xyz";
assert_eq!(
normalize_resource_arn(listener),
"arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/web/abc"
);
}
#[test]
fn elb_load_balancer_arn_passes_through() {
let lb = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/web/abc";
assert_eq!(normalize_resource_arn(lb), lb);
}
#[test]
fn nlb_listener_arn_collapses_to_network_load_balancer_arn() {
let listener =
"arn:aws:elasticloadbalancing:eu-west-1:123456789012:listener/net/wire/abc/xyz";
assert_eq!(
normalize_resource_arn(listener),
"arn:aws:elasticloadbalancing:eu-west-1:123456789012:loadbalancer/net/wire/abc"
);
}
#[test]
fn non_elbv2_arn_passes_through() {
let apigw = "arn:aws:apigateway:us-east-1::/restapis/abc/stages/prod";
assert_eq!(normalize_resource_arn(apigw), apigw);
let cog = "arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_xxx";
assert_eq!(normalize_resource_arn(cog), cog);
}
}
#[cfg(test)]
mod managed_rule_set_validation_tests {
use super::*;
fn make_body(fields: &[(&str, &str)]) -> Value {
let mut m = serde_json::Map::new();
for (k, v) in fields {
m.insert(k.to_string(), Value::String(v.to_string()));
}
Value::Object(m)
}
#[test]
fn put_managed_rule_set_versions_rejects_empty_name() {
let body = make_body(&[
("Name", ""),
("Id", "id"),
("LockToken", "tok"),
("Scope", "REGIONAL"),
("RecommendedVersion", "1.0"),
]);
let svc = Wafv2Service::default();
let req = AwsRequest {
service: "wafv2".into(),
action: "PutManagedRuleSetVersions".into(),
method: http::Method::POST,
raw_path: "/".into(),
raw_query: String::new(),
path_segments: Vec::new(),
query_params: std::collections::HashMap::new(),
headers: http::HeaderMap::new(),
body: serde_json::to_vec(&body).unwrap().into(),
body_stream: parking_lot::Mutex::new(None),
account_id: "123456789012".into(),
region: "us-east-1".into(),
request_id: "r".into(),
is_query_protocol: false,
access_key_id: None,
principal: None,
};
let res = svc.put_managed_rule_set_versions(&req);
assert!(res.is_err());
assert_eq!(res.err().unwrap().code(), "ValidationException");
}
#[test]
fn put_managed_rule_set_versions_rejects_long_name() {
let body = make_body(&[
("Name", &"x".repeat(129)),
("Id", "id"),
("LockToken", "tok"),
("Scope", "REGIONAL"),
("RecommendedVersion", "1.0"),
]);
let svc = Wafv2Service::default();
let req = AwsRequest {
service: "wafv2".into(),
action: "PutManagedRuleSetVersions".into(),
method: http::Method::POST,
raw_path: "/".into(),
raw_query: String::new(),
path_segments: Vec::new(),
query_params: std::collections::HashMap::new(),
headers: http::HeaderMap::new(),
body: serde_json::to_vec(&body).unwrap().into(),
body_stream: parking_lot::Mutex::new(None),
account_id: "123456789012".into(),
region: "us-east-1".into(),
request_id: "r".into(),
is_query_protocol: false,
access_key_id: None,
principal: None,
};
let res = svc.put_managed_rule_set_versions(&req);
assert!(res.is_err());
assert_eq!(res.err().unwrap().code(), "ValidationException");
}
#[test]
fn update_managed_rule_set_version_expiry_date_rejects_missing_timestamp() {
let body = make_body(&[
("Name", "name"),
("Id", "id"),
("LockToken", "tok"),
("Scope", "REGIONAL"),
("VersionToExpire", "1.0"),
]);
let svc = Wafv2Service::default();
let req = AwsRequest {
service: "wafv2".into(),
action: "UpdateManagedRuleSetVersionExpiryDate".into(),
method: http::Method::POST,
raw_path: "/".into(),
raw_query: String::new(),
path_segments: Vec::new(),
query_params: std::collections::HashMap::new(),
headers: http::HeaderMap::new(),
body: serde_json::to_vec(&body).unwrap().into(),
body_stream: parking_lot::Mutex::new(None),
account_id: "123456789012".into(),
region: "us-east-1".into(),
request_id: "r".into(),
is_query_protocol: false,
access_key_id: None,
principal: None,
};
let res = svc.update_managed_rule_set_version_expiry_date(&req);
assert!(res.is_err());
assert_eq!(res.err().unwrap().code(), "WAFInvalidParameterException");
}
#[test]
fn paginate_checked_rejects_invalid_token() {
use fakecloud_core::pagination::paginate_checked;
let items: Vec<i32> = (0..5).collect();
assert!(paginate_checked(&items, Some("not-a-valid-token"), 3).is_err());
assert!(paginate_checked(&items, Some("2"), 3).is_ok());
assert!(paginate_checked(&items, None, 3).is_ok());
}
}