Skip to main content

fakecloud_lambda/
service.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use chrono::Utc;
5use http::{Method, StatusCode};
6use serde_json::{json, Value};
7use sha2::{Digest, Sha256};
8
9use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
10
11use crate::runtime::ContainerRuntime;
12use crate::state::{EventSourceMapping, LambdaFunction, SharedLambdaState};
13
14pub struct LambdaService {
15    state: SharedLambdaState,
16    runtime: Option<Arc<ContainerRuntime>>,
17}
18
19impl LambdaService {
20    pub fn new(state: SharedLambdaState) -> Self {
21        Self {
22            state,
23            runtime: None,
24        }
25    }
26
27    pub fn with_runtime(mut self, runtime: Arc<ContainerRuntime>) -> Self {
28        self.runtime = Some(runtime);
29        self
30    }
31
32    /// Determine the action from the HTTP method and path segments.
33    /// Lambda uses REST-style routing:
34    ///   POST   /2015-03-31/functions                         -> CreateFunction
35    ///   GET    /2015-03-31/functions                         -> ListFunctions
36    ///   GET    /2015-03-31/functions/{name}                  -> GetFunction
37    ///   DELETE /2015-03-31/functions/{name}                  -> DeleteFunction
38    ///   POST   /2015-03-31/functions/{name}/invocations      -> Invoke
39    ///   POST   /2015-03-31/functions/{name}/versions         -> PublishVersion
40    ///   POST   /2015-03-31/event-source-mappings             -> CreateEventSourceMapping
41    ///   GET    /2015-03-31/event-source-mappings             -> ListEventSourceMappings
42    ///   GET    /2015-03-31/event-source-mappings/{uuid}      -> GetEventSourceMapping
43    ///   DELETE /2015-03-31/event-source-mappings/{uuid}      -> DeleteEventSourceMapping
44    fn resolve_action(req: &AwsRequest) -> Option<(&str, Option<String>)> {
45        let segs = &req.path_segments;
46        if segs.is_empty() {
47            return None;
48        }
49
50        // Expect first segment to be "2015-03-31"
51        if segs[0] != "2015-03-31" {
52            return None;
53        }
54
55        match (req.method.clone(), segs.len()) {
56            // /2015-03-31/functions
57            (Method::POST, 2) if segs[1] == "functions" => Some(("CreateFunction", None)),
58            (Method::GET, 2) if segs[1] == "functions" => Some(("ListFunctions", None)),
59            // /2015-03-31/functions/{name}
60            (Method::GET, 3) if segs[1] == "functions" => {
61                Some(("GetFunction", Some(segs[2].clone())))
62            }
63            (Method::DELETE, 3) if segs[1] == "functions" => {
64                Some(("DeleteFunction", Some(segs[2].clone())))
65            }
66            // /2015-03-31/functions/{name}/invocations
67            (Method::POST, 4) if segs[1] == "functions" && segs[3] == "invocations" => {
68                Some(("Invoke", Some(segs[2].clone())))
69            }
70            // /2015-03-31/functions/{name}/versions
71            (Method::POST, 4) if segs[1] == "functions" && segs[3] == "versions" => {
72                Some(("PublishVersion", Some(segs[2].clone())))
73            }
74            // /2015-03-31/event-source-mappings
75            (Method::POST, 2) if segs[1] == "event-source-mappings" => {
76                Some(("CreateEventSourceMapping", None))
77            }
78            (Method::GET, 2) if segs[1] == "event-source-mappings" => {
79                Some(("ListEventSourceMappings", None))
80            }
81            // /2015-03-31/event-source-mappings/{uuid}
82            (Method::GET, 3) if segs[1] == "event-source-mappings" => {
83                Some(("GetEventSourceMapping", Some(segs[2].clone())))
84            }
85            (Method::DELETE, 3) if segs[1] == "event-source-mappings" => {
86                Some(("DeleteEventSourceMapping", Some(segs[2].clone())))
87            }
88            _ => None,
89        }
90    }
91
92    fn create_function(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
93        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
94        let function_name = body["FunctionName"]
95            .as_str()
96            .ok_or_else(|| {
97                AwsServiceError::aws_error(
98                    StatusCode::BAD_REQUEST,
99                    "InvalidParameterValueException",
100                    "FunctionName is required",
101                )
102            })?
103            .to_string();
104
105        let mut state = self.state.write();
106
107        if state.functions.contains_key(&function_name) {
108            return Err(AwsServiceError::aws_error(
109                StatusCode::CONFLICT,
110                "ResourceConflictException",
111                format!("Function already exist: {}", function_name),
112            ));
113        }
114
115        let runtime = body["Runtime"].as_str().unwrap_or("python3.12").to_string();
116        let role = body["Role"].as_str().unwrap_or("").to_string();
117        let handler = body["Handler"]
118            .as_str()
119            .unwrap_or("index.handler")
120            .to_string();
121        let description = body["Description"].as_str().unwrap_or("").to_string();
122        let timeout = body["Timeout"].as_i64().unwrap_or(3);
123        let memory_size = body["MemorySize"].as_i64().unwrap_or(128);
124        let package_type = body["PackageType"].as_str().unwrap_or("Zip").to_string();
125
126        let tags: std::collections::HashMap<String, String> = body["Tags"]
127            .as_object()
128            .map(|m| {
129                m.iter()
130                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
131                    .collect()
132            })
133            .unwrap_or_default();
134
135        let environment: std::collections::HashMap<String, String> = body["Environment"]
136            ["Variables"]
137            .as_object()
138            .map(|m| {
139                m.iter()
140                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
141                    .collect()
142            })
143            .unwrap_or_default();
144
145        let architectures = body["Architectures"]
146            .as_array()
147            .map(|a| {
148                a.iter()
149                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
150                    .collect()
151            })
152            .unwrap_or_else(|| vec!["x86_64".to_string()]);
153
154        // Decode Code.ZipFile if present (base64-encoded ZIP)
155        let code_zip: Option<Vec<u8>> = match body["Code"]["ZipFile"].as_str() {
156            Some(b64) => Some(
157                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64).map_err(
158                    |_| {
159                        AwsServiceError::aws_error(
160                            StatusCode::BAD_REQUEST,
161                            "InvalidParameterValueException",
162                            "Could not decode Code.ZipFile: invalid base64",
163                        )
164                    },
165                )?,
166            ),
167            None => None,
168        };
169
170        // Compute a code hash from the actual ZIP bytes (or from the Code JSON as fallback)
171        let code_fallback = serde_json::to_vec(&body["Code"]).unwrap_or_default();
172        let code_bytes = code_zip.as_deref().unwrap_or(&code_fallback);
173        let mut hasher = Sha256::new();
174        hasher.update(code_bytes);
175        let hash = hasher.finalize();
176        let code_sha256 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash);
177        let code_size = code_bytes.len() as i64;
178
179        let function_arn = format!(
180            "arn:aws:lambda:{}:{}:function:{}",
181            state.region, state.account_id, function_name
182        );
183        let now = Utc::now();
184
185        let func = LambdaFunction {
186            function_name: function_name.clone(),
187            function_arn: function_arn.clone(),
188            runtime: runtime.clone(),
189            role: role.clone(),
190            handler: handler.clone(),
191            description: description.clone(),
192            timeout,
193            memory_size,
194            code_sha256: code_sha256.clone(),
195            code_size,
196            version: "$LATEST".to_string(),
197            last_modified: now,
198            tags,
199            environment: environment.clone(),
200            architectures: architectures.clone(),
201            package_type: package_type.clone(),
202            code_zip,
203        };
204
205        let response = self.function_config_json(&func);
206
207        state.functions.insert(function_name, func);
208
209        Ok(AwsResponse::json(StatusCode::CREATED, response.to_string()))
210    }
211
212    fn get_function(&self, function_name: &str) -> Result<AwsResponse, AwsServiceError> {
213        let state = self.state.read();
214        let func = state.functions.get(function_name).ok_or_else(|| {
215            AwsServiceError::aws_error(
216                StatusCode::NOT_FOUND,
217                "ResourceNotFoundException",
218                format!(
219                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
220                    state.region, state.account_id, function_name
221                ),
222            )
223        })?;
224
225        let config = self.function_config_json(func);
226        let response = json!({
227            "Code": {
228                "Location": format!("https://awslambda-{}-tasks.s3.{}.amazonaws.com/stub",
229                    func.function_arn.split(':').nth(3).unwrap_or("us-east-1"),
230                    func.function_arn.split(':').nth(3).unwrap_or("us-east-1")),
231                "RepositoryType": "S3"
232            },
233            "Configuration": config,
234            "Tags": func.tags,
235        });
236
237        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
238    }
239
240    fn delete_function(&self, function_name: &str) -> Result<AwsResponse, AwsServiceError> {
241        let mut state = self.state.write();
242        let region = state.region.clone();
243        let account_id = state.account_id.clone();
244        if state.functions.remove(function_name).is_none() {
245            return Err(AwsServiceError::aws_error(
246                StatusCode::NOT_FOUND,
247                "ResourceNotFoundException",
248                format!(
249                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
250                    region, account_id, function_name
251                ),
252            ));
253        }
254
255        // Clean up any running container for this function
256        if let Some(ref runtime) = self.runtime {
257            let rt = runtime.clone();
258            let name = function_name.to_string();
259            tokio::spawn(async move { rt.stop_container(&name).await });
260        }
261
262        Ok(AwsResponse::json(StatusCode::NO_CONTENT, ""))
263    }
264
265    fn list_functions(&self) -> Result<AwsResponse, AwsServiceError> {
266        let state = self.state.read();
267        let functions: Vec<Value> = state
268            .functions
269            .values()
270            .map(|f| self.function_config_json(f))
271            .collect();
272
273        let response = json!({
274            "Functions": functions,
275        });
276
277        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
278    }
279
280    async fn invoke(
281        &self,
282        function_name: &str,
283        payload: &[u8],
284    ) -> Result<AwsResponse, AwsServiceError> {
285        let func = {
286            let state = self.state.read();
287            state.functions.get(function_name).cloned().ok_or_else(|| {
288                AwsServiceError::aws_error(
289                    StatusCode::NOT_FOUND,
290                    "ResourceNotFoundException",
291                    format!(
292                        "Function not found: arn:aws:lambda:{}:{}:function:{}",
293                        state.region, state.account_id, function_name
294                    ),
295                )
296            })?
297        };
298
299        let runtime = self.runtime.as_ref().ok_or_else(|| {
300            AwsServiceError::aws_error(
301                StatusCode::INTERNAL_SERVER_ERROR,
302                "ServiceException",
303                "Docker/Podman is required for Lambda execution but is not available",
304            )
305        })?;
306
307        if func.code_zip.is_none() {
308            return Err(AwsServiceError::aws_error(
309                StatusCode::BAD_REQUEST,
310                "InvalidParameterValueException",
311                "Function has no deployment package",
312            ));
313        }
314
315        match runtime.invoke(&func, payload).await {
316            Ok(response_bytes) => {
317                let mut resp = AwsResponse::json(StatusCode::OK, response_bytes);
318                resp.headers.insert(
319                    http::header::HeaderName::from_static("x-amz-executed-version"),
320                    http::header::HeaderValue::from_static("$LATEST"),
321                );
322                Ok(resp)
323            }
324            Err(e) => {
325                tracing::error!(function = %function_name, error = %e, "Lambda invocation failed");
326                Err(AwsServiceError::aws_error(
327                    StatusCode::INTERNAL_SERVER_ERROR,
328                    "ServiceException",
329                    format!("Lambda execution failed: {e}"),
330                ))
331            }
332        }
333    }
334
335    fn publish_version(&self, function_name: &str) -> Result<AwsResponse, AwsServiceError> {
336        let state = self.state.read();
337        let func = state.functions.get(function_name).ok_or_else(|| {
338            AwsServiceError::aws_error(
339                StatusCode::NOT_FOUND,
340                "ResourceNotFoundException",
341                format!(
342                    "Function not found: arn:aws:lambda:{}:{}:function:{}",
343                    state.region, state.account_id, function_name
344                ),
345            )
346        })?;
347
348        let mut config = self.function_config_json(func);
349        // Stub: always return version "1"
350        config["Version"] = json!("1");
351        config["FunctionArn"] = json!(format!("{}:1", func.function_arn));
352
353        Ok(AwsResponse::json(StatusCode::CREATED, config.to_string()))
354    }
355
356    fn create_event_source_mapping(
357        &self,
358        req: &AwsRequest,
359    ) -> Result<AwsResponse, AwsServiceError> {
360        let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
361        let event_source_arn = body["EventSourceArn"]
362            .as_str()
363            .ok_or_else(|| {
364                AwsServiceError::aws_error(
365                    StatusCode::BAD_REQUEST,
366                    "InvalidParameterValueException",
367                    "EventSourceArn is required",
368                )
369            })?
370            .to_string();
371
372        let function_name = body["FunctionName"]
373            .as_str()
374            .ok_or_else(|| {
375                AwsServiceError::aws_error(
376                    StatusCode::BAD_REQUEST,
377                    "InvalidParameterValueException",
378                    "FunctionName is required",
379                )
380            })?
381            .to_string();
382
383        let mut state = self.state.write();
384
385        // Resolve function name to ARN
386        let function_arn = if function_name.starts_with("arn:") {
387            function_name.clone()
388        } else {
389            let func = state.functions.get(&function_name).ok_or_else(|| {
390                AwsServiceError::aws_error(
391                    StatusCode::NOT_FOUND,
392                    "ResourceNotFoundException",
393                    format!(
394                        "Function not found: arn:aws:lambda:{}:{}:function:{}",
395                        state.region, state.account_id, function_name
396                    ),
397                )
398            })?;
399            func.function_arn.clone()
400        };
401
402        let batch_size = body["BatchSize"].as_i64().unwrap_or(10);
403        let enabled = body["Enabled"].as_bool().unwrap_or(true);
404        let mapping_uuid = uuid::Uuid::new_v4().to_string();
405        let now = Utc::now();
406
407        let mapping = EventSourceMapping {
408            uuid: mapping_uuid.clone(),
409            function_arn: function_arn.clone(),
410            event_source_arn: event_source_arn.clone(),
411            batch_size,
412            enabled,
413            state: if enabled {
414                "Enabled".to_string()
415            } else {
416                "Disabled".to_string()
417            },
418            last_modified: now,
419        };
420
421        let response = self.event_source_mapping_json(&mapping);
422        state.event_source_mappings.insert(mapping_uuid, mapping);
423
424        Ok(AwsResponse::json(
425            StatusCode::ACCEPTED,
426            response.to_string(),
427        ))
428    }
429
430    fn list_event_source_mappings(&self) -> Result<AwsResponse, AwsServiceError> {
431        let state = self.state.read();
432        let mappings: Vec<Value> = state
433            .event_source_mappings
434            .values()
435            .map(|m| self.event_source_mapping_json(m))
436            .collect();
437
438        let response = json!({
439            "EventSourceMappings": mappings,
440        });
441
442        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
443    }
444
445    fn get_event_source_mapping(&self, uuid: &str) -> Result<AwsResponse, AwsServiceError> {
446        let state = self.state.read();
447        let mapping = state.event_source_mappings.get(uuid).ok_or_else(|| {
448            AwsServiceError::aws_error(
449                StatusCode::NOT_FOUND,
450                "ResourceNotFoundException",
451                format!("The resource you requested does not exist. (Service: Lambda, Status Code: 404, Request ID: {uuid})"),
452            )
453        })?;
454
455        let response = self.event_source_mapping_json(mapping);
456        Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
457    }
458
459    fn delete_event_source_mapping(&self, uuid: &str) -> Result<AwsResponse, AwsServiceError> {
460        let mut state = self.state.write();
461        let mapping = state.event_source_mappings.remove(uuid).ok_or_else(|| {
462            AwsServiceError::aws_error(
463                StatusCode::NOT_FOUND,
464                "ResourceNotFoundException",
465                format!("The resource you requested does not exist. (Service: Lambda, Status Code: 404, Request ID: {uuid})"),
466            )
467        })?;
468
469        let mut response = self.event_source_mapping_json(&mapping);
470        response["State"] = json!("Deleting");
471        Ok(AwsResponse::json(
472            StatusCode::ACCEPTED,
473            response.to_string(),
474        ))
475    }
476
477    fn function_config_json(&self, func: &LambdaFunction) -> Value {
478        let mut env_vars = json!({});
479        if !func.environment.is_empty() {
480            env_vars = json!({ "Variables": func.environment });
481        }
482
483        json!({
484            "FunctionName": func.function_name,
485            "FunctionArn": func.function_arn,
486            "Runtime": func.runtime,
487            "Role": func.role,
488            "Handler": func.handler,
489            "Description": func.description,
490            "Timeout": func.timeout,
491            "MemorySize": func.memory_size,
492            "CodeSha256": func.code_sha256,
493            "CodeSize": func.code_size,
494            "Version": func.version,
495            "LastModified": func.last_modified.format("%Y-%m-%dT%H:%M:%S%.3f+0000").to_string(),
496            "PackageType": func.package_type,
497            "Architectures": func.architectures,
498            "Environment": env_vars,
499            "State": "Active",
500            "LastUpdateStatus": "Successful",
501            "TracingConfig": { "Mode": "PassThrough" },
502            "RevisionId": uuid::Uuid::new_v4().to_string(),
503        })
504    }
505
506    fn event_source_mapping_json(&self, mapping: &EventSourceMapping) -> Value {
507        json!({
508            "UUID": mapping.uuid,
509            "FunctionArn": mapping.function_arn,
510            "EventSourceArn": mapping.event_source_arn,
511            "BatchSize": mapping.batch_size,
512            "State": mapping.state,
513            "LastModified": mapping.last_modified.timestamp_millis() as f64 / 1000.0,
514        })
515    }
516}
517
518#[async_trait]
519impl AwsService for LambdaService {
520    fn service_name(&self) -> &str {
521        "lambda"
522    }
523
524    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
525        let (action, resource_name) = Self::resolve_action(&req).ok_or_else(|| {
526            AwsServiceError::aws_error(
527                StatusCode::NOT_FOUND,
528                "UnknownOperationException",
529                format!("Unknown operation: {} {}", req.method, req.raw_path),
530            )
531        })?;
532
533        match action {
534            "CreateFunction" => self.create_function(&req),
535            "ListFunctions" => self.list_functions(),
536            "GetFunction" => self.get_function(resource_name.as_deref().unwrap_or("")),
537            "DeleteFunction" => self.delete_function(resource_name.as_deref().unwrap_or("")),
538            "Invoke" => {
539                self.invoke(resource_name.as_deref().unwrap_or(""), &req.body)
540                    .await
541            }
542            "PublishVersion" => self.publish_version(resource_name.as_deref().unwrap_or("")),
543            "CreateEventSourceMapping" => self.create_event_source_mapping(&req),
544            "ListEventSourceMappings" => self.list_event_source_mappings(),
545            "GetEventSourceMapping" => {
546                self.get_event_source_mapping(resource_name.as_deref().unwrap_or(""))
547            }
548            "DeleteEventSourceMapping" => {
549                self.delete_event_source_mapping(resource_name.as_deref().unwrap_or(""))
550            }
551            _ => Err(AwsServiceError::action_not_implemented("lambda", action)),
552        }
553    }
554
555    fn supported_actions(&self) -> &[&str] {
556        &[
557            "CreateFunction",
558            "GetFunction",
559            "DeleteFunction",
560            "ListFunctions",
561            "Invoke",
562            "PublishVersion",
563            "CreateEventSourceMapping",
564            "ListEventSourceMappings",
565            "GetEventSourceMapping",
566            "DeleteEventSourceMapping",
567        ]
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use crate::state::LambdaState;
575    use bytes::Bytes;
576    use http::{HeaderMap, Method};
577    use parking_lot::RwLock;
578    use std::collections::HashMap;
579    use std::sync::Arc;
580
581    fn make_state() -> SharedLambdaState {
582        Arc::new(RwLock::new(LambdaState::new("123456789012", "us-east-1")))
583    }
584
585    fn make_request(method: Method, path: &str, body: &str) -> AwsRequest {
586        let path_segments: Vec<String> = path
587            .split('/')
588            .filter(|s| !s.is_empty())
589            .map(|s| s.to_string())
590            .collect();
591        AwsRequest {
592            service: "lambda".to_string(),
593            action: String::new(),
594            region: "us-east-1".to_string(),
595            account_id: "123456789012".to_string(),
596            request_id: "test-request-id".to_string(),
597            headers: HeaderMap::new(),
598            query_params: HashMap::new(),
599            body: Bytes::from(body.to_string()),
600            path_segments,
601            raw_path: path.to_string(),
602            raw_query: String::new(),
603            method,
604            is_query_protocol: false,
605            access_key_id: None,
606        }
607    }
608
609    #[tokio::test]
610    async fn test_create_and_get_function() {
611        let state = make_state();
612        let svc = LambdaService::new(state);
613
614        let create_body = json!({
615            "FunctionName": "my-func",
616            "Runtime": "python3.12",
617            "Role": "arn:aws:iam::123456789012:role/test-role",
618            "Handler": "index.handler",
619            "Code": { "ZipFile": "UEsFBgAAAAAAAAAAAAAAAAAAAAA=" }
620        });
621
622        let req = make_request(
623            Method::POST,
624            "/2015-03-31/functions",
625            &create_body.to_string(),
626        );
627        let resp = svc.handle(req).await.unwrap();
628        assert_eq!(resp.status, StatusCode::CREATED);
629
630        let body: Value = serde_json::from_slice(&resp.body).unwrap();
631        assert_eq!(body["FunctionName"], "my-func");
632        assert_eq!(body["Runtime"], "python3.12");
633
634        // Get
635        let req = make_request(Method::GET, "/2015-03-31/functions/my-func", "");
636        let resp = svc.handle(req).await.unwrap();
637        assert_eq!(resp.status, StatusCode::OK);
638        let body: Value = serde_json::from_slice(&resp.body).unwrap();
639        assert_eq!(body["Configuration"]["FunctionName"], "my-func");
640    }
641
642    #[tokio::test]
643    async fn test_delete_function() {
644        let state = make_state();
645        let svc = LambdaService::new(state);
646
647        let create_body = json!({
648            "FunctionName": "to-delete",
649            "Runtime": "nodejs20.x",
650            "Role": "arn:aws:iam::123456789012:role/test",
651            "Handler": "index.handler",
652            "Code": {}
653        });
654
655        let req = make_request(
656            Method::POST,
657            "/2015-03-31/functions",
658            &create_body.to_string(),
659        );
660        svc.handle(req).await.unwrap();
661
662        let req = make_request(Method::DELETE, "/2015-03-31/functions/to-delete", "");
663        let resp = svc.handle(req).await.unwrap();
664        assert_eq!(resp.status, StatusCode::NO_CONTENT);
665
666        // Verify deleted
667        let req = make_request(Method::GET, "/2015-03-31/functions/to-delete", "");
668        let resp = svc.handle(req).await;
669        assert!(resp.is_err());
670    }
671
672    #[tokio::test]
673    async fn test_invoke_without_runtime_returns_error() {
674        let state = make_state();
675        let svc = LambdaService::new(state);
676
677        let create_body = json!({
678            "FunctionName": "invoke-me",
679            "Runtime": "python3.12",
680            "Role": "arn:aws:iam::123456789012:role/test",
681            "Handler": "index.handler",
682            "Code": {}
683        });
684
685        let req = make_request(
686            Method::POST,
687            "/2015-03-31/functions",
688            &create_body.to_string(),
689        );
690        svc.handle(req).await.unwrap();
691
692        let req = make_request(
693            Method::POST,
694            "/2015-03-31/functions/invoke-me/invocations",
695            r#"{"key": "value"}"#,
696        );
697        let resp = svc.handle(req).await;
698        assert!(resp.is_err());
699    }
700
701    #[tokio::test]
702    async fn test_invoke_nonexistent_function() {
703        let state = make_state();
704        let svc = LambdaService::new(state);
705
706        let req = make_request(
707            Method::POST,
708            "/2015-03-31/functions/does-not-exist/invocations",
709            "{}",
710        );
711        let resp = svc.handle(req).await;
712        assert!(resp.is_err());
713    }
714
715    #[tokio::test]
716    async fn test_list_functions() {
717        let state = make_state();
718        let svc = LambdaService::new(state);
719
720        for name in &["func-a", "func-b"] {
721            let create_body = json!({
722                "FunctionName": name,
723                "Runtime": "python3.12",
724                "Role": "arn:aws:iam::123456789012:role/test",
725                "Handler": "index.handler",
726                "Code": {}
727            });
728            let req = make_request(
729                Method::POST,
730                "/2015-03-31/functions",
731                &create_body.to_string(),
732            );
733            svc.handle(req).await.unwrap();
734        }
735
736        let req = make_request(Method::GET, "/2015-03-31/functions", "");
737        let resp = svc.handle(req).await.unwrap();
738        let body: Value = serde_json::from_slice(&resp.body).unwrap();
739        assert_eq!(body["Functions"].as_array().unwrap().len(), 2);
740    }
741
742    #[tokio::test]
743    async fn test_event_source_mapping() {
744        let state = make_state();
745        let svc = LambdaService::new(state);
746
747        // Create function first
748        let create_body = json!({
749            "FunctionName": "esm-func",
750            "Runtime": "python3.12",
751            "Role": "arn:aws:iam::123456789012:role/test",
752            "Handler": "index.handler",
753            "Code": {}
754        });
755        let req = make_request(
756            Method::POST,
757            "/2015-03-31/functions",
758            &create_body.to_string(),
759        );
760        svc.handle(req).await.unwrap();
761
762        // Create mapping
763        let mapping_body = json!({
764            "FunctionName": "esm-func",
765            "EventSourceArn": "arn:aws:sqs:us-east-1:123456789012:my-queue",
766            "BatchSize": 5
767        });
768        let req = make_request(
769            Method::POST,
770            "/2015-03-31/event-source-mappings",
771            &mapping_body.to_string(),
772        );
773        let resp = svc.handle(req).await.unwrap();
774        assert_eq!(resp.status, StatusCode::ACCEPTED);
775        let body: Value = serde_json::from_slice(&resp.body).unwrap();
776        let uuid = body["UUID"].as_str().unwrap().to_string();
777
778        // List mappings
779        let req = make_request(Method::GET, "/2015-03-31/event-source-mappings", "");
780        let resp = svc.handle(req).await.unwrap();
781        let body: Value = serde_json::from_slice(&resp.body).unwrap();
782        assert_eq!(body["EventSourceMappings"].as_array().unwrap().len(), 1);
783
784        // Delete mapping
785        let req = make_request(
786            Method::DELETE,
787            &format!("/2015-03-31/event-source-mappings/{uuid}"),
788            "",
789        );
790        let resp = svc.handle(req).await.unwrap();
791        assert_eq!(resp.status, StatusCode::ACCEPTED);
792    }
793}