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