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/// All fields of a `CreateFunction` request, already parsed and
21/// defaulted. The code zip (if any) is eagerly base64-decoded so the
22/// caller can hash it without doing the decode again.
23struct CreateFunctionInput {
24    function_name: String,
25    runtime: String,
26    role: String,
27    handler: String,
28    description: String,
29    timeout: i64,
30    memory_size: i64,
31    package_type: String,
32    tags: HashMap<String, String>,
33    environment: HashMap<String, String>,
34    architectures: Vec<String>,
35    code_zip: Option<Vec<u8>>,
36    code_fallback: Vec<u8>,
37}
38
39impl CreateFunctionInput {
40    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
41        let function_name = body["FunctionName"]
42            .as_str()
43            .ok_or_else(|| {
44                AwsServiceError::aws_error(
45                    StatusCode::BAD_REQUEST,
46                    "InvalidParameterValueException",
47                    "FunctionName is required",
48                )
49            })?
50            .to_string();
51
52        let tags: HashMap<String, String> = body["Tags"]
53            .as_object()
54            .map(|m| {
55                m.iter()
56                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
57                    .collect()
58            })
59            .unwrap_or_default();
60
61        let environment: HashMap<String, String> = body["Environment"]["Variables"]
62            .as_object()
63            .map(|m| {
64                m.iter()
65                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
66                    .collect()
67            })
68            .unwrap_or_default();
69
70        let architectures = body["Architectures"]
71            .as_array()
72            .map(|a| {
73                a.iter()
74                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
75                    .collect()
76            })
77            .unwrap_or_else(|| vec!["x86_64".to_string()]);
78
79        let code_zip: Option<Vec<u8>> = match body["Code"]["ZipFile"].as_str() {
80            Some(b64) => Some(
81                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64).map_err(
82                    |_| {
83                        AwsServiceError::aws_error(
84                            StatusCode::BAD_REQUEST,
85                            "InvalidParameterValueException",
86                            "Could not decode Code.ZipFile: invalid base64",
87                        )
88                    },
89                )?,
90            ),
91            None => None,
92        };
93
94        let code_fallback = serde_json::to_vec(&body["Code"]).unwrap_or_default();
95
96        Ok(Self {
97            function_name,
98            runtime: body["Runtime"].as_str().unwrap_or("python3.12").to_string(),
99            role: body["Role"].as_str().unwrap_or("").to_string(),
100            handler: body["Handler"]
101                .as_str()
102                .unwrap_or("index.handler")
103                .to_string(),
104            description: body["Description"].as_str().unwrap_or("").to_string(),
105            timeout: body["Timeout"].as_i64().unwrap_or(3),
106            memory_size: body["MemorySize"].as_i64().unwrap_or(128),
107            package_type: body["PackageType"].as_str().unwrap_or("Zip").to_string(),
108            tags,
109            environment,
110            architectures,
111            code_zip,
112            code_fallback,
113        })
114    }
115}
116
117pub struct LambdaService {
118    state: SharedLambdaState,
119    runtime: Option<Arc<ContainerRuntime>>,
120    snapshot_store: Option<Arc<dyn SnapshotStore>>,
121    snapshot_lock: Arc<AsyncMutex<()>>,
122}
123
124impl LambdaService {
125    pub fn new(state: SharedLambdaState) -> Self {
126        Self {
127            state,
128            runtime: None,
129            snapshot_store: None,
130            snapshot_lock: Arc::new(AsyncMutex::new(())),
131        }
132    }
133
134    pub fn with_runtime(mut self, runtime: Arc<ContainerRuntime>) -> Self {
135        self.runtime = Some(runtime);
136        self
137    }
138
139    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
140        self.snapshot_store = Some(store);
141        self
142    }
143
144    async fn save_snapshot(&self) {
145        let Some(store) = self.snapshot_store.clone() else {
146            return;
147        };
148        let _guard = self.snapshot_lock.lock().await;
149        let snapshot = LambdaSnapshot {
150            schema_version: LAMBDA_SNAPSHOT_SCHEMA_VERSION,
151            accounts: Some(self.state.read().clone()),
152            state: None,
153        };
154        let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
155            let bytes = serde_json::to_vec(&snapshot)
156                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
157            store.save(&bytes)
158        })
159        .await;
160        match join {
161            Ok(Ok(())) => {}
162            Ok(Err(err)) => tracing::error!(%err, "failed to write lambda snapshot"),
163            Err(err) => tracing::error!(%err, "lambda snapshot task panicked"),
164        }
165    }
166
167    /// Determine the action from the HTTP method and path segments.
168    /// Lambda uses REST-style routing:
169    ///   POST   /2015-03-31/functions                         -> CreateFunction
170    ///   GET    /2015-03-31/functions                         -> ListFunctions
171    ///   GET    /2015-03-31/functions/{name}                  -> GetFunction
172    ///   DELETE /2015-03-31/functions/{name}                  -> DeleteFunction
173    ///   POST   /2015-03-31/functions/{name}/invocations      -> Invoke
174    ///   POST   /2015-03-31/functions/{name}/versions         -> PublishVersion
175    ///   POST   /2015-03-31/event-source-mappings             -> CreateEventSourceMapping
176    ///   GET    /2015-03-31/event-source-mappings             -> ListEventSourceMappings
177    ///   GET    /2015-03-31/event-source-mappings/{uuid}      -> GetEventSourceMapping
178    ///   DELETE /2015-03-31/event-source-mappings/{uuid}      -> DeleteEventSourceMapping
179    fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Option<String>)> {
180        let segs = &req.path_segments;
181        if segs.is_empty() || segs[0] != "2015-03-31" {
182            return None;
183        }
184
185        // Second segment is the collection (`functions` /
186        // `event-source-mappings`); third is the resource name when
187        // present. Bind the resource name once so the match arms don't
188        // each repeat `segs[2].clone()`.
189        let collection = segs.get(1).map(|s| s.as_str());
190        let resource = segs.get(2).map(|s| s.to_string());
191
192        let action = match (
193            &req.method,
194            segs.len(),
195            collection,
196            segs.get(3).map(|s| s.as_str()),
197        ) {
198            // /2015-03-31/functions
199            (&Method::POST, 2, Some("functions"), _) => "CreateFunction",
200            (&Method::GET, 2, Some("functions"), _) => "ListFunctions",
201            // /2015-03-31/functions/{name}
202            (&Method::GET, 3, Some("functions"), _) => "GetFunction",
203            (&Method::DELETE, 3, Some("functions"), _) => "DeleteFunction",
204            // /2015-03-31/functions/{name}/invocations
205            (&Method::POST, 4, Some("functions"), Some("invocations")) => "Invoke",
206            // /2015-03-31/functions/{name}/versions
207            (&Method::POST, 4, Some("functions"), Some("versions")) => "PublishVersion",
208            // /2015-03-31/functions/{name}/policy
209            (&Method::POST, 4, Some("functions"), Some("policy")) => "AddPermission",
210            (&Method::GET, 4, Some("functions"), Some("policy")) => "GetPolicy",
211            // /2015-03-31/functions/{name}/policy/{statement-id}
212            (&Method::DELETE, 5, Some("functions"), Some("policy")) => "RemovePermission",
213            // /2015-03-31/event-source-mappings
214            (&Method::POST, 2, Some("event-source-mappings"), _) => "CreateEventSourceMapping",
215            (&Method::GET, 2, Some("event-source-mappings"), _) => "ListEventSourceMappings",
216            // /2015-03-31/event-source-mappings/{uuid}
217            (&Method::GET, 3, Some("event-source-mappings"), _) => "GetEventSourceMapping",
218            (&Method::DELETE, 3, Some("event-source-mappings"), _) => "DeleteEventSourceMapping",
219            _ => return None,
220        };
221
222        Some((action, resource))
223    }
224
225    fn create_function(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
226        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
227        let input = CreateFunctionInput::from_body(&body)?;
228
229        let mut accounts = self.state.write();
230        let state = accounts.get_or_create(&req.account_id);
231
232        if state.functions.contains_key(&input.function_name) {
233            return Err(AwsServiceError::aws_error(
234                StatusCode::CONFLICT,
235                "ResourceConflictException",
236                format!("Function already exist: {}", input.function_name),
237            ));
238        }
239
240        // Hash the actual ZIP bytes when available, falling back to the
241        // raw Code JSON so image-based functions still get a stable id.
242        let code_bytes = input.code_zip.as_deref().unwrap_or(&input.code_fallback);
243        let mut hasher = Sha256::new();
244        hasher.update(code_bytes);
245        let hash = hasher.finalize();
246        let code_sha256 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash);
247        let code_size = code_bytes.len() as i64;
248
249        let function_arn = format!(
250            "arn:aws:lambda:{}:{}:function:{}",
251            state.region, state.account_id, input.function_name
252        );
253        let now = Utc::now();
254
255        let func = LambdaFunction {
256            function_name: input.function_name.clone(),
257            function_arn,
258            runtime: input.runtime,
259            role: input.role,
260            handler: input.handler,
261            description: input.description,
262            timeout: input.timeout,
263            memory_size: input.memory_size,
264            code_sha256,
265            code_size,
266            version: "$LATEST".to_string(),
267            last_modified: now,
268            tags: input.tags,
269            environment: input.environment,
270            architectures: input.architectures,
271            package_type: input.package_type,
272            code_zip: input.code_zip,
273            policy: None,
274        };
275
276        let response = self.function_config_json(&func);
277
278        state.functions.insert(input.function_name, func);
279
280        Ok(AwsResponse::json(StatusCode::CREATED, response.to_string()))
281    }
282
283    fn get_function(
284        &self,
285        function_name: &str,
286        account_id: &str,
287        region: &str,
288    ) -> Result<AwsResponse, AwsServiceError> {
289        let accounts = self.state.read();
290        let empty = LambdaState::new(account_id, region);
291        let state = accounts.get(account_id).unwrap_or(&empty);
292        let func = state.functions.get(function_name).ok_or_else(|| {
293            AwsServiceError::aws_error(
294                StatusCode::NOT_FOUND,
295                "ResourceNotFoundException",
296                format!(
297                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
298                    state.region, state.account_id, function_name
299                ),
300            )
301        })?;
302
303        let config = self.function_config_json(func);
304        let response = json!({
305            "Code": {
306                "Location": format!("https://awslambda-{}-tasks.s3.{}.amazonaws.com/stub",
307                    func.function_arn.split(':').nth(3).unwrap_or("us-east-1"),
308                    func.function_arn.split(':').nth(3).unwrap_or("us-east-1")),
309                "RepositoryType": "S3"
310            },
311            "Configuration": config,
312            "Tags": func.tags,
313        });
314
315        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
316    }
317
318    fn delete_function(
319        &self,
320        function_name: &str,
321        account_id: &str,
322    ) -> Result<AwsResponse, AwsServiceError> {
323        let mut accounts = self.state.write();
324        let state = accounts.get_or_create(account_id);
325        let region = state.region.clone();
326        let account_id = state.account_id.clone();
327        if state.functions.remove(function_name).is_none() {
328            return Err(AwsServiceError::aws_error(
329                StatusCode::NOT_FOUND,
330                "ResourceNotFoundException",
331                format!(
332                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
333                    region, account_id, function_name
334                ),
335            ));
336        }
337
338        // Clean up any running container for this function
339        if let Some(ref runtime) = self.runtime {
340            let rt = runtime.clone();
341            let name = function_name.to_string();
342            tokio::spawn(async move { rt.stop_container(&name).await });
343        }
344
345        Ok(AwsResponse::json(StatusCode::NO_CONTENT, ""))
346    }
347
348    fn list_functions(&self, account_id: &str) -> Result<AwsResponse, AwsServiceError> {
349        let accounts = self.state.read();
350        let empty = LambdaState::new(account_id, "");
351        let state = accounts.get(account_id).unwrap_or(&empty);
352        let functions: Vec<Value> = state
353            .functions
354            .values()
355            .map(|f| self.function_config_json(f))
356            .collect();
357
358        let response = json!({
359            "Functions": functions,
360        });
361
362        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
363    }
364
365    async fn invoke(
366        &self,
367        function_name: &str,
368        payload: &[u8],
369        account_id: &str,
370    ) -> Result<AwsResponse, AwsServiceError> {
371        let func = {
372            let accounts = self.state.read();
373            let empty = LambdaState::new(account_id, "");
374            let state = accounts.get(account_id).unwrap_or(&empty);
375            state.functions.get(function_name).cloned().ok_or_else(|| {
376                AwsServiceError::aws_error(
377                    StatusCode::NOT_FOUND,
378                    "ResourceNotFoundException",
379                    format!(
380                        "Function not found: arn:aws:lambda:{}:{}:function:{}",
381                        state.region, state.account_id, function_name
382                    ),
383                )
384            })?
385        };
386
387        let runtime = self.runtime.as_ref().ok_or_else(|| {
388            AwsServiceError::aws_error(
389                StatusCode::INTERNAL_SERVER_ERROR,
390                "ServiceException",
391                "Docker/Podman is required for Lambda execution but is not available",
392            )
393        })?;
394
395        if func.code_zip.is_none() {
396            return Err(AwsServiceError::aws_error(
397                StatusCode::BAD_REQUEST,
398                "InvalidParameterValueException",
399                "Function has no deployment package",
400            ));
401        }
402
403        match runtime.invoke(&func, payload).await {
404            Ok(response_bytes) => {
405                let mut resp = AwsResponse::json(StatusCode::OK, response_bytes);
406                resp.headers.insert(
407                    http::header::HeaderName::from_static("x-amz-executed-version"),
408                    http::header::HeaderValue::from_static("$LATEST"),
409                );
410                Ok(resp)
411            }
412            Err(e) => {
413                tracing::error!(function = %function_name, error = %e, "Lambda invocation failed");
414                Err(AwsServiceError::aws_error(
415                    StatusCode::INTERNAL_SERVER_ERROR,
416                    "ServiceException",
417                    format!("Lambda execution failed: {e}"),
418                ))
419            }
420        }
421    }
422
423    fn publish_version(
424        &self,
425        function_name: &str,
426        account_id: &str,
427    ) -> Result<AwsResponse, AwsServiceError> {
428        let accounts = self.state.read();
429        let empty = LambdaState::new(account_id, "");
430        let state = accounts.get(account_id).unwrap_or(&empty);
431        let func = state.functions.get(function_name).ok_or_else(|| {
432            AwsServiceError::aws_error(
433                StatusCode::NOT_FOUND,
434                "ResourceNotFoundException",
435                format!(
436                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
437                    state.region, state.account_id, function_name
438                ),
439            )
440        })?;
441
442        let mut config = self.function_config_json(func);
443        // Stub: always return version "1"
444        config["Version"] = json!("1");
445        config["FunctionArn"] = json!(format!("{}:1", func.function_arn));
446
447        Ok(AwsResponse::json(StatusCode::CREATED, config.to_string()))
448    }
449
450    fn create_event_source_mapping(
451        &self,
452        req: &AwsRequest,
453    ) -> Result<AwsResponse, AwsServiceError> {
454        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
455        let event_source_arn = body["EventSourceArn"]
456            .as_str()
457            .ok_or_else(|| {
458                AwsServiceError::aws_error(
459                    StatusCode::BAD_REQUEST,
460                    "InvalidParameterValueException",
461                    "EventSourceArn is required",
462                )
463            })?
464            .to_string();
465
466        let function_name = body["FunctionName"]
467            .as_str()
468            .ok_or_else(|| {
469                AwsServiceError::aws_error(
470                    StatusCode::BAD_REQUEST,
471                    "InvalidParameterValueException",
472                    "FunctionName is required",
473                )
474            })?
475            .to_string();
476
477        let mut accounts = self.state.write();
478        let state = accounts.get_or_create(&req.account_id);
479
480        // Resolve function name to ARN
481        let function_arn = if function_name.starts_with("arn:") {
482            function_name.clone()
483        } else {
484            let func = state.functions.get(&function_name).ok_or_else(|| {
485                AwsServiceError::aws_error(
486                    StatusCode::NOT_FOUND,
487                    "ResourceNotFoundException",
488                    format!(
489                        "Function not found: arn:aws:lambda:{}:{}:function:{}",
490                        state.region, state.account_id, function_name
491                    ),
492                )
493            })?;
494            func.function_arn.clone()
495        };
496
497        let batch_size = body["BatchSize"].as_i64().unwrap_or(10);
498        let enabled = body["Enabled"].as_bool().unwrap_or(true);
499        let mapping_uuid = uuid::Uuid::new_v4().to_string();
500        let now = Utc::now();
501
502        let mapping = EventSourceMapping {
503            uuid: mapping_uuid.clone(),
504            function_arn: function_arn.clone(),
505            event_source_arn: event_source_arn.clone(),
506            batch_size,
507            enabled,
508            state: if enabled {
509                "Enabled".to_string()
510            } else {
511                "Disabled".to_string()
512            },
513            last_modified: now,
514        };
515
516        let response = self.event_source_mapping_json(&mapping);
517        state.event_source_mappings.insert(mapping_uuid, mapping);
518
519        Ok(AwsResponse::json(
520            StatusCode::ACCEPTED,
521            response.to_string(),
522        ))
523    }
524
525    fn list_event_source_mappings(&self, account_id: &str) -> Result<AwsResponse, AwsServiceError> {
526        let accounts = self.state.read();
527        let empty = LambdaState::new(account_id, "");
528        let state = accounts.get(account_id).unwrap_or(&empty);
529        let mappings: Vec<Value> = state
530            .event_source_mappings
531            .values()
532            .map(|m| self.event_source_mapping_json(m))
533            .collect();
534
535        let response = json!({
536            "EventSourceMappings": mappings,
537        });
538
539        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
540    }
541
542    fn get_event_source_mapping(
543        &self,
544        uuid: &str,
545        account_id: &str,
546    ) -> Result<AwsResponse, AwsServiceError> {
547        let accounts = self.state.read();
548        let empty = LambdaState::new(account_id, "");
549        let state = accounts.get(account_id).unwrap_or(&empty);
550        let mapping = state.event_source_mappings.get(uuid).ok_or_else(|| {
551            AwsServiceError::aws_error(
552                StatusCode::NOT_FOUND,
553                "ResourceNotFoundException",
554                format!("The resource you requested does not exist. (Service: Lambda, Status Code: 404, Request ID: {uuid})"),
555            )
556        })?;
557
558        let response = self.event_source_mapping_json(mapping);
559        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
560    }
561
562    fn delete_event_source_mapping(
563        &self,
564        uuid: &str,
565        account_id: &str,
566    ) -> Result<AwsResponse, AwsServiceError> {
567        let mut accounts = self.state.write();
568        let state = accounts.get_or_create(account_id);
569        let mapping = state.event_source_mappings.remove(uuid).ok_or_else(|| {
570            AwsServiceError::aws_error(
571                StatusCode::NOT_FOUND,
572                "ResourceNotFoundException",
573                format!("The resource you requested does not exist. (Service: Lambda, Status Code: 404, Request ID: {uuid})"),
574            )
575        })?;
576
577        let mut response = self.event_source_mapping_json(&mapping);
578        response["State"] = json!("Deleting");
579        Ok(AwsResponse::json(
580            StatusCode::ACCEPTED,
581            response.to_string(),
582        ))
583    }
584
585    fn function_config_json(&self, func: &LambdaFunction) -> Value {
586        let mut env_vars = json!({});
587        if !func.environment.is_empty() {
588            env_vars = json!({ "Variables": func.environment });
589        }
590
591        json!({
592            "FunctionName": func.function_name,
593            "FunctionArn": func.function_arn,
594            "Runtime": func.runtime,
595            "Role": func.role,
596            "Handler": func.handler,
597            "Description": func.description,
598            "Timeout": func.timeout,
599            "MemorySize": func.memory_size,
600            "CodeSha256": func.code_sha256,
601            "CodeSize": func.code_size,
602            "Version": func.version,
603            "LastModified": func.last_modified.format("%Y-%m-%dT%H:%M:%S%.3f+0000").to_string(),
604            "PackageType": func.package_type,
605            "Architectures": func.architectures,
606            "Environment": env_vars,
607            "State": "Active",
608            "LastUpdateStatus": "Successful",
609            "TracingConfig": { "Mode": "PassThrough" },
610            "RevisionId": uuid::Uuid::new_v4().to_string(),
611        })
612    }
613
614    fn event_source_mapping_json(&self, mapping: &EventSourceMapping) -> Value {
615        json!({
616            "UUID": mapping.uuid,
617            "FunctionArn": mapping.function_arn,
618            "EventSourceArn": mapping.event_source_arn,
619            "BatchSize": mapping.batch_size,
620            "State": mapping.state,
621            "LastModified": mapping.last_modified.timestamp_millis() as f64 / 1000.0,
622        })
623    }
624
625    /// Grant a permission on a Lambda function by appending a
626    /// statement to its resource-based policy.
627    ///
628    /// Mirrors AWS: the caller passes `(StatementId, Action,
629    /// Principal, SourceArn?, SourceAccount?)` and the service
630    /// composes a canonical policy document so that the existing
631    /// evaluator can read it without a Lambda-specific fork. Per the
632    /// S3 rollout's #427 evaluator, `SourceArn` becomes an `ArnLike`
633    /// Condition and `SourceAccount` becomes a `StringEquals`
634    /// Condition — both are already supported by the Phase 2 operator
635    /// set, so the permission gate behaves end-to-end without any new
636    /// evaluator code.
637    fn add_permission(
638        &self,
639        function_name: &str,
640        req: &AwsRequest,
641    ) -> Result<AwsResponse, AwsServiceError> {
642        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
643        let statement_id = body
644            .get("StatementId")
645            .and_then(|v| v.as_str())
646            .ok_or_else(|| {
647                AwsServiceError::aws_error(
648                    StatusCode::BAD_REQUEST,
649                    "InvalidParameterValueException",
650                    "StatementId is required",
651                )
652            })?
653            .to_string();
654        let action = body
655            .get("Action")
656            .and_then(|v| v.as_str())
657            .ok_or_else(|| {
658                AwsServiceError::aws_error(
659                    StatusCode::BAD_REQUEST,
660                    "InvalidParameterValueException",
661                    "Action is required",
662                )
663            })?
664            .to_string();
665        let principal_raw = body
666            .get("Principal")
667            .and_then(|v| v.as_str())
668            .ok_or_else(|| {
669                AwsServiceError::aws_error(
670                    StatusCode::BAD_REQUEST,
671                    "InvalidParameterValueException",
672                    "Principal is required",
673                )
674            })?
675            .to_string();
676        let source_arn = body
677            .get("SourceArn")
678            .and_then(|v| v.as_str())
679            .map(str::to_string);
680        let source_account = body
681            .get("SourceAccount")
682            .and_then(|v| v.as_str())
683            .map(str::to_string);
684
685        let mut accounts = self.state.write();
686        let state = accounts.get_or_create(&req.account_id);
687        let func = state.functions.get_mut(function_name).ok_or_else(|| {
688            AwsServiceError::aws_error(
689                StatusCode::NOT_FOUND,
690                "ResourceNotFoundException",
691                format!("Function not found: {function_name}"),
692            )
693        })?;
694
695        // Load current policy or seed a fresh canonical doc. Any
696        // stored blob that doesn't parse as a JSON object is treated
697        // as corrupt and replaced — `AddPermission` is the only
698        // mutation path for this field and it always writes valid
699        // JSON, so seeing a non-object here means something else
700        // wrote garbage, and silently propagating it would make
701        // later reads harder to debug.
702        let mut doc: Value = func
703            .policy
704            .as_deref()
705            .and_then(|s| serde_json::from_str::<Value>(s).ok())
706            .filter(|v| v.is_object())
707            .unwrap_or_else(|| json!({"Version": "2012-10-17", "Statement": []}));
708
709        // Ensure Statement is an array so we can push into it.
710        if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
711            doc["Statement"] = json!([]);
712        }
713        let statements = doc["Statement"].as_array_mut().unwrap();
714
715        // Reject duplicate StatementId — matches AWS's
716        // ResourceConflictException.
717        if statements
718            .iter()
719            .any(|s| s.get("Sid").and_then(|v| v.as_str()) == Some(statement_id.as_str()))
720        {
721            return Err(AwsServiceError::aws_error(
722                StatusCode::CONFLICT,
723                "ResourceConflictException",
724                format!("The statement id ({statement_id}) provided already exists"),
725            ));
726        }
727
728        // Canonicalize Principal: a service host string becomes
729        // `{"Service": "<host>"}`, an account-id or ARN becomes
730        // `{"AWS": "<raw>"}`. AWS accepts both shapes on the wire;
731        // storing the object form uniformly means the existing
732        // evaluator path handles everything without reading back the
733        // raw input.
734        let principal_value =
735            if principal_raw.ends_with(".amazonaws.com") || principal_raw.contains(".amazon") {
736                json!({ "Service": principal_raw })
737            } else {
738                json!({ "AWS": principal_raw })
739            };
740
741        // Emit SourceArn / SourceAccount as Condition keys so the
742        // existing Phase 2 ArnLike / StringEquals operators gate the
743        // grant without new evaluator code.
744        let mut condition = serde_json::Map::new();
745        if let Some(arn) = source_arn.as_ref() {
746            condition.insert("ArnLike".to_string(), json!({ "aws:SourceArn": arn }));
747        }
748        if let Some(acct) = source_account.as_ref() {
749            condition.insert(
750                "StringEquals".to_string(),
751                json!({ "aws:SourceAccount": acct }),
752            );
753        }
754
755        let mut new_statement = serde_json::Map::new();
756        new_statement.insert("Sid".to_string(), json!(statement_id));
757        new_statement.insert("Effect".to_string(), json!("Allow"));
758        new_statement.insert("Principal".to_string(), principal_value);
759        new_statement.insert("Action".to_string(), json!(format!("lambda:{action}")));
760        new_statement.insert("Resource".to_string(), json!(func.function_arn));
761        if !condition.is_empty() {
762            new_statement.insert("Condition".to_string(), Value::Object(condition));
763        }
764        let statement_json = Value::Object(new_statement);
765        statements.push(statement_json.clone());
766
767        func.policy = Some(serde_json::to_string(&doc).unwrap());
768
769        Ok(AwsResponse::json(
770            StatusCode::CREATED,
771            json!({ "Statement": serde_json::to_string(&statement_json).unwrap() }).to_string(),
772        ))
773    }
774
775    fn remove_permission(
776        &self,
777        function_name: &str,
778        statement_id: &str,
779        account_id: &str,
780    ) -> Result<AwsResponse, AwsServiceError> {
781        let mut accounts = self.state.write();
782        let state = accounts.get_or_create(account_id);
783        let func = state.functions.get_mut(function_name).ok_or_else(|| {
784            AwsServiceError::aws_error(
785                StatusCode::NOT_FOUND,
786                "ResourceNotFoundException",
787                format!("Function not found: {function_name}"),
788            )
789        })?;
790        let policy_str = func.policy.as_deref().ok_or_else(|| {
791            AwsServiceError::aws_error(
792                StatusCode::NOT_FOUND,
793                "ResourceNotFoundException",
794                format!("No policy is associated with function {function_name}"),
795            )
796        })?;
797        let mut doc: Value = serde_json::from_str(policy_str).map_err(|_| {
798            AwsServiceError::aws_error(
799                StatusCode::INTERNAL_SERVER_ERROR,
800                "InternalError",
801                "stored resource policy is not valid JSON",
802            )
803        })?;
804        let statements = doc
805            .get_mut("Statement")
806            .and_then(|s| s.as_array_mut())
807            .ok_or_else(|| {
808                AwsServiceError::aws_error(
809                    StatusCode::INTERNAL_SERVER_ERROR,
810                    "InternalError",
811                    "stored resource policy has no Statement array",
812                )
813            })?;
814        let before = statements.len();
815        statements.retain(|s| s.get("Sid").and_then(|v| v.as_str()) != Some(statement_id));
816        if statements.len() == before {
817            return Err(AwsServiceError::aws_error(
818                StatusCode::NOT_FOUND,
819                "ResourceNotFoundException",
820                format!("Statement {statement_id} is not found in resource policy"),
821            ));
822        }
823        // Leave an empty {"Statement":[]} behind rather than clearing
824        // the field to None — AWS's GetPolicy keeps returning the
825        // (empty) doc until the function itself is deleted.
826        func.policy = Some(serde_json::to_string(&doc).unwrap());
827        Ok(AwsResponse::json(StatusCode::NO_CONTENT, String::new()))
828    }
829
830    fn get_policy(
831        &self,
832        function_name: &str,
833        account_id: &str,
834    ) -> Result<AwsResponse, AwsServiceError> {
835        let accounts = self.state.read();
836        let empty = LambdaState::new(account_id, "");
837        let state = accounts.get(account_id).unwrap_or(&empty);
838        let func = state.functions.get(function_name).ok_or_else(|| {
839            AwsServiceError::aws_error(
840                StatusCode::NOT_FOUND,
841                "ResourceNotFoundException",
842                format!("Function not found: {function_name}"),
843            )
844        })?;
845        let policy = func.policy.as_deref().ok_or_else(|| {
846            AwsServiceError::aws_error(
847                StatusCode::NOT_FOUND,
848                "ResourceNotFoundException",
849                format!("No policy is associated with function {function_name}"),
850            )
851        })?;
852        Ok(AwsResponse::json(
853            StatusCode::OK,
854            json!({
855                "Policy": policy,
856                "RevisionId": uuid::Uuid::new_v4().to_string(),
857            })
858            .to_string(),
859        ))
860    }
861}
862
863#[async_trait]
864impl AwsService for LambdaService {
865    fn service_name(&self) -> &str {
866        "lambda"
867    }
868
869    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
870        let (action, resource_name) = Self::resolve_action(&req).ok_or_else(|| {
871            AwsServiceError::aws_error(
872                StatusCode::NOT_FOUND,
873                "UnknownOperationException",
874                format!("Unknown operation: {} {}", req.method, req.raw_path),
875            )
876        })?;
877
878        let mutates = matches!(
879            action,
880            "CreateFunction"
881                | "DeleteFunction"
882                | "PublishVersion"
883                | "AddPermission"
884                | "RemovePermission"
885                | "CreateEventSourceMapping"
886                | "DeleteEventSourceMapping"
887        );
888
889        let aid = &req.account_id;
890        let result = match action {
891            "CreateFunction" => self.create_function(&req),
892            "ListFunctions" => self.list_functions(aid),
893            "GetFunction" => self.get_function(
894                resource_name.as_deref().unwrap_or(""),
895                aid,
896                req.region.as_str(),
897            ),
898            "DeleteFunction" => self.delete_function(resource_name.as_deref().unwrap_or(""), aid),
899            "Invoke" => {
900                self.invoke(resource_name.as_deref().unwrap_or(""), &req.body, aid)
901                    .await
902            }
903            "PublishVersion" => self.publish_version(resource_name.as_deref().unwrap_or(""), aid),
904            "AddPermission" => self.add_permission(resource_name.as_deref().unwrap_or(""), &req),
905            "GetPolicy" => self.get_policy(resource_name.as_deref().unwrap_or(""), aid),
906            "RemovePermission" => {
907                // Path: /2015-03-31/functions/{name}/policy/{sid}
908                let sid = req.path_segments.get(4).cloned().unwrap_or_default();
909                self.remove_permission(resource_name.as_deref().unwrap_or(""), &sid, aid)
910            }
911            "CreateEventSourceMapping" => self.create_event_source_mapping(&req),
912            "ListEventSourceMappings" => self.list_event_source_mappings(aid),
913            "GetEventSourceMapping" => {
914                self.get_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
915            }
916            "DeleteEventSourceMapping" => {
917                self.delete_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
918            }
919            _ => Err(AwsServiceError::action_not_implemented("lambda", action)),
920        };
921        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
922            self.save_snapshot().await;
923        }
924        result
925    }
926
927    fn supported_actions(&self) -> &[&str] {
928        &[
929            "CreateFunction",
930            "GetFunction",
931            "DeleteFunction",
932            "ListFunctions",
933            "Invoke",
934            "PublishVersion",
935            "AddPermission",
936            "RemovePermission",
937            "GetPolicy",
938            "CreateEventSourceMapping",
939            "ListEventSourceMappings",
940            "GetEventSourceMapping",
941            "DeleteEventSourceMapping",
942        ]
943    }
944
945    fn iam_enforceable(&self) -> bool {
946        true
947    }
948
949    /// Lambda resources are function ARNs. Function-scoped ops
950    /// resolve the target ARN from the path; list ops target `*`
951    /// (the whole service), matching how AWS models them.
952    fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
953        // REST-JSON services don't have `request.action` populated at
954        // dispatch time — it's derived from method+path inside
955        // `handle()`. Reuse the same resolver so the two can never
956        // drift.
957        let (action_str, resource_name) = Self::resolve_action(request)?;
958        let action: &'static str = match action_str {
959            "CreateFunction" => "CreateFunction",
960            "ListFunctions" => "ListFunctions",
961            "GetFunction" => "GetFunction",
962            "DeleteFunction" => "DeleteFunction",
963            "Invoke" => "InvokeFunction",
964            "PublishVersion" => "PublishVersion",
965            "AddPermission" => "AddPermission",
966            "RemovePermission" => "RemovePermission",
967            "GetPolicy" => "GetPolicy",
968            "CreateEventSourceMapping" => "CreateEventSourceMapping",
969            "ListEventSourceMappings" => "ListEventSourceMappings",
970            "GetEventSourceMapping" => "GetEventSourceMapping",
971            "DeleteEventSourceMapping" => "DeleteEventSourceMapping",
972            _ => return None,
973        };
974        let accounts = self.state.read();
975        let empty = LambdaState::new(&request.account_id, &request.region);
976        let state = accounts.get(&request.account_id).unwrap_or(&empty);
977        let resource = match action {
978            "GetFunction" | "DeleteFunction" | "InvokeFunction" | "PublishVersion"
979            | "AddPermission" | "RemovePermission" | "GetPolicy" => {
980                let name = resource_name.unwrap_or_default();
981                if name.is_empty() {
982                    "*".to_string()
983                } else {
984                    format!(
985                        "arn:aws:lambda:{}:{}:function:{}",
986                        state.region, state.account_id, name
987                    )
988                }
989            }
990            "CreateFunction" => {
991                // Best-effort: parse the FunctionName from the body so
992                // CreateFunction can be resource-scoped against the
993                // to-be-created ARN. Falls back to `*` when the body
994                // isn't JSON yet (e.g. soft-mode observability).
995                serde_json::from_slice::<Value>(&request.body)
996                    .ok()
997                    .and_then(|v| {
998                        v.get("FunctionName").and_then(|f| f.as_str()).map(|n| {
999                            format!(
1000                                "arn:aws:lambda:{}:{}:function:{}",
1001                                state.region, state.account_id, n
1002                            )
1003                        })
1004                    })
1005                    .unwrap_or_else(|| "*".to_string())
1006            }
1007            _ => "*".to_string(),
1008        };
1009        Some(fakecloud_core::auth::IamAction {
1010            service: "lambda",
1011            action,
1012            resource,
1013        })
1014    }
1015
1016    fn iam_condition_keys_for(
1017        &self,
1018        request: &AwsRequest,
1019        action: &fakecloud_core::auth::IamAction,
1020    ) -> std::collections::BTreeMap<String, Vec<String>> {
1021        let mut out = std::collections::BTreeMap::new();
1022        if action.action == "AddPermission" {
1023            if action.resource != "*" {
1024                out.insert(
1025                    "lambda:functionarn".to_string(),
1026                    vec![action.resource.clone()],
1027                );
1028            }
1029            if let Ok(body) = serde_json::from_slice::<Value>(&request.body) {
1030                if let Some(principal) = body.get("Principal").and_then(|p| p.as_str()) {
1031                    out.insert("lambda:principal".to_string(), vec![principal.to_string()]);
1032                }
1033            }
1034        }
1035        out
1036    }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042    use bytes::Bytes;
1043    use http::{HeaderMap, Method};
1044    use parking_lot::RwLock;
1045    use std::collections::HashMap;
1046    use std::sync::Arc;
1047
1048    fn make_state() -> SharedLambdaState {
1049        Arc::new(RwLock::new(
1050            fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
1051        ))
1052    }
1053
1054    fn make_request(method: Method, path: &str, body: &str) -> AwsRequest {
1055        let path_segments: Vec<String> = path
1056            .split('/')
1057            .filter(|s| !s.is_empty())
1058            .map(|s| s.to_string())
1059            .collect();
1060        AwsRequest {
1061            service: "lambda".to_string(),
1062            action: String::new(),
1063            region: "us-east-1".to_string(),
1064            account_id: "123456789012".to_string(),
1065            request_id: "test-request-id".to_string(),
1066            headers: HeaderMap::new(),
1067            query_params: HashMap::new(),
1068            body: Bytes::from(body.to_string()),
1069            path_segments,
1070            raw_path: path.to_string(),
1071            raw_query: String::new(),
1072            method,
1073            is_query_protocol: false,
1074            access_key_id: None,
1075            principal: None,
1076        }
1077    }
1078
1079    #[test]
1080    fn iam_condition_keys_for_add_permission_populates_arn_and_principal() {
1081        let svc = LambdaService::new(make_state());
1082        let body = json!({
1083            "StatementId": "stmt",
1084            "Action": "lambda:InvokeFunction",
1085            "Principal": "s3.amazonaws.com",
1086        })
1087        .to_string();
1088        let req = make_request(Method::POST, "/2015-03-31/functions/my-func/policy", &body);
1089        let action = fakecloud_core::auth::IamAction {
1090            service: "lambda",
1091            action: "AddPermission",
1092            resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string(),
1093        };
1094        let keys = svc.iam_condition_keys_for(&req, &action);
1095        assert_eq!(
1096            keys.get("lambda:functionarn"),
1097            Some(&vec![
1098                "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string()
1099            ])
1100        );
1101        assert_eq!(
1102            keys.get("lambda:principal"),
1103            Some(&vec!["s3.amazonaws.com".to_string()])
1104        );
1105    }
1106
1107    #[test]
1108    fn iam_condition_keys_for_add_permission_omits_missing_principal() {
1109        let svc = LambdaService::new(make_state());
1110        let body = json!({"StatementId": "stmt", "Action": "lambda:InvokeFunction"}).to_string();
1111        let req = make_request(Method::POST, "/2015-03-31/functions/my-func/policy", &body);
1112        let action = fakecloud_core::auth::IamAction {
1113            service: "lambda",
1114            action: "AddPermission",
1115            resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string(),
1116        };
1117        let keys = svc.iam_condition_keys_for(&req, &action);
1118        assert!(!keys.contains_key("lambda:principal"));
1119        assert!(keys.contains_key("lambda:functionarn"));
1120    }
1121
1122    #[test]
1123    fn iam_condition_keys_for_non_add_permission_is_empty() {
1124        let svc = LambdaService::new(make_state());
1125        let req = make_request(Method::GET, "/2015-03-31/functions/my-func", "");
1126        let action = fakecloud_core::auth::IamAction {
1127            service: "lambda",
1128            action: "GetFunction",
1129            resource: "arn:aws:lambda:us-east-1:123456789012:function:my-func".to_string(),
1130        };
1131        assert!(svc.iam_condition_keys_for(&req, &action).is_empty());
1132    }
1133
1134    #[tokio::test]
1135    async fn test_create_and_get_function() {
1136        let state = make_state();
1137        let svc = LambdaService::new(state);
1138
1139        let create_body = json!({
1140            "FunctionName": "my-func",
1141            "Runtime": "python3.12",
1142            "Role": "arn:aws:iam::123456789012:role/test-role",
1143            "Handler": "index.handler",
1144            "Code": { "ZipFile": "UEsFBgAAAAAAAAAAAAAAAAAAAAA=" }
1145        });
1146
1147        let req = make_request(
1148            Method::POST,
1149            "/2015-03-31/functions",
1150            &create_body.to_string(),
1151        );
1152        let resp = svc.handle(req).await.unwrap();
1153        assert_eq!(resp.status, StatusCode::CREATED);
1154
1155        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1156        assert_eq!(body["FunctionName"], "my-func");
1157        assert_eq!(body["Runtime"], "python3.12");
1158
1159        // Get
1160        let req = make_request(Method::GET, "/2015-03-31/functions/my-func", "");
1161        let resp = svc.handle(req).await.unwrap();
1162        assert_eq!(resp.status, StatusCode::OK);
1163        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1164        assert_eq!(body["Configuration"]["FunctionName"], "my-func");
1165    }
1166
1167    #[tokio::test]
1168    async fn test_delete_function() {
1169        let state = make_state();
1170        let svc = LambdaService::new(state);
1171
1172        let create_body = json!({
1173            "FunctionName": "to-delete",
1174            "Runtime": "nodejs20.x",
1175            "Role": "arn:aws:iam::123456789012:role/test",
1176            "Handler": "index.handler",
1177            "Code": {}
1178        });
1179
1180        let req = make_request(
1181            Method::POST,
1182            "/2015-03-31/functions",
1183            &create_body.to_string(),
1184        );
1185        svc.handle(req).await.unwrap();
1186
1187        let req = make_request(Method::DELETE, "/2015-03-31/functions/to-delete", "");
1188        let resp = svc.handle(req).await.unwrap();
1189        assert_eq!(resp.status, StatusCode::NO_CONTENT);
1190
1191        // Verify deleted
1192        let req = make_request(Method::GET, "/2015-03-31/functions/to-delete", "");
1193        let resp = svc.handle(req).await;
1194        assert!(resp.is_err());
1195    }
1196
1197    #[tokio::test]
1198    async fn test_invoke_without_runtime_returns_error() {
1199        let state = make_state();
1200        let svc = LambdaService::new(state);
1201
1202        let create_body = json!({
1203            "FunctionName": "invoke-me",
1204            "Runtime": "python3.12",
1205            "Role": "arn:aws:iam::123456789012:role/test",
1206            "Handler": "index.handler",
1207            "Code": {}
1208        });
1209
1210        let req = make_request(
1211            Method::POST,
1212            "/2015-03-31/functions",
1213            &create_body.to_string(),
1214        );
1215        svc.handle(req).await.unwrap();
1216
1217        let req = make_request(
1218            Method::POST,
1219            "/2015-03-31/functions/invoke-me/invocations",
1220            r#"{"key": "value"}"#,
1221        );
1222        let resp = svc.handle(req).await;
1223        assert!(resp.is_err());
1224    }
1225
1226    #[tokio::test]
1227    async fn test_invoke_nonexistent_function() {
1228        let state = make_state();
1229        let svc = LambdaService::new(state);
1230
1231        let req = make_request(
1232            Method::POST,
1233            "/2015-03-31/functions/does-not-exist/invocations",
1234            "{}",
1235        );
1236        let resp = svc.handle(req).await;
1237        assert!(resp.is_err());
1238    }
1239
1240    #[tokio::test]
1241    async fn test_list_functions() {
1242        let state = make_state();
1243        let svc = LambdaService::new(state);
1244
1245        for name in &["func-a", "func-b"] {
1246            let create_body = json!({
1247                "FunctionName": name,
1248                "Runtime": "python3.12",
1249                "Role": "arn:aws:iam::123456789012:role/test",
1250                "Handler": "index.handler",
1251                "Code": {}
1252            });
1253            let req = make_request(
1254                Method::POST,
1255                "/2015-03-31/functions",
1256                &create_body.to_string(),
1257            );
1258            svc.handle(req).await.unwrap();
1259        }
1260
1261        let req = make_request(Method::GET, "/2015-03-31/functions", "");
1262        let resp = svc.handle(req).await.unwrap();
1263        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1264        assert_eq!(body["Functions"].as_array().unwrap().len(), 2);
1265    }
1266
1267    #[tokio::test]
1268    async fn test_event_source_mapping() {
1269        let state = make_state();
1270        let svc = LambdaService::new(state);
1271
1272        // Create function first
1273        let create_body = json!({
1274            "FunctionName": "esm-func",
1275            "Runtime": "python3.12",
1276            "Role": "arn:aws:iam::123456789012:role/test",
1277            "Handler": "index.handler",
1278            "Code": {}
1279        });
1280        let req = make_request(
1281            Method::POST,
1282            "/2015-03-31/functions",
1283            &create_body.to_string(),
1284        );
1285        svc.handle(req).await.unwrap();
1286
1287        // Create mapping
1288        let mapping_body = json!({
1289            "FunctionName": "esm-func",
1290            "EventSourceArn": "arn:aws:sqs:us-east-1:123456789012:my-queue",
1291            "BatchSize": 5
1292        });
1293        let req = make_request(
1294            Method::POST,
1295            "/2015-03-31/event-source-mappings",
1296            &mapping_body.to_string(),
1297        );
1298        let resp = svc.handle(req).await.unwrap();
1299        assert_eq!(resp.status, StatusCode::ACCEPTED);
1300        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1301        let uuid = body["UUID"].as_str().unwrap().to_string();
1302
1303        // List mappings
1304        let req = make_request(Method::GET, "/2015-03-31/event-source-mappings", "");
1305        let resp = svc.handle(req).await.unwrap();
1306        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1307        assert_eq!(body["EventSourceMappings"].as_array().unwrap().len(), 1);
1308
1309        // Delete mapping
1310        let req = make_request(
1311            Method::DELETE,
1312            &format!("/2015-03-31/event-source-mappings/{uuid}"),
1313            "",
1314        );
1315        let resp = svc.handle(req).await.unwrap();
1316        assert_eq!(resp.status, StatusCode::ACCEPTED);
1317    }
1318
1319    async fn seed_function(svc: &LambdaService, name: &str) {
1320        let body = json!({
1321            "FunctionName": name,
1322            "Runtime": "python3.12",
1323            "Role": "arn:aws:iam::123456789012:role/r",
1324            "Handler": "index.handler",
1325            "Code": {}
1326        });
1327        let req = make_request(Method::POST, "/2015-03-31/functions", &body.to_string());
1328        svc.handle(req).await.unwrap();
1329    }
1330
1331    #[tokio::test]
1332    async fn add_permission_builds_canonical_statement() {
1333        let svc = LambdaService::new(make_state());
1334        seed_function(&svc, "f").await;
1335
1336        let body = json!({
1337            "StatementId": "s3-invoke",
1338            "Action": "InvokeFunction",
1339            "Principal": "s3.amazonaws.com",
1340            "SourceArn": "arn:aws:s3:::my-bucket",
1341            "SourceAccount": "123456789012",
1342        });
1343        let req = make_request(
1344            Method::POST,
1345            "/2015-03-31/functions/f/policy",
1346            &body.to_string(),
1347        );
1348        let resp = svc.handle(req).await.unwrap();
1349        assert_eq!(resp.status, StatusCode::CREATED);
1350
1351        let out: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1352        let statement: Value = serde_json::from_str(out["Statement"].as_str().unwrap()).unwrap();
1353        assert_eq!(statement["Sid"], "s3-invoke");
1354        assert_eq!(statement["Effect"], "Allow");
1355        assert_eq!(statement["Principal"]["Service"], "s3.amazonaws.com");
1356        assert_eq!(statement["Action"], "lambda:InvokeFunction");
1357        assert_eq!(
1358            statement["Resource"],
1359            "arn:aws:lambda:us-east-1:123456789012:function:f"
1360        );
1361        assert_eq!(
1362            statement["Condition"]["ArnLike"]["aws:SourceArn"],
1363            "arn:aws:s3:::my-bucket"
1364        );
1365        assert_eq!(
1366            statement["Condition"]["StringEquals"]["aws:SourceAccount"],
1367            "123456789012"
1368        );
1369    }
1370
1371    #[tokio::test]
1372    async fn add_permission_aws_principal_emits_aws_key() {
1373        let svc = LambdaService::new(make_state());
1374        seed_function(&svc, "f").await;
1375
1376        let body = json!({
1377            "StatementId": "user-invoke",
1378            "Action": "InvokeFunction",
1379            "Principal": "arn:aws:iam::123456789012:user/alice",
1380        });
1381        let req = make_request(
1382            Method::POST,
1383            "/2015-03-31/functions/f/policy",
1384            &body.to_string(),
1385        );
1386        svc.handle(req).await.unwrap();
1387
1388        // Fetch via GetPolicy and inspect the stored doc.
1389        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
1390        let resp = svc.handle(req).await.unwrap();
1391        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1392        let doc: Value = serde_json::from_str(body["Policy"].as_str().unwrap()).unwrap();
1393        let statements = doc["Statement"].as_array().unwrap();
1394        assert_eq!(statements.len(), 1);
1395        assert_eq!(
1396            statements[0]["Principal"]["AWS"],
1397            "arn:aws:iam::123456789012:user/alice"
1398        );
1399        assert!(statements[0].get("Condition").is_none());
1400    }
1401
1402    #[tokio::test]
1403    async fn add_permission_rejects_duplicate_statement_id() {
1404        let svc = LambdaService::new(make_state());
1405        seed_function(&svc, "f").await;
1406
1407        let body = json!({
1408            "StatementId": "dup",
1409            "Action": "InvokeFunction",
1410            "Principal": "arn:aws:iam::123456789012:user/a",
1411        });
1412        let req = make_request(
1413            Method::POST,
1414            "/2015-03-31/functions/f/policy",
1415            &body.to_string(),
1416        );
1417        svc.handle(req).await.unwrap();
1418
1419        let req = make_request(
1420            Method::POST,
1421            "/2015-03-31/functions/f/policy",
1422            &body.to_string(),
1423        );
1424        let err = match svc.handle(req).await {
1425            Err(e) => e,
1426            Ok(_) => panic!("expected error"),
1427        };
1428        assert_eq!(err.status(), StatusCode::CONFLICT);
1429    }
1430
1431    #[tokio::test]
1432    async fn get_policy_returns_404_when_no_policy_attached() {
1433        let svc = LambdaService::new(make_state());
1434        seed_function(&svc, "f").await;
1435
1436        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
1437        let err = match svc.handle(req).await {
1438            Err(e) => e,
1439            Ok(_) => panic!("expected error"),
1440        };
1441        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1442    }
1443
1444    #[tokio::test]
1445    async fn remove_permission_strips_matching_sid_and_leaves_empty_doc() {
1446        let svc = LambdaService::new(make_state());
1447        seed_function(&svc, "f").await;
1448
1449        for sid in ["a", "b"] {
1450            let body = json!({
1451                "StatementId": sid,
1452                "Action": "InvokeFunction",
1453                "Principal": "arn:aws:iam::123456789012:user/u",
1454            });
1455            let req = make_request(
1456                Method::POST,
1457                "/2015-03-31/functions/f/policy",
1458                &body.to_string(),
1459            );
1460            svc.handle(req).await.unwrap();
1461        }
1462
1463        // Remove "a"
1464        let req = make_request(Method::DELETE, "/2015-03-31/functions/f/policy/a", "");
1465        let resp = svc.handle(req).await.unwrap();
1466        assert_eq!(resp.status, StatusCode::NO_CONTENT);
1467
1468        // GetPolicy still returns the doc with just "b".
1469        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
1470        let resp = svc.handle(req).await.unwrap();
1471        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1472        let doc: Value = serde_json::from_str(body["Policy"].as_str().unwrap()).unwrap();
1473        let stmts = doc["Statement"].as_array().unwrap();
1474        assert_eq!(stmts.len(), 1);
1475        assert_eq!(stmts[0]["Sid"], "b");
1476
1477        // Remove the last one — doc stays (empty Statement array).
1478        let req = make_request(Method::DELETE, "/2015-03-31/functions/f/policy/b", "");
1479        svc.handle(req).await.unwrap();
1480
1481        let req = make_request(Method::GET, "/2015-03-31/functions/f/policy", "");
1482        let resp = svc.handle(req).await.unwrap();
1483        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1484        let doc: Value = serde_json::from_str(body["Policy"].as_str().unwrap()).unwrap();
1485        assert_eq!(doc["Statement"].as_array().unwrap().len(), 0);
1486    }
1487
1488    #[tokio::test]
1489    async fn remove_permission_unknown_sid_is_404() {
1490        let svc = LambdaService::new(make_state());
1491        seed_function(&svc, "f").await;
1492
1493        let body = json!({
1494            "StatementId": "known",
1495            "Action": "InvokeFunction",
1496            "Principal": "arn:aws:iam::123456789012:user/u",
1497        });
1498        let req = make_request(
1499            Method::POST,
1500            "/2015-03-31/functions/f/policy",
1501            &body.to_string(),
1502        );
1503        svc.handle(req).await.unwrap();
1504
1505        let req = make_request(Method::DELETE, "/2015-03-31/functions/f/policy/other", "");
1506        let err = match svc.handle(req).await {
1507            Err(e) => e,
1508            Ok(_) => panic!("expected error"),
1509        };
1510        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1511    }
1512
1513    #[tokio::test]
1514    async fn add_permission_on_missing_function_is_404() {
1515        let svc = LambdaService::new(make_state());
1516        let body = json!({
1517            "StatementId": "s",
1518            "Action": "InvokeFunction",
1519            "Principal": "arn:aws:iam::123456789012:user/u",
1520        });
1521        let req = make_request(
1522            Method::POST,
1523            "/2015-03-31/functions/missing/policy",
1524            &body.to_string(),
1525        );
1526        let err = match svc.handle(req).await {
1527            Err(e) => e,
1528            Ok(_) => panic!("expected error"),
1529        };
1530        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1531    }
1532
1533    #[test]
1534    fn iam_action_for_maps_invoke_to_function_arn() {
1535        let svc = LambdaService::new(make_state());
1536        let req = make_request(Method::POST, "/2015-03-31/functions/f/invocations", "");
1537        let action = svc.iam_action_for(&req).unwrap();
1538        assert_eq!(action.service, "lambda");
1539        assert_eq!(action.action, "InvokeFunction");
1540        assert_eq!(
1541            action.resource,
1542            "arn:aws:lambda:us-east-1:123456789012:function:f"
1543        );
1544    }
1545
1546    #[test]
1547    fn iam_action_for_maps_list_to_star() {
1548        let svc = LambdaService::new(make_state());
1549        let req = make_request(Method::GET, "/2015-03-31/functions", "");
1550        let action = svc.iam_action_for(&req).unwrap();
1551        assert_eq!(action.action, "ListFunctions");
1552        assert_eq!(action.resource, "*");
1553    }
1554
1555    #[test]
1556    fn iam_action_for_create_reads_function_name_from_body() {
1557        let svc = LambdaService::new(make_state());
1558        let body = json!({
1559            "FunctionName": "newfn",
1560            "Runtime": "python3.12",
1561            "Role": "arn:aws:iam::123456789012:role/r",
1562            "Handler": "index.handler",
1563            "Code": {}
1564        });
1565        let req = make_request(Method::POST, "/2015-03-31/functions", &body.to_string());
1566        let action = svc.iam_action_for(&req).unwrap();
1567        assert_eq!(action.action, "CreateFunction");
1568        assert_eq!(
1569            action.resource,
1570            "arn:aws:lambda:us-east-1:123456789012:function:newfn"
1571        );
1572    }
1573
1574    // ── Error branch tests ──
1575
1576    #[tokio::test]
1577    async fn create_function_duplicate_returns_conflict() {
1578        let svc = LambdaService::new(make_state());
1579        seed_function(&svc, "dup-fn").await;
1580
1581        let body = json!({
1582            "FunctionName": "dup-fn",
1583            "Runtime": "python3.12",
1584            "Role": "arn:aws:iam::123456789012:role/r",
1585            "Handler": "index.handler",
1586            "Code": {"ZipFile": "UEsDBBQ="},
1587        });
1588        let req = make_request(Method::POST, "/2015-03-31/functions", &body.to_string());
1589        let err = match svc.handle(req).await {
1590            Err(e) => e,
1591            Ok(_) => panic!("expected ResourceConflictException"),
1592        };
1593        assert_eq!(err.status(), StatusCode::CONFLICT);
1594    }
1595
1596    #[tokio::test]
1597    async fn get_function_not_found() {
1598        let svc = LambdaService::new(make_state());
1599        let req = make_request(Method::GET, "/2015-03-31/functions/nope", "");
1600        let err = match svc.handle(req).await {
1601            Err(e) => e,
1602            Ok(_) => panic!("expected error"),
1603        };
1604        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1605    }
1606
1607    #[tokio::test]
1608    async fn delete_function_not_found() {
1609        let svc = LambdaService::new(make_state());
1610        let req = make_request(Method::DELETE, "/2015-03-31/functions/nope", "");
1611        let err = match svc.handle(req).await {
1612            Err(e) => e,
1613            Ok(_) => panic!("expected error"),
1614        };
1615        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1616    }
1617
1618    #[tokio::test]
1619    async fn get_event_source_mapping_not_found() {
1620        let svc = LambdaService::new(make_state());
1621        let req = make_request(
1622            Method::GET,
1623            "/2015-03-31/event-source-mappings/nonexistent",
1624            "",
1625        );
1626        let err = match svc.handle(req).await {
1627            Err(e) => e,
1628            Ok(_) => panic!("expected error"),
1629        };
1630        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1631    }
1632
1633    #[tokio::test]
1634    async fn delete_event_source_mapping_not_found() {
1635        let svc = LambdaService::new(make_state());
1636        let req = make_request(
1637            Method::DELETE,
1638            "/2015-03-31/event-source-mappings/nonexistent",
1639            "",
1640        );
1641        let err = match svc.handle(req).await {
1642            Err(e) => e,
1643            Ok(_) => panic!("expected error"),
1644        };
1645        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1646    }
1647
1648    #[tokio::test]
1649    async fn get_policy_on_missing_function() {
1650        let svc = LambdaService::new(make_state());
1651        let req = make_request(Method::GET, "/2015-03-31/functions/nope/policy", "");
1652        let err = match svc.handle(req).await {
1653            Err(e) => e,
1654            Ok(_) => panic!("expected error"),
1655        };
1656        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1657    }
1658
1659    #[tokio::test]
1660    async fn remove_permission_on_missing_function() {
1661        let svc = LambdaService::new(make_state());
1662        let req = make_request(
1663            Method::DELETE,
1664            "/2015-03-31/functions/nope/policy/stmt1",
1665            "",
1666        );
1667        let err = match svc.handle(req).await {
1668            Err(e) => e,
1669            Ok(_) => panic!("expected error"),
1670        };
1671        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1672    }
1673
1674    #[tokio::test]
1675    async fn publish_version_on_missing_function() {
1676        let svc = LambdaService::new(make_state());
1677        let req = make_request(Method::POST, "/2015-03-31/functions/nope/versions", "{}");
1678        let err = match svc.handle(req).await {
1679            Err(e) => e,
1680            Ok(_) => panic!("expected error"),
1681        };
1682        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1683    }
1684
1685    #[tokio::test]
1686    async fn unknown_route_returns_error() {
1687        let svc = LambdaService::new(make_state());
1688        let req = make_request(Method::POST, "/unknown/route", "{}");
1689        assert!(svc.handle(req).await.is_err());
1690    }
1691
1692    #[tokio::test]
1693    async fn publish_version_unknown_function_errors() {
1694        let svc = LambdaService::new(make_state());
1695        assert!(svc.publish_version("ghost", "123456789012").is_err());
1696    }
1697
1698    #[tokio::test]
1699    async fn get_function_unknown_errors() {
1700        let svc = LambdaService::new(make_state());
1701        assert!(svc
1702            .get_function("ghost", "123456789012", "us-east-1")
1703            .is_err());
1704    }
1705
1706    #[tokio::test]
1707    async fn delete_function_unknown_errors() {
1708        let svc = LambdaService::new(make_state());
1709        assert!(svc.delete_function("ghost", "123456789012").is_err());
1710    }
1711
1712    #[tokio::test]
1713    async fn get_event_source_mapping_unknown_errors() {
1714        let svc = LambdaService::new(make_state());
1715        assert!(svc
1716            .get_event_source_mapping("ghost", "123456789012")
1717            .is_err());
1718    }
1719
1720    #[tokio::test]
1721    async fn delete_event_source_mapping_unknown_errors() {
1722        let svc = LambdaService::new(make_state());
1723        assert!(svc
1724            .delete_event_source_mapping("ghost", "123456789012")
1725            .is_err());
1726    }
1727
1728    #[tokio::test]
1729    async fn list_functions_empty_ok() {
1730        let svc = LambdaService::new(make_state());
1731        let resp = svc.list_functions("123456789012").unwrap();
1732        assert_eq!(resp.status, http::StatusCode::OK);
1733    }
1734
1735    #[tokio::test]
1736    async fn list_event_source_mappings_empty_ok() {
1737        let svc = LambdaService::new(make_state());
1738        let resp = svc.list_event_source_mappings("123456789012").unwrap();
1739        assert_eq!(resp.status, http::StatusCode::OK);
1740    }
1741}