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 }
634 }
635
636 #[tokio::test]
637 async fn test_create_and_get_function() {
638 let state = make_state();
639 let svc = LambdaService::new(state);
640
641 let create_body = json!({
642 "FunctionName": "my-func",
643 "Runtime": "python3.12",
644 "Role": "arn:aws:iam::123456789012:role/test-role",
645 "Handler": "index.handler",
646 "Code": { "ZipFile": "UEsFBgAAAAAAAAAAAAAAAAAAAAA=" }
647 });
648
649 let req = make_request(
650 Method::POST,
651 "/2015-03-31/functions",
652 &create_body.to_string(),
653 );
654 let resp = svc.handle(req).await.unwrap();
655 assert_eq!(resp.status, StatusCode::CREATED);
656
657 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
658 assert_eq!(body["FunctionName"], "my-func");
659 assert_eq!(body["Runtime"], "python3.12");
660
661 let req = make_request(Method::GET, "/2015-03-31/functions/my-func", "");
663 let resp = svc.handle(req).await.unwrap();
664 assert_eq!(resp.status, StatusCode::OK);
665 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
666 assert_eq!(body["Configuration"]["FunctionName"], "my-func");
667 }
668
669 #[tokio::test]
670 async fn test_delete_function() {
671 let state = make_state();
672 let svc = LambdaService::new(state);
673
674 let create_body = json!({
675 "FunctionName": "to-delete",
676 "Runtime": "nodejs20.x",
677 "Role": "arn:aws:iam::123456789012:role/test",
678 "Handler": "index.handler",
679 "Code": {}
680 });
681
682 let req = make_request(
683 Method::POST,
684 "/2015-03-31/functions",
685 &create_body.to_string(),
686 );
687 svc.handle(req).await.unwrap();
688
689 let req = make_request(Method::DELETE, "/2015-03-31/functions/to-delete", "");
690 let resp = svc.handle(req).await.unwrap();
691 assert_eq!(resp.status, StatusCode::NO_CONTENT);
692
693 let req = make_request(Method::GET, "/2015-03-31/functions/to-delete", "");
695 let resp = svc.handle(req).await;
696 assert!(resp.is_err());
697 }
698
699 #[tokio::test]
700 async fn test_invoke_without_runtime_returns_error() {
701 let state = make_state();
702 let svc = LambdaService::new(state);
703
704 let create_body = json!({
705 "FunctionName": "invoke-me",
706 "Runtime": "python3.12",
707 "Role": "arn:aws:iam::123456789012:role/test",
708 "Handler": "index.handler",
709 "Code": {}
710 });
711
712 let req = make_request(
713 Method::POST,
714 "/2015-03-31/functions",
715 &create_body.to_string(),
716 );
717 svc.handle(req).await.unwrap();
718
719 let req = make_request(
720 Method::POST,
721 "/2015-03-31/functions/invoke-me/invocations",
722 r#"{"key": "value"}"#,
723 );
724 let resp = svc.handle(req).await;
725 assert!(resp.is_err());
726 }
727
728 #[tokio::test]
729 async fn test_invoke_nonexistent_function() {
730 let state = make_state();
731 let svc = LambdaService::new(state);
732
733 let req = make_request(
734 Method::POST,
735 "/2015-03-31/functions/does-not-exist/invocations",
736 "{}",
737 );
738 let resp = svc.handle(req).await;
739 assert!(resp.is_err());
740 }
741
742 #[tokio::test]
743 async fn test_list_functions() {
744 let state = make_state();
745 let svc = LambdaService::new(state);
746
747 for name in &["func-a", "func-b"] {
748 let create_body = json!({
749 "FunctionName": name,
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
763 let req = make_request(Method::GET, "/2015-03-31/functions", "");
764 let resp = svc.handle(req).await.unwrap();
765 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
766 assert_eq!(body["Functions"].as_array().unwrap().len(), 2);
767 }
768
769 #[tokio::test]
770 async fn test_event_source_mapping() {
771 let state = make_state();
772 let svc = LambdaService::new(state);
773
774 let create_body = json!({
776 "FunctionName": "esm-func",
777 "Runtime": "python3.12",
778 "Role": "arn:aws:iam::123456789012:role/test",
779 "Handler": "index.handler",
780 "Code": {}
781 });
782 let req = make_request(
783 Method::POST,
784 "/2015-03-31/functions",
785 &create_body.to_string(),
786 );
787 svc.handle(req).await.unwrap();
788
789 let mapping_body = json!({
791 "FunctionName": "esm-func",
792 "EventSourceArn": "arn:aws:sqs:us-east-1:123456789012:my-queue",
793 "BatchSize": 5
794 });
795 let req = make_request(
796 Method::POST,
797 "/2015-03-31/event-source-mappings",
798 &mapping_body.to_string(),
799 );
800 let resp = svc.handle(req).await.unwrap();
801 assert_eq!(resp.status, StatusCode::ACCEPTED);
802 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
803 let uuid = body["UUID"].as_str().unwrap().to_string();
804
805 let req = make_request(Method::GET, "/2015-03-31/event-source-mappings", "");
807 let resp = svc.handle(req).await.unwrap();
808 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
809 assert_eq!(body["EventSourceMappings"].as_array().unwrap().len(), 1);
810
811 let req = make_request(
813 Method::DELETE,
814 &format!("/2015-03-31/event-source-mappings/{uuid}"),
815 "",
816 );
817 let resp = svc.handle(req).await.unwrap();
818 assert_eq!(resp.status, StatusCode::ACCEPTED);
819 }
820}