Skip to main content

fakecloud_lambda/
service.rs

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