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};
9
10use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
11
12use crate::runtime::ContainerRuntime;
13use crate::state::{EventSourceMapping, LambdaFunction, SharedLambdaState};
14
15/// All fields of a `CreateFunction` request, already parsed and
16/// defaulted. The code zip (if any) is eagerly base64-decoded so the
17/// caller can hash it without doing the decode again.
18struct 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    /// Determine the action from the HTTP method and path segments.
131    /// Lambda uses REST-style routing:
132    ///   POST   /2015-03-31/functions                         -> CreateFunction
133    ///   GET    /2015-03-31/functions                         -> ListFunctions
134    ///   GET    /2015-03-31/functions/{name}                  -> GetFunction
135    ///   DELETE /2015-03-31/functions/{name}                  -> DeleteFunction
136    ///   POST   /2015-03-31/functions/{name}/invocations      -> Invoke
137    ///   POST   /2015-03-31/functions/{name}/versions         -> PublishVersion
138    ///   POST   /2015-03-31/event-source-mappings             -> CreateEventSourceMapping
139    ///   GET    /2015-03-31/event-source-mappings             -> ListEventSourceMappings
140    ///   GET    /2015-03-31/event-source-mappings/{uuid}      -> GetEventSourceMapping
141    ///   DELETE /2015-03-31/event-source-mappings/{uuid}      -> DeleteEventSourceMapping
142    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        // Second segment is the collection (`functions` /
149        // `event-source-mappings`); third is the resource name when
150        // present. Bind the resource name once so the match arms don't
151        // each repeat `segs[2].clone()`.
152        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            // /2015-03-31/functions
162            (&Method::POST, 2, Some("functions"), _) => "CreateFunction",
163            (&Method::GET, 2, Some("functions"), _) => "ListFunctions",
164            // /2015-03-31/functions/{name}
165            (&Method::GET, 3, Some("functions"), _) => "GetFunction",
166            (&Method::DELETE, 3, Some("functions"), _) => "DeleteFunction",
167            // /2015-03-31/functions/{name}/invocations
168            (&Method::POST, 4, Some("functions"), Some("invocations")) => "Invoke",
169            // /2015-03-31/functions/{name}/versions
170            (&Method::POST, 4, Some("functions"), Some("versions")) => "PublishVersion",
171            // /2015-03-31/event-source-mappings
172            (&Method::POST, 2, Some("event-source-mappings"), _) => "CreateEventSourceMapping",
173            (&Method::GET, 2, Some("event-source-mappings"), _) => "ListEventSourceMappings",
174            // /2015-03-31/event-source-mappings/{uuid}
175            (&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        // Hash the actual ZIP bytes when available, falling back to the
198        // raw Code JSON so image-based functions still get a stable id.
199        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        // Clean up any running container for this function
283        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        // Stub: always return version "1"
377        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        // Resolve function name to ARN
413        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        // Get
663        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        // Verify deleted
695        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        // Create function first
776        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        // Create mapping
791        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        // List mappings
807        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        // Delete mapping
813        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}