Skip to main content

fakecloud_lambda/
service.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use chrono::Utc;
6use http::{Method, StatusCode};
7use serde_json::{json, Value};
8use sha2::{Digest, Sha256};
9use tokio::sync::Mutex as AsyncMutex;
10
11use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
12use fakecloud_persistence::SnapshotStore;
13
14use crate::runtime::ContainerRuntime;
15use crate::state::{
16    EventSourceMapping, LambdaFunction, LambdaSnapshot, LambdaState, SharedLambdaState,
17    LAMBDA_SNAPSHOT_SCHEMA_VERSION,
18};
19
20/// Lambda actions whose URL `resource_name` slot is a `FunctionName`
21/// (and therefore accepts ARN / partial ARN / `name:qualifier` forms).
22/// Layer / event-source-mapping / code-signing-config actions key off
23/// other resource identifiers and are excluded.
24pub(crate) fn action_takes_function_name(action: &str) -> bool {
25    matches!(
26        action,
27        "GetFunction"
28            | "DeleteFunction"
29            | "Invoke"
30            | "InvokeAsync"
31            | "InvokeWithResponseStream"
32            | "PublishVersion"
33            | "ListVersionsByFunction"
34            | "AddPermission"
35            | "RemovePermission"
36            | "GetPolicy"
37            | "GetFunctionConfiguration"
38            | "UpdateFunctionConfiguration"
39            | "UpdateFunctionCode"
40            | "GetFunctionConcurrency"
41            | "PutFunctionConcurrency"
42            | "DeleteFunctionConcurrency"
43            | "PutProvisionedConcurrencyConfig"
44            | "GetProvisionedConcurrencyConfig"
45            | "DeleteProvisionedConcurrencyConfig"
46            | "ListProvisionedConcurrencyConfigs"
47            | "PutFunctionEventInvokeConfig"
48            | "UpdateFunctionEventInvokeConfig"
49            | "GetFunctionEventInvokeConfig"
50            | "DeleteFunctionEventInvokeConfig"
51            | "ListFunctionEventInvokeConfigs"
52            | "CreateFunctionUrlConfig"
53            | "UpdateFunctionUrlConfig"
54            | "GetFunctionUrlConfig"
55            | "DeleteFunctionUrlConfig"
56            | "ListFunctionUrlConfigs"
57            | "PutFunctionCodeSigningConfig"
58            | "GetFunctionCodeSigningConfig"
59            | "DeleteFunctionCodeSigningConfig"
60            | "GetFunctionScalingConfig"
61            | "PutFunctionRecursionConfig"
62            | "GetFunctionRecursionConfig"
63            | "CreateAlias"
64            | "GetAlias"
65            | "ListAliases"
66            | "UpdateAlias"
67            | "DeleteAlias"
68            | "PutRuntimeManagementConfig"
69            | "GetRuntimeManagementConfig"
70            | "ListDurableExecutionsByFunction"
71    )
72}
73
74/// Strip an ARN, partial ARN, or trailing `:qualifier` from a Lambda
75/// `FunctionName` input down to the bare function name used as the
76/// state map key. AWS Lambda accepts four forms in URL path slots and
77/// API params:
78///
79///   - `MyFunction`
80///   - `MyFunction:Qualifier`
81///   - `123456789012:function:MyFunction[:Qualifier]`           (partial ARN)
82///   - `arn:aws:lambda:REGION:ACCOUNT:function:MyFunction[:Qualifier]`
83///
84/// Inputs that don't match any of those structures are returned
85/// unchanged. The qualifier (version or alias) is dropped because most
86/// callers look up the function by name and resolve qualifier
87/// separately.
88pub(crate) fn normalize_function_name(input: &str) -> String {
89    if input.is_empty() {
90        return String::new();
91    }
92
93    // SDKs URL-encode `:` in path segments, so `arn:aws:lambda:...`
94    // arrives as `arn%3Aaws%3Alambda%3A...`. Decode first; legitimate
95    // function names contain no percent-encoded characters, so this is
96    // safe for the bare-name path too.
97    let decoded = percent_encoding::percent_decode_str(input)
98        .decode_utf8_lossy()
99        .into_owned();
100    let input = decoded.as_str();
101
102    // Full ARN: arn:aws:lambda:REGION:ACCOUNT:function:NAME[:QUALIFIER]
103    if let Some(rest) = input.strip_prefix("arn:aws:lambda:") {
104        let parts: Vec<&str> = rest.splitn(5, ':').collect();
105        // parts: [region, account, "function", name, qualifier?]
106        if parts.len() >= 4 && parts[2] == "function" && !parts[3].is_empty() {
107            return parts[3].to_string();
108        }
109        return input.to_string();
110    }
111
112    // Partial ARN: ACCOUNT:function:NAME[:QUALIFIER]
113    let parts: Vec<&str> = input.splitn(4, ':').collect();
114    if parts.len() >= 3 && parts[1] == "function" && parts[0].chars().all(|c| c.is_ascii_digit()) {
115        if !parts[2].is_empty() {
116            return parts[2].to_string();
117        }
118        return input.to_string();
119    }
120
121    // Bare name with qualifier: NAME:QUALIFIER. Only apply when the
122    // input contains exactly one colon and the name part is a valid
123    // Lambda function-name token, so malformed ARNs (e.g. wrong service
124    // or wrong format) fall through unchanged rather than getting their
125    // first colon-segment returned.
126    if input.matches(':').count() == 1 {
127        if let Some((name, _qualifier)) = input.split_once(':') {
128            if !name.is_empty() && name.chars().all(is_function_name_char) {
129                return name.to_string();
130            }
131        }
132    }
133
134    input.to_string()
135}
136
137fn is_function_name_char(c: char) -> bool {
138    c.is_ascii_alphanumeric() || c == '-' || c == '_'
139}
140
141/// All fields of a `CreateFunction` request, already parsed and
142/// defaulted. The code zip (if any) is eagerly base64-decoded so the
143/// caller can hash it without doing the decode again.
144struct CreateFunctionInput {
145    function_name: String,
146    runtime: String,
147    role: String,
148    handler: String,
149    description: String,
150    timeout: i64,
151    memory_size: i64,
152    package_type: String,
153    tags: BTreeMap<String, String>,
154    environment: BTreeMap<String, String>,
155    architectures: Vec<String>,
156    code_zip: Option<Vec<u8>>,
157    code_fallback: Vec<u8>,
158    image_uri: Option<String>,
159    layer_arns: Vec<String>,
160}
161
162impl CreateFunctionInput {
163    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
164        let function_name = body["FunctionName"]
165            .as_str()
166            .ok_or_else(|| {
167                AwsServiceError::aws_error(
168                    StatusCode::BAD_REQUEST,
169                    "InvalidParameterValueException",
170                    "FunctionName is required",
171                )
172            })?
173            .to_string();
174
175        let tags: BTreeMap<String, String> = body["Tags"]
176            .as_object()
177            .map(|m| {
178                m.iter()
179                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
180                    .collect()
181            })
182            .unwrap_or_default();
183
184        let environment: BTreeMap<String, String> = body["Environment"]["Variables"]
185            .as_object()
186            .map(|m| {
187                m.iter()
188                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
189                    .collect()
190            })
191            .unwrap_or_default();
192
193        let architectures = body["Architectures"]
194            .as_array()
195            .map(|a| {
196                a.iter()
197                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
198                    .collect()
199            })
200            .unwrap_or_else(|| vec!["x86_64".to_string()]);
201
202        let code_zip: Option<Vec<u8>> = match body["Code"]["ZipFile"].as_str() {
203            Some(b64) => Some(
204                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64).map_err(
205                    |_| {
206                        AwsServiceError::aws_error(
207                            StatusCode::BAD_REQUEST,
208                            "InvalidParameterValueException",
209                            "Could not decode Code.ZipFile: invalid base64",
210                        )
211                    },
212                )?,
213            ),
214            None => None,
215        };
216
217        let code_fallback = serde_json::to_vec(&body["Code"]).unwrap_or_default();
218
219        let package_type = body["PackageType"].as_str().unwrap_or("Zip").to_string();
220        // ImageUri belongs to `PackageType=Image` functions. Silently
221        // dropping it on `Zip` functions avoids GetFunction returning
222        // ECR code metadata for a Zip-based function (AWS ignores the
223        // field entirely in that case too).
224        let image_uri = if package_type == "Image" {
225            body["Code"]["ImageUri"].as_str().map(String::from)
226        } else {
227            None
228        };
229
230        // PackageType=Image requires Code.ImageUri; PackageType=Zip requires
231        // code content. Reject inconsistent shapes with AWS's error code so
232        // SDK-level validation tests see matching behaviour.
233        if package_type == "Image" && image_uri.is_none() {
234            return Err(AwsServiceError::aws_error(
235                StatusCode::BAD_REQUEST,
236                "InvalidParameterValueException",
237                "Code.ImageUri is required when PackageType is Image",
238            ));
239        }
240
241        let layer_arns: Vec<String> = body["Layers"]
242            .as_array()
243            .map(|arr| {
244                arr.iter()
245                    .filter_map(|v| v.as_str().map(String::from))
246                    .collect()
247            })
248            .unwrap_or_default();
249
250        Ok(Self {
251            function_name,
252            runtime: body["Runtime"].as_str().unwrap_or("python3.12").to_string(),
253            role: body["Role"].as_str().unwrap_or("").to_string(),
254            handler: body["Handler"]
255                .as_str()
256                .unwrap_or("index.handler")
257                .to_string(),
258            description: body["Description"].as_str().unwrap_or("").to_string(),
259            timeout: body["Timeout"].as_i64().unwrap_or(3),
260            memory_size: body["MemorySize"].as_i64().unwrap_or(128),
261            package_type,
262            tags,
263            environment,
264            architectures,
265            code_zip,
266            code_fallback,
267            image_uri,
268            layer_arns,
269        })
270    }
271}
272
273/// AWS Lambda's InvocationType: synchronous, async (event), or dry-run.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum InvocationType {
276    RequestResponse,
277    Event,
278    DryRun,
279}
280
281impl InvocationType {
282    pub fn from_header(value: Option<&str>) -> Self {
283        match value {
284            Some("Event") => Self::Event,
285            Some("DryRun") => Self::DryRun,
286            _ => Self::RequestResponse,
287        }
288    }
289}
290
291/// Route an async-invoke result to the configured OnSuccess / OnFailure
292/// destination. Destination is matched by ARN scheme: SQS, SNS, EventBridge,
293/// or another Lambda. Mirrors the AWS Lambda destinations record schema.
294fn route_to_destination(
295    bus: Arc<fakecloud_core::delivery::DeliveryBus>,
296    function_arn: &str,
297    request_payload: &[u8],
298    result: &Result<Vec<u8>, String>,
299    destination_config: Option<&serde_json::Value>,
300) {
301    let Some(cfg) = destination_config else {
302        return;
303    };
304    let (key, condition, response_value): (&str, &str, serde_json::Value) = match result {
305        Ok(bytes) => (
306            "OnSuccess",
307            "Success",
308            serde_json::from_slice(bytes).unwrap_or(serde_json::Value::Null),
309        ),
310        Err(err) => (
311            "OnFailure",
312            "RetriesExhausted",
313            serde_json::json!({ "errorMessage": err }),
314        ),
315    };
316    let Some(dest) = cfg
317        .get(key)
318        .and_then(|v| v.get("Destination"))
319        .and_then(|v| v.as_str())
320    else {
321        return;
322    };
323    let request_payload_v: serde_json::Value =
324        serde_json::from_slice(request_payload).unwrap_or(serde_json::Value::Null);
325    let record = serde_json::json!({
326        "version": "1.0",
327        "timestamp": chrono::Utc::now().to_rfc3339(),
328        "requestContext": {
329            "requestId": uuid::Uuid::new_v4().to_string(),
330            "functionArn": format!("{function_arn}:$LATEST"),
331            "condition": condition,
332            "approximateInvokeCount": 1,
333        },
334        "requestPayload": request_payload_v,
335        "responseContext": {
336            "statusCode": 200,
337            "executedVersion": "$LATEST",
338        },
339        "responsePayload": response_value,
340    });
341    let body = record.to_string();
342    if dest.contains(":sqs:") {
343        bus.send_to_sqs(dest, &body, &std::collections::HashMap::new());
344    } else if dest.contains(":sns:") {
345        bus.publish_to_sns(dest, &body, None);
346    } else if dest.contains(":lambda:") {
347        let dest = dest.to_string();
348        let payload = body.clone();
349        tokio::spawn(async move {
350            let _ = bus.invoke_lambda(&dest, &payload).await;
351        });
352    } else if dest.contains(":events:") || dest.contains(":eventbridge:") {
353        let detail_type = if result.is_ok() {
354            "Lambda Function Invocation Result - Success"
355        } else {
356            "Lambda Function Invocation Result - Failure"
357        };
358        bus.put_event_to_eventbridge("lambda", detail_type, &body, "default");
359    }
360}
361
362pub struct LambdaService {
363    pub(crate) state: SharedLambdaState,
364    runtime: Option<Arc<ContainerRuntime>>,
365    snapshot_store: Option<Arc<dyn SnapshotStore>>,
366    snapshot_lock: Arc<AsyncMutex<()>>,
367    pub(crate) delivery_bus: Option<Arc<fakecloud_core::delivery::DeliveryBus>>,
368    pub(crate) role_trust_validator: Option<Arc<dyn fakecloud_core::auth::RoleTrustValidator>>,
369}
370
371impl LambdaService {
372    pub fn new(state: SharedLambdaState) -> Self {
373        Self {
374            state,
375            runtime: None,
376            snapshot_store: None,
377            snapshot_lock: Arc::new(AsyncMutex::new(())),
378            delivery_bus: None,
379            role_trust_validator: None,
380        }
381    }
382
383    pub fn with_runtime(mut self, runtime: Arc<ContainerRuntime>) -> Self {
384        self.runtime = Some(runtime);
385        self
386    }
387
388    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
389        self.snapshot_store = Some(store);
390        self
391    }
392
393    pub fn with_delivery_bus(mut self, bus: Arc<fakecloud_core::delivery::DeliveryBus>) -> Self {
394        self.delivery_bus = Some(bus);
395        self
396    }
397
398    pub fn with_role_trust_validator(
399        mut self,
400        validator: Arc<dyn fakecloud_core::auth::RoleTrustValidator>,
401    ) -> Self {
402        self.role_trust_validator = Some(validator);
403        self
404    }
405
406    async fn save_snapshot(&self) {
407        let Some(store) = self.snapshot_store.clone() else {
408            return;
409        };
410        let _guard = self.snapshot_lock.lock().await;
411        let snapshot = LambdaSnapshot {
412            schema_version: LAMBDA_SNAPSHOT_SCHEMA_VERSION,
413            accounts: Some(self.state.read().clone()),
414            state: None,
415        };
416        let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
417            let bytes = serde_json::to_vec(&snapshot)
418                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
419            store.save(&bytes)
420        })
421        .await;
422        match join {
423            Ok(Ok(())) => {}
424            Ok(Err(err)) => tracing::error!(%err, "failed to write lambda snapshot"),
425            Err(err) => tracing::error!(%err, "lambda snapshot task panicked"),
426        }
427    }
428
429    /// Determine the action from the HTTP method and path segments.
430    /// Lambda uses REST-style routing:
431    ///   POST   /2015-03-31/functions                         -> CreateFunction
432    ///   GET    /2015-03-31/functions                         -> ListFunctions
433    ///   GET    /2015-03-31/functions/{name}                  -> GetFunction
434    ///   DELETE /2015-03-31/functions/{name}                  -> DeleteFunction
435    ///   POST   /2015-03-31/functions/{name}/invocations      -> Invoke
436    ///   POST   /2015-03-31/functions/{name}/versions         -> PublishVersion
437    ///   POST   /2015-03-31/event-source-mappings             -> CreateEventSourceMapping
438    ///   GET    /2015-03-31/event-source-mappings             -> ListEventSourceMappings
439    ///   GET    /2015-03-31/event-source-mappings/{uuid}      -> GetEventSourceMapping
440    ///   DELETE /2015-03-31/event-source-mappings/{uuid}      -> DeleteEventSourceMapping
441    fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Option<String>)> {
442        let segs = &req.path_segments;
443        if segs.is_empty() {
444            return None;
445        }
446        // The Lambda data API uses many date prefixes (one per
447        // operation family). Recognise any well-formed YYYY-MM-DD
448        // prefix and route based on the path structure that follows.
449        let prefix = segs[0].as_str();
450
451        // Account settings + InvokeAsync — any prefix.
452        if segs.get(1).map(|s| s.as_str()) == Some("account-settings") && req.method == Method::GET
453        {
454            return Some(("GetAccountSettings", None));
455        }
456        if segs.get(1).map(|s| s.as_str()) == Some("functions")
457            && segs.get(3).map(|s| s.as_str()) == Some("invoke-async")
458            && req.method == Method::POST
459        {
460            return Some(("InvokeAsync", segs.get(2).map(|s| s.to_string())));
461        }
462        if segs.get(1).map(|s| s.as_str()) == Some("functions")
463            && segs.get(3).map(|s| s.as_str()) == Some("response-streaming-invocations")
464            && req.method == Method::POST
465        {
466            return Some((
467                "InvokeWithResponseStream",
468                segs.get(2).map(|s| s.to_string()),
469            ));
470        }
471
472        // Concurrency (reserved + provisioned) — any prefix.
473        if segs.get(1).map(|s| s.as_str()) == Some("functions")
474            && segs.get(3).map(|s| s.as_str()) == Some("concurrency")
475        {
476            let res = segs.get(2).map(|s| s.to_string());
477            return match req.method {
478                Method::PUT => Some(("PutFunctionConcurrency", res)),
479                Method::GET => Some(("GetFunctionConcurrency", res)),
480                Method::DELETE => Some(("DeleteFunctionConcurrency", res)),
481                _ => None,
482            };
483        }
484
485        // Provisioned concurrency at any prefix.
486        if segs.get(1).map(|s| s.as_str()) == Some("functions")
487            && segs.get(3).map(|s| s.as_str()) == Some("provisioned-concurrency")
488        {
489            let res = segs.get(2).map(|s| s.to_string());
490            return match req.method {
491                Method::PUT => Some(("PutProvisionedConcurrencyConfig", res)),
492                Method::GET => Some(("GetProvisionedConcurrencyConfig", res)),
493                Method::DELETE => Some(("DeleteProvisionedConcurrencyConfig", res)),
494                _ => None,
495            };
496        }
497        if segs.get(1).map(|s| s.as_str()) == Some("functions")
498            && segs.get(3).map(|s| s.as_str()) == Some("provisioned-concurrency-configs")
499            && req.method == Method::GET
500        {
501            return Some((
502                "ListProvisionedConcurrencyConfigs",
503                segs.get(2).map(|s| s.to_string()),
504            ));
505        }
506
507        // Event invoke config — any prefix.
508        if segs.get(1).map(|s| s.as_str()) == Some("functions")
509            && segs.get(3).map(|s| s.as_str()) == Some("event-invoke-config")
510        {
511            let res = segs.get(2).map(|s| s.to_string());
512            return match req.method {
513                Method::POST => Some(("PutFunctionEventInvokeConfig", res)),
514                Method::PUT => Some(("UpdateFunctionEventInvokeConfig", res)),
515                Method::GET => Some(("GetFunctionEventInvokeConfig", res)),
516                Method::DELETE => Some(("DeleteFunctionEventInvokeConfig", res)),
517                _ => None,
518            };
519        }
520        if segs.get(1).map(|s| s.as_str()) == Some("functions")
521            && (segs.get(3).map(|s| s.as_str()) == Some("event-invoke-config-list")
522                || (segs.get(3).map(|s| s.as_str()) == Some("event-invoke-config")
523                    && segs.get(4).map(|s| s.as_str()) == Some("list")))
524            && req.method == Method::GET
525        {
526            return Some((
527                "ListFunctionEventInvokeConfigs",
528                segs.get(2).map(|s| s.to_string()),
529            ));
530        }
531
532        // Recursion config — any prefix.
533        if segs.get(1).map(|s| s.as_str()) == Some("functions")
534            && segs.get(3).map(|s| s.as_str()) == Some("recursion-config")
535        {
536            let res = segs.get(2).map(|s| s.to_string());
537            return match req.method {
538                Method::PUT => Some(("PutFunctionRecursionConfig", res)),
539                Method::GET => Some(("GetFunctionRecursionConfig", res)),
540                _ => None,
541            };
542        }
543
544        // Runtime management config — any prefix.
545        if segs.get(1).map(|s| s.as_str()) == Some("functions")
546            && segs.get(3).map(|s| s.as_str()) == Some("runtime-management-config")
547        {
548            let res = segs.get(2).map(|s| s.to_string());
549            return match req.method {
550                Method::PUT => Some(("PutRuntimeManagementConfig", res)),
551                Method::GET => Some(("GetRuntimeManagementConfig", res)),
552                _ => None,
553            };
554        }
555
556        // Code signing config (function and global) — any prefix.
557        if segs.get(1).map(|s| s.as_str()) == Some("functions")
558            && segs.get(3).map(|s| s.as_str()) == Some("code-signing-config")
559        {
560            let res = segs.get(2).map(|s| s.to_string());
561            return match req.method {
562                Method::PUT => Some(("PutFunctionCodeSigningConfig", res)),
563                Method::GET => Some(("GetFunctionCodeSigningConfig", res)),
564                Method::DELETE => Some(("DeleteFunctionCodeSigningConfig", res)),
565                _ => None,
566            };
567        }
568        if segs.get(1).map(|s| s.as_str()) == Some("code-signing-configs") {
569            let res = segs.get(2).map(|s| s.to_string());
570            return match (
571                req.method.clone(),
572                segs.len(),
573                segs.get(3).map(|s| s.as_str()),
574            ) {
575                (Method::POST, 2, _) => Some(("CreateCodeSigningConfig", None)),
576                (Method::GET, 2, _) => Some(("ListCodeSigningConfigs", None)),
577                (Method::GET, 3, _) => Some(("GetCodeSigningConfig", res)),
578                (Method::PUT, 3, _) => Some(("UpdateCodeSigningConfig", res)),
579                (Method::DELETE, 3, _) => Some(("DeleteCodeSigningConfig", res)),
580                (Method::GET, 4, Some("functions")) => {
581                    Some(("ListFunctionsByCodeSigningConfig", res))
582                }
583                _ => None,
584            };
585        }
586
587        // Tags resource ARN at any prefix.
588        if segs.get(1).map(|s| s.as_str()) == Some("tags") && segs.len() >= 3 {
589            let res = segs[2..].join("/");
590            return match req.method {
591                Method::POST => Some(("TagResource", Some(res))),
592                Method::DELETE => Some(("UntagResource", Some(res))),
593                Method::GET => Some(("ListTags", Some(res))),
594                _ => None,
595            };
596        }
597
598        // Function URL config + scaling config (any prefix).
599        if segs.get(1).map(|s| s.as_str()) == Some("functions")
600            && segs.get(3).map(|s| s.as_str()) == Some("url")
601        {
602            let res = segs.get(2).map(|s| s.to_string());
603            return match req.method {
604                Method::POST => Some(("CreateFunctionUrlConfig", res)),
605                Method::GET => Some(("GetFunctionUrlConfig", res)),
606                Method::PUT => Some(("UpdateFunctionUrlConfig", res)),
607                Method::DELETE => Some(("DeleteFunctionUrlConfig", res)),
608                _ => None,
609            };
610        }
611        if segs.get(1).map(|s| s.as_str()) == Some("function-urls") && req.method == Method::GET {
612            return Some(("ListFunctionUrlConfigs", None));
613        }
614        if segs.get(1).map(|s| s.as_str()) == Some("functions")
615            && segs.get(3).map(|s| s.as_str()) == Some("urls")
616            && req.method == Method::GET
617        {
618            return Some(("ListFunctionUrlConfigs", segs.get(2).map(|s| s.to_string())));
619        }
620        if segs.get(1).map(|s| s.as_str()) == Some("event-source-mappings")
621            && segs.get(3).map(|s| s.as_str()) == Some("scaling-config")
622        {
623            let res = segs.get(2).map(|s| s.to_string());
624            return match req.method {
625                Method::PUT => Some(("PutFunctionScalingConfig", res)),
626                Method::GET => Some(("GetFunctionScalingConfig", res)),
627                _ => None,
628            };
629        }
630
631        // Capacity providers (any prefix).
632        if segs.get(1).map(|s| s.as_str()) == Some("capacity-providers") {
633            let res = segs.get(2).map(|s| s.to_string());
634            return match (
635                req.method.clone(),
636                segs.len(),
637                segs.get(3).map(|s| s.as_str()),
638            ) {
639                (Method::POST, 2, _) => Some(("CreateCapacityProvider", None)),
640                (Method::GET, 2, _) => Some(("ListCapacityProviders", None)),
641                (Method::GET, 3, _) => Some(("GetCapacityProvider", res)),
642                (Method::PUT, 3, _) => Some(("UpdateCapacityProvider", res)),
643                (Method::DELETE, 3, _) => Some(("DeleteCapacityProvider", res)),
644                (Method::GET, 4, Some("function-versions")) => {
645                    Some(("ListFunctionVersionsByCapacityProvider", res))
646                }
647                _ => None,
648            };
649        }
650
651        // ListDurableExecutionsByFunction lives under functions/{name}.
652        if segs.get(1).map(|s| s.as_str()) == Some("functions")
653            && segs.get(3).map(|s| s.as_str()) == Some("durable-executions")
654            && req.method == Method::GET
655        {
656            return Some((
657                "ListDurableExecutionsByFunction",
658                segs.get(2).map(|s| s.to_string()),
659            ));
660        }
661
662        // Durable execution callbacks at /durable-execution-callbacks/{id}/{kind}
663        if segs.get(1).map(|s| s.as_str()) == Some("durable-execution-callbacks")
664            && req.method == Method::POST
665        {
666            let res = segs.get(2).map(|s| s.to_string());
667            return match segs.get(3).map(|s| s.as_str()) {
668                Some("success") | Some("succeed") => {
669                    Some(("SendDurableExecutionCallbackSuccess", res))
670                }
671                Some("failure") | Some("fail") => {
672                    Some(("SendDurableExecutionCallbackFailure", res))
673                }
674                Some("heartbeat") => Some(("SendDurableExecutionCallbackHeartbeat", res)),
675                _ => None,
676            };
677        }
678
679        // Durable executions (any prefix).
680        if segs.get(1).map(|s| s.as_str()) == Some("durable-executions") {
681            let res = segs.get(2).map(|s| s.to_string());
682            return match (
683                req.method.clone(),
684                segs.len(),
685                segs.get(3).map(|s| s.as_str()),
686                segs.get(4).map(|s| s.as_str()),
687            ) {
688                (Method::GET, 3, _, _) => Some(("GetDurableExecution", res)),
689                (Method::GET, 4, Some("history"), _) => Some(("GetDurableExecutionHistory", res)),
690                (Method::GET, 4, Some("state"), _) => Some(("GetDurableExecutionState", res)),
691                (Method::POST, 4, Some("checkpoint"), _) => {
692                    Some(("CheckpointDurableExecution", res))
693                }
694                (Method::POST, 4, Some("stop"), _) => Some(("StopDurableExecution", res)),
695                (Method::POST, 5, Some("callback"), Some("success")) => {
696                    Some(("SendDurableExecutionCallbackSuccess", res))
697                }
698                (Method::POST, 5, Some("callback"), Some("failure")) => {
699                    Some(("SendDurableExecutionCallbackFailure", res))
700                }
701                (Method::POST, 5, Some("callback"), Some("heartbeat")) => {
702                    Some(("SendDurableExecutionCallbackHeartbeat", res))
703                }
704                _ => None,
705            };
706        }
707
708        // NOTE: concurrency, event-invoke-config, recursion-config,
709        // capacity-providers, durable-executions, and code-signing-configs
710        // routes are all handled by the prefix-agnostic blocks above.
711        // The previously-present date-specific blocks were dead code.
712
713        // /2018-10-31/layers
714        if prefix == "2018-10-31" && segs.get(1).map(|s| s.as_str()) == Some("layers") {
715            let layer = segs.get(2).map(|s| s.to_string());
716            let third = segs.get(3).map(|s| s.as_str());
717            let version = segs.get(4).map(|s| s.to_string());
718            return match (&req.method, segs.len(), third, version.is_some()) {
719                (&Method::GET, 2, _, _) => Some(("ListLayers", None)),
720                (&Method::POST, 4, Some("versions"), false) => Some(("PublishLayerVersion", layer)),
721                (&Method::GET, 4, Some("versions"), false) => Some(("ListLayerVersions", layer)),
722                (&Method::GET, 5, Some("versions"), true) => Some(("GetLayerVersion", version)),
723                (&Method::DELETE, 5, Some("versions"), true) => {
724                    Some(("DeleteLayerVersion", version))
725                }
726                (&Method::GET, 6, Some("versions"), true)
727                    if segs.get(5).map(|s| s.as_str()) == Some("policy") =>
728                {
729                    Some(("GetLayerVersionPolicy", version))
730                }
731                (&Method::POST, 6, Some("versions"), true)
732                    if segs.get(5).map(|s| s.as_str()) == Some("policy") =>
733                {
734                    Some(("AddLayerVersionPermission", version))
735                }
736                (&Method::DELETE, 7, Some("versions"), true)
737                    if segs.get(5).map(|s| s.as_str()) == Some("policy") =>
738                {
739                    Some(("RemoveLayerVersionPermission", version))
740                }
741                _ => None,
742            };
743        }
744
745        // /2018-10-31/layers-by-arn
746        if prefix == "2018-10-31"
747            && segs.get(1).map(|s| s.as_str()) == Some("layers-by-arn")
748            && req.method == Method::GET
749        {
750            return Some(("GetLayerVersionByArn", None));
751        }
752
753        // NOTE: 2021-10-31/functions/{name}/url and ListFunctionUrlConfigs
754        // are handled by the prefix-agnostic blocks above.
755
756        if prefix != "2015-03-31" {
757            return None;
758        }
759
760        let collection = segs.get(1).map(|s| s.as_str());
761        let resource = segs.get(2).map(|s| s.to_string());
762        let third = segs.get(3).map(|s| s.as_str());
763        let fourth = segs.get(4).map(|s| s.as_str());
764
765        let action = match (&req.method, segs.len(), collection, third) {
766            (&Method::POST, 2, Some("functions"), _) => "CreateFunction",
767            (&Method::GET, 2, Some("functions"), _) => "ListFunctions",
768            (&Method::GET, 3, Some("functions"), _) => "GetFunction",
769            (&Method::DELETE, 3, Some("functions"), _) => "DeleteFunction",
770            (&Method::POST, 4, Some("functions"), Some("invocations")) => "Invoke",
771            (&Method::POST, 4, Some("functions"), Some("invoke-async")) => "InvokeAsync",
772            (&Method::POST, 4, Some("functions"), Some("response-streaming-invocations")) => {
773                "InvokeWithResponseStream"
774            }
775            (&Method::POST, 4, Some("functions"), Some("versions")) => "PublishVersion",
776            (&Method::GET, 4, Some("functions"), Some("versions")) => "ListVersionsByFunction",
777            (&Method::POST, 4, Some("functions"), Some("policy")) => "AddPermission",
778            (&Method::GET, 4, Some("functions"), Some("policy")) => "GetPolicy",
779            (&Method::DELETE, 5, Some("functions"), Some("policy")) => "RemovePermission",
780            (&Method::POST, 4, Some("functions"), Some("aliases")) => "CreateAlias",
781            (&Method::GET, 4, Some("functions"), Some("aliases")) => "ListAliases",
782            (&Method::GET, 5, Some("functions"), Some("aliases")) => "GetAlias",
783            (&Method::PUT, 5, Some("functions"), Some("aliases")) => "UpdateAlias",
784            (&Method::DELETE, 5, Some("functions"), Some("aliases")) => "DeleteAlias",
785            (&Method::GET, 4, Some("functions"), Some("configuration")) => {
786                "GetFunctionConfiguration"
787            }
788            (&Method::PUT, 4, Some("functions"), Some("configuration")) => {
789                "UpdateFunctionConfiguration"
790            }
791            (&Method::PUT, 4, Some("functions"), Some("code")) => "UpdateFunctionCode",
792            (&Method::PUT, 4, Some("functions"), Some("concurrency")) => "PutFunctionConcurrency",
793            (&Method::GET, 4, Some("functions"), Some("concurrency")) => "GetFunctionConcurrency",
794            (&Method::DELETE, 4, Some("functions"), Some("concurrency")) => {
795                "DeleteFunctionConcurrency"
796            }
797            (&Method::PUT, 4, Some("functions"), Some("provisioned-concurrency")) => {
798                "PutProvisionedConcurrencyConfig"
799            }
800            (&Method::GET, 4, Some("functions"), Some("provisioned-concurrency")) => {
801                "GetProvisionedConcurrencyConfig"
802            }
803            (&Method::DELETE, 4, Some("functions"), Some("provisioned-concurrency")) => {
804                "DeleteProvisionedConcurrencyConfig"
805            }
806            (&Method::GET, 4, Some("functions"), Some("provisioned-concurrency-configs")) => {
807                "ListProvisionedConcurrencyConfigs"
808            }
809            (&Method::PUT, 4, Some("functions"), Some("event-invoke-config")) => {
810                "UpdateFunctionEventInvokeConfig"
811            }
812            (&Method::POST, 4, Some("functions"), Some("event-invoke-config")) => {
813                "PutFunctionEventInvokeConfig"
814            }
815            (&Method::GET, 4, Some("functions"), Some("event-invoke-config")) => {
816                "GetFunctionEventInvokeConfig"
817            }
818            (&Method::DELETE, 4, Some("functions"), Some("event-invoke-config")) => {
819                "DeleteFunctionEventInvokeConfig"
820            }
821            (&Method::GET, 4, Some("functions"), Some("event-invoke-config-list")) => {
822                "ListFunctionEventInvokeConfigs"
823            }
824            (&Method::PUT, 4, Some("functions"), Some("code-signing-config")) => {
825                "PutFunctionCodeSigningConfig"
826            }
827            (&Method::GET, 4, Some("functions"), Some("code-signing-config")) => {
828                "GetFunctionCodeSigningConfig"
829            }
830            (&Method::DELETE, 4, Some("functions"), Some("code-signing-config")) => {
831                "DeleteFunctionCodeSigningConfig"
832            }
833            (&Method::PUT, 4, Some("functions"), Some("runtime-management-config")) => {
834                "PutRuntimeManagementConfig"
835            }
836            (&Method::GET, 4, Some("functions"), Some("runtime-management-config")) => {
837                "GetRuntimeManagementConfig"
838            }
839            (&Method::PUT, 4, Some("functions"), Some("scaling-config")) => {
840                "PutFunctionScalingConfig"
841            }
842            (&Method::GET, 4, Some("functions"), Some("scaling-config")) => {
843                "GetFunctionScalingConfig"
844            }
845            (&Method::PUT, 4, Some("functions"), Some("recursion-config")) => {
846                "PutFunctionRecursionConfig"
847            }
848            (&Method::GET, 4, Some("functions"), Some("recursion-config")) => {
849                "GetFunctionRecursionConfig"
850            }
851            (&Method::GET, 4, Some("functions"), Some("durable-executions")) => {
852                "ListDurableExecutionsByFunction"
853            }
854            (&Method::POST, 2, Some("event-source-mappings"), _) => "CreateEventSourceMapping",
855            (&Method::GET, 2, Some("event-source-mappings"), _) => "ListEventSourceMappings",
856            (&Method::GET, 3, Some("event-source-mappings"), _) => "GetEventSourceMapping",
857            (&Method::PUT, 3, Some("event-source-mappings"), _) => "UpdateEventSourceMapping",
858            (&Method::DELETE, 3, Some("event-source-mappings"), _) => "DeleteEventSourceMapping",
859            (&Method::POST, 3, Some("tags"), _) => "TagResource",
860            (&Method::DELETE, 3, Some("tags"), _) => "UntagResource",
861            (&Method::GET, 3, Some("tags"), _) => "ListTags",
862            _ => return None,
863        };
864        let _ = fourth;
865
866        Some((action, resource))
867    }
868
869    fn create_function(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
870        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
871        let input = CreateFunctionInput::from_body(&body)?;
872
873        // PassRole trust-policy check: the supplied execution role must
874        // have a trust policy that allows lambda.amazonaws.com to call
875        // sts:AssumeRole. Real AWS rejects with InvalidParameterValueException
876        // when the trust policy doesn't include the service principal.
877        if let Some(ref validator) = self.role_trust_validator {
878            if let Err(err) =
879                validator.validate(&req.account_id, &input.role, "lambda.amazonaws.com")
880            {
881                return Err(AwsServiceError::aws_error(
882                    StatusCode::BAD_REQUEST,
883                    "InvalidParameterValueException",
884                    err.to_string(),
885                ));
886            }
887        }
888
889        let mut accounts = self.state.write();
890        // Pre-resolve layer attachments before re-borrowing accounts mutably.
891        // Layer ARNs may live in sibling accounts.
892        let layer_attachments =
893            crate::extras::resolve_layer_attachments(&accounts, input.layer_arns.clone());
894        let state = accounts.get_or_create(&req.account_id);
895
896        if state.functions.contains_key(&input.function_name) {
897            return Err(AwsServiceError::aws_error(
898                StatusCode::CONFLICT,
899                "ResourceConflictException",
900                format!("Function already exist: {}", input.function_name),
901            ));
902        }
903
904        // Hash the actual ZIP bytes when available, falling back to the
905        // raw Code JSON so image-based functions still get a stable id.
906        let code_bytes = input.code_zip.as_deref().unwrap_or(&input.code_fallback);
907        let mut hasher = Sha256::new();
908        hasher.update(code_bytes);
909        let hash = hasher.finalize();
910        let code_sha256 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash);
911        let code_size = code_bytes.len() as i64;
912
913        let function_arn = format!(
914            "arn:aws:lambda:{}:{}:function:{}",
915            state.region, state.account_id, input.function_name
916        );
917        let now = Utc::now();
918
919        let func = LambdaFunction {
920            function_name: input.function_name.clone(),
921            function_arn,
922            runtime: input.runtime,
923            role: input.role,
924            handler: input.handler,
925            description: input.description,
926            timeout: input.timeout,
927            memory_size: input.memory_size,
928            code_sha256,
929            code_size,
930            version: "$LATEST".to_string(),
931            last_modified: now,
932            tags: input.tags,
933            environment: input.environment,
934            architectures: input.architectures,
935            package_type: input.package_type,
936            code_zip: input.code_zip,
937            image_uri: input.image_uri,
938            policy: None,
939            layers: layer_attachments,
940        };
941
942        let response = self.function_config_json(&func);
943
944        state.functions.insert(input.function_name, func);
945
946        Ok(AwsResponse::json(StatusCode::CREATED, response.to_string()))
947    }
948
949    fn get_function(
950        &self,
951        function_name: &str,
952        account_id: &str,
953        region: &str,
954    ) -> Result<AwsResponse, AwsServiceError> {
955        let accounts = self.state.read();
956        let empty = LambdaState::new(account_id, region);
957        let state = accounts.get(account_id).unwrap_or(&empty);
958        let func = state.functions.get(function_name).ok_or_else(|| {
959            AwsServiceError::aws_error(
960                StatusCode::NOT_FOUND,
961                "ResourceNotFoundException",
962                format!(
963                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
964                    state.region, state.account_id, function_name
965                ),
966            )
967        })?;
968
969        let config = self.function_config_json(func);
970        let code = if let Some(ref uri) = func.image_uri {
971            json!({
972                "ImageUri": uri,
973                "ResolvedImageUri": uri,
974                "RepositoryType": "ECR",
975            })
976        } else {
977            json!({
978                "Location": format!(
979                    "https://awslambda-{}-tasks.s3.{}.amazonaws.com/stub",
980                    func.function_arn.split(':').nth(3).unwrap_or("us-east-1"),
981                    func.function_arn.split(':').nth(3).unwrap_or("us-east-1")
982                ),
983                "RepositoryType": "S3",
984            })
985        };
986        let response = json!({
987            "Code": code,
988            "Configuration": config,
989            "Tags": func.tags,
990        });
991
992        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
993    }
994
995    fn delete_function(
996        &self,
997        function_name: &str,
998        account_id: &str,
999    ) -> Result<AwsResponse, AwsServiceError> {
1000        let mut accounts = self.state.write();
1001        let state = accounts.get_or_create(account_id);
1002        let region = state.region.clone();
1003        let account_id = state.account_id.clone();
1004        if state.functions.remove(function_name).is_none() {
1005            return Err(AwsServiceError::aws_error(
1006                StatusCode::NOT_FOUND,
1007                "ResourceNotFoundException",
1008                format!(
1009                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
1010                    region, account_id, function_name
1011                ),
1012            ));
1013        }
1014
1015        // Clean up any running container for this function
1016        if let Some(ref runtime) = self.runtime {
1017            let rt = runtime.clone();
1018            let name = function_name.to_string();
1019            tokio::spawn(async move { rt.stop_container(&name).await });
1020        }
1021
1022        Ok(AwsResponse::json(StatusCode::NO_CONTENT, ""))
1023    }
1024
1025    fn list_functions(&self, account_id: &str) -> Result<AwsResponse, AwsServiceError> {
1026        let accounts = self.state.read();
1027        let empty = LambdaState::new(account_id, "");
1028        let state = accounts.get(account_id).unwrap_or(&empty);
1029        let functions: Vec<Value> = state
1030            .functions
1031            .values()
1032            .map(|f| self.function_config_json(f))
1033            .collect();
1034
1035        let response = json!({
1036            "Functions": functions,
1037        });
1038
1039        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1040    }
1041
1042    async fn invoke(
1043        &self,
1044        function_name: &str,
1045        payload: &[u8],
1046        account_id: &str,
1047        invocation_type: InvocationType,
1048    ) -> Result<AwsResponse, AwsServiceError> {
1049        let (func, layer_zips) = {
1050            let accounts = self.state.read();
1051            let empty = LambdaState::new(account_id, "");
1052            let state = accounts.get(account_id).unwrap_or(&empty);
1053            let func = state.functions.get(function_name).cloned().ok_or_else(|| {
1054                AwsServiceError::aws_error(
1055                    StatusCode::NOT_FOUND,
1056                    "ResourceNotFoundException",
1057                    format!(
1058                        "Function not found: arn:aws:lambda:{}:{}:function:{}",
1059                        state.region, state.account_id, function_name
1060                    ),
1061                )
1062            })?;
1063            // Resolve attached layer ARNs to ZIP bytes under the same read
1064            // lock. Layers may live in sibling accounts (cross-account
1065            // attach is legal in AWS); fall back to no bytes for unknown
1066            // ARNs and warn — invoke proceeds without that layer.
1067            let mut layer_zips: Vec<Vec<u8>> = Vec::with_capacity(func.layers.len());
1068            for attached in &func.layers {
1069                let bytes = crate::extras::parse_layer_version_arn(&attached.arn).and_then(
1070                    |(acct, name, ver)| {
1071                        accounts
1072                            .get(&acct)
1073                            .and_then(|s| s.layers.get(&name))
1074                            .and_then(|l| l.versions.iter().find(|v| v.version == ver))
1075                            .and_then(|v| v.code_zip.clone())
1076                    },
1077                );
1078                match bytes {
1079                    Some(b) => layer_zips.push(b),
1080                    None => tracing::warn!(
1081                        function = %function_name,
1082                        layer_arn = %attached.arn,
1083                        "attached layer not resolvable; skipping /opt mount for this layer"
1084                    ),
1085                }
1086            }
1087            (func, layer_zips)
1088        };
1089
1090        if func.code_zip.is_none() {
1091            return Err(AwsServiceError::aws_error(
1092                StatusCode::BAD_REQUEST,
1093                "InvalidParameterValueException",
1094                "Function has no deployment package",
1095            ));
1096        }
1097
1098        if matches!(invocation_type, InvocationType::DryRun) {
1099            let mut resp = AwsResponse::json(StatusCode::NO_CONTENT, "");
1100            resp.headers.insert(
1101                http::header::HeaderName::from_static("x-amz-executed-version"),
1102                http::header::HeaderValue::from_static("$LATEST"),
1103            );
1104            return Ok(resp);
1105        }
1106
1107        let runtime = self.runtime.as_ref().ok_or_else(|| {
1108            AwsServiceError::aws_error(
1109                StatusCode::INTERNAL_SERVER_ERROR,
1110                "ServiceException",
1111                "Docker/Podman is required for Lambda execution but is not available",
1112            )
1113        })?;
1114
1115        match invocation_type {
1116            InvocationType::Event => {
1117                // Fire-and-forget. AWS returns 202 with no body.
1118                let runtime = runtime.clone();
1119                let func_clone = func.clone();
1120                let payload_vec = payload.to_vec();
1121                let bus = self.delivery_bus.clone();
1122                let destination_config = self.lookup_destination_config(&func, account_id);
1123                let function_arn = func.function_arn.clone();
1124                let layer_zips_async = layer_zips.clone();
1125                tokio::spawn(async move {
1126                    let result = match runtime
1127                        .invoke(&func_clone, &payload_vec, &layer_zips_async)
1128                        .await
1129                    {
1130                        Ok(bytes) => {
1131                            // Lambda runtime returns 200 even on uncaught
1132                            // function errors; the body has errorMessage /
1133                            // errorType. Treat that as failure for routing.
1134                            let parsed: Option<serde_json::Value> =
1135                                serde_json::from_slice(&bytes).ok();
1136                            let is_error = parsed
1137                                .as_ref()
1138                                .and_then(|v| v.as_object())
1139                                .map(|m| {
1140                                    m.contains_key("errorMessage") || m.contains_key("errorType")
1141                                })
1142                                .unwrap_or(false);
1143                            if is_error {
1144                                let msg = parsed
1145                                    .as_ref()
1146                                    .and_then(|v| v.get("errorMessage"))
1147                                    .and_then(|v| v.as_str())
1148                                    .unwrap_or("function error")
1149                                    .to_string();
1150                                Err(msg)
1151                            } else {
1152                                Ok(bytes)
1153                            }
1154                        }
1155                        Err(e) => Err(e.to_string()),
1156                    };
1157                    if let Some(bus) = bus {
1158                        route_to_destination(
1159                            bus,
1160                            &function_arn,
1161                            &payload_vec,
1162                            &result,
1163                            destination_config.as_ref(),
1164                        );
1165                    }
1166                });
1167                let mut resp = AwsResponse::json(StatusCode::ACCEPTED, "");
1168                resp.headers.insert(
1169                    http::header::HeaderName::from_static("x-amz-executed-version"),
1170                    http::header::HeaderValue::from_static("$LATEST"),
1171                );
1172                Ok(resp)
1173            }
1174            InvocationType::RequestResponse | InvocationType::DryRun => {
1175                match runtime.invoke(&func, payload, &layer_zips).await {
1176                    Ok(response_bytes) => {
1177                        let mut resp = AwsResponse::json(StatusCode::OK, response_bytes);
1178                        resp.headers.insert(
1179                            http::header::HeaderName::from_static("x-amz-executed-version"),
1180                            http::header::HeaderValue::from_static("$LATEST"),
1181                        );
1182                        Ok(resp)
1183                    }
1184                    Err(e) => {
1185                        tracing::error!(function = %function_name, error = %e, "Lambda invocation failed");
1186                        Err(AwsServiceError::aws_error(
1187                            StatusCode::INTERNAL_SERVER_ERROR,
1188                            "ServiceException",
1189                            format!("Lambda execution failed: {e}"),
1190                        ))
1191                    }
1192                }
1193            }
1194        }
1195    }
1196
1197    /// Pull EventInvokeConfig.DestinationConfig for the function. The
1198    /// stored key is `<function_name>:<qualifier>`; treat unqualified
1199    /// invokes as the empty qualifier (matches `parse_qualifier` in
1200    /// `extras.rs` when no `Qualifier` is supplied).
1201    fn lookup_destination_config(
1202        &self,
1203        func: &crate::state::LambdaFunction,
1204        account_id: &str,
1205    ) -> Option<serde_json::Value> {
1206        let accounts = self.state.read();
1207        let state = accounts.get(account_id)?;
1208        let key = format!("{}:$LATEST", func.function_name);
1209        state
1210            .event_invoke_configs
1211            .get(&key)
1212            .map(|cfg| cfg.destination_config.clone())
1213            .filter(|v| !v.is_null() && !v.as_object().map(|o| o.is_empty()).unwrap_or(false))
1214    }
1215
1216    fn publish_version(
1217        &self,
1218        function_name: &str,
1219        account_id: &str,
1220    ) -> Result<AwsResponse, AwsServiceError> {
1221        let accounts = self.state.read();
1222        let empty = LambdaState::new(account_id, "");
1223        let state = accounts.get(account_id).unwrap_or(&empty);
1224        let func = state.functions.get(function_name).ok_or_else(|| {
1225            AwsServiceError::aws_error(
1226                StatusCode::NOT_FOUND,
1227                "ResourceNotFoundException",
1228                format!(
1229                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
1230                    state.region, state.account_id, function_name
1231                ),
1232            )
1233        })?;
1234
1235        let mut config = self.function_config_json(func);
1236        // Stub: always return version "1"
1237        config["Version"] = json!("1");
1238        config["FunctionArn"] = json!(format!("{}:1", func.function_arn));
1239
1240        Ok(AwsResponse::json(StatusCode::CREATED, config.to_string()))
1241    }
1242
1243    fn create_event_source_mapping(
1244        &self,
1245        req: &AwsRequest,
1246    ) -> Result<AwsResponse, AwsServiceError> {
1247        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1248        let event_source_arn = body["EventSourceArn"]
1249            .as_str()
1250            .ok_or_else(|| {
1251                AwsServiceError::aws_error(
1252                    StatusCode::BAD_REQUEST,
1253                    "InvalidParameterValueException",
1254                    "EventSourceArn is required",
1255                )
1256            })?
1257            .to_string();
1258
1259        let function_name = body["FunctionName"]
1260            .as_str()
1261            .ok_or_else(|| {
1262                AwsServiceError::aws_error(
1263                    StatusCode::BAD_REQUEST,
1264                    "InvalidParameterValueException",
1265                    "FunctionName is required",
1266                )
1267            })?
1268            .to_string();
1269
1270        let mut accounts = self.state.write();
1271        let state = accounts.get_or_create(&req.account_id);
1272
1273        // Resolve function name to ARN
1274        let function_arn = if function_name.starts_with("arn:") {
1275            function_name.clone()
1276        } else {
1277            let func = state.functions.get(&function_name).ok_or_else(|| {
1278                AwsServiceError::aws_error(
1279                    StatusCode::NOT_FOUND,
1280                    "ResourceNotFoundException",
1281                    format!(
1282                        "Function not found: arn:aws:lambda:{}:{}:function:{}",
1283                        state.region, state.account_id, function_name
1284                    ),
1285                )
1286            })?;
1287            func.function_arn.clone()
1288        };
1289
1290        let batch_size = body["BatchSize"].as_i64().unwrap_or(10);
1291        let enabled = body["Enabled"].as_bool().unwrap_or(true);
1292        let mapping_uuid = uuid::Uuid::new_v4().to_string();
1293        let now = Utc::now();
1294
1295        // Extract Filters[].Pattern strictly: any entry where
1296        // `Pattern` is missing or not a string is a hard error,
1297        // matching AWS. Doing this before `validate` keeps malformed
1298        // values from being silently dropped by the lossy serializer.
1299        // FilterCriteria itself must be an object (or absent) — non-
1300        // object values would otherwise be silently dropped by
1301        // `Value::get`, masking client bugs.
1302        let filter_patterns: Vec<String> = match body.get("FilterCriteria") {
1303            None | Some(Value::Null) => Vec::new(),
1304            Some(Value::Object(_)) => {
1305                match body.get("FilterCriteria").and_then(|v| v.get("Filters")) {
1306                    None => Vec::new(),
1307                    Some(Value::Array(arr)) => {
1308                        let mut out = Vec::with_capacity(arr.len());
1309                        for f in arr {
1310                            match f.get("Pattern") {
1311                                Some(Value::String(s)) => out.push(s.clone()),
1312                                _ => {
1313                                    return Err(AwsServiceError::aws_error(
1314                                        StatusCode::BAD_REQUEST,
1315                                        "InvalidParameterValueException",
1316                                        "FilterCriteria.Filters[].Pattern must be a string",
1317                                    ));
1318                                }
1319                            }
1320                        }
1321                        out
1322                    }
1323                    Some(_) => {
1324                        return Err(AwsServiceError::aws_error(
1325                            StatusCode::BAD_REQUEST,
1326                            "InvalidParameterValueException",
1327                            "FilterCriteria.Filters must be an array",
1328                        ));
1329                    }
1330                }
1331            }
1332            Some(_) => {
1333                return Err(AwsServiceError::aws_error(
1334                    StatusCode::BAD_REQUEST,
1335                    "InvalidParameterValueException",
1336                    "FilterCriteria must be an object",
1337                ));
1338            }
1339        };
1340        // AWS rejects malformed FilterCriteria at create time.
1341        if let Err(err) = crate::filter::FilterSet::validate(filter_patterns.iter()) {
1342            return Err(AwsServiceError::aws_error(
1343                StatusCode::BAD_REQUEST,
1344                "InvalidParameterValueException",
1345                err,
1346            ));
1347        }
1348        let function_response_types: Vec<String> = body
1349            .get("FunctionResponseTypes")
1350            .and_then(|v| v.as_array())
1351            .map(|arr| {
1352                arr.iter()
1353                    .filter_map(|v| v.as_str().map(String::from))
1354                    .collect()
1355            })
1356            .unwrap_or_default();
1357        let starting_position = body
1358            .get("StartingPosition")
1359            .and_then(|v| v.as_str())
1360            .map(String::from);
1361        let starting_position_timestamp = body
1362            .get("StartingPositionTimestamp")
1363            .and_then(|v| v.as_f64());
1364        let parallelization_factor = body.get("ParallelizationFactor").and_then(|v| v.as_i64());
1365        let maximum_batching_window_in_seconds = body
1366            .get("MaximumBatchingWindowInSeconds")
1367            .and_then(|v| v.as_i64());
1368
1369        let mapping = EventSourceMapping {
1370            uuid: mapping_uuid.clone(),
1371            function_arn: function_arn.clone(),
1372            event_source_arn: event_source_arn.clone(),
1373            batch_size,
1374            enabled,
1375            state: if enabled {
1376                "Enabled".to_string()
1377            } else {
1378                "Disabled".to_string()
1379            },
1380            last_modified: now,
1381            filter_patterns,
1382            maximum_batching_window_in_seconds,
1383            starting_position,
1384            starting_position_timestamp,
1385            parallelization_factor,
1386            function_response_types,
1387        };
1388
1389        let response = self.event_source_mapping_json(&mapping);
1390        state.event_source_mappings.insert(mapping_uuid, mapping);
1391
1392        Ok(AwsResponse::json(
1393            StatusCode::ACCEPTED,
1394            response.to_string(),
1395        ))
1396    }
1397
1398    fn list_event_source_mappings(&self, account_id: &str) -> Result<AwsResponse, AwsServiceError> {
1399        let accounts = self.state.read();
1400        let empty = LambdaState::new(account_id, "");
1401        let state = accounts.get(account_id).unwrap_or(&empty);
1402        let mappings: Vec<Value> = state
1403            .event_source_mappings
1404            .values()
1405            .map(|m| self.event_source_mapping_json(m))
1406            .collect();
1407
1408        let response = json!({
1409            "EventSourceMappings": mappings,
1410        });
1411
1412        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1413    }
1414
1415    fn get_event_source_mapping(
1416        &self,
1417        uuid: &str,
1418        account_id: &str,
1419    ) -> Result<AwsResponse, AwsServiceError> {
1420        let accounts = self.state.read();
1421        let empty = LambdaState::new(account_id, "");
1422        let state = accounts.get(account_id).unwrap_or(&empty);
1423        let mapping = state.event_source_mappings.get(uuid).ok_or_else(|| {
1424            AwsServiceError::aws_error(
1425                StatusCode::NOT_FOUND,
1426                "ResourceNotFoundException",
1427                format!("The resource you requested does not exist. (Service: Lambda, Status Code: 404, Request ID: {uuid})"),
1428            )
1429        })?;
1430
1431        let response = self.event_source_mapping_json(mapping);
1432        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1433    }
1434
1435    fn delete_event_source_mapping(
1436        &self,
1437        uuid: &str,
1438        account_id: &str,
1439    ) -> Result<AwsResponse, AwsServiceError> {
1440        let mut accounts = self.state.write();
1441        let state = accounts.get_or_create(account_id);
1442        let mapping = state.event_source_mappings.remove(uuid).ok_or_else(|| {
1443            AwsServiceError::aws_error(
1444                StatusCode::NOT_FOUND,
1445                "ResourceNotFoundException",
1446                format!("The resource you requested does not exist. (Service: Lambda, Status Code: 404, Request ID: {uuid})"),
1447            )
1448        })?;
1449
1450        let mut response = self.event_source_mapping_json(&mapping);
1451        response["State"] = json!("Deleting");
1452        Ok(AwsResponse::json(
1453            StatusCode::ACCEPTED,
1454            response.to_string(),
1455        ))
1456    }
1457
1458    pub(crate) fn function_config_json(&self, func: &LambdaFunction) -> Value {
1459        let mut env_vars = json!({});
1460        if !func.environment.is_empty() {
1461            env_vars = json!({ "Variables": func.environment });
1462        }
1463
1464        let mut config = json!({
1465            "FunctionName": func.function_name,
1466            "FunctionArn": func.function_arn,
1467            "Runtime": func.runtime,
1468            "Role": func.role,
1469            "Handler": func.handler,
1470            "Description": func.description,
1471            "Timeout": func.timeout,
1472            "MemorySize": func.memory_size,
1473            "CodeSha256": func.code_sha256,
1474            "CodeSize": func.code_size,
1475            "Version": func.version,
1476            "LastModified": func.last_modified.format("%Y-%m-%dT%H:%M:%S%.3f+0000").to_string(),
1477            "PackageType": func.package_type,
1478            "Architectures": func.architectures,
1479            "Environment": env_vars,
1480            "State": "Active",
1481            "LastUpdateStatus": "Successful",
1482            "TracingConfig": { "Mode": "PassThrough" },
1483            "RevisionId": uuid::Uuid::new_v4().to_string(),
1484        });
1485        if let Some(ref uri) = func.image_uri {
1486            config["Code"] = json!({
1487                "ImageUri": uri,
1488                "ResolvedImageUri": uri,
1489            });
1490        }
1491        if !func.layers.is_empty() {
1492            config["Layers"] = json!(func
1493                .layers
1494                .iter()
1495                .map(|l| json!({"Arn": l.arn, "CodeSize": l.code_size}))
1496                .collect::<Vec<_>>());
1497        }
1498        config
1499    }
1500
1501    fn event_source_mapping_json(&self, mapping: &EventSourceMapping) -> Value {
1502        let mut out = json!({
1503            "UUID": mapping.uuid,
1504            "FunctionArn": mapping.function_arn,
1505            "EventSourceArn": mapping.event_source_arn,
1506            "BatchSize": mapping.batch_size,
1507            "State": mapping.state,
1508            "LastModified": mapping.last_modified.timestamp_millis() as f64 / 1000.0,
1509        });
1510        let obj = out.as_object_mut().expect("json! built object");
1511        if !mapping.filter_patterns.is_empty() {
1512            obj.insert(
1513                "FilterCriteria".into(),
1514                json!({
1515                    "Filters": mapping.filter_patterns.iter().map(|p| json!({"Pattern": p})).collect::<Vec<_>>(),
1516                }),
1517            );
1518        }
1519        if !mapping.function_response_types.is_empty() {
1520            obj.insert(
1521                "FunctionResponseTypes".into(),
1522                json!(mapping.function_response_types),
1523            );
1524        }
1525        if let Some(sp) = &mapping.starting_position {
1526            obj.insert("StartingPosition".into(), json!(sp));
1527        }
1528        if let Some(ts) = mapping.starting_position_timestamp {
1529            obj.insert("StartingPositionTimestamp".into(), json!(ts));
1530        }
1531        if let Some(pf) = mapping.parallelization_factor {
1532            obj.insert("ParallelizationFactor".into(), json!(pf));
1533        }
1534        if let Some(w) = mapping.maximum_batching_window_in_seconds {
1535            obj.insert("MaximumBatchingWindowInSeconds".into(), json!(w));
1536        }
1537        out
1538    }
1539
1540    /// Grant a permission on a Lambda function by appending a
1541    /// statement to its resource-based policy.
1542    ///
1543    /// Mirrors AWS: the caller passes `(StatementId, Action,
1544    /// Principal, SourceArn?, SourceAccount?)` and the service
1545    /// composes a canonical policy document so that the existing
1546    /// evaluator can read it without a Lambda-specific fork. Per the
1547    /// S3 rollout's #427 evaluator, `SourceArn` becomes an `ArnLike`
1548    /// Condition and `SourceAccount` becomes a `StringEquals`
1549    /// Condition — both are already supported by the Phase 2 operator
1550    /// set, so the permission gate behaves end-to-end without any new
1551    /// evaluator code.
1552    fn add_permission(
1553        &self,
1554        function_name: &str,
1555        req: &AwsRequest,
1556    ) -> Result<AwsResponse, AwsServiceError> {
1557        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1558        let statement_id = body
1559            .get("StatementId")
1560            .and_then(|v| v.as_str())
1561            .ok_or_else(|| {
1562                AwsServiceError::aws_error(
1563                    StatusCode::BAD_REQUEST,
1564                    "InvalidParameterValueException",
1565                    "StatementId is required",
1566                )
1567            })?
1568            .to_string();
1569        let action = body
1570            .get("Action")
1571            .and_then(|v| v.as_str())
1572            .ok_or_else(|| {
1573                AwsServiceError::aws_error(
1574                    StatusCode::BAD_REQUEST,
1575                    "InvalidParameterValueException",
1576                    "Action is required",
1577                )
1578            })?
1579            .to_string();
1580        let principal_raw = body
1581            .get("Principal")
1582            .and_then(|v| v.as_str())
1583            .ok_or_else(|| {
1584                AwsServiceError::aws_error(
1585                    StatusCode::BAD_REQUEST,
1586                    "InvalidParameterValueException",
1587                    "Principal is required",
1588                )
1589            })?
1590            .to_string();
1591        let source_arn = body
1592            .get("SourceArn")
1593            .and_then(|v| v.as_str())
1594            .map(str::to_string);
1595        let source_account = body
1596            .get("SourceAccount")
1597            .and_then(|v| v.as_str())
1598            .map(str::to_string);
1599
1600        let mut accounts = self.state.write();
1601        let state = accounts.get_or_create(&req.account_id);
1602        let func = state.functions.get_mut(function_name).ok_or_else(|| {
1603            AwsServiceError::aws_error(
1604                StatusCode::NOT_FOUND,
1605                "ResourceNotFoundException",
1606                format!("Function not found: {function_name}"),
1607            )
1608        })?;
1609
1610        // Load current policy or seed a fresh canonical doc. Any
1611        // stored blob that doesn't parse as a JSON object is treated
1612        // as corrupt and replaced — `AddPermission` is the only
1613        // mutation path for this field and it always writes valid
1614        // JSON, so seeing a non-object here means something else
1615        // wrote garbage, and silently propagating it would make
1616        // later reads harder to debug.
1617        let mut doc: Value = func
1618            .policy
1619            .as_deref()
1620            .and_then(|s| serde_json::from_str::<Value>(s).ok())
1621            .filter(|v| v.is_object())
1622            .unwrap_or_else(|| json!({"Version": "2012-10-17", "Statement": []}));
1623
1624        // Ensure Statement is an array so we can push into it.
1625        if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
1626            doc["Statement"] = json!([]);
1627        }
1628        let statements = doc["Statement"].as_array_mut().unwrap();
1629
1630        // Reject duplicate StatementId — matches AWS's
1631        // ResourceConflictException.
1632        if statements
1633            .iter()
1634            .any(|s| s.get("Sid").and_then(|v| v.as_str()) == Some(statement_id.as_str()))
1635        {
1636            return Err(AwsServiceError::aws_error(
1637                StatusCode::CONFLICT,
1638                "ResourceConflictException",
1639                format!("The statement id ({statement_id}) provided already exists"),
1640            ));
1641        }
1642
1643        // Canonicalize Principal: a service host string becomes
1644        // `{"Service": "<host>"}`, an account-id or ARN becomes
1645        // `{"AWS": "<raw>"}`. AWS accepts both shapes on the wire;
1646        // storing the object form uniformly means the existing
1647        // evaluator path handles everything without reading back the
1648        // raw input.
1649        let principal_value =
1650            if principal_raw.ends_with(".amazonaws.com") || principal_raw.contains(".amazon") {
1651                json!({ "Service": principal_raw })
1652            } else {
1653                json!({ "AWS": principal_raw })
1654            };
1655
1656        // Emit SourceArn / SourceAccount as Condition keys so the
1657        // existing Phase 2 ArnLike / StringEquals operators gate the
1658        // grant without new evaluator code.
1659        let mut condition = serde_json::Map::new();
1660        if let Some(arn) = source_arn.as_ref() {
1661            condition.insert("ArnLike".to_string(), json!({ "aws:SourceArn": arn }));
1662        }
1663        if let Some(acct) = source_account.as_ref() {
1664            condition.insert(
1665                "StringEquals".to_string(),
1666                json!({ "aws:SourceAccount": acct }),
1667            );
1668        }
1669
1670        let mut new_statement = serde_json::Map::new();
1671        new_statement.insert("Sid".to_string(), json!(statement_id));
1672        new_statement.insert("Effect".to_string(), json!("Allow"));
1673        new_statement.insert("Principal".to_string(), principal_value);
1674        new_statement.insert("Action".to_string(), json!(format!("lambda:{action}")));
1675        new_statement.insert("Resource".to_string(), json!(func.function_arn));
1676        if !condition.is_empty() {
1677            new_statement.insert("Condition".to_string(), Value::Object(condition));
1678        }
1679        let statement_json = Value::Object(new_statement);
1680        statements.push(statement_json.clone());
1681
1682        func.policy = Some(serde_json::to_string(&doc).unwrap());
1683
1684        Ok(AwsResponse::json(
1685            StatusCode::CREATED,
1686            json!({ "Statement": serde_json::to_string(&statement_json).unwrap() }).to_string(),
1687        ))
1688    }
1689
1690    fn remove_permission(
1691        &self,
1692        function_name: &str,
1693        statement_id: &str,
1694        account_id: &str,
1695    ) -> Result<AwsResponse, AwsServiceError> {
1696        let mut accounts = self.state.write();
1697        let state = accounts.get_or_create(account_id);
1698        let func = state.functions.get_mut(function_name).ok_or_else(|| {
1699            AwsServiceError::aws_error(
1700                StatusCode::NOT_FOUND,
1701                "ResourceNotFoundException",
1702                format!("Function not found: {function_name}"),
1703            )
1704        })?;
1705        let policy_str = func.policy.as_deref().ok_or_else(|| {
1706            AwsServiceError::aws_error(
1707                StatusCode::NOT_FOUND,
1708                "ResourceNotFoundException",
1709                format!("No policy is associated with function {function_name}"),
1710            )
1711        })?;
1712        let mut doc: Value = serde_json::from_str(policy_str).map_err(|_| {
1713            AwsServiceError::aws_error(
1714                StatusCode::INTERNAL_SERVER_ERROR,
1715                "InternalError",
1716                "stored resource policy is not valid JSON",
1717            )
1718        })?;
1719        let statements = doc
1720            .get_mut("Statement")
1721            .and_then(|s| s.as_array_mut())
1722            .ok_or_else(|| {
1723                AwsServiceError::aws_error(
1724                    StatusCode::INTERNAL_SERVER_ERROR,
1725                    "InternalError",
1726                    "stored resource policy has no Statement array",
1727                )
1728            })?;
1729        let before = statements.len();
1730        statements.retain(|s| s.get("Sid").and_then(|v| v.as_str()) != Some(statement_id));
1731        if statements.len() == before {
1732            return Err(AwsServiceError::aws_error(
1733                StatusCode::NOT_FOUND,
1734                "ResourceNotFoundException",
1735                format!("Statement {statement_id} is not found in resource policy"),
1736            ));
1737        }
1738        // Leave an empty {"Statement":[]} behind rather than clearing
1739        // the field to None — AWS's GetPolicy keeps returning the
1740        // (empty) doc until the function itself is deleted.
1741        func.policy = Some(serde_json::to_string(&doc).unwrap());
1742        Ok(AwsResponse::json(StatusCode::NO_CONTENT, String::new()))
1743    }
1744
1745    fn get_policy(
1746        &self,
1747        function_name: &str,
1748        account_id: &str,
1749    ) -> Result<AwsResponse, AwsServiceError> {
1750        let accounts = self.state.read();
1751        let empty = LambdaState::new(account_id, "");
1752        let state = accounts.get(account_id).unwrap_or(&empty);
1753        let func = state.functions.get(function_name).ok_or_else(|| {
1754            AwsServiceError::aws_error(
1755                StatusCode::NOT_FOUND,
1756                "ResourceNotFoundException",
1757                format!("Function not found: {function_name}"),
1758            )
1759        })?;
1760        let policy = func.policy.as_deref().ok_or_else(|| {
1761            AwsServiceError::aws_error(
1762                StatusCode::NOT_FOUND,
1763                "ResourceNotFoundException",
1764                format!("No policy is associated with function {function_name}"),
1765            )
1766        })?;
1767        Ok(AwsResponse::json(
1768            StatusCode::OK,
1769            json!({
1770                "Policy": policy,
1771                "RevisionId": uuid::Uuid::new_v4().to_string(),
1772            })
1773            .to_string(),
1774        ))
1775    }
1776}
1777
1778#[async_trait]
1779impl AwsService for LambdaService {
1780    fn service_name(&self) -> &str {
1781        "lambda"
1782    }
1783
1784    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1785        let (action, resource_name) = Self::resolve_action(&req).ok_or_else(|| {
1786            AwsServiceError::aws_error(
1787                StatusCode::NOT_FOUND,
1788                "UnknownOperationException",
1789                format!("Unknown operation: {} {}", req.method, req.raw_path),
1790            )
1791        })?;
1792
1793        // Normalize FunctionName-bearing resource slots: AWS Lambda accepts
1794        // bare name, name:qualifier, partial ARN, and full ARN in any URL
1795        // slot that names a function. Layer / event-source-mapping resource
1796        // names go through different routes and are left as-is.
1797        let resource_name = if action_takes_function_name(action) {
1798            resource_name.map(|s| normalize_function_name(&s))
1799        } else {
1800            resource_name
1801        };
1802
1803        let mutates = matches!(
1804            action,
1805            "CreateFunction"
1806                | "DeleteFunction"
1807                | "PublishVersion"
1808                | "AddPermission"
1809                | "RemovePermission"
1810                | "CreateEventSourceMapping"
1811                | "DeleteEventSourceMapping"
1812                | "UpdateEventSourceMapping"
1813                | "UpdateFunctionCode"
1814                | "UpdateFunctionConfiguration"
1815                | "CreateAlias"
1816                | "DeleteAlias"
1817                | "UpdateAlias"
1818                | "PublishLayerVersion"
1819                | "DeleteLayerVersion"
1820                | "AddLayerVersionPermission"
1821                | "RemoveLayerVersionPermission"
1822                | "CreateFunctionUrlConfig"
1823                | "DeleteFunctionUrlConfig"
1824                | "UpdateFunctionUrlConfig"
1825                | "PutFunctionConcurrency"
1826                | "DeleteFunctionConcurrency"
1827                | "PutProvisionedConcurrencyConfig"
1828                | "DeleteProvisionedConcurrencyConfig"
1829                | "CreateCodeSigningConfig"
1830                | "UpdateCodeSigningConfig"
1831                | "DeleteCodeSigningConfig"
1832                | "PutFunctionCodeSigningConfig"
1833                | "DeleteFunctionCodeSigningConfig"
1834                | "PutFunctionEventInvokeConfig"
1835                | "UpdateFunctionEventInvokeConfig"
1836                | "DeleteFunctionEventInvokeConfig"
1837                | "PutRuntimeManagementConfig"
1838                | "PutFunctionScalingConfig"
1839                | "PutFunctionRecursionConfig"
1840                | "TagResource"
1841                | "UntagResource"
1842                | "CreateCapacityProvider"
1843                | "UpdateCapacityProvider"
1844                | "DeleteCapacityProvider"
1845                | "CheckpointDurableExecution"
1846                | "StopDurableExecution"
1847                | "SendDurableExecutionCallbackSuccess"
1848                | "SendDurableExecutionCallbackFailure"
1849                | "SendDurableExecutionCallbackHeartbeat"
1850                | "InvokeAsync"
1851                | "InvokeWithResponseStream"
1852        );
1853
1854        let aid = &req.account_id;
1855        let result = match action {
1856            "CreateFunction" => self.create_function(&req),
1857            "ListFunctions" => self.list_functions(aid),
1858            "GetFunction" => self.get_function(
1859                resource_name.as_deref().unwrap_or(""),
1860                aid,
1861                req.region.as_str(),
1862            ),
1863            "DeleteFunction" => self.delete_function(resource_name.as_deref().unwrap_or(""), aid),
1864            "Invoke" => {
1865                let invocation_type = InvocationType::from_header(
1866                    req.headers
1867                        .get("x-amz-invocation-type")
1868                        .and_then(|v| v.to_str().ok()),
1869                );
1870                self.invoke(
1871                    resource_name.as_deref().unwrap_or(""),
1872                    &req.body,
1873                    aid,
1874                    invocation_type,
1875                )
1876                .await
1877            }
1878            "InvokeAsync" => {
1879                self.invoke(
1880                    resource_name.as_deref().unwrap_or(""),
1881                    &req.body,
1882                    aid,
1883                    InvocationType::Event,
1884                )
1885                .await
1886            }
1887            "PublishVersion" => self.publish_version(resource_name.as_deref().unwrap_or(""), aid),
1888            "AddPermission" => self.add_permission(resource_name.as_deref().unwrap_or(""), &req),
1889            "GetPolicy" => self.get_policy(resource_name.as_deref().unwrap_or(""), aid),
1890            "RemovePermission" => {
1891                // Path: /2015-03-31/functions/{name}/policy/{sid}
1892                let sid = req.path_segments.get(4).cloned().unwrap_or_default();
1893                self.remove_permission(resource_name.as_deref().unwrap_or(""), &sid, aid)
1894            }
1895            "CreateEventSourceMapping" => self.create_event_source_mapping(&req),
1896            "ListEventSourceMappings" => self.list_event_source_mappings(aid),
1897            "GetEventSourceMapping" => {
1898                self.get_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
1899            }
1900            "DeleteEventSourceMapping" => {
1901                self.delete_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
1902            }
1903            other => {
1904                self.handle_extra(other, resource_name.as_deref(), &req)
1905                    .await
1906            }
1907        };
1908        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
1909            self.save_snapshot().await;
1910        }
1911        result
1912    }
1913
1914    fn supported_actions(&self) -> &[&str] {
1915        &[
1916            "CreateFunction",
1917            "GetFunction",
1918            "DeleteFunction",
1919            "ListFunctions",
1920            "Invoke",
1921            "InvokeAsync",
1922            "InvokeWithResponseStream",
1923            "PublishVersion",
1924            "ListVersionsByFunction",
1925            "AddPermission",
1926            "RemovePermission",
1927            "GetPolicy",
1928            "CreateEventSourceMapping",
1929            "ListEventSourceMappings",
1930            "GetEventSourceMapping",
1931            "UpdateEventSourceMapping",
1932            "DeleteEventSourceMapping",
1933            "GetFunctionConfiguration",
1934            "UpdateFunctionConfiguration",
1935            "UpdateFunctionCode",
1936            "GetAccountSettings",
1937            "CreateAlias",
1938            "GetAlias",
1939            "ListAliases",
1940            "UpdateAlias",
1941            "DeleteAlias",
1942            "PublishLayerVersion",
1943            "GetLayerVersion",
1944            "GetLayerVersionByArn",
1945            "DeleteLayerVersion",
1946            "ListLayerVersions",
1947            "ListLayers",
1948            "GetLayerVersionPolicy",
1949            "AddLayerVersionPermission",
1950            "RemoveLayerVersionPermission",
1951            "CreateFunctionUrlConfig",
1952            "GetFunctionUrlConfig",
1953            "UpdateFunctionUrlConfig",
1954            "DeleteFunctionUrlConfig",
1955            "ListFunctionUrlConfigs",
1956            "PutFunctionConcurrency",
1957            "GetFunctionConcurrency",
1958            "DeleteFunctionConcurrency",
1959            "PutProvisionedConcurrencyConfig",
1960            "GetProvisionedConcurrencyConfig",
1961            "DeleteProvisionedConcurrencyConfig",
1962            "ListProvisionedConcurrencyConfigs",
1963            "CreateCodeSigningConfig",
1964            "GetCodeSigningConfig",
1965            "UpdateCodeSigningConfig",
1966            "DeleteCodeSigningConfig",
1967            "ListCodeSigningConfigs",
1968            "PutFunctionCodeSigningConfig",
1969            "GetFunctionCodeSigningConfig",
1970            "DeleteFunctionCodeSigningConfig",
1971            "ListFunctionsByCodeSigningConfig",
1972            "PutFunctionEventInvokeConfig",
1973            "GetFunctionEventInvokeConfig",
1974            "UpdateFunctionEventInvokeConfig",
1975            "DeleteFunctionEventInvokeConfig",
1976            "ListFunctionEventInvokeConfigs",
1977            "PutRuntimeManagementConfig",
1978            "GetRuntimeManagementConfig",
1979            "PutFunctionScalingConfig",
1980            "GetFunctionScalingConfig",
1981            "PutFunctionRecursionConfig",
1982            "GetFunctionRecursionConfig",
1983            "TagResource",
1984            "UntagResource",
1985            "ListTags",
1986            "CreateCapacityProvider",
1987            "GetCapacityProvider",
1988            "UpdateCapacityProvider",
1989            "DeleteCapacityProvider",
1990            "ListCapacityProviders",
1991            "ListFunctionVersionsByCapacityProvider",
1992            "CheckpointDurableExecution",
1993            "GetDurableExecution",
1994            "GetDurableExecutionHistory",
1995            "GetDurableExecutionState",
1996            "ListDurableExecutionsByFunction",
1997            "StopDurableExecution",
1998            "SendDurableExecutionCallbackSuccess",
1999            "SendDurableExecutionCallbackFailure",
2000            "SendDurableExecutionCallbackHeartbeat",
2001        ]
2002    }
2003
2004    fn iam_enforceable(&self) -> bool {
2005        true
2006    }
2007
2008    /// Lambda resources are function ARNs. Function-scoped ops
2009    /// resolve the target ARN from the path; list ops target `*`
2010    /// (the whole service), matching how AWS models them.
2011    fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
2012        // REST-JSON services don't have `request.action` populated at
2013        // dispatch time — it's derived from method+path inside
2014        // `handle()`. Reuse the same resolver so the two can never
2015        // drift.
2016        let (action_str, resource_name) = Self::resolve_action(request)?;
2017        let action: &'static str = match action_str {
2018            "CreateFunction" => "CreateFunction",
2019            "ListFunctions" => "ListFunctions",
2020            "GetFunction" => "GetFunction",
2021            "DeleteFunction" => "DeleteFunction",
2022            "Invoke" => "InvokeFunction",
2023            "PublishVersion" => "PublishVersion",
2024            "AddPermission" => "AddPermission",
2025            "RemovePermission" => "RemovePermission",
2026            "GetPolicy" => "GetPolicy",
2027            "CreateEventSourceMapping" => "CreateEventSourceMapping",
2028            "ListEventSourceMappings" => "ListEventSourceMappings",
2029            "GetEventSourceMapping" => "GetEventSourceMapping",
2030            "DeleteEventSourceMapping" => "DeleteEventSourceMapping",
2031            _ => return None,
2032        };
2033        let accounts = self.state.read();
2034        let empty = LambdaState::new(&request.account_id, &request.region);
2035        let state = accounts.get(&request.account_id).unwrap_or(&empty);
2036        let resource = match action {
2037            "GetFunction" | "DeleteFunction" | "InvokeFunction" | "PublishVersion"
2038            | "AddPermission" | "RemovePermission" | "GetPolicy" => {
2039                let name = resource_name.unwrap_or_default();
2040                if name.is_empty() {
2041                    "*".to_string()
2042                } else {
2043                    format!(
2044                        "arn:aws:lambda:{}:{}:function:{}",
2045                        state.region, state.account_id, name
2046                    )
2047                }
2048            }
2049            "CreateFunction" => {
2050                // Best-effort: parse the FunctionName from the body so
2051                // CreateFunction can be resource-scoped against the
2052                // to-be-created ARN. Falls back to `*` when the body
2053                // isn't JSON yet (e.g. soft-mode observability).
2054                serde_json::from_slice::<Value>(&request.body)
2055                    .ok()
2056                    .and_then(|v| {
2057                        v.get("FunctionName").and_then(|f| f.as_str()).map(|n| {
2058                            format!(
2059                                "arn:aws:lambda:{}:{}:function:{}",
2060                                state.region, state.account_id, n
2061                            )
2062                        })
2063                    })
2064                    .unwrap_or_else(|| "*".to_string())
2065            }
2066            _ => "*".to_string(),
2067        };
2068        Some(fakecloud_core::auth::IamAction {
2069            service: "lambda",
2070            action,
2071            resource,
2072        })
2073    }
2074
2075    fn iam_condition_keys_for(
2076        &self,
2077        request: &AwsRequest,
2078        action: &fakecloud_core::auth::IamAction,
2079    ) -> std::collections::BTreeMap<String, Vec<String>> {
2080        let mut out = std::collections::BTreeMap::new();
2081        if action.action == "AddPermission" {
2082            if action.resource != "*" {
2083                out.insert(
2084                    "lambda:functionarn".to_string(),
2085                    vec![action.resource.clone()],
2086                );
2087            }
2088            if let Ok(body) = serde_json::from_slice::<Value>(&request.body) {
2089                if let Some(principal) = body.get("Principal").and_then(|p| p.as_str()) {
2090                    out.insert("lambda:principal".to_string(), vec![principal.to_string()]);
2091                }
2092            }
2093        }
2094        out
2095    }
2096}
2097
2098#[cfg(test)]
2099mod tests {
2100    use super::*;
2101    use bytes::Bytes;
2102    use http::{HeaderMap, Method};
2103    use parking_lot::RwLock;
2104    use std::collections::HashMap;
2105    use std::sync::Arc;
2106
2107    fn make_state() -> SharedLambdaState {
2108        Arc::new(RwLock::new(
2109            fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
2110        ))
2111    }
2112
2113    fn make_request(method: Method, path: &str, body: &str) -> AwsRequest {
2114        let path_segments: Vec<String> = path
2115            .split('/')
2116            .filter(|s| !s.is_empty())
2117            .map(|s| s.to_string())
2118            .collect();
2119        AwsRequest {
2120            service: "lambda".to_string(),
2121            action: String::new(),
2122            region: "us-east-1".to_string(),
2123            account_id: "123456789012".to_string(),
2124            request_id: "test-request-id".to_string(),
2125            headers: HeaderMap::new(),
2126            query_params: HashMap::new(),
2127            body: Bytes::from(body.to_string()),
2128            body_stream: parking_lot::Mutex::new(None),
2129            path_segments,
2130            raw_path: path.to_string(),
2131            raw_query: String::new(),
2132            method,
2133            is_query_protocol: false,
2134            access_key_id: None,
2135            principal: None,
2136        }
2137    }
2138
2139    #[test]
2140    fn normalize_function_name_bare_name_passes_through() {
2141        assert_eq!(normalize_function_name("MyFunction"), "MyFunction");
2142    }
2143
2144    #[test]
2145    fn normalize_function_name_strips_qualifier_from_bare_name() {
2146        assert_eq!(normalize_function_name("MyFunction:PROD"), "MyFunction");
2147        assert_eq!(normalize_function_name("MyFunction:1"), "MyFunction");
2148    }
2149
2150    #[test]
2151    fn normalize_function_name_strips_full_arn() {
2152        assert_eq!(
2153            normalize_function_name("arn:aws:lambda:us-east-1:123456789012:function:MyFunction"),
2154            "MyFunction"
2155        );
2156    }
2157
2158    #[test]
2159    fn normalize_function_name_strips_qualified_full_arn() {
2160        assert_eq!(
2161            normalize_function_name(
2162                "arn:aws:lambda:us-east-1:123456789012:function:MyFunction:PROD"
2163            ),
2164            "MyFunction"
2165        );
2166    }
2167
2168    #[test]
2169    fn normalize_function_name_strips_partial_arn() {
2170        assert_eq!(
2171            normalize_function_name("123456789012:function:MyFunction"),
2172            "MyFunction"
2173        );
2174        assert_eq!(
2175            normalize_function_name("123456789012:function:MyFunction:1"),
2176            "MyFunction"
2177        );
2178    }
2179
2180    #[test]
2181    fn normalize_function_name_leaves_malformed_arn_alone() {
2182        // wrong service in ARN — multiple colons, no lambda prefix → unchanged
2183        let s = "arn:aws:s3:us-east-1:123456789012:function:Foo";
2184        assert_eq!(normalize_function_name(s), s);
2185        // partial ARN with non-numeric account-shaped prefix → unchanged
2186        let s2 = "abc:function:Foo";
2187        assert_eq!(normalize_function_name(s2), s2);
2188    }
2189
2190    #[test]
2191    fn normalize_function_name_empty() {
2192        assert_eq!(normalize_function_name(""), "");
2193    }
2194
2195    #[test]
2196    fn normalize_function_name_decodes_percent_encoded_arn() {
2197        // SDKs URL-encode `:` in path segments. The toolkit / aws-sdk-lambda
2198        // wire form for `arn:aws:lambda:...` is `arn%3Aaws%3Alambda%3A...`.
2199        let encoded = "arn%3Aaws%3Alambda%3Aus-east-1%3A123456789012%3Afunction%3AMyFunc";
2200        assert_eq!(normalize_function_name(encoded), "MyFunc");
2201    }
2202
2203    #[tokio::test]
2204    async fn get_function_accepts_full_arn() {
2205        let svc = LambdaService::new(make_state());
2206        // Seed a function via CreateFunction
2207        let create_body = json!({
2208            "FunctionName": "MyFunc",
2209            "Runtime": "nodejs20.x",
2210            "Role": "arn:aws:iam::123456789012:role/lambda-role",
2211            "Handler": "index.handler",
2212            "Code": {"ZipFile": ""},
2213        })
2214        .to_string();
2215        let req = make_request(Method::POST, "/2015-03-31/functions", &create_body);
2216        svc.handle(req).await.expect("create function");
2217
2218        // GetFunction by full ARN
2219        let req = make_request(
2220            Method::GET,
2221            "/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:MyFunc",
2222            "",
2223        );
2224        let resp = svc.handle(req).await.expect("get function by ARN");
2225        assert_eq!(resp.status, StatusCode::OK);
2226    }
2227
2228    #[tokio::test]
2229    async fn get_function_accepts_partial_arn() {
2230        let svc = LambdaService::new(make_state());
2231        let create_body = json!({
2232            "FunctionName": "MyFunc",
2233            "Runtime": "nodejs20.x",
2234            "Role": "arn:aws:iam::123456789012:role/lambda-role",
2235            "Handler": "index.handler",
2236            "Code": {"ZipFile": ""},
2237        })
2238        .to_string();
2239        let req = make_request(Method::POST, "/2015-03-31/functions", &create_body);
2240        svc.handle(req).await.expect("create function");
2241
2242        let req = make_request(
2243            Method::GET,
2244            "/2015-03-31/functions/123456789012:function:MyFunc",
2245            "",
2246        );
2247        let resp = svc.handle(req).await.expect("get function by partial ARN");
2248        assert_eq!(resp.status, StatusCode::OK);
2249    }
2250
2251    #[tokio::test]
2252    async fn get_function_accepts_name_with_qualifier() {
2253        let svc = LambdaService::new(make_state());
2254        let create_body = json!({
2255            "FunctionName": "MyFunc",
2256            "Runtime": "nodejs20.x",
2257            "Role": "arn:aws:iam::123456789012:role/lambda-role",
2258            "Handler": "index.handler",
2259            "Code": {"ZipFile": ""},
2260        })
2261        .to_string();
2262        let req = make_request(Method::POST, "/2015-03-31/functions", &create_body);
2263        svc.handle(req).await.expect("create function");
2264
2265        let req = make_request(Method::GET, "/2015-03-31/functions/MyFunc:1", "");
2266        let resp = svc
2267            .handle(req)
2268            .await
2269            .expect("get function by name:qualifier");
2270        assert_eq!(resp.status, StatusCode::OK);
2271    }
2272
2273    #[test]
2274    fn iam_condition_keys_for_add_permission_populates_arn_and_principal() {
2275        let svc = LambdaService::new(make_state());
2276        let body = json!({
2277            "StatementId": "stmt",
2278            "Action": "lambda:InvokeFunction",
2279            "Principal": "s3.amazonaws.com",
2280        })
2281        .to_string();
2282        let req = make_request(Method::POST, "/2015-03-31/functions/my-func/policy", &body);
2283        let action = fakecloud_core::auth::IamAction {
2284            service: "lambda",
2285            action: "AddPermission",
2286            resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string(),
2287        };
2288        let keys = svc.iam_condition_keys_for(&req, &action);
2289        assert_eq!(
2290            keys.get("lambda:functionarn"),
2291            Some(&vec![
2292                "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string()
2293            ])
2294        );
2295        assert_eq!(
2296            keys.get("lambda:principal"),
2297            Some(&vec!["s3.amazonaws.com".to_string()])
2298        );
2299    }
2300
2301    #[test]
2302    fn iam_condition_keys_for_add_permission_omits_missing_principal() {
2303        let svc = LambdaService::new(make_state());
2304        let body = json!({"StatementId": "stmt", "Action": "lambda:InvokeFunction"}).to_string();
2305        let req = make_request(Method::POST, "/2015-03-31/functions/my-func/policy", &body);
2306        let action = fakecloud_core::auth::IamAction {
2307            service: "lambda",
2308            action: "AddPermission",
2309            resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string(),
2310        };
2311        let keys = svc.iam_condition_keys_for(&req, &action);
2312        assert!(!keys.contains_key("lambda:principal"));
2313        assert!(keys.contains_key("lambda:functionarn"));
2314    }
2315
2316    #[test]
2317    fn iam_condition_keys_for_non_add_permission_is_empty() {
2318        let svc = LambdaService::new(make_state());
2319        let req = make_request(Method::GET, "/2015-03-31/functions/my-func", "");
2320        let action = fakecloud_core::auth::IamAction {
2321            service: "lambda",
2322            action: "GetFunction",
2323            resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string(),
2324        };
2325        assert!(svc.iam_condition_keys_for(&req, &action).is_empty());
2326    }
2327
2328    #[tokio::test]
2329    async fn test_create_and_get_function() {
2330        let state = make_state();
2331        let svc = LambdaService::new(state);
2332
2333        let create_body = json!({
2334            "FunctionName": "my-func",
2335            "Runtime": "python3.12",
2336            "Role": "arn:aws:iam::123456789012:role/test-role",
2337            "Handler": "index.handler",
2338            "Code": { "ZipFile": "UEsFBgAAAAAAAAAAAAAAAAAAAAA=" }
2339        });
2340
2341        let req = make_request(
2342            Method::POST,
2343            "/2015-03-31/functions",
2344            &create_body.to_string(),
2345        );
2346        let resp = svc.handle(req).await.unwrap();
2347        assert_eq!(resp.status, StatusCode::CREATED);
2348
2349        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2350        assert_eq!(body["FunctionName"], "my-func");
2351        assert_eq!(body["Runtime"], "python3.12");
2352
2353        // Get
2354        let req = make_request(Method::GET, "/2015-03-31/functions/my-func", "");
2355        let resp = svc.handle(req).await.unwrap();
2356        assert_eq!(resp.status, StatusCode::OK);
2357        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2358        assert_eq!(body["Configuration"]["FunctionName"], "my-func");
2359    }
2360
2361    #[tokio::test]
2362    async fn test_delete_function() {
2363        let state = make_state();
2364        let svc = LambdaService::new(state);
2365
2366        let create_body = json!({
2367            "FunctionName": "to-delete",
2368            "Runtime": "nodejs20.x",
2369            "Role": "arn:aws:iam::123456789012:role/test",
2370            "Handler": "index.handler",
2371            "Code": {}
2372        });
2373
2374        let req = make_request(
2375            Method::POST,
2376            "/2015-03-31/functions",
2377            &create_body.to_string(),
2378        );
2379        svc.handle(req).await.unwrap();
2380
2381        let req = make_request(Method::DELETE, "/2015-03-31/functions/to-delete", "");
2382        let resp = svc.handle(req).await.unwrap();
2383        assert_eq!(resp.status, StatusCode::NO_CONTENT);
2384
2385        // Verify deleted
2386        let req = make_request(Method::GET, "/2015-03-31/functions/to-delete", "");
2387        let resp = svc.handle(req).await;
2388        assert!(resp.is_err());
2389    }
2390
2391    #[tokio::test]
2392    async fn test_invoke_without_runtime_returns_error() {
2393        let state = make_state();
2394        let svc = LambdaService::new(state);
2395
2396        let create_body = json!({
2397            "FunctionName": "invoke-me",
2398            "Runtime": "python3.12",
2399            "Role": "arn:aws:iam::123456789012:role/test",
2400            "Handler": "index.handler",
2401            "Code": {}
2402        });
2403
2404        let req = make_request(
2405            Method::POST,
2406            "/2015-03-31/functions",
2407            &create_body.to_string(),
2408        );
2409        svc.handle(req).await.unwrap();
2410
2411        let req = make_request(
2412            Method::POST,
2413            "/2015-03-31/functions/invoke-me/invocations",
2414            r#"{"key": "value"}"#,
2415        );
2416        let resp = svc.handle(req).await;
2417        assert!(resp.is_err());
2418    }
2419
2420    #[tokio::test]
2421    async fn test_invoke_nonexistent_function() {
2422        let state = make_state();
2423        let svc = LambdaService::new(state);
2424
2425        let req = make_request(
2426            Method::POST,
2427            "/2015-03-31/functions/does-not-exist/invocations",
2428            "{}",
2429        );
2430        let resp = svc.handle(req).await;
2431        assert!(resp.is_err());
2432    }
2433
2434    #[tokio::test]
2435    async fn test_list_functions() {
2436        let state = make_state();
2437        let svc = LambdaService::new(state);
2438
2439        for name in &["func-a", "func-b"] {
2440            let create_body = json!({
2441                "FunctionName": name,
2442                "Runtime": "python3.12",
2443                "Role": "arn:aws:iam::123456789012:role/test",
2444                "Handler": "index.handler",
2445                "Code": {}
2446            });
2447            let req = make_request(
2448                Method::POST,
2449                "/2015-03-31/functions",
2450                &create_body.to_string(),
2451            );
2452            svc.handle(req).await.unwrap();
2453        }
2454
2455        let req = make_request(Method::GET, "/2015-03-31/functions", "");
2456        let resp = svc.handle(req).await.unwrap();
2457        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2458        assert_eq!(body["Functions"].as_array().unwrap().len(), 2);
2459    }
2460
2461    #[tokio::test]
2462    async fn test_event_source_mapping() {
2463        let state = make_state();
2464        let svc = LambdaService::new(state);
2465
2466        // Create function first
2467        let create_body = json!({
2468            "FunctionName": "esm-func",
2469            "Runtime": "python3.12",
2470            "Role": "arn:aws:iam::123456789012:role/test",
2471            "Handler": "index.handler",
2472            "Code": {}
2473        });
2474        let req = make_request(
2475            Method::POST,
2476            "/2015-03-31/functions",
2477            &create_body.to_string(),
2478        );
2479        svc.handle(req).await.unwrap();
2480
2481        // Create mapping
2482        let mapping_body = json!({
2483            "FunctionName": "esm-func",
2484            "EventSourceArn": "arn:aws:sqs:us-east-1:123456789012:my-queue",
2485            "BatchSize": 5
2486        });
2487        let req = make_request(
2488            Method::POST,
2489            "/2015-03-31/event-source-mappings",
2490            &mapping_body.to_string(),
2491        );
2492        let resp = svc.handle(req).await.unwrap();
2493        assert_eq!(resp.status, StatusCode::ACCEPTED);
2494        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2495        let uuid = body["UUID"].as_str().unwrap().to_string();
2496
2497        // List mappings
2498        let req = make_request(Method::GET, "/2015-03-31/event-source-mappings", "");
2499        let resp = svc.handle(req).await.unwrap();
2500        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2501        assert_eq!(body["EventSourceMappings"].as_array().unwrap().len(), 1);
2502
2503        // Delete mapping
2504        let req = make_request(
2505            Method::DELETE,
2506            &format!("/2015-03-31/event-source-mappings/{uuid}"),
2507            "",
2508        );
2509        let resp = svc.handle(req).await.unwrap();
2510        assert_eq!(resp.status, StatusCode::ACCEPTED);
2511    }
2512
2513    async fn seed_function(svc: &LambdaService, name: &str) {
2514        let body = json!({
2515            "FunctionName": name,
2516            "Runtime": "python3.12",
2517            "Role": "arn:aws:iam::123456789012:role/r",
2518            "Handler": "index.handler",
2519            "Code": {}
2520        });
2521        let req = make_request(Method::POST, "/2015-03-31/functions", &body.to_string());
2522        svc.handle(req).await.unwrap();
2523    }
2524
2525    #[tokio::test]
2526    async fn add_permission_builds_canonical_statement() {
2527        let svc = LambdaService::new(make_state());
2528        seed_function(&svc, "f").await;
2529
2530        let body = json!({
2531            "StatementId": "s3-invoke",
2532            "Action": "InvokeFunction",
2533            "Principal": "s3.amazonaws.com",
2534            "SourceArn": "arn:aws:s3:::my-bucket",
2535            "SourceAccount": "123456789012",
2536        });
2537        let req = make_request(
2538            Method::POST,
2539            "/2015-03-31/functions/f/policy",
2540            &body.to_string(),
2541        );
2542        let resp = svc.handle(req).await.unwrap();
2543        assert_eq!(resp.status, StatusCode::CREATED);
2544
2545        let out: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2546        let statement: Value = serde_json::from_str(out["Statement"].as_str().unwrap()).unwrap();
2547        assert_eq!(statement["Sid"], "s3-invoke");
2548        assert_eq!(statement["Effect"], "Allow");
2549        assert_eq!(statement["Principal"]["Service"], "s3.amazonaws.com");
2550        assert_eq!(statement["Action"], "lambda:InvokeFunction");
2551        assert_eq!(
2552            statement["Resource"],
2553            "arn:aws:lambda:us-east-1:123456789012:function:f"
2554        );
2555        assert_eq!(
2556            statement["Condition"]["ArnLike"]["aws:SourceArn"],
2557            "arn:aws:s3:::my-bucket"
2558        );
2559        assert_eq!(
2560            statement["Condition"]["StringEquals"]["aws:SourceAccount"],
2561            "123456789012"
2562        );
2563    }
2564
2565    #[tokio::test]
2566    async fn add_permission_aws_principal_emits_aws_key() {
2567        let svc = LambdaService::new(make_state());
2568        seed_function(&svc, "f").await;
2569
2570        let body = json!({
2571            "StatementId": "user-invoke",
2572            "Action": "InvokeFunction",
2573            "Principal": "arn:aws:iam::123456789012:user/alice",
2574        });
2575        let req = make_request(
2576            Method::POST,
2577            "/2015-03-31/functions/f/policy",
2578            &body.to_string(),
2579        );
2580        svc.handle(req).await.unwrap();
2581
2582        // Fetch via GetPolicy and inspect the stored doc.
2583        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
2584        let resp = svc.handle(req).await.unwrap();
2585        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2586        let doc: Value = serde_json::from_str(body["Policy"].as_str().unwrap()).unwrap();
2587        let statements = doc["Statement"].as_array().unwrap();
2588        assert_eq!(statements.len(), 1);
2589        assert_eq!(
2590            statements[0]["Principal"]["AWS"],
2591            "arn:aws:iam::123456789012:user/alice"
2592        );
2593        assert!(statements[0].get("Condition").is_none());
2594    }
2595
2596    #[tokio::test]
2597    async fn add_permission_rejects_duplicate_statement_id() {
2598        let svc = LambdaService::new(make_state());
2599        seed_function(&svc, "f").await;
2600
2601        let body = json!({
2602            "StatementId": "dup",
2603            "Action": "InvokeFunction",
2604            "Principal": "arn:aws:iam::123456789012:user/a",
2605        });
2606        let req = make_request(
2607            Method::POST,
2608            "/2015-03-31/functions/f/policy",
2609            &body.to_string(),
2610        );
2611        svc.handle(req).await.unwrap();
2612
2613        let req = make_request(
2614            Method::POST,
2615            "/2015-03-31/functions/f/policy",
2616            &body.to_string(),
2617        );
2618        let err = match svc.handle(req).await {
2619            Err(e) => e,
2620            Ok(_) => panic!("expected error"),
2621        };
2622        assert_eq!(err.status(), StatusCode::CONFLICT);
2623    }
2624
2625    #[tokio::test]
2626    async fn get_policy_returns_404_when_no_policy_attached() {
2627        let svc = LambdaService::new(make_state());
2628        seed_function(&svc, "f").await;
2629
2630        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
2631        let err = match svc.handle(req).await {
2632            Err(e) => e,
2633            Ok(_) => panic!("expected error"),
2634        };
2635        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2636    }
2637
2638    #[tokio::test]
2639    async fn remove_permission_strips_matching_sid_and_leaves_empty_doc() {
2640        let svc = LambdaService::new(make_state());
2641        seed_function(&svc, "f").await;
2642
2643        for sid in ["a", "b"] {
2644            let body = json!({
2645                "StatementId": sid,
2646                "Action": "InvokeFunction",
2647                "Principal": "arn:aws:iam::123456789012:user/u",
2648            });
2649            let req = make_request(
2650                Method::POST,
2651                "/2015-03-31/functions/f/policy",
2652                &body.to_string(),
2653            );
2654            svc.handle(req).await.unwrap();
2655        }
2656
2657        // Remove "a"
2658        let req = make_request(Method::DELETE, "/2015-03-31/functions/f/policy/a", "");
2659        let resp = svc.handle(req).await.unwrap();
2660        assert_eq!(resp.status, StatusCode::NO_CONTENT);
2661
2662        // GetPolicy still returns the doc with just "b".
2663        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
2664        let resp = svc.handle(req).await.unwrap();
2665        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2666        let doc: Value = serde_json::from_str(body["Policy"].as_str().unwrap()).unwrap();
2667        let stmts = doc["Statement"].as_array().unwrap();
2668        assert_eq!(stmts.len(), 1);
2669        assert_eq!(stmts[0]["Sid"], "b");
2670
2671        // Remove the last one — doc stays (empty Statement array).
2672        let req = make_request(Method::DELETE, "/2015-03-31/functions/f/policy/b", "");
2673        svc.handle(req).await.unwrap();
2674
2675        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
2676        let resp = svc.handle(req).await.unwrap();
2677        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2678        let doc: Value = serde_json::from_str(body["Policy"].as_str().unwrap()).unwrap();
2679        assert_eq!(doc["Statement"].as_array().unwrap().len(), 0);
2680    }
2681
2682    #[tokio::test]
2683    async fn remove_permission_unknown_sid_is_404() {
2684        let svc = LambdaService::new(make_state());
2685        seed_function(&svc, "f").await;
2686
2687        let body = json!({
2688            "StatementId": "known",
2689            "Action": "InvokeFunction",
2690            "Principal": "arn:aws:iam::123456789012:user/u",
2691        });
2692        let req = make_request(
2693            Method::POST,
2694            "/2015-03-31/functions/f/policy",
2695            &body.to_string(),
2696        );
2697        svc.handle(req).await.unwrap();
2698
2699        let req = make_request(Method::DELETE, "/2015-03-31/functions/f/policy/other", "");
2700        let err = match svc.handle(req).await {
2701            Err(e) => e,
2702            Ok(_) => panic!("expected error"),
2703        };
2704        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2705    }
2706
2707    #[tokio::test]
2708    async fn add_permission_on_missing_function_is_404() {
2709        let svc = LambdaService::new(make_state());
2710        let body = json!({
2711            "StatementId": "s",
2712            "Action": "InvokeFunction",
2713            "Principal": "arn:aws:iam::123456789012:user/u",
2714        });
2715        let req = make_request(
2716            Method::POST,
2717            "/2015-03-31/functions/missing/policy",
2718            &body.to_string(),
2719        );
2720        let err = match svc.handle(req).await {
2721            Err(e) => e,
2722            Ok(_) => panic!("expected error"),
2723        };
2724        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2725    }
2726
2727    #[test]
2728    fn iam_action_for_maps_invoke_to_function_arn() {
2729        let svc = LambdaService::new(make_state());
2730        let req = make_request(Method::POST, "/2015-03-31/functions/f/invocations", "");
2731        let action = svc.iam_action_for(&req).unwrap();
2732        assert_eq!(action.service, "lambda");
2733        assert_eq!(action.action, "InvokeFunction");
2734        assert_eq!(
2735            action.resource,
2736            "arn:aws:lambda:us-east-1:123456789012:function:f"
2737        );
2738    }
2739
2740    #[test]
2741    fn iam_action_for_maps_list_to_star() {
2742        let svc = LambdaService::new(make_state());
2743        let req = make_request(Method::GET, "/2015-03-31/functions", "");
2744        let action = svc.iam_action_for(&req).unwrap();
2745        assert_eq!(action.action, "ListFunctions");
2746        assert_eq!(action.resource, "*");
2747    }
2748
2749    #[test]
2750    fn iam_action_for_create_reads_function_name_from_body() {
2751        let svc = LambdaService::new(make_state());
2752        let body = json!({
2753            "FunctionName": "newfn",
2754            "Runtime": "python3.12",
2755            "Role": "arn:aws:iam::123456789012:role/r",
2756            "Handler": "index.handler",
2757            "Code": {}
2758        });
2759        let req = make_request(Method::POST, "/2015-03-31/functions", &body.to_string());
2760        let action = svc.iam_action_for(&req).unwrap();
2761        assert_eq!(action.action, "CreateFunction");
2762        assert_eq!(
2763            action.resource,
2764            "arn:aws:lambda:us-east-1:123456789012:function:newfn"
2765        );
2766    }
2767
2768    // ── Error branch tests ──
2769
2770    #[tokio::test]
2771    async fn create_function_duplicate_returns_conflict() {
2772        let svc = LambdaService::new(make_state());
2773        seed_function(&svc, "dup-fn").await;
2774
2775        let body = json!({
2776            "FunctionName": "dup-fn",
2777            "Runtime": "python3.12",
2778            "Role": "arn:aws:iam::123456789012:role/r",
2779            "Handler": "index.handler",
2780            "Code": {"ZipFile": "UEsDBBQ="},
2781        });
2782        let req = make_request(Method::POST, "/2015-03-31/functions", &body.to_string());
2783        let err = match svc.handle(req).await {
2784            Err(e) => e,
2785            Ok(_) => panic!("expected ResourceConflictException"),
2786        };
2787        assert_eq!(err.status(), StatusCode::CONFLICT);
2788    }
2789
2790    #[tokio::test]
2791    async fn get_function_not_found() {
2792        let svc = LambdaService::new(make_state());
2793        let req = make_request(Method::GET, "/2015-03-31/functions/nope", "");
2794        let err = match svc.handle(req).await {
2795            Err(e) => e,
2796            Ok(_) => panic!("expected error"),
2797        };
2798        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2799    }
2800
2801    #[tokio::test]
2802    async fn delete_function_not_found() {
2803        let svc = LambdaService::new(make_state());
2804        let req = make_request(Method::DELETE, "/2015-03-31/functions/nope", "");
2805        let err = match svc.handle(req).await {
2806            Err(e) => e,
2807            Ok(_) => panic!("expected error"),
2808        };
2809        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2810    }
2811
2812    #[tokio::test]
2813    async fn get_event_source_mapping_not_found() {
2814        let svc = LambdaService::new(make_state());
2815        let req = make_request(
2816            Method::GET,
2817            "/2015-03-31/event-source-mappings/nonexistent",
2818            "",
2819        );
2820        let err = match svc.handle(req).await {
2821            Err(e) => e,
2822            Ok(_) => panic!("expected error"),
2823        };
2824        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2825    }
2826
2827    #[tokio::test]
2828    async fn delete_event_source_mapping_not_found() {
2829        let svc = LambdaService::new(make_state());
2830        let req = make_request(
2831            Method::DELETE,
2832            "/2015-03-31/event-source-mappings/nonexistent",
2833            "",
2834        );
2835        let err = match svc.handle(req).await {
2836            Err(e) => e,
2837            Ok(_) => panic!("expected error"),
2838        };
2839        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2840    }
2841
2842    #[tokio::test]
2843    async fn get_policy_on_missing_function() {
2844        let svc = LambdaService::new(make_state());
2845        let req = make_request(Method::GET, "/2015-03-31/functions/nope/policy", "");
2846        let err = match svc.handle(req).await {
2847            Err(e) => e,
2848            Ok(_) => panic!("expected error"),
2849        };
2850        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2851    }
2852
2853    #[tokio::test]
2854    async fn remove_permission_on_missing_function() {
2855        let svc = LambdaService::new(make_state());
2856        let req = make_request(
2857            Method::DELETE,
2858            "/2015-03-31/functions/nope/policy/stmt1",
2859            "",
2860        );
2861        let err = match svc.handle(req).await {
2862            Err(e) => e,
2863            Ok(_) => panic!("expected error"),
2864        };
2865        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2866    }
2867
2868    #[tokio::test]
2869    async fn publish_version_on_missing_function() {
2870        let svc = LambdaService::new(make_state());
2871        let req = make_request(Method::POST, "/2015-03-31/functions/nope/versions", "{}");
2872        let err = match svc.handle(req).await {
2873            Err(e) => e,
2874            Ok(_) => panic!("expected error"),
2875        };
2876        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2877    }
2878
2879    #[tokio::test]
2880    async fn unknown_route_returns_error() {
2881        let svc = LambdaService::new(make_state());
2882        let req = make_request(Method::POST, "/unknown/route", "{}");
2883        assert!(svc.handle(req).await.is_err());
2884    }
2885
2886    #[tokio::test]
2887    async fn publish_version_unknown_function_errors() {
2888        let svc = LambdaService::new(make_state());
2889        assert!(svc.publish_version("ghost", "123456789012").is_err());
2890    }
2891
2892    #[tokio::test]
2893    async fn get_function_unknown_errors() {
2894        let svc = LambdaService::new(make_state());
2895        assert!(svc
2896            .get_function("ghost", "123456789012", "us-east-1")
2897            .is_err());
2898    }
2899
2900    #[tokio::test]
2901    async fn delete_function_unknown_errors() {
2902        let svc = LambdaService::new(make_state());
2903        assert!(svc.delete_function("ghost", "123456789012").is_err());
2904    }
2905
2906    #[tokio::test]
2907    async fn get_event_source_mapping_unknown_errors() {
2908        let svc = LambdaService::new(make_state());
2909        assert!(svc
2910            .get_event_source_mapping("ghost", "123456789012")
2911            .is_err());
2912    }
2913
2914    #[tokio::test]
2915    async fn delete_event_source_mapping_unknown_errors() {
2916        let svc = LambdaService::new(make_state());
2917        assert!(svc
2918            .delete_event_source_mapping("ghost", "123456789012")
2919            .is_err());
2920    }
2921
2922    #[tokio::test]
2923    async fn list_functions_empty_ok() {
2924        let svc = LambdaService::new(make_state());
2925        let resp = svc.list_functions("123456789012").unwrap();
2926        assert_eq!(resp.status, http::StatusCode::OK);
2927    }
2928
2929    #[tokio::test]
2930    async fn list_event_source_mappings_empty_ok() {
2931        let svc = LambdaService::new(make_state());
2932        let resp = svc.list_event_source_mappings("123456789012").unwrap();
2933        assert_eq!(resp.status, http::StatusCode::OK);
2934    }
2935}