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