1mod account;
2mod configuration_sets;
3mod contact_lists;
4mod identities;
5mod misc;
6mod sending;
7mod suppression;
8mod templates;
9
10use async_trait::async_trait;
11use http::{Method, StatusCode};
12use serde_json::{json, Value};
13
14use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
15
16use crate::fanout::SesDeliveryContext;
17use crate::state::{EventDestination, SharedSesState, Topic, TopicPreference};
18
19pub struct SesV2Service {
20 state: SharedSesState,
21 delivery_ctx: Option<SesDeliveryContext>,
22}
23
24impl SesV2Service {
25 pub fn new(state: SharedSesState) -> Self {
26 Self {
27 state,
28 delivery_ctx: None,
29 }
30 }
31
32 pub fn with_delivery(mut self, ctx: SesDeliveryContext) -> Self {
34 self.delivery_ctx = Some(ctx);
35 self
36 }
37
38 fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Option<String>, Option<String>)> {
121 let segs = &req.path_segments;
122
123 if segs.len() < 3 || segs[0] != "v2" || segs[1] != "email" {
124 return None;
125 }
126
127 let method = &req.method;
128 let resource = segs.get(3).map(|s| decode_segment(s));
129 let collection = segs[2].as_str();
130
131 match collection {
132 "account" => resolve_account_action(method, segs),
133 "identities" => resolve_identities_action(method, segs, resource),
134 "configuration-sets" => resolve_configuration_sets_action(method, segs, resource),
135 "templates" => resolve_templates_action(method, segs, resource),
136 "contact-lists" => resolve_contact_lists_action(method, segs, resource),
137 "suppression" => resolve_suppression_action(method, segs),
138 "tags" if segs.len() == 3 => match *method {
139 Method::POST => Some(("TagResource", None, None)),
140 Method::DELETE => Some(("UntagResource", None, None)),
141 Method::GET => Some(("ListTagsForResource", None, None)),
142 _ => None,
143 },
144 "outbound-emails" if segs.len() == 3 && *method == Method::POST => {
145 Some(("SendEmail", None, None))
146 }
147 "outbound-bulk-emails" if segs.len() == 3 && *method == Method::POST => {
148 Some(("SendBulkEmail", None, None))
149 }
150 "outbound-custom-verification-emails" if segs.len() == 3 && *method == Method::POST => {
151 Some(("SendCustomVerificationEmail", None, None))
152 }
153 "custom-verification-email-templates" => {
154 resolve_custom_verification_template_action(method, segs, resource)
155 }
156 "dedicated-ip-pools" => resolve_dedicated_ip_pools_action(method, segs, resource),
157 "dedicated-ips" => resolve_dedicated_ips_action(method, segs, resource),
158 "multi-region-endpoints" => {
159 resolve_multi_region_endpoints_action(method, segs, resource)
160 }
161 "import-jobs" => resolve_import_jobs_action(method, segs, resource),
162 "export-jobs" => resolve_export_jobs_action(method, segs, resource),
163 "list-export-jobs" if segs.len() == 3 && *method == Method::POST => {
164 Some(("ListExportJobs", None, None))
165 }
166 "tenants" => resolve_tenants_action(method, segs),
167 "resources" => resolve_resources_action(method, segs),
168 "reputation" => resolve_reputation_action(method, segs),
169 "metrics" if segs.len() == 4 && segs[3] == "batch" && *method == Method::POST => {
170 Some(("BatchGetMetricData", None, None))
171 }
172 _ => None,
173 }
174 }
175
176 fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
177 serde_json::from_slice(&req.body).map_err(|_| {
178 AwsServiceError::aws_error(
179 StatusCode::BAD_REQUEST,
180 "BadRequestException",
181 "Invalid JSON in request body",
182 )
183 })
184 }
185
186 fn json_error(status: StatusCode, code: &str, message: &str) -> AwsResponse {
187 let body = json!({
188 "__type": code,
189 "message": message,
190 });
191 AwsResponse::json(status, body.to_string())
192 }
193}
194
195fn decode_segment(s: &str) -> String {
197 percent_encoding::percent_decode_str(s)
198 .decode_utf8_lossy()
199 .into_owned()
200}
201
202type ResolvedAction = Option<(&'static str, Option<String>, Option<String>)>;
203
204fn resolve_account_action(method: &Method, segs: &[String]) -> ResolvedAction {
205 match (method, segs.len()) {
206 (&Method::GET, 3) => Some(("GetAccount", None, None)),
207 (&Method::POST, 4) if segs[3] == "details" => Some(("PutAccountDetails", None, None)),
208 (&Method::PUT, 4) if segs[3] == "sending" => {
209 Some(("PutAccountSendingAttributes", None, None))
210 }
211 (&Method::PUT, 4) if segs[3] == "suppression" => {
212 Some(("PutAccountSuppressionAttributes", None, None))
213 }
214 (&Method::PUT, 4) if segs[3] == "vdm" => Some(("PutAccountVdmAttributes", None, None)),
215 (&Method::PUT, 5) if segs[3] == "dedicated-ips" && segs[4] == "warmup" => {
216 Some(("PutAccountDedicatedIpWarmupAttributes", None, None))
217 }
218 _ => None,
219 }
220}
221
222fn resolve_identities_action(
223 method: &Method,
224 segs: &[String],
225 resource: Option<String>,
226) -> ResolvedAction {
227 match (method, segs.len()) {
228 (&Method::POST, 3) => Some(("CreateEmailIdentity", None, None)),
229 (&Method::GET, 3) => Some(("ListEmailIdentities", None, None)),
230 (&Method::GET, 4) => Some(("GetEmailIdentity", resource, None)),
231 (&Method::DELETE, 4) => Some(("DeleteEmailIdentity", resource, None)),
232 (&Method::PUT, 5) if segs[4] == "dkim" => {
233 Some(("PutEmailIdentityDkimAttributes", resource, None))
234 }
235 (&Method::PUT, 5) if segs[4] == "feedback" => {
236 Some(("PutEmailIdentityFeedbackAttributes", resource, None))
237 }
238 (&Method::PUT, 5) if segs[4] == "mail-from" => {
239 Some(("PutEmailIdentityMailFromAttributes", resource, None))
240 }
241 (&Method::PUT, 5) if segs[4] == "configuration-set" => {
242 Some(("PutEmailIdentityConfigurationSetAttributes", resource, None))
243 }
244 (&Method::GET, 5) if segs[4] == "policies" => {
245 Some(("GetEmailIdentityPolicies", resource, None))
246 }
247 (&Method::PUT, 6) if segs[4] == "dkim" && segs[5] == "signing" => {
248 Some(("PutEmailIdentityDkimSigningAttributes", resource, None))
249 }
250 (&Method::POST, 6) if segs[4] == "policies" => Some((
251 "CreateEmailIdentityPolicy",
252 resource,
253 Some(decode_segment(&segs[5])),
254 )),
255 (&Method::PUT, 6) if segs[4] == "policies" => Some((
256 "UpdateEmailIdentityPolicy",
257 resource,
258 Some(decode_segment(&segs[5])),
259 )),
260 (&Method::DELETE, 6) if segs[4] == "policies" => Some((
261 "DeleteEmailIdentityPolicy",
262 resource,
263 Some(decode_segment(&segs[5])),
264 )),
265 _ => None,
266 }
267}
268
269fn resolve_configuration_sets_action(
270 method: &Method,
271 segs: &[String],
272 resource: Option<String>,
273) -> ResolvedAction {
274 match (method, segs.len()) {
275 (&Method::POST, 3) => Some(("CreateConfigurationSet", None, None)),
276 (&Method::GET, 3) => Some(("ListConfigurationSets", None, None)),
277 (&Method::GET, 4) => Some(("GetConfigurationSet", resource, None)),
278 (&Method::DELETE, 4) => Some(("DeleteConfigurationSet", resource, None)),
279 (&Method::POST, 5) if segs[4] == "event-destinations" => {
280 Some(("CreateConfigurationSetEventDestination", resource, None))
281 }
282 (&Method::GET, 5) if segs[4] == "event-destinations" => {
283 Some(("GetConfigurationSetEventDestinations", resource, None))
284 }
285 (&Method::PUT, 5) if segs[4] == "sending" => {
286 Some(("PutConfigurationSetSendingOptions", resource, None))
287 }
288 (&Method::PUT, 5) if segs[4] == "delivery-options" => {
289 Some(("PutConfigurationSetDeliveryOptions", resource, None))
290 }
291 (&Method::PUT, 5) if segs[4] == "tracking-options" => {
292 Some(("PutConfigurationSetTrackingOptions", resource, None))
293 }
294 (&Method::PUT, 5) if segs[4] == "suppression-options" => {
295 Some(("PutConfigurationSetSuppressionOptions", resource, None))
296 }
297 (&Method::PUT, 5) if segs[4] == "reputation-options" => {
298 Some(("PutConfigurationSetReputationOptions", resource, None))
299 }
300 (&Method::PUT, 5) if segs[4] == "vdm-options" => {
301 Some(("PutConfigurationSetVdmOptions", resource, None))
302 }
303 (&Method::PUT, 5) if segs[4] == "archiving-options" => {
304 Some(("PutConfigurationSetArchivingOptions", resource, None))
305 }
306 (&Method::PUT, 6) if segs[4] == "event-destinations" => Some((
307 "UpdateConfigurationSetEventDestination",
308 resource,
309 Some(decode_segment(&segs[5])),
310 )),
311 (&Method::DELETE, 6) if segs[4] == "event-destinations" => Some((
312 "DeleteConfigurationSetEventDestination",
313 resource,
314 Some(decode_segment(&segs[5])),
315 )),
316 _ => None,
317 }
318}
319
320fn resolve_templates_action(
321 method: &Method,
322 segs: &[String],
323 resource: Option<String>,
324) -> ResolvedAction {
325 match (method, segs.len()) {
326 (&Method::POST, 3) => Some(("CreateEmailTemplate", None, None)),
327 (&Method::GET, 3) => Some(("ListEmailTemplates", None, None)),
328 (&Method::GET, 4) => Some(("GetEmailTemplate", resource, None)),
329 (&Method::PUT, 4) => Some(("UpdateEmailTemplate", resource, None)),
330 (&Method::DELETE, 4) => Some(("DeleteEmailTemplate", resource, None)),
331 (&Method::POST, 5) if segs[4] == "render" => {
332 Some(("TestRenderEmailTemplate", resource, None))
333 }
334 _ => None,
335 }
336}
337
338fn resolve_contact_lists_action(
339 method: &Method,
340 segs: &[String],
341 resource: Option<String>,
342) -> ResolvedAction {
343 match (method, segs.len()) {
344 (&Method::POST, 3) => Some(("CreateContactList", None, None)),
345 (&Method::GET, 3) => Some(("ListContactLists", None, None)),
346 (&Method::GET, 4) => Some(("GetContactList", resource, None)),
347 (&Method::PUT, 4) => Some(("UpdateContactList", resource, None)),
348 (&Method::DELETE, 4) => Some(("DeleteContactList", resource, None)),
349 (&Method::POST, 5) if segs[4] == "contacts" => Some(("CreateContact", resource, None)),
350 (&Method::GET, 5) if segs[4] == "contacts" => Some(("ListContacts", resource, None)),
351 (&Method::POST, 6) if segs[4] == "contacts" && segs[5] == "list" => {
353 Some(("ListContacts", resource, None))
354 }
355 (&Method::GET, 6) if segs[4] == "contacts" => {
356 Some(("GetContact", resource, Some(decode_segment(&segs[5]))))
357 }
358 (&Method::PUT, 6) if segs[4] == "contacts" => {
359 Some(("UpdateContact", resource, Some(decode_segment(&segs[5]))))
360 }
361 (&Method::DELETE, 6) if segs[4] == "contacts" => {
362 Some(("DeleteContact", resource, Some(decode_segment(&segs[5]))))
363 }
364 _ => None,
365 }
366}
367
368fn resolve_suppression_action(method: &Method, segs: &[String]) -> ResolvedAction {
369 if segs.get(3).map(|s| s.as_str()) != Some("addresses") {
370 return None;
371 }
372 match (method, segs.len()) {
373 (&Method::PUT, 4) => Some(("PutSuppressedDestination", None, None)),
374 (&Method::GET, 4) => Some(("ListSuppressedDestinations", None, None)),
375 (&Method::GET, 5) => Some((
376 "GetSuppressedDestination",
377 Some(decode_segment(&segs[4])),
378 None,
379 )),
380 (&Method::DELETE, 5) => Some((
381 "DeleteSuppressedDestination",
382 Some(decode_segment(&segs[4])),
383 None,
384 )),
385 _ => None,
386 }
387}
388
389fn resolve_custom_verification_template_action(
390 method: &Method,
391 segs: &[String],
392 resource: Option<String>,
393) -> ResolvedAction {
394 match (method, segs.len()) {
395 (&Method::POST, 3) => Some(("CreateCustomVerificationEmailTemplate", None, None)),
396 (&Method::GET, 3) => Some(("ListCustomVerificationEmailTemplates", None, None)),
397 (&Method::GET, 4) => Some(("GetCustomVerificationEmailTemplate", resource, None)),
398 (&Method::PUT, 4) => Some(("UpdateCustomVerificationEmailTemplate", resource, None)),
399 (&Method::DELETE, 4) => Some(("DeleteCustomVerificationEmailTemplate", resource, None)),
400 _ => None,
401 }
402}
403
404fn resolve_dedicated_ip_pools_action(
405 method: &Method,
406 segs: &[String],
407 resource: Option<String>,
408) -> ResolvedAction {
409 match (method, segs.len()) {
410 (&Method::POST, 3) => Some(("CreateDedicatedIpPool", None, None)),
411 (&Method::GET, 3) => Some(("ListDedicatedIpPools", None, None)),
412 (&Method::DELETE, 4) => Some(("DeleteDedicatedIpPool", resource, None)),
413 (&Method::PUT, 5) if segs[4] == "scaling" => {
414 Some(("PutDedicatedIpPoolScalingAttributes", resource, None))
415 }
416 _ => None,
417 }
418}
419
420fn resolve_dedicated_ips_action(
421 method: &Method,
422 segs: &[String],
423 resource: Option<String>,
424) -> ResolvedAction {
425 match (method, segs.len()) {
426 (&Method::GET, 3) => Some(("GetDedicatedIps", None, None)),
427 (&Method::GET, 4) => Some(("GetDedicatedIp", resource, None)),
428 (&Method::PUT, 5) if segs[4] == "pool" => Some(("PutDedicatedIpInPool", resource, None)),
429 (&Method::PUT, 5) if segs[4] == "warmup" => {
430 Some(("PutDedicatedIpWarmupAttributes", resource, None))
431 }
432 _ => None,
433 }
434}
435
436fn resolve_multi_region_endpoints_action(
437 method: &Method,
438 segs: &[String],
439 resource: Option<String>,
440) -> ResolvedAction {
441 match (method, segs.len()) {
442 (&Method::POST, 3) => Some(("CreateMultiRegionEndpoint", None, None)),
443 (&Method::GET, 3) => Some(("ListMultiRegionEndpoints", None, None)),
444 (&Method::GET, 4) => Some(("GetMultiRegionEndpoint", resource, None)),
445 (&Method::DELETE, 4) => Some(("DeleteMultiRegionEndpoint", resource, None)),
446 _ => None,
447 }
448}
449
450fn resolve_import_jobs_action(
451 method: &Method,
452 segs: &[String],
453 resource: Option<String>,
454) -> ResolvedAction {
455 match (method, segs.len()) {
456 (&Method::POST, 3) => Some(("CreateImportJob", None, None)),
457 (&Method::POST, 4) if segs[3] == "list" => Some(("ListImportJobs", None, None)),
458 (&Method::GET, 4) => Some(("GetImportJob", resource, None)),
459 _ => None,
460 }
461}
462
463fn resolve_export_jobs_action(
464 method: &Method,
465 segs: &[String],
466 resource: Option<String>,
467) -> ResolvedAction {
468 match (method, segs.len()) {
469 (&Method::POST, 3) => Some(("CreateExportJob", None, None)),
470 (&Method::GET, 4) => Some(("GetExportJob", resource, None)),
471 (&Method::PUT, 5) if segs[4] == "cancel" => Some(("CancelExportJob", resource, None)),
472 _ => None,
473 }
474}
475
476fn resolve_tenants_action(method: &Method, segs: &[String]) -> ResolvedAction {
477 match (method, segs.len()) {
478 (&Method::POST, 3) => Some(("CreateTenant", None, None)),
479 (&Method::POST, 4) if segs[3] == "list" => Some(("ListTenants", None, None)),
480 (&Method::POST, 4) if segs[3] == "get" => Some(("GetTenant", None, None)),
481 (&Method::POST, 4) if segs[3] == "delete" => Some(("DeleteTenant", None, None)),
482 (&Method::POST, 4) if segs[3] == "resources" => {
483 Some(("CreateTenantResourceAssociation", None, None))
484 }
485 (&Method::POST, 5) if segs[3] == "resources" && segs[4] == "delete" => {
486 Some(("DeleteTenantResourceAssociation", None, None))
487 }
488 (&Method::POST, 5) if segs[3] == "resources" && segs[4] == "list" => {
489 Some(("ListTenantResources", None, None))
490 }
491 _ => None,
492 }
493}
494
495fn resolve_resources_action(method: &Method, segs: &[String]) -> ResolvedAction {
496 match (method, segs.len()) {
497 (&Method::POST, 5) if segs[3] == "tenants" && segs[4] == "list" => {
498 Some(("ListResourceTenants", None, None))
499 }
500 _ => None,
501 }
502}
503
504fn resolve_reputation_action(method: &Method, segs: &[String]) -> ResolvedAction {
505 if segs.get(3).map(|s| s.as_str()) != Some("entities") {
506 return None;
507 }
508 match (method, segs.len()) {
509 (&Method::POST, 4) => Some(("ListReputationEntities", None, None)),
510 (&Method::GET, 6) => Some((
511 "GetReputationEntity",
512 Some(decode_segment(&segs[4])),
513 Some(decode_segment(&segs[5])),
514 )),
515 (&Method::PUT, 7) if segs[6] == "customer-managed-status" => Some((
516 "UpdateReputationEntityCustomerManagedStatus",
517 Some(decode_segment(&segs[4])),
518 Some(decode_segment(&segs[5])),
519 )),
520 (&Method::PUT, 7) if segs[6] == "policy" => Some((
521 "UpdateReputationEntityPolicy",
522 Some(decode_segment(&segs[4])),
523 Some(decode_segment(&segs[5])),
524 )),
525 _ => None,
526 }
527}
528
529fn parse_topics(value: &Value) -> Vec<Topic> {
530 value
531 .as_array()
532 .map(|arr| {
533 arr.iter()
534 .filter_map(|v| {
535 let topic_name = v["TopicName"].as_str()?.to_string();
536 let display_name = v["DisplayName"].as_str().unwrap_or("").to_string();
537 let description = v["Description"].as_str().unwrap_or("").to_string();
538 let default_subscription_status = v["DefaultSubscriptionStatus"]
539 .as_str()
540 .unwrap_or("OPT_OUT")
541 .to_string();
542 Some(Topic {
543 topic_name,
544 display_name,
545 description,
546 default_subscription_status,
547 })
548 })
549 .collect()
550 })
551 .unwrap_or_default()
552}
553
554fn parse_topic_preferences(value: &Value) -> Vec<TopicPreference> {
555 value
556 .as_array()
557 .map(|arr| {
558 arr.iter()
559 .filter_map(|v| {
560 let topic_name = v["TopicName"].as_str()?.to_string();
561 let subscription_status = v["SubscriptionStatus"]
562 .as_str()
563 .unwrap_or("OPT_OUT")
564 .to_string();
565 Some(TopicPreference {
566 topic_name,
567 subscription_status,
568 })
569 })
570 .collect()
571 })
572 .unwrap_or_default()
573}
574
575fn extract_string_array(value: &Value) -> Vec<String> {
576 value
577 .as_array()
578 .map(|arr| {
579 arr.iter()
580 .filter_map(|v| v.as_str().map(|s| s.to_string()))
581 .collect()
582 })
583 .unwrap_or_default()
584}
585
586fn parse_event_destination_definition(name: &str, def: &Value) -> EventDestination {
587 let enabled = def["Enabled"].as_bool().unwrap_or(false);
588 let matching_event_types = extract_string_array(&def["MatchingEventTypes"]);
589 let kinesis_firehose_destination = def
590 .get("KinesisFirehoseDestination")
591 .filter(|v| v.is_object())
592 .cloned();
593 let cloud_watch_destination = def
594 .get("CloudWatchDestination")
595 .filter(|v| v.is_object())
596 .cloned();
597 let sns_destination = def.get("SnsDestination").filter(|v| v.is_object()).cloned();
598 let event_bridge_destination = def
599 .get("EventBridgeDestination")
600 .filter(|v| v.is_object())
601 .cloned();
602 let pinpoint_destination = def
603 .get("PinpointDestination")
604 .filter(|v| v.is_object())
605 .cloned();
606
607 EventDestination {
608 name: name.to_string(),
609 enabled,
610 matching_event_types,
611 kinesis_firehose_destination,
612 cloud_watch_destination,
613 sns_destination,
614 event_bridge_destination,
615 pinpoint_destination,
616 }
617}
618
619fn event_destination_to_json(dest: &EventDestination) -> Value {
620 let mut obj = json!({
621 "Name": dest.name,
622 "Enabled": dest.enabled,
623 "MatchingEventTypes": dest.matching_event_types,
624 });
625 if let Some(ref v) = dest.kinesis_firehose_destination {
626 obj["KinesisFirehoseDestination"] = v.clone();
627 }
628 if let Some(ref v) = dest.cloud_watch_destination {
629 obj["CloudWatchDestination"] = v.clone();
630 }
631 if let Some(ref v) = dest.sns_destination {
632 obj["SnsDestination"] = v.clone();
633 }
634 if let Some(ref v) = dest.event_bridge_destination {
635 obj["EventBridgeDestination"] = v.clone();
636 }
637 if let Some(ref v) = dest.pinpoint_destination {
638 obj["PinpointDestination"] = v.clone();
639 }
640 obj
641}
642
643#[async_trait]
644impl fakecloud_core::service::AwsService for SesV2Service {
645 fn service_name(&self) -> &str {
646 "ses"
647 }
648
649 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
650 if req.is_query_protocol {
652 return crate::v1::handle_v1_action(&self.state, &req);
653 }
654
655 let (action, resource_name, sub_resource) =
656 Self::resolve_action(&req).ok_or_else(|| {
657 AwsServiceError::aws_error(
658 StatusCode::NOT_FOUND,
659 "UnknownOperationException",
660 format!("Unknown operation: {} {}", req.method, req.raw_path),
661 )
662 })?;
663
664 let res = resource_name.as_deref().unwrap_or("");
665 let sub = sub_resource.as_deref().unwrap_or("");
666
667 match action {
668 "GetAccount" => self.get_account(),
669 "CreateEmailIdentity" => self.create_email_identity(&req),
670 "ListEmailIdentities" => self.list_email_identities(),
671 "GetEmailIdentity" => self.get_email_identity(res),
672 "DeleteEmailIdentity" => self.delete_email_identity(res, &req),
673 "CreateConfigurationSet" => self.create_configuration_set(&req),
674 "ListConfigurationSets" => self.list_configuration_sets(),
675 "GetConfigurationSet" => self.get_configuration_set(res),
676 "DeleteConfigurationSet" => self.delete_configuration_set(res, &req),
677 "CreateEmailTemplate" => self.create_email_template(&req),
678 "ListEmailTemplates" => self.list_email_templates(),
679 "GetEmailTemplate" => self.get_email_template(res),
680 "UpdateEmailTemplate" => self.update_email_template(res, &req),
681 "DeleteEmailTemplate" => self.delete_email_template(res),
682 "SendEmail" => self.send_email(&req),
683 "SendBulkEmail" => self.send_bulk_email(&req),
684 "TagResource" => self.tag_resource(&req),
685 "UntagResource" => self.untag_resource(&req),
686 "ListTagsForResource" => self.list_tags_for_resource(&req),
687 "CreateContactList" => self.create_contact_list(&req),
688 "GetContactList" => self.get_contact_list(res),
689 "ListContactLists" => self.list_contact_lists(),
690 "UpdateContactList" => self.update_contact_list(res, &req),
691 "DeleteContactList" => self.delete_contact_list(res, &req),
692 "CreateContact" => self.create_contact(res, &req),
693 "GetContact" => self.get_contact(res, sub),
694 "ListContacts" => self.list_contacts(res),
695 "UpdateContact" => self.update_contact(res, sub, &req),
696 "DeleteContact" => self.delete_contact(res, sub),
697 "PutSuppressedDestination" => self.put_suppressed_destination(&req),
698 "GetSuppressedDestination" => self.get_suppressed_destination(res),
699 "DeleteSuppressedDestination" => self.delete_suppressed_destination(res),
700 "ListSuppressedDestinations" => self.list_suppressed_destinations(),
701 "CreateConfigurationSetEventDestination" => {
702 self.create_configuration_set_event_destination(res, &req)
703 }
704 "GetConfigurationSetEventDestinations" => {
705 self.get_configuration_set_event_destinations(res)
706 }
707 "UpdateConfigurationSetEventDestination" => {
708 self.update_configuration_set_event_destination(res, sub, &req)
709 }
710 "DeleteConfigurationSetEventDestination" => {
711 self.delete_configuration_set_event_destination(res, sub)
712 }
713 "CreateEmailIdentityPolicy" => self.create_email_identity_policy(res, sub, &req),
714 "GetEmailIdentityPolicies" => self.get_email_identity_policies(res),
715 "UpdateEmailIdentityPolicy" => self.update_email_identity_policy(res, sub, &req),
716 "DeleteEmailIdentityPolicy" => self.delete_email_identity_policy(res, sub),
717 "PutEmailIdentityDkimAttributes" => self.put_email_identity_dkim_attributes(res, &req),
718 "PutEmailIdentityDkimSigningAttributes" => {
719 self.put_email_identity_dkim_signing_attributes(res, &req)
720 }
721 "PutEmailIdentityFeedbackAttributes" => {
722 self.put_email_identity_feedback_attributes(res, &req)
723 }
724 "PutEmailIdentityMailFromAttributes" => {
725 self.put_email_identity_mail_from_attributes(res, &req)
726 }
727 "PutEmailIdentityConfigurationSetAttributes" => {
728 self.put_email_identity_configuration_set_attributes(res, &req)
729 }
730 "PutConfigurationSetSendingOptions" => {
731 self.put_configuration_set_sending_options(res, &req)
732 }
733 "PutConfigurationSetDeliveryOptions" => {
734 self.put_configuration_set_delivery_options(res, &req)
735 }
736 "PutConfigurationSetTrackingOptions" => {
737 self.put_configuration_set_tracking_options(res, &req)
738 }
739 "PutConfigurationSetSuppressionOptions" => {
740 self.put_configuration_set_suppression_options(res, &req)
741 }
742 "PutConfigurationSetReputationOptions" => {
743 self.put_configuration_set_reputation_options(res, &req)
744 }
745 "PutConfigurationSetVdmOptions" => self.put_configuration_set_vdm_options(res, &req),
746 "PutConfigurationSetArchivingOptions" => {
747 self.put_configuration_set_archiving_options(res, &req)
748 }
749 "CreateCustomVerificationEmailTemplate" => {
750 self.create_custom_verification_email_template(&req)
751 }
752 "GetCustomVerificationEmailTemplate" => {
753 self.get_custom_verification_email_template(res)
754 }
755 "ListCustomVerificationEmailTemplates" => {
756 self.list_custom_verification_email_templates(&req)
757 }
758 "UpdateCustomVerificationEmailTemplate" => {
759 self.update_custom_verification_email_template(res, &req)
760 }
761 "DeleteCustomVerificationEmailTemplate" => {
762 self.delete_custom_verification_email_template(res)
763 }
764 "SendCustomVerificationEmail" => self.send_custom_verification_email(&req),
765 "TestRenderEmailTemplate" => self.test_render_email_template(res, &req),
766 "CreateDedicatedIpPool" => self.create_dedicated_ip_pool(&req),
767 "ListDedicatedIpPools" => self.list_dedicated_ip_pools(),
768 "DeleteDedicatedIpPool" => self.delete_dedicated_ip_pool(res),
769 "GetDedicatedIp" => self.get_dedicated_ip(res),
770 "GetDedicatedIps" => self.get_dedicated_ips(&req),
771 "PutDedicatedIpInPool" => self.put_dedicated_ip_in_pool(res, &req),
772 "PutDedicatedIpPoolScalingAttributes" => {
773 self.put_dedicated_ip_pool_scaling_attributes(res, &req)
774 }
775 "PutDedicatedIpWarmupAttributes" => self.put_dedicated_ip_warmup_attributes(res, &req),
776 "PutAccountDedicatedIpWarmupAttributes" => {
777 self.put_account_dedicated_ip_warmup_attributes(&req)
778 }
779 "CreateMultiRegionEndpoint" => self.create_multi_region_endpoint(&req),
780 "GetMultiRegionEndpoint" => self.get_multi_region_endpoint(res),
781 "ListMultiRegionEndpoints" => self.list_multi_region_endpoints(),
782 "DeleteMultiRegionEndpoint" => self.delete_multi_region_endpoint(res),
783 "PutAccountDetails" => self.put_account_details(&req),
784 "PutAccountSendingAttributes" => self.put_account_sending_attributes(&req),
785 "PutAccountSuppressionAttributes" => self.put_account_suppression_attributes(&req),
786 "PutAccountVdmAttributes" => self.put_account_vdm_attributes(&req),
787 "CreateImportJob" => self.create_import_job(&req),
788 "GetImportJob" => self.get_import_job(res),
789 "ListImportJobs" => self.list_import_jobs(&req),
790 "CreateExportJob" => self.create_export_job(&req),
791 "GetExportJob" => self.get_export_job(res),
792 "ListExportJobs" => self.list_export_jobs(&req),
793 "CancelExportJob" => self.cancel_export_job(res),
794 "CreateTenant" => self.create_tenant(&req),
795 "GetTenant" => self.get_tenant(&req),
796 "ListTenants" => self.list_tenants(&req),
797 "DeleteTenant" => self.delete_tenant(&req),
798 "CreateTenantResourceAssociation" => self.create_tenant_resource_association(&req),
799 "DeleteTenantResourceAssociation" => self.delete_tenant_resource_association(&req),
800 "ListTenantResources" => self.list_tenant_resources(&req),
801 "ListResourceTenants" => self.list_resource_tenants(&req),
802 "GetReputationEntity" => self.get_reputation_entity(res, sub),
803 "ListReputationEntities" => self.list_reputation_entities(&req),
804 "UpdateReputationEntityCustomerManagedStatus" => {
805 self.update_reputation_entity_customer_managed_status(res, sub, &req)
806 }
807 "UpdateReputationEntityPolicy" => self.update_reputation_entity_policy(res, sub, &req),
808 "BatchGetMetricData" => self.batch_get_metric_data(&req),
809 _ => Err(AwsServiceError::action_not_implemented("ses", action)),
810 }
811 }
812
813 fn supported_actions(&self) -> &[&str] {
814 &[
815 "GetAccount",
816 "CreateEmailIdentity",
817 "ListEmailIdentities",
818 "GetEmailIdentity",
819 "DeleteEmailIdentity",
820 "CreateConfigurationSet",
821 "ListConfigurationSets",
822 "GetConfigurationSet",
823 "DeleteConfigurationSet",
824 "CreateEmailTemplate",
825 "ListEmailTemplates",
826 "GetEmailTemplate",
827 "UpdateEmailTemplate",
828 "DeleteEmailTemplate",
829 "SendEmail",
830 "SendBulkEmail",
831 "TagResource",
832 "UntagResource",
833 "ListTagsForResource",
834 "CreateContactList",
835 "GetContactList",
836 "ListContactLists",
837 "UpdateContactList",
838 "DeleteContactList",
839 "CreateContact",
840 "GetContact",
841 "ListContacts",
842 "UpdateContact",
843 "DeleteContact",
844 "PutSuppressedDestination",
845 "GetSuppressedDestination",
846 "DeleteSuppressedDestination",
847 "ListSuppressedDestinations",
848 "CreateConfigurationSetEventDestination",
849 "GetConfigurationSetEventDestinations",
850 "UpdateConfigurationSetEventDestination",
851 "DeleteConfigurationSetEventDestination",
852 "CreateEmailIdentityPolicy",
853 "GetEmailIdentityPolicies",
854 "UpdateEmailIdentityPolicy",
855 "DeleteEmailIdentityPolicy",
856 "PutEmailIdentityDkimAttributes",
857 "PutEmailIdentityDkimSigningAttributes",
858 "PutEmailIdentityFeedbackAttributes",
859 "PutEmailIdentityMailFromAttributes",
860 "PutEmailIdentityConfigurationSetAttributes",
861 "PutConfigurationSetSendingOptions",
862 "PutConfigurationSetDeliveryOptions",
863 "PutConfigurationSetTrackingOptions",
864 "PutConfigurationSetSuppressionOptions",
865 "PutConfigurationSetReputationOptions",
866 "PutConfigurationSetVdmOptions",
867 "PutConfigurationSetArchivingOptions",
868 "CreateCustomVerificationEmailTemplate",
869 "GetCustomVerificationEmailTemplate",
870 "ListCustomVerificationEmailTemplates",
871 "UpdateCustomVerificationEmailTemplate",
872 "DeleteCustomVerificationEmailTemplate",
873 "SendCustomVerificationEmail",
874 "TestRenderEmailTemplate",
875 "CreateDedicatedIpPool",
876 "ListDedicatedIpPools",
877 "DeleteDedicatedIpPool",
878 "GetDedicatedIp",
879 "GetDedicatedIps",
880 "PutDedicatedIpInPool",
881 "PutDedicatedIpPoolScalingAttributes",
882 "PutDedicatedIpWarmupAttributes",
883 "PutAccountDedicatedIpWarmupAttributes",
884 "CreateMultiRegionEndpoint",
885 "GetMultiRegionEndpoint",
886 "ListMultiRegionEndpoints",
887 "DeleteMultiRegionEndpoint",
888 "PutAccountDetails",
889 "PutAccountSendingAttributes",
890 "PutAccountSuppressionAttributes",
891 "PutAccountVdmAttributes",
892 "CreateImportJob",
893 "GetImportJob",
894 "ListImportJobs",
895 "CreateExportJob",
896 "GetExportJob",
897 "ListExportJobs",
898 "CancelExportJob",
899 "CreateTenant",
900 "GetTenant",
901 "ListTenants",
902 "DeleteTenant",
903 "CreateTenantResourceAssociation",
904 "DeleteTenantResourceAssociation",
905 "ListTenantResources",
906 "ListResourceTenants",
907 "GetReputationEntity",
908 "ListReputationEntities",
909 "UpdateReputationEntityCustomerManagedStatus",
910 "UpdateReputationEntityPolicy",
911 "BatchGetMetricData",
912 ]
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922 use crate::state::SesState;
923 use bytes::Bytes;
924 use fakecloud_core::service::AwsService;
925 use http::{HeaderMap, Method};
926 use parking_lot::RwLock;
927 use std::collections::HashMap;
928 use std::sync::Arc;
929
930 fn make_state() -> SharedSesState {
931 Arc::new(RwLock::new(SesState::new("123456789012", "us-east-1")))
932 }
933
934 fn make_request(method: Method, path: &str, body: &str) -> AwsRequest {
935 make_request_with_query(method, path, body, "", HashMap::new())
936 }
937
938 fn make_request_with_query(
939 method: Method,
940 path: &str,
941 body: &str,
942 raw_query: &str,
943 query_params: HashMap<String, String>,
944 ) -> AwsRequest {
945 let path_segments: Vec<String> = path
946 .split('/')
947 .filter(|s| !s.is_empty())
948 .map(|s| s.to_string())
949 .collect();
950 AwsRequest {
951 service: "ses".to_string(),
952 action: String::new(),
953 region: "us-east-1".to_string(),
954 account_id: "123456789012".to_string(),
955 request_id: "test-request-id".to_string(),
956 headers: HeaderMap::new(),
957 query_params,
958 body: Bytes::from(body.to_string()),
959 path_segments,
960 raw_path: path.to_string(),
961 raw_query: raw_query.to_string(),
962 method,
963 is_query_protocol: false,
964 access_key_id: None,
965 principal: None,
966 }
967 }
968
969 #[tokio::test]
970 async fn test_identity_lifecycle() {
971 let state = make_state();
972 let svc = SesV2Service::new(state);
973
974 let req = make_request(
976 Method::POST,
977 "/v2/email/identities",
978 r#"{"EmailIdentity": "test@example.com"}"#,
979 );
980 let resp = svc.handle(req).await.unwrap();
981 assert_eq!(resp.status, StatusCode::OK);
982 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
983 assert_eq!(body["VerifiedForSendingStatus"], true);
984 assert_eq!(body["IdentityType"], "EMAIL_ADDRESS");
985
986 let req = make_request(Method::GET, "/v2/email/identities", "");
988 let resp = svc.handle(req).await.unwrap();
989 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
990 assert_eq!(body["EmailIdentities"].as_array().unwrap().len(), 1);
991
992 let req = make_request(Method::GET, "/v2/email/identities/test%40example.com", "");
994 let resp = svc.handle(req).await.unwrap();
995 assert_eq!(resp.status, StatusCode::OK);
996 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
997 assert_eq!(body["VerifiedForSendingStatus"], true);
998 assert_eq!(body["DkimAttributes"]["Status"], "SUCCESS");
999
1000 let req = make_request(
1002 Method::DELETE,
1003 "/v2/email/identities/test%40example.com",
1004 "",
1005 );
1006 let resp = svc.handle(req).await.unwrap();
1007 assert_eq!(resp.status, StatusCode::OK);
1008
1009 let req = make_request(Method::GET, "/v2/email/identities/test%40example.com", "");
1011 let resp = svc.handle(req).await.unwrap();
1012 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1013 }
1014
1015 #[tokio::test]
1016 async fn test_domain_identity() {
1017 let state = make_state();
1018 let svc = SesV2Service::new(state);
1019
1020 let req = make_request(
1021 Method::POST,
1022 "/v2/email/identities",
1023 r#"{"EmailIdentity": "example.com"}"#,
1024 );
1025 let resp = svc.handle(req).await.unwrap();
1026 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1027 assert_eq!(body["IdentityType"], "DOMAIN");
1028 }
1029
1030 #[tokio::test]
1031 async fn test_duplicate_identity() {
1032 let state = make_state();
1033 let svc = SesV2Service::new(state);
1034
1035 let req = make_request(
1036 Method::POST,
1037 "/v2/email/identities",
1038 r#"{"EmailIdentity": "test@example.com"}"#,
1039 );
1040 svc.handle(req).await.unwrap();
1041
1042 let req = make_request(
1043 Method::POST,
1044 "/v2/email/identities",
1045 r#"{"EmailIdentity": "test@example.com"}"#,
1046 );
1047 let resp = svc.handle(req).await.unwrap();
1048 assert_eq!(resp.status, StatusCode::CONFLICT);
1049 }
1050
1051 #[tokio::test]
1052 async fn test_template_lifecycle() {
1053 let state = make_state();
1054 let svc = SesV2Service::new(state);
1055
1056 let req = make_request(
1058 Method::POST,
1059 "/v2/email/templates",
1060 r#"{"TemplateName": "welcome", "TemplateContent": {"Subject": "Welcome", "Html": "<h1>Hi</h1>", "Text": "Hi"}}"#,
1061 );
1062 let resp = svc.handle(req).await.unwrap();
1063 assert_eq!(resp.status, StatusCode::OK);
1064
1065 let req = make_request(Method::GET, "/v2/email/templates/welcome", "");
1067 let resp = svc.handle(req).await.unwrap();
1068 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1069 assert_eq!(body["TemplateName"], "welcome");
1070 assert_eq!(body["TemplateContent"]["Subject"], "Welcome");
1071
1072 let req = make_request(
1074 Method::PUT,
1075 "/v2/email/templates/welcome",
1076 r#"{"TemplateContent": {"Subject": "Updated Welcome"}}"#,
1077 );
1078 let resp = svc.handle(req).await.unwrap();
1079 assert_eq!(resp.status, StatusCode::OK);
1080
1081 let req = make_request(Method::GET, "/v2/email/templates/welcome", "");
1083 let resp = svc.handle(req).await.unwrap();
1084 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1085 assert_eq!(body["TemplateContent"]["Subject"], "Updated Welcome");
1086
1087 let req = make_request(Method::GET, "/v2/email/templates", "");
1089 let resp = svc.handle(req).await.unwrap();
1090 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1091 assert_eq!(body["TemplatesMetadata"].as_array().unwrap().len(), 1);
1092
1093 let req = make_request(Method::DELETE, "/v2/email/templates/welcome", "");
1095 let resp = svc.handle(req).await.unwrap();
1096 assert_eq!(resp.status, StatusCode::OK);
1097
1098 let req = make_request(Method::GET, "/v2/email/templates/welcome", "");
1100 let resp = svc.handle(req).await.unwrap();
1101 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1102 }
1103
1104 #[tokio::test]
1105 async fn test_send_email() {
1106 let state = make_state();
1107 let svc = SesV2Service::new(state.clone());
1108
1109 let req = make_request(
1110 Method::POST,
1111 "/v2/email/outbound-emails",
1112 r#"{
1113 "FromEmailAddress": "sender@example.com",
1114 "Destination": {
1115 "ToAddresses": ["recipient@example.com"]
1116 },
1117 "Content": {
1118 "Simple": {
1119 "Subject": {"Data": "Test Subject"},
1120 "Body": {
1121 "Text": {"Data": "Hello world"},
1122 "Html": {"Data": "<p>Hello world</p>"}
1123 }
1124 }
1125 }
1126 }"#,
1127 );
1128 let resp = svc.handle(req).await.unwrap();
1129 assert_eq!(resp.status, StatusCode::OK);
1130 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1131 assert!(body["MessageId"].as_str().is_some());
1132
1133 let s = state.read();
1135 assert_eq!(s.sent_emails.len(), 1);
1136 assert_eq!(s.sent_emails[0].from, "sender@example.com");
1137 assert_eq!(s.sent_emails[0].to, vec!["recipient@example.com"]);
1138 assert_eq!(s.sent_emails[0].subject.as_deref(), Some("Test Subject"));
1139 }
1140
1141 #[tokio::test]
1142 async fn test_get_account() {
1143 let state = make_state();
1144 let svc = SesV2Service::new(state);
1145
1146 let req = make_request(Method::GET, "/v2/email/account", "");
1147 let resp = svc.handle(req).await.unwrap();
1148 assert_eq!(resp.status, StatusCode::OK);
1149 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1150 assert_eq!(body["SendingEnabled"], true);
1151 assert!(body["SendQuota"]["Max24HourSend"].as_f64().unwrap() > 0.0);
1152 }
1153
1154 #[tokio::test]
1155 async fn test_configuration_set_lifecycle() {
1156 let state = make_state();
1157 let svc = SesV2Service::new(state);
1158
1159 let req = make_request(
1161 Method::POST,
1162 "/v2/email/configuration-sets",
1163 r#"{"ConfigurationSetName": "my-config"}"#,
1164 );
1165 let resp = svc.handle(req).await.unwrap();
1166 assert_eq!(resp.status, StatusCode::OK);
1167
1168 let req = make_request(Method::GET, "/v2/email/configuration-sets/my-config", "");
1170 let resp = svc.handle(req).await.unwrap();
1171 assert_eq!(resp.status, StatusCode::OK);
1172 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1173 assert_eq!(body["ConfigurationSetName"], "my-config");
1174
1175 let req = make_request(Method::GET, "/v2/email/configuration-sets", "");
1177 let resp = svc.handle(req).await.unwrap();
1178 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1179 assert_eq!(body["ConfigurationSets"].as_array().unwrap().len(), 1);
1180
1181 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/my-config", "");
1183 let resp = svc.handle(req).await.unwrap();
1184 assert_eq!(resp.status, StatusCode::OK);
1185
1186 let req = make_request(Method::GET, "/v2/email/configuration-sets/my-config", "");
1188 let resp = svc.handle(req).await.unwrap();
1189 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1190 }
1191
1192 #[tokio::test]
1193 async fn test_send_email_raw_content() {
1194 let state = make_state();
1195 let svc = SesV2Service::new(state.clone());
1196
1197 let req = make_request(
1198 Method::POST,
1199 "/v2/email/outbound-emails",
1200 r#"{
1201 "FromEmailAddress": "sender@example.com",
1202 "Destination": {
1203 "ToAddresses": ["to@example.com"]
1204 },
1205 "Content": {
1206 "Raw": {
1207 "Data": "From: sender@example.com\r\nTo: to@example.com\r\nSubject: Raw\r\n\r\nBody"
1208 }
1209 }
1210 }"#,
1211 );
1212 let resp = svc.handle(req).await.unwrap();
1213 assert_eq!(resp.status, StatusCode::OK);
1214 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1215 assert!(body["MessageId"].as_str().is_some());
1216
1217 let s = state.read();
1218 assert_eq!(s.sent_emails.len(), 1);
1219 assert!(s.sent_emails[0].raw_data.is_some());
1220 assert!(
1221 s.sent_emails[0].subject.is_none(),
1222 "Raw emails should not have parsed subject"
1223 );
1224 }
1225
1226 #[tokio::test]
1227 async fn test_send_email_template_content() {
1228 let state = make_state();
1229 let svc = SesV2Service::new(state.clone());
1230
1231 let req = make_request(
1232 Method::POST,
1233 "/v2/email/outbound-emails",
1234 r#"{
1235 "FromEmailAddress": "sender@example.com",
1236 "Destination": {
1237 "ToAddresses": ["to@example.com"]
1238 },
1239 "Content": {
1240 "Template": {
1241 "TemplateName": "welcome",
1242 "TemplateData": "{\"name\": \"Alice\"}"
1243 }
1244 }
1245 }"#,
1246 );
1247 let resp = svc.handle(req).await.unwrap();
1248 assert_eq!(resp.status, StatusCode::OK);
1249
1250 let s = state.read();
1251 assert_eq!(s.sent_emails.len(), 1);
1252 assert_eq!(s.sent_emails[0].template_name.as_deref(), Some("welcome"));
1253 assert_eq!(
1254 s.sent_emails[0].template_data.as_deref(),
1255 Some("{\"name\": \"Alice\"}")
1256 );
1257 }
1258
1259 #[tokio::test]
1260 async fn test_send_email_missing_content() {
1261 let state = make_state();
1262 let svc = SesV2Service::new(state);
1263
1264 let req = make_request(
1265 Method::POST,
1266 "/v2/email/outbound-emails",
1267 r#"{"FromEmailAddress": "sender@example.com", "Destination": {"ToAddresses": ["to@example.com"]}}"#,
1268 );
1269 let resp = svc.handle(req).await.unwrap();
1270 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
1271 }
1272
1273 #[tokio::test]
1274 async fn test_send_email_with_cc_and_bcc() {
1275 let state = make_state();
1276 let svc = SesV2Service::new(state.clone());
1277
1278 let req = make_request(
1279 Method::POST,
1280 "/v2/email/outbound-emails",
1281 r#"{
1282 "FromEmailAddress": "sender@example.com",
1283 "Destination": {
1284 "ToAddresses": ["to@example.com"],
1285 "CcAddresses": ["cc@example.com"],
1286 "BccAddresses": ["bcc@example.com"]
1287 },
1288 "Content": {
1289 "Simple": {
1290 "Subject": {"Data": "Test"},
1291 "Body": {"Text": {"Data": "Hello"}}
1292 }
1293 }
1294 }"#,
1295 );
1296 let resp = svc.handle(req).await.unwrap();
1297 assert_eq!(resp.status, StatusCode::OK);
1298
1299 let s = state.read();
1300 assert_eq!(s.sent_emails[0].cc, vec!["cc@example.com"]);
1301 assert_eq!(s.sent_emails[0].bcc, vec!["bcc@example.com"]);
1302 }
1303
1304 #[tokio::test]
1305 async fn test_send_bulk_email() {
1306 let state = make_state();
1307 let svc = SesV2Service::new(state.clone());
1308
1309 let req = make_request(
1310 Method::POST,
1311 "/v2/email/outbound-bulk-emails",
1312 r#"{
1313 "FromEmailAddress": "sender@example.com",
1314 "DefaultContent": {
1315 "Template": {
1316 "TemplateName": "bulk-template",
1317 "TemplateData": "{\"default\": true}"
1318 }
1319 },
1320 "BulkEmailEntries": [
1321 {"Destination": {"ToAddresses": ["a@example.com"]}},
1322 {"Destination": {"ToAddresses": ["b@example.com"]}}
1323 ]
1324 }"#,
1325 );
1326 let resp = svc.handle(req).await.unwrap();
1327 assert_eq!(resp.status, StatusCode::OK);
1328 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1329 let results = body["BulkEmailEntryResults"].as_array().unwrap();
1330 assert_eq!(results.len(), 2);
1331 assert_eq!(results[0]["Status"], "SUCCESS");
1332 assert_eq!(results[1]["Status"], "SUCCESS");
1333
1334 let s = state.read();
1335 assert_eq!(s.sent_emails.len(), 2);
1336 assert_eq!(s.sent_emails[0].to, vec!["a@example.com"]);
1337 assert_eq!(s.sent_emails[1].to, vec!["b@example.com"]);
1338 }
1339
1340 #[tokio::test]
1341 async fn test_send_bulk_email_empty_entries() {
1342 let state = make_state();
1343 let svc = SesV2Service::new(state);
1344
1345 let req = make_request(
1346 Method::POST,
1347 "/v2/email/outbound-bulk-emails",
1348 r#"{"FromEmailAddress": "s@example.com", "BulkEmailEntries": []}"#,
1349 );
1350 let resp = svc.handle(req).await.unwrap();
1351 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
1352 }
1353
1354 #[tokio::test]
1355 async fn test_delete_nonexistent_identity() {
1356 let state = make_state();
1357 let svc = SesV2Service::new(state);
1358
1359 let req = make_request(
1360 Method::DELETE,
1361 "/v2/email/identities/nobody%40example.com",
1362 "",
1363 );
1364 let resp = svc.handle(req).await.unwrap();
1365 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1366 }
1367
1368 #[tokio::test]
1369 async fn test_duplicate_configuration_set() {
1370 let state = make_state();
1371 let svc = SesV2Service::new(state);
1372
1373 let req = make_request(
1374 Method::POST,
1375 "/v2/email/configuration-sets",
1376 r#"{"ConfigurationSetName": "dup-config"}"#,
1377 );
1378 svc.handle(req).await.unwrap();
1379
1380 let req = make_request(
1381 Method::POST,
1382 "/v2/email/configuration-sets",
1383 r#"{"ConfigurationSetName": "dup-config"}"#,
1384 );
1385 let resp = svc.handle(req).await.unwrap();
1386 assert_eq!(resp.status, StatusCode::CONFLICT);
1387 }
1388
1389 #[tokio::test]
1390 async fn test_duplicate_template() {
1391 let state = make_state();
1392 let svc = SesV2Service::new(state);
1393
1394 let req = make_request(
1395 Method::POST,
1396 "/v2/email/templates",
1397 r#"{"TemplateName": "dup-tmpl", "TemplateContent": {}}"#,
1398 );
1399 svc.handle(req).await.unwrap();
1400
1401 let req = make_request(
1402 Method::POST,
1403 "/v2/email/templates",
1404 r#"{"TemplateName": "dup-tmpl", "TemplateContent": {}}"#,
1405 );
1406 let resp = svc.handle(req).await.unwrap();
1407 assert_eq!(resp.status, StatusCode::CONFLICT);
1408 }
1409
1410 #[tokio::test]
1411 async fn test_delete_nonexistent_template() {
1412 let state = make_state();
1413 let svc = SesV2Service::new(state);
1414
1415 let req = make_request(Method::DELETE, "/v2/email/templates/nope", "");
1416 let resp = svc.handle(req).await.unwrap();
1417 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1418 }
1419
1420 #[tokio::test]
1421 async fn test_delete_nonexistent_configuration_set() {
1422 let state = make_state();
1423 let svc = SesV2Service::new(state);
1424
1425 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/nope", "");
1426 let resp = svc.handle(req).await.unwrap();
1427 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1428 }
1429
1430 #[tokio::test]
1431 async fn test_unknown_route() {
1432 let state = make_state();
1433 let svc = SesV2Service::new(state);
1434
1435 let req = make_request(Method::GET, "/v2/email/unknown-resource", "");
1436 let result = svc.handle(req).await;
1437 assert!(result.is_err(), "Unknown route should return error");
1438 }
1439
1440 #[tokio::test]
1441 async fn test_update_nonexistent_template() {
1442 let state = make_state();
1443 let svc = SesV2Service::new(state);
1444
1445 let req = make_request(
1446 Method::PUT,
1447 "/v2/email/templates/nonexistent",
1448 r#"{"TemplateContent": {"Subject": "Updated"}}"#,
1449 );
1450 let resp = svc.handle(req).await.unwrap();
1451 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1452 }
1453
1454 #[tokio::test]
1455 async fn test_invalid_json_body() {
1456 let state = make_state();
1457 let svc = SesV2Service::new(state);
1458
1459 let req = make_request(Method::POST, "/v2/email/identities", "not valid json {{{");
1460 let result = svc.handle(req).await;
1461 assert!(result.is_err(), "Invalid JSON body should return error");
1462 }
1463
1464 #[tokio::test]
1465 async fn test_create_identity_missing_name() {
1466 let state = make_state();
1467 let svc = SesV2Service::new(state);
1468
1469 let req = make_request(Method::POST, "/v2/email/identities", r#"{}"#);
1470 let resp = svc.handle(req).await.unwrap();
1471 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
1472 }
1473
1474 #[tokio::test]
1477 async fn test_contact_list_lifecycle() {
1478 let state = make_state();
1479 let svc = SesV2Service::new(state);
1480
1481 let req = make_request(
1483 Method::POST,
1484 "/v2/email/contact-lists",
1485 r#"{
1486 "ContactListName": "my-list",
1487 "Description": "Test list",
1488 "Topics": [
1489 {
1490 "TopicName": "newsletters",
1491 "DisplayName": "Newsletters",
1492 "Description": "Weekly newsletters",
1493 "DefaultSubscriptionStatus": "OPT_IN"
1494 }
1495 ]
1496 }"#,
1497 );
1498 let resp = svc.handle(req).await.unwrap();
1499 assert_eq!(resp.status, StatusCode::OK);
1500
1501 let req = make_request(Method::GET, "/v2/email/contact-lists/my-list", "{}");
1503 let resp = svc.handle(req).await.unwrap();
1504 assert_eq!(resp.status, StatusCode::OK);
1505 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1506 assert_eq!(body["ContactListName"], "my-list");
1507 assert_eq!(body["Description"], "Test list");
1508 assert_eq!(body["Topics"][0]["TopicName"], "newsletters");
1509 assert_eq!(body["Topics"][0]["DefaultSubscriptionStatus"], "OPT_IN");
1510 assert!(body["CreatedTimestamp"].as_f64().is_some());
1511 assert!(body["LastUpdatedTimestamp"].as_f64().is_some());
1512
1513 let req = make_request(Method::GET, "/v2/email/contact-lists", "{}");
1515 let resp = svc.handle(req).await.unwrap();
1516 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1517 assert_eq!(body["ContactLists"].as_array().unwrap().len(), 1);
1518 assert_eq!(body["ContactLists"][0]["ContactListName"], "my-list");
1519
1520 let req = make_request(
1522 Method::PUT,
1523 "/v2/email/contact-lists/my-list",
1524 r#"{
1525 "Description": "Updated description",
1526 "Topics": [
1527 {
1528 "TopicName": "newsletters",
1529 "DisplayName": "Updated Newsletters",
1530 "Description": "Updated desc",
1531 "DefaultSubscriptionStatus": "OPT_OUT"
1532 },
1533 {
1534 "TopicName": "promotions",
1535 "DisplayName": "Promotions",
1536 "Description": "Promo emails",
1537 "DefaultSubscriptionStatus": "OPT_OUT"
1538 }
1539 ]
1540 }"#,
1541 );
1542 let resp = svc.handle(req).await.unwrap();
1543 assert_eq!(resp.status, StatusCode::OK);
1544
1545 let req = make_request(Method::GET, "/v2/email/contact-lists/my-list", "{}");
1547 let resp = svc.handle(req).await.unwrap();
1548 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1549 assert_eq!(body["Description"], "Updated description");
1550 assert_eq!(body["Topics"].as_array().unwrap().len(), 2);
1551
1552 let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
1554 let resp = svc.handle(req).await.unwrap();
1555 assert_eq!(resp.status, StatusCode::OK);
1556
1557 let req = make_request(Method::GET, "/v2/email/contact-lists/my-list", "{}");
1559 let resp = svc.handle(req).await.unwrap();
1560 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1561 }
1562
1563 #[tokio::test]
1564 async fn test_duplicate_contact_list() {
1565 let state = make_state();
1566 let svc = SesV2Service::new(state);
1567
1568 let req = make_request(
1569 Method::POST,
1570 "/v2/email/contact-lists",
1571 r#"{"ContactListName": "dup-list"}"#,
1572 );
1573 svc.handle(req).await.unwrap();
1574
1575 let req = make_request(
1576 Method::POST,
1577 "/v2/email/contact-lists",
1578 r#"{"ContactListName": "dup-list"}"#,
1579 );
1580 let resp = svc.handle(req).await.unwrap();
1581 assert_eq!(resp.status, StatusCode::CONFLICT);
1582 }
1583
1584 #[tokio::test]
1585 async fn test_contact_list_not_found() {
1586 let state = make_state();
1587 let svc = SesV2Service::new(state);
1588
1589 let req = make_request(Method::GET, "/v2/email/contact-lists/nonexistent", "{}");
1590 let resp = svc.handle(req).await.unwrap();
1591 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1592 }
1593
1594 #[tokio::test]
1597 async fn test_contact_lifecycle() {
1598 let state = make_state();
1599 let svc = SesV2Service::new(state);
1600
1601 let req = make_request(
1603 Method::POST,
1604 "/v2/email/contact-lists",
1605 r#"{
1606 "ContactListName": "my-list",
1607 "Topics": [
1608 {
1609 "TopicName": "newsletters",
1610 "DisplayName": "Newsletters",
1611 "Description": "Weekly newsletters",
1612 "DefaultSubscriptionStatus": "OPT_OUT"
1613 }
1614 ]
1615 }"#,
1616 );
1617 svc.handle(req).await.unwrap();
1618
1619 let req = make_request(
1621 Method::POST,
1622 "/v2/email/contact-lists/my-list/contacts",
1623 r#"{
1624 "EmailAddress": "user@example.com",
1625 "TopicPreferences": [
1626 {"TopicName": "newsletters", "SubscriptionStatus": "OPT_IN"}
1627 ],
1628 "UnsubscribeAll": false
1629 }"#,
1630 );
1631 let resp = svc.handle(req).await.unwrap();
1632 assert_eq!(resp.status, StatusCode::OK);
1633
1634 let req = make_request(
1636 Method::GET,
1637 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
1638 "{}",
1639 );
1640 let resp = svc.handle(req).await.unwrap();
1641 assert_eq!(resp.status, StatusCode::OK);
1642 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1643 assert_eq!(body["EmailAddress"], "user@example.com");
1644 assert_eq!(body["ContactListName"], "my-list");
1645 assert_eq!(body["UnsubscribeAll"], false);
1646 assert_eq!(body["TopicPreferences"][0]["TopicName"], "newsletters");
1647 assert_eq!(body["TopicPreferences"][0]["SubscriptionStatus"], "OPT_IN");
1648 assert_eq!(
1649 body["TopicDefaultPreferences"][0]["SubscriptionStatus"],
1650 "OPT_OUT"
1651 );
1652 assert!(body["CreatedTimestamp"].as_f64().is_some());
1653
1654 let req = make_request(
1656 Method::GET,
1657 "/v2/email/contact-lists/my-list/contacts",
1658 "{}",
1659 );
1660 let resp = svc.handle(req).await.unwrap();
1661 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1662 assert_eq!(body["Contacts"].as_array().unwrap().len(), 1);
1663 assert_eq!(body["Contacts"][0]["EmailAddress"], "user@example.com");
1664
1665 let req = make_request(
1667 Method::PUT,
1668 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
1669 r#"{
1670 "TopicPreferences": [
1671 {"TopicName": "newsletters", "SubscriptionStatus": "OPT_OUT"}
1672 ],
1673 "UnsubscribeAll": true
1674 }"#,
1675 );
1676 let resp = svc.handle(req).await.unwrap();
1677 assert_eq!(resp.status, StatusCode::OK);
1678
1679 let req = make_request(
1681 Method::GET,
1682 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
1683 "{}",
1684 );
1685 let resp = svc.handle(req).await.unwrap();
1686 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1687 assert_eq!(body["UnsubscribeAll"], true);
1688 assert_eq!(body["TopicPreferences"][0]["SubscriptionStatus"], "OPT_OUT");
1689
1690 let req = make_request(
1692 Method::DELETE,
1693 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
1694 "",
1695 );
1696 let resp = svc.handle(req).await.unwrap();
1697 assert_eq!(resp.status, StatusCode::OK);
1698
1699 let req = make_request(
1701 Method::GET,
1702 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
1703 "{}",
1704 );
1705 let resp = svc.handle(req).await.unwrap();
1706 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1707 }
1708
1709 #[tokio::test]
1710 async fn test_duplicate_contact() {
1711 let state = make_state();
1712 let svc = SesV2Service::new(state);
1713
1714 let req = make_request(
1715 Method::POST,
1716 "/v2/email/contact-lists",
1717 r#"{"ContactListName": "my-list"}"#,
1718 );
1719 svc.handle(req).await.unwrap();
1720
1721 let req = make_request(
1722 Method::POST,
1723 "/v2/email/contact-lists/my-list/contacts",
1724 r#"{"EmailAddress": "dup@example.com"}"#,
1725 );
1726 svc.handle(req).await.unwrap();
1727
1728 let req = make_request(
1729 Method::POST,
1730 "/v2/email/contact-lists/my-list/contacts",
1731 r#"{"EmailAddress": "dup@example.com"}"#,
1732 );
1733 let resp = svc.handle(req).await.unwrap();
1734 assert_eq!(resp.status, StatusCode::CONFLICT);
1735 }
1736
1737 #[tokio::test]
1738 async fn test_contact_in_nonexistent_list() {
1739 let state = make_state();
1740 let svc = SesV2Service::new(state);
1741
1742 let req = make_request(
1743 Method::POST,
1744 "/v2/email/contact-lists/no-such-list/contacts",
1745 r#"{"EmailAddress": "user@example.com"}"#,
1746 );
1747 let resp = svc.handle(req).await.unwrap();
1748 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1749 }
1750
1751 #[tokio::test]
1752 async fn test_get_nonexistent_contact() {
1753 let state = make_state();
1754 let svc = SesV2Service::new(state);
1755
1756 let req = make_request(
1757 Method::POST,
1758 "/v2/email/contact-lists",
1759 r#"{"ContactListName": "my-list"}"#,
1760 );
1761 svc.handle(req).await.unwrap();
1762
1763 let req = make_request(
1764 Method::GET,
1765 "/v2/email/contact-lists/my-list/contacts/nobody%40example.com",
1766 "{}",
1767 );
1768 let resp = svc.handle(req).await.unwrap();
1769 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1770 }
1771
1772 #[tokio::test]
1773 async fn test_delete_contact_list_cascades_contacts() {
1774 let state = make_state();
1775 let svc = SesV2Service::new(state.clone());
1776
1777 let req = make_request(
1779 Method::POST,
1780 "/v2/email/contact-lists",
1781 r#"{"ContactListName": "my-list"}"#,
1782 );
1783 svc.handle(req).await.unwrap();
1784
1785 let req = make_request(
1786 Method::POST,
1787 "/v2/email/contact-lists/my-list/contacts",
1788 r#"{"EmailAddress": "user@example.com"}"#,
1789 );
1790 svc.handle(req).await.unwrap();
1791
1792 let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
1794 svc.handle(req).await.unwrap();
1795
1796 let s = state.read();
1798 assert!(!s.contacts.contains_key("my-list"));
1799 }
1800
1801 #[tokio::test]
1802 async fn test_tag_resource() {
1803 let state = make_state();
1804 let svc = SesV2Service::new(state.clone());
1805
1806 let req = make_request(
1808 Method::POST,
1809 "/v2/email/identities",
1810 r#"{"EmailIdentity": "test@example.com"}"#,
1811 );
1812 svc.handle(req).await.unwrap();
1813
1814 let req = make_request(
1816 Method::POST,
1817 "/v2/email/tags",
1818 r#"{"ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com", "Tags": [{"Key": "env", "Value": "prod"}, {"Key": "team", "Value": "backend"}]}"#,
1819 );
1820 let resp = svc.handle(req).await.unwrap();
1821 assert_eq!(resp.status, StatusCode::OK);
1822
1823 let mut qp = HashMap::new();
1825 qp.insert(
1826 "ResourceArn".to_string(),
1827 "arn:aws:ses:us-east-1:123456789012:identity/test@example.com".to_string(),
1828 );
1829 let req = make_request_with_query(
1830 Method::GET,
1831 "/v2/email/tags",
1832 "",
1833 "ResourceArn=arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com",
1834 qp,
1835 );
1836 let resp = svc.handle(req).await.unwrap();
1837 assert_eq!(resp.status, StatusCode::OK);
1838 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1839 let tags = body["Tags"].as_array().unwrap();
1840 assert_eq!(tags.len(), 2);
1841 }
1842
1843 #[tokio::test]
1844 async fn test_untag_resource() {
1845 let state = make_state();
1846 let svc = SesV2Service::new(state.clone());
1847
1848 let req = make_request(
1850 Method::POST,
1851 "/v2/email/identities",
1852 r#"{"EmailIdentity": "test@example.com"}"#,
1853 );
1854 svc.handle(req).await.unwrap();
1855
1856 let arn = "arn:aws:ses:us-east-1:123456789012:identity/test@example.com";
1857
1858 let req = make_request(
1860 Method::POST,
1861 "/v2/email/tags",
1862 &format!(
1863 r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "env", "Value": "prod"}}, {{"Key": "team", "Value": "backend"}}]}}"#
1864 ),
1865 );
1866 svc.handle(req).await.unwrap();
1867
1868 let mut qp = HashMap::new();
1870 qp.insert("ResourceArn".to_string(), arn.to_string());
1871 qp.insert("TagKeys".to_string(), "env".to_string());
1872 let raw_query = format!("ResourceArn={}&TagKeys=env", urlencoded(arn));
1873 let req = make_request_with_query(Method::DELETE, "/v2/email/tags", "", &raw_query, qp);
1874 let resp = svc.handle(req).await.unwrap();
1875 assert_eq!(resp.status, StatusCode::OK);
1876
1877 let s = state.read();
1879 let tags = s.tags.get(arn).unwrap();
1880 assert_eq!(tags.len(), 1);
1881 assert_eq!(tags.get("team").unwrap(), "backend");
1882 }
1883
1884 #[tokio::test]
1885 async fn test_tag_nonexistent_resource() {
1886 let state = make_state();
1887 let svc = SesV2Service::new(state);
1888
1889 let req = make_request(
1890 Method::POST,
1891 "/v2/email/tags",
1892 r#"{"ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/nope", "Tags": [{"Key": "k", "Value": "v"}]}"#,
1893 );
1894 let resp = svc.handle(req).await.unwrap();
1895 assert_eq!(resp.status, StatusCode::NOT_FOUND);
1896 }
1897
1898 #[tokio::test]
1899 async fn test_delete_identity_removes_tags() {
1900 let state = make_state();
1901 let svc = SesV2Service::new(state.clone());
1902
1903 let req = make_request(
1904 Method::POST,
1905 "/v2/email/identities",
1906 r#"{"EmailIdentity": "test@example.com"}"#,
1907 );
1908 svc.handle(req).await.unwrap();
1909
1910 let arn = "arn:aws:ses:us-east-1:123456789012:identity/test@example.com";
1911 let req = make_request(
1912 Method::POST,
1913 "/v2/email/tags",
1914 &format!(r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "k", "Value": "v"}}]}}"#),
1915 );
1916 svc.handle(req).await.unwrap();
1917
1918 let req = make_request(
1920 Method::DELETE,
1921 "/v2/email/identities/test%40example.com",
1922 "",
1923 );
1924 svc.handle(req).await.unwrap();
1925
1926 let s = state.read();
1928 assert!(!s.tags.contains_key(arn));
1929 }
1930
1931 #[tokio::test]
1932 async fn test_delete_config_set_removes_tags() {
1933 let state = make_state();
1934 let svc = SesV2Service::new(state.clone());
1935
1936 let req = make_request(
1937 Method::POST,
1938 "/v2/email/configuration-sets",
1939 r#"{"ConfigurationSetName": "my-config"}"#,
1940 );
1941 svc.handle(req).await.unwrap();
1942
1943 let arn = "arn:aws:ses:us-east-1:123456789012:configuration-set/my-config";
1944 let req = make_request(
1945 Method::POST,
1946 "/v2/email/tags",
1947 &format!(r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "k", "Value": "v"}}]}}"#),
1948 );
1949 svc.handle(req).await.unwrap();
1950
1951 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/my-config", "");
1953 svc.handle(req).await.unwrap();
1954
1955 let s = state.read();
1956 assert!(!s.tags.contains_key(arn));
1957 }
1958
1959 #[tokio::test]
1960 async fn test_delete_contact_list_removes_tags() {
1961 let state = make_state();
1962 let svc = SesV2Service::new(state.clone());
1963
1964 let req = make_request(
1965 Method::POST,
1966 "/v2/email/contact-lists",
1967 r#"{"ContactListName": "my-list"}"#,
1968 );
1969 svc.handle(req).await.unwrap();
1970
1971 let arn = "arn:aws:ses:us-east-1:123456789012:contact-list/my-list";
1972 let req = make_request(
1973 Method::POST,
1974 "/v2/email/tags",
1975 &format!(r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "k", "Value": "v"}}]}}"#),
1976 );
1977 svc.handle(req).await.unwrap();
1978
1979 let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
1981 svc.handle(req).await.unwrap();
1982
1983 let s = state.read();
1984 assert!(!s.tags.contains_key(arn));
1985 }
1986
1987 fn urlencoded(s: &str) -> String {
1988 form_urlencoded::byte_serialize(s.as_bytes()).collect()
1989 }
1990
1991 #[tokio::test]
1994 async fn test_suppressed_destination_lifecycle() {
1995 let state = make_state();
1996 let svc = SesV2Service::new(state);
1997
1998 let req = make_request(
2000 Method::PUT,
2001 "/v2/email/suppression/addresses",
2002 r#"{"EmailAddress": "bounce@example.com", "Reason": "BOUNCE"}"#,
2003 );
2004 let resp = svc.handle(req).await.unwrap();
2005 assert_eq!(resp.status, StatusCode::OK);
2006
2007 let req = make_request(
2009 Method::GET,
2010 "/v2/email/suppression/addresses/bounce%40example.com",
2011 "",
2012 );
2013 let resp = svc.handle(req).await.unwrap();
2014 assert_eq!(resp.status, StatusCode::OK);
2015 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2016 assert_eq!(
2017 body["SuppressedDestination"]["EmailAddress"],
2018 "bounce@example.com"
2019 );
2020 assert_eq!(body["SuppressedDestination"]["Reason"], "BOUNCE");
2021 assert!(body["SuppressedDestination"]["LastUpdateTime"]
2022 .as_f64()
2023 .is_some());
2024
2025 let req = make_request(Method::GET, "/v2/email/suppression/addresses", "");
2027 let resp = svc.handle(req).await.unwrap();
2028 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2029 assert_eq!(
2030 body["SuppressedDestinationSummaries"]
2031 .as_array()
2032 .unwrap()
2033 .len(),
2034 1
2035 );
2036
2037 let req = make_request(
2039 Method::DELETE,
2040 "/v2/email/suppression/addresses/bounce%40example.com",
2041 "",
2042 );
2043 let resp = svc.handle(req).await.unwrap();
2044 assert_eq!(resp.status, StatusCode::OK);
2045
2046 let req = make_request(
2048 Method::GET,
2049 "/v2/email/suppression/addresses/bounce%40example.com",
2050 "",
2051 );
2052 let resp = svc.handle(req).await.unwrap();
2053 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2054 }
2055
2056 #[tokio::test]
2057 async fn test_suppressed_destination_complaint() {
2058 let state = make_state();
2059 let svc = SesV2Service::new(state);
2060
2061 let req = make_request(
2062 Method::PUT,
2063 "/v2/email/suppression/addresses",
2064 r#"{"EmailAddress": "complaint@example.com", "Reason": "COMPLAINT"}"#,
2065 );
2066 let resp = svc.handle(req).await.unwrap();
2067 assert_eq!(resp.status, StatusCode::OK);
2068
2069 let req = make_request(
2070 Method::GET,
2071 "/v2/email/suppression/addresses/complaint%40example.com",
2072 "",
2073 );
2074 let resp = svc.handle(req).await.unwrap();
2075 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2076 assert_eq!(body["SuppressedDestination"]["Reason"], "COMPLAINT");
2077 }
2078
2079 #[tokio::test]
2080 async fn test_suppressed_destination_invalid_reason() {
2081 let state = make_state();
2082 let svc = SesV2Service::new(state);
2083
2084 let req = make_request(
2085 Method::PUT,
2086 "/v2/email/suppression/addresses",
2087 r#"{"EmailAddress": "bad@example.com", "Reason": "INVALID"}"#,
2088 );
2089 let resp = svc.handle(req).await.unwrap();
2090 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
2091 }
2092
2093 #[tokio::test]
2094 async fn test_suppressed_destination_upsert() {
2095 let state = make_state();
2096 let svc = SesV2Service::new(state);
2097
2098 let req = make_request(
2100 Method::PUT,
2101 "/v2/email/suppression/addresses",
2102 r#"{"EmailAddress": "user@example.com", "Reason": "BOUNCE"}"#,
2103 );
2104 svc.handle(req).await.unwrap();
2105
2106 let req = make_request(
2108 Method::PUT,
2109 "/v2/email/suppression/addresses",
2110 r#"{"EmailAddress": "user@example.com", "Reason": "COMPLAINT"}"#,
2111 );
2112 svc.handle(req).await.unwrap();
2113
2114 let req = make_request(
2115 Method::GET,
2116 "/v2/email/suppression/addresses/user%40example.com",
2117 "",
2118 );
2119 let resp = svc.handle(req).await.unwrap();
2120 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2121 assert_eq!(body["SuppressedDestination"]["Reason"], "COMPLAINT");
2122 }
2123
2124 #[tokio::test]
2125 async fn test_delete_nonexistent_suppressed_destination() {
2126 let state = make_state();
2127 let svc = SesV2Service::new(state);
2128
2129 let req = make_request(
2130 Method::DELETE,
2131 "/v2/email/suppression/addresses/nobody%40example.com",
2132 "",
2133 );
2134 let resp = svc.handle(req).await.unwrap();
2135 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2136 }
2137
2138 #[tokio::test]
2141 async fn test_event_destination_lifecycle() {
2142 let state = make_state();
2143 let svc = SesV2Service::new(state);
2144
2145 let req = make_request(
2147 Method::POST,
2148 "/v2/email/configuration-sets",
2149 r#"{"ConfigurationSetName": "my-config"}"#,
2150 );
2151 svc.handle(req).await.unwrap();
2152
2153 let req = make_request(
2155 Method::POST,
2156 "/v2/email/configuration-sets/my-config/event-destinations",
2157 r#"{
2158 "EventDestinationName": "my-dest",
2159 "EventDestination": {
2160 "Enabled": true,
2161 "MatchingEventTypes": ["SEND", "BOUNCE"],
2162 "SnsDestination": {"TopicArn": "arn:aws:sns:us-east-1:123456789012:my-topic"}
2163 }
2164 }"#,
2165 );
2166 let resp = svc.handle(req).await.unwrap();
2167 assert_eq!(resp.status, StatusCode::OK);
2168
2169 let req = make_request(
2171 Method::GET,
2172 "/v2/email/configuration-sets/my-config/event-destinations",
2173 "",
2174 );
2175 let resp = svc.handle(req).await.unwrap();
2176 assert_eq!(resp.status, StatusCode::OK);
2177 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2178 let dests = body["EventDestinations"].as_array().unwrap();
2179 assert_eq!(dests.len(), 1);
2180 assert_eq!(dests[0]["Name"], "my-dest");
2181 assert_eq!(dests[0]["Enabled"], true);
2182 assert_eq!(dests[0]["MatchingEventTypes"], json!(["SEND", "BOUNCE"]));
2183 assert_eq!(
2184 dests[0]["SnsDestination"]["TopicArn"],
2185 "arn:aws:sns:us-east-1:123456789012:my-topic"
2186 );
2187
2188 let req = make_request(
2190 Method::PUT,
2191 "/v2/email/configuration-sets/my-config/event-destinations/my-dest",
2192 r#"{
2193 "EventDestination": {
2194 "Enabled": false,
2195 "MatchingEventTypes": ["DELIVERY"],
2196 "SnsDestination": {"TopicArn": "arn:aws:sns:us-east-1:123456789012:updated-topic"}
2197 }
2198 }"#,
2199 );
2200 let resp = svc.handle(req).await.unwrap();
2201 assert_eq!(resp.status, StatusCode::OK);
2202
2203 let req = make_request(
2205 Method::GET,
2206 "/v2/email/configuration-sets/my-config/event-destinations",
2207 "",
2208 );
2209 let resp = svc.handle(req).await.unwrap();
2210 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2211 let dests = body["EventDestinations"].as_array().unwrap();
2212 assert_eq!(dests[0]["Enabled"], false);
2213 assert_eq!(dests[0]["MatchingEventTypes"], json!(["DELIVERY"]));
2214
2215 let req = make_request(
2217 Method::DELETE,
2218 "/v2/email/configuration-sets/my-config/event-destinations/my-dest",
2219 "",
2220 );
2221 let resp = svc.handle(req).await.unwrap();
2222 assert_eq!(resp.status, StatusCode::OK);
2223
2224 let req = make_request(
2226 Method::GET,
2227 "/v2/email/configuration-sets/my-config/event-destinations",
2228 "",
2229 );
2230 let resp = svc.handle(req).await.unwrap();
2231 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2232 assert!(body["EventDestinations"].as_array().unwrap().is_empty());
2233 }
2234
2235 #[tokio::test]
2236 async fn test_event_destination_config_set_not_found() {
2237 let state = make_state();
2238 let svc = SesV2Service::new(state);
2239
2240 let req = make_request(
2241 Method::POST,
2242 "/v2/email/configuration-sets/nonexistent/event-destinations",
2243 r#"{
2244 "EventDestinationName": "dest",
2245 "EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}
2246 }"#,
2247 );
2248 let resp = svc.handle(req).await.unwrap();
2249 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2250 }
2251
2252 #[tokio::test]
2253 async fn test_event_destination_duplicate() {
2254 let state = make_state();
2255 let svc = SesV2Service::new(state);
2256
2257 let req = make_request(
2258 Method::POST,
2259 "/v2/email/configuration-sets",
2260 r#"{"ConfigurationSetName": "my-config"}"#,
2261 );
2262 svc.handle(req).await.unwrap();
2263
2264 let body = r#"{
2265 "EventDestinationName": "dup-dest",
2266 "EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}
2267 }"#;
2268
2269 let req = make_request(
2270 Method::POST,
2271 "/v2/email/configuration-sets/my-config/event-destinations",
2272 body,
2273 );
2274 svc.handle(req).await.unwrap();
2275
2276 let req = make_request(
2277 Method::POST,
2278 "/v2/email/configuration-sets/my-config/event-destinations",
2279 body,
2280 );
2281 let resp = svc.handle(req).await.unwrap();
2282 assert_eq!(resp.status, StatusCode::CONFLICT);
2283 }
2284
2285 #[tokio::test]
2286 async fn test_update_nonexistent_event_destination() {
2287 let state = make_state();
2288 let svc = SesV2Service::new(state);
2289
2290 let req = make_request(
2291 Method::POST,
2292 "/v2/email/configuration-sets",
2293 r#"{"ConfigurationSetName": "my-config"}"#,
2294 );
2295 svc.handle(req).await.unwrap();
2296
2297 let req = make_request(
2298 Method::PUT,
2299 "/v2/email/configuration-sets/my-config/event-destinations/nonexistent",
2300 r#"{"EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}}"#,
2301 );
2302 let resp = svc.handle(req).await.unwrap();
2303 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2304 }
2305
2306 #[tokio::test]
2307 async fn test_delete_nonexistent_event_destination() {
2308 let state = make_state();
2309 let svc = SesV2Service::new(state);
2310
2311 let req = make_request(
2312 Method::POST,
2313 "/v2/email/configuration-sets",
2314 r#"{"ConfigurationSetName": "my-config"}"#,
2315 );
2316 svc.handle(req).await.unwrap();
2317
2318 let req = make_request(
2319 Method::DELETE,
2320 "/v2/email/configuration-sets/my-config/event-destinations/nonexistent",
2321 "",
2322 );
2323 let resp = svc.handle(req).await.unwrap();
2324 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2325 }
2326
2327 #[tokio::test]
2328 async fn test_event_destinations_cleaned_on_config_set_delete() {
2329 let state = make_state();
2330 let svc = SesV2Service::new(state.clone());
2331
2332 let req = make_request(
2333 Method::POST,
2334 "/v2/email/configuration-sets",
2335 r#"{"ConfigurationSetName": "my-config"}"#,
2336 );
2337 svc.handle(req).await.unwrap();
2338
2339 let req = make_request(
2340 Method::POST,
2341 "/v2/email/configuration-sets/my-config/event-destinations",
2342 r#"{
2343 "EventDestinationName": "dest1",
2344 "EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}
2345 }"#,
2346 );
2347 svc.handle(req).await.unwrap();
2348
2349 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/my-config", "");
2350 svc.handle(req).await.unwrap();
2351
2352 let s = state.read();
2353 assert!(!s.event_destinations.contains_key("my-config"));
2354 }
2355
2356 #[tokio::test]
2359 async fn test_identity_policy_lifecycle() {
2360 let state = make_state();
2361 let svc = SesV2Service::new(state);
2362
2363 let req = make_request(
2365 Method::POST,
2366 "/v2/email/identities",
2367 r#"{"EmailIdentity": "test@example.com"}"#,
2368 );
2369 svc.handle(req).await.unwrap();
2370
2371 let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"ses:SendEmail","Resource":"*"}]}"#;
2373 let req = make_request(
2374 Method::POST,
2375 "/v2/email/identities/test%40example.com/policies/my-policy",
2376 &format!(
2377 r#"{{"Policy": {}}}"#,
2378 serde_json::to_string(policy_doc).unwrap()
2379 ),
2380 );
2381 let resp = svc.handle(req).await.unwrap();
2382 assert_eq!(resp.status, StatusCode::OK);
2383
2384 let req = make_request(
2386 Method::GET,
2387 "/v2/email/identities/test%40example.com/policies",
2388 "",
2389 );
2390 let resp = svc.handle(req).await.unwrap();
2391 assert_eq!(resp.status, StatusCode::OK);
2392 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2393 assert!(body["Policies"]["my-policy"].is_string());
2394 assert_eq!(body["Policies"]["my-policy"].as_str().unwrap(), policy_doc);
2395
2396 let updated_doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
2398 let req = make_request(
2399 Method::PUT,
2400 "/v2/email/identities/test%40example.com/policies/my-policy",
2401 &format!(
2402 r#"{{"Policy": {}}}"#,
2403 serde_json::to_string(updated_doc).unwrap()
2404 ),
2405 );
2406 let resp = svc.handle(req).await.unwrap();
2407 assert_eq!(resp.status, StatusCode::OK);
2408
2409 let req = make_request(
2411 Method::GET,
2412 "/v2/email/identities/test%40example.com/policies",
2413 "",
2414 );
2415 let resp = svc.handle(req).await.unwrap();
2416 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2417 assert_eq!(body["Policies"]["my-policy"].as_str().unwrap(), updated_doc);
2418
2419 let req = make_request(
2421 Method::DELETE,
2422 "/v2/email/identities/test%40example.com/policies/my-policy",
2423 "",
2424 );
2425 let resp = svc.handle(req).await.unwrap();
2426 assert_eq!(resp.status, StatusCode::OK);
2427
2428 let req = make_request(
2430 Method::GET,
2431 "/v2/email/identities/test%40example.com/policies",
2432 "",
2433 );
2434 let resp = svc.handle(req).await.unwrap();
2435 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2436 assert!(body["Policies"].as_object().unwrap().is_empty());
2437 }
2438
2439 #[tokio::test]
2440 async fn test_identity_policy_identity_not_found() {
2441 let state = make_state();
2442 let svc = SesV2Service::new(state);
2443
2444 let req = make_request(
2445 Method::POST,
2446 "/v2/email/identities/nonexistent%40example.com/policies/my-policy",
2447 r#"{"Policy": "{}"}"#,
2448 );
2449 let resp = svc.handle(req).await.unwrap();
2450 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2451 }
2452
2453 #[tokio::test]
2454 async fn test_identity_policy_duplicate() {
2455 let state = make_state();
2456 let svc = SesV2Service::new(state);
2457
2458 let req = make_request(
2459 Method::POST,
2460 "/v2/email/identities",
2461 r#"{"EmailIdentity": "test@example.com"}"#,
2462 );
2463 svc.handle(req).await.unwrap();
2464
2465 let req = make_request(
2466 Method::POST,
2467 "/v2/email/identities/test%40example.com/policies/my-policy",
2468 r#"{"Policy": "{}"}"#,
2469 );
2470 svc.handle(req).await.unwrap();
2471
2472 let req = make_request(
2473 Method::POST,
2474 "/v2/email/identities/test%40example.com/policies/my-policy",
2475 r#"{"Policy": "{}"}"#,
2476 );
2477 let resp = svc.handle(req).await.unwrap();
2478 assert_eq!(resp.status, StatusCode::CONFLICT);
2479 }
2480
2481 #[tokio::test]
2482 async fn test_update_nonexistent_policy() {
2483 let state = make_state();
2484 let svc = SesV2Service::new(state);
2485
2486 let req = make_request(
2487 Method::POST,
2488 "/v2/email/identities",
2489 r#"{"EmailIdentity": "test@example.com"}"#,
2490 );
2491 svc.handle(req).await.unwrap();
2492
2493 let req = make_request(
2494 Method::PUT,
2495 "/v2/email/identities/test%40example.com/policies/nonexistent",
2496 r#"{"Policy": "{}"}"#,
2497 );
2498 let resp = svc.handle(req).await.unwrap();
2499 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2500 }
2501
2502 #[tokio::test]
2503 async fn test_delete_nonexistent_policy() {
2504 let state = make_state();
2505 let svc = SesV2Service::new(state);
2506
2507 let req = make_request(
2508 Method::POST,
2509 "/v2/email/identities",
2510 r#"{"EmailIdentity": "test@example.com"}"#,
2511 );
2512 svc.handle(req).await.unwrap();
2513
2514 let req = make_request(
2515 Method::DELETE,
2516 "/v2/email/identities/test%40example.com/policies/nonexistent",
2517 "",
2518 );
2519 let resp = svc.handle(req).await.unwrap();
2520 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2521 }
2522
2523 #[tokio::test]
2524 async fn test_policies_cleaned_on_identity_delete() {
2525 let state = make_state();
2526 let svc = SesV2Service::new(state.clone());
2527
2528 let req = make_request(
2529 Method::POST,
2530 "/v2/email/identities",
2531 r#"{"EmailIdentity": "test@example.com"}"#,
2532 );
2533 svc.handle(req).await.unwrap();
2534
2535 let req = make_request(
2536 Method::POST,
2537 "/v2/email/identities/test%40example.com/policies/my-policy",
2538 r#"{"Policy": "{}"}"#,
2539 );
2540 svc.handle(req).await.unwrap();
2541
2542 let req = make_request(
2543 Method::DELETE,
2544 "/v2/email/identities/test%40example.com",
2545 "",
2546 );
2547 svc.handle(req).await.unwrap();
2548
2549 let s = state.read();
2550 assert!(!s.identity_policies.contains_key("test@example.com"));
2551 }
2552
2553 #[tokio::test]
2556 async fn test_put_email_identity_dkim_attributes() {
2557 let state = make_state();
2558 let svc = SesV2Service::new(state.clone());
2559
2560 let req = make_request(
2562 Method::POST,
2563 "/v2/email/identities",
2564 r#"{"EmailIdentity": "example.com"}"#,
2565 );
2566 svc.handle(req).await.unwrap();
2567
2568 let req = make_request(
2570 Method::PUT,
2571 "/v2/email/identities/example.com/dkim",
2572 r#"{"SigningEnabled": false}"#,
2573 );
2574 let resp = svc.handle(req).await.unwrap();
2575 assert_eq!(resp.status, StatusCode::OK);
2576
2577 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
2579 let resp = svc.handle(req).await.unwrap();
2580 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2581 assert_eq!(body["DkimAttributes"]["SigningEnabled"], false);
2582 }
2583
2584 #[tokio::test]
2585 async fn test_put_email_identity_dkim_attributes_not_found() {
2586 let state = make_state();
2587 let svc = SesV2Service::new(state);
2588
2589 let req = make_request(
2590 Method::PUT,
2591 "/v2/email/identities/nonexistent.com/dkim",
2592 r#"{"SigningEnabled": false}"#,
2593 );
2594 let resp = svc.handle(req).await.unwrap();
2595 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2596 }
2597
2598 #[tokio::test]
2599 async fn test_put_email_identity_dkim_signing_attributes() {
2600 let state = make_state();
2601 let svc = SesV2Service::new(state.clone());
2602
2603 let req = make_request(
2604 Method::POST,
2605 "/v2/email/identities",
2606 r#"{"EmailIdentity": "example.com"}"#,
2607 );
2608 svc.handle(req).await.unwrap();
2609
2610 let req = make_request(
2611 Method::PUT,
2612 "/v2/email/identities/example.com/dkim/signing",
2613 r#"{"SigningAttributesOrigin": "EXTERNAL", "SigningAttributes": {"DomainSigningPrivateKey": "key123", "DomainSigningSelector": "sel1"}}"#,
2614 );
2615 let resp = svc.handle(req).await.unwrap();
2616 assert_eq!(resp.status, StatusCode::OK);
2617 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2618 assert_eq!(body["DkimStatus"], "SUCCESS");
2619 assert!(!body["DkimTokens"].as_array().unwrap().is_empty());
2620
2621 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
2623 let resp = svc.handle(req).await.unwrap();
2624 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2625 assert_eq!(
2626 body["DkimAttributes"]["SigningAttributesOrigin"],
2627 "EXTERNAL"
2628 );
2629 }
2630
2631 #[tokio::test]
2632 async fn test_put_email_identity_feedback_attributes() {
2633 let state = make_state();
2634 let svc = SesV2Service::new(state.clone());
2635
2636 let req = make_request(
2637 Method::POST,
2638 "/v2/email/identities",
2639 r#"{"EmailIdentity": "test@example.com"}"#,
2640 );
2641 svc.handle(req).await.unwrap();
2642
2643 let req = make_request(
2644 Method::PUT,
2645 "/v2/email/identities/test%40example.com/feedback",
2646 r#"{"EmailForwardingEnabled": false}"#,
2647 );
2648 let resp = svc.handle(req).await.unwrap();
2649 assert_eq!(resp.status, StatusCode::OK);
2650
2651 let req = make_request(Method::GET, "/v2/email/identities/test%40example.com", "");
2652 let resp = svc.handle(req).await.unwrap();
2653 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2654 assert_eq!(body["FeedbackForwardingStatus"], false);
2655 }
2656
2657 #[tokio::test]
2658 async fn test_put_email_identity_mail_from_attributes() {
2659 let state = make_state();
2660 let svc = SesV2Service::new(state.clone());
2661
2662 let req = make_request(
2663 Method::POST,
2664 "/v2/email/identities",
2665 r#"{"EmailIdentity": "example.com"}"#,
2666 );
2667 svc.handle(req).await.unwrap();
2668
2669 let req = make_request(
2670 Method::PUT,
2671 "/v2/email/identities/example.com/mail-from",
2672 r#"{"MailFromDomain": "mail.example.com", "BehaviorOnMxFailure": "REJECT_MESSAGE"}"#,
2673 );
2674 let resp = svc.handle(req).await.unwrap();
2675 assert_eq!(resp.status, StatusCode::OK);
2676
2677 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
2678 let resp = svc.handle(req).await.unwrap();
2679 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2680 assert_eq!(
2681 body["MailFromAttributes"]["MailFromDomain"],
2682 "mail.example.com"
2683 );
2684 assert_eq!(
2685 body["MailFromAttributes"]["BehaviorOnMxFailure"],
2686 "REJECT_MESSAGE"
2687 );
2688 }
2689
2690 #[tokio::test]
2691 async fn test_put_email_identity_configuration_set_attributes() {
2692 let state = make_state();
2693 let svc = SesV2Service::new(state.clone());
2694
2695 let req = make_request(
2696 Method::POST,
2697 "/v2/email/identities",
2698 r#"{"EmailIdentity": "example.com"}"#,
2699 );
2700 svc.handle(req).await.unwrap();
2701
2702 let req = make_request(
2703 Method::PUT,
2704 "/v2/email/identities/example.com/configuration-set",
2705 r#"{"ConfigurationSetName": "my-config"}"#,
2706 );
2707 let resp = svc.handle(req).await.unwrap();
2708 assert_eq!(resp.status, StatusCode::OK);
2709
2710 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
2711 let resp = svc.handle(req).await.unwrap();
2712 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2713 assert_eq!(body["ConfigurationSetName"], "my-config");
2714 }
2715
2716 #[tokio::test]
2719 async fn test_put_configuration_set_sending_options() {
2720 let state = make_state();
2721 let svc = SesV2Service::new(state);
2722
2723 let req = make_request(
2725 Method::POST,
2726 "/v2/email/configuration-sets",
2727 r#"{"ConfigurationSetName": "test-config"}"#,
2728 );
2729 svc.handle(req).await.unwrap();
2730
2731 let req = make_request(
2733 Method::PUT,
2734 "/v2/email/configuration-sets/test-config/sending",
2735 r#"{"SendingEnabled": false}"#,
2736 );
2737 let resp = svc.handle(req).await.unwrap();
2738 assert_eq!(resp.status, StatusCode::OK);
2739
2740 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
2742 let resp = svc.handle(req).await.unwrap();
2743 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2744 assert_eq!(body["SendingOptions"]["SendingEnabled"], false);
2745 }
2746
2747 #[tokio::test]
2748 async fn test_put_configuration_set_sending_options_not_found() {
2749 let state = make_state();
2750 let svc = SesV2Service::new(state);
2751
2752 let req = make_request(
2753 Method::PUT,
2754 "/v2/email/configuration-sets/nonexistent/sending",
2755 r#"{"SendingEnabled": false}"#,
2756 );
2757 let resp = svc.handle(req).await.unwrap();
2758 assert_eq!(resp.status, StatusCode::NOT_FOUND);
2759 }
2760
2761 #[tokio::test]
2762 async fn test_put_configuration_set_delivery_options() {
2763 let state = make_state();
2764 let svc = SesV2Service::new(state);
2765
2766 let req = make_request(
2767 Method::POST,
2768 "/v2/email/configuration-sets",
2769 r#"{"ConfigurationSetName": "test-config"}"#,
2770 );
2771 svc.handle(req).await.unwrap();
2772
2773 let req = make_request(
2774 Method::PUT,
2775 "/v2/email/configuration-sets/test-config/delivery-options",
2776 r#"{"TlsPolicy": "REQUIRE", "SendingPoolName": "my-pool"}"#,
2777 );
2778 let resp = svc.handle(req).await.unwrap();
2779 assert_eq!(resp.status, StatusCode::OK);
2780
2781 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
2782 let resp = svc.handle(req).await.unwrap();
2783 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2784 assert_eq!(body["DeliveryOptions"]["TlsPolicy"], "REQUIRE");
2785 assert_eq!(body["DeliveryOptions"]["SendingPoolName"], "my-pool");
2786 }
2787
2788 #[tokio::test]
2789 async fn test_put_configuration_set_tracking_options() {
2790 let state = make_state();
2791 let svc = SesV2Service::new(state);
2792
2793 let req = make_request(
2794 Method::POST,
2795 "/v2/email/configuration-sets",
2796 r#"{"ConfigurationSetName": "test-config"}"#,
2797 );
2798 svc.handle(req).await.unwrap();
2799
2800 let req = make_request(
2801 Method::PUT,
2802 "/v2/email/configuration-sets/test-config/tracking-options",
2803 r#"{"CustomRedirectDomain": "track.example.com", "HttpsPolicy": "REQUIRE"}"#,
2804 );
2805 let resp = svc.handle(req).await.unwrap();
2806 assert_eq!(resp.status, StatusCode::OK);
2807
2808 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
2809 let resp = svc.handle(req).await.unwrap();
2810 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2811 assert_eq!(
2812 body["TrackingOptions"]["CustomRedirectDomain"],
2813 "track.example.com"
2814 );
2815 assert_eq!(body["TrackingOptions"]["HttpsPolicy"], "REQUIRE");
2816 }
2817
2818 #[tokio::test]
2819 async fn test_put_configuration_set_suppression_options() {
2820 let state = make_state();
2821 let svc = SesV2Service::new(state);
2822
2823 let req = make_request(
2824 Method::POST,
2825 "/v2/email/configuration-sets",
2826 r#"{"ConfigurationSetName": "test-config"}"#,
2827 );
2828 svc.handle(req).await.unwrap();
2829
2830 let req = make_request(
2831 Method::PUT,
2832 "/v2/email/configuration-sets/test-config/suppression-options",
2833 r#"{"SuppressedReasons": ["BOUNCE", "COMPLAINT"]}"#,
2834 );
2835 let resp = svc.handle(req).await.unwrap();
2836 assert_eq!(resp.status, StatusCode::OK);
2837
2838 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
2839 let resp = svc.handle(req).await.unwrap();
2840 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2841 let reasons = body["SuppressionOptions"]["SuppressedReasons"]
2842 .as_array()
2843 .unwrap();
2844 assert_eq!(reasons.len(), 2);
2845 }
2846
2847 #[tokio::test]
2848 async fn test_put_configuration_set_reputation_options() {
2849 let state = make_state();
2850 let svc = SesV2Service::new(state);
2851
2852 let req = make_request(
2853 Method::POST,
2854 "/v2/email/configuration-sets",
2855 r#"{"ConfigurationSetName": "test-config"}"#,
2856 );
2857 svc.handle(req).await.unwrap();
2858
2859 let req = make_request(
2860 Method::PUT,
2861 "/v2/email/configuration-sets/test-config/reputation-options",
2862 r#"{"ReputationMetricsEnabled": true}"#,
2863 );
2864 let resp = svc.handle(req).await.unwrap();
2865 assert_eq!(resp.status, StatusCode::OK);
2866
2867 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
2868 let resp = svc.handle(req).await.unwrap();
2869 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2870 assert_eq!(body["ReputationOptions"]["ReputationMetricsEnabled"], true);
2871 }
2872
2873 #[tokio::test]
2874 async fn test_put_configuration_set_vdm_options() {
2875 let state = make_state();
2876 let svc = SesV2Service::new(state);
2877
2878 let req = make_request(
2879 Method::POST,
2880 "/v2/email/configuration-sets",
2881 r#"{"ConfigurationSetName": "test-config"}"#,
2882 );
2883 svc.handle(req).await.unwrap();
2884
2885 let req = make_request(
2886 Method::PUT,
2887 "/v2/email/configuration-sets/test-config/vdm-options",
2888 r#"{"DashboardOptions": {"EngagementMetrics": "ENABLED"}, "GuardianOptions": {"OptimizedSharedDelivery": "ENABLED"}}"#,
2889 );
2890 let resp = svc.handle(req).await.unwrap();
2891 assert_eq!(resp.status, StatusCode::OK);
2892
2893 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
2894 let resp = svc.handle(req).await.unwrap();
2895 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2896 assert_eq!(
2897 body["VdmOptions"]["DashboardOptions"]["EngagementMetrics"],
2898 "ENABLED"
2899 );
2900 }
2901
2902 #[tokio::test]
2903 async fn test_put_configuration_set_archiving_options() {
2904 let state = make_state();
2905 let svc = SesV2Service::new(state);
2906
2907 let req = make_request(
2908 Method::POST,
2909 "/v2/email/configuration-sets",
2910 r#"{"ConfigurationSetName": "test-config"}"#,
2911 );
2912 svc.handle(req).await.unwrap();
2913
2914 let req = make_request(
2915 Method::PUT,
2916 "/v2/email/configuration-sets/test-config/archiving-options",
2917 r#"{"ArchiveArn": "arn:aws:ses:us-east-1:123456789012:mailmanager-archive/my-archive"}"#,
2918 );
2919 let resp = svc.handle(req).await.unwrap();
2920 assert_eq!(resp.status, StatusCode::OK);
2921
2922 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
2923 let resp = svc.handle(req).await.unwrap();
2924 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2925 assert!(body["ArchivingOptions"]["ArchiveArn"]
2926 .as_str()
2927 .unwrap()
2928 .contains("my-archive"));
2929 }
2930
2931 #[tokio::test]
2934 async fn test_custom_verification_email_template_lifecycle() {
2935 let state = make_state();
2936 let svc = SesV2Service::new(state);
2937
2938 let req = make_request(
2940 Method::POST,
2941 "/v2/email/custom-verification-email-templates",
2942 r#"{
2943 "TemplateName": "my-verification",
2944 "FromEmailAddress": "noreply@example.com",
2945 "TemplateSubject": "Verify your email",
2946 "TemplateContent": "<h1>Please verify</h1>",
2947 "SuccessRedirectionURL": "https://example.com/success",
2948 "FailureRedirectionURL": "https://example.com/failure"
2949 }"#,
2950 );
2951 let resp = svc.handle(req).await.unwrap();
2952 assert_eq!(resp.status, StatusCode::OK);
2953
2954 let req = make_request(
2956 Method::GET,
2957 "/v2/email/custom-verification-email-templates/my-verification",
2958 "",
2959 );
2960 let resp = svc.handle(req).await.unwrap();
2961 assert_eq!(resp.status, StatusCode::OK);
2962 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2963 assert_eq!(body["TemplateName"], "my-verification");
2964 assert_eq!(body["FromEmailAddress"], "noreply@example.com");
2965 assert_eq!(body["TemplateSubject"], "Verify your email");
2966 assert_eq!(body["TemplateContent"], "<h1>Please verify</h1>");
2967 assert_eq!(body["SuccessRedirectionURL"], "https://example.com/success");
2968 assert_eq!(body["FailureRedirectionURL"], "https://example.com/failure");
2969
2970 let req = make_request(
2972 Method::GET,
2973 "/v2/email/custom-verification-email-templates",
2974 "",
2975 );
2976 let resp = svc.handle(req).await.unwrap();
2977 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2978 assert_eq!(
2979 body["CustomVerificationEmailTemplates"]
2980 .as_array()
2981 .unwrap()
2982 .len(),
2983 1
2984 );
2985
2986 let req = make_request(
2988 Method::PUT,
2989 "/v2/email/custom-verification-email-templates/my-verification",
2990 r#"{"TemplateSubject": "Updated subject"}"#,
2991 );
2992 let resp = svc.handle(req).await.unwrap();
2993 assert_eq!(resp.status, StatusCode::OK);
2994
2995 let req = make_request(
2997 Method::GET,
2998 "/v2/email/custom-verification-email-templates/my-verification",
2999 "",
3000 );
3001 let resp = svc.handle(req).await.unwrap();
3002 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3003 assert_eq!(body["TemplateSubject"], "Updated subject");
3004
3005 let req = make_request(
3007 Method::DELETE,
3008 "/v2/email/custom-verification-email-templates/my-verification",
3009 "",
3010 );
3011 let resp = svc.handle(req).await.unwrap();
3012 assert_eq!(resp.status, StatusCode::OK);
3013
3014 let req = make_request(
3016 Method::GET,
3017 "/v2/email/custom-verification-email-templates/my-verification",
3018 "",
3019 );
3020 let resp = svc.handle(req).await.unwrap();
3021 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3022 }
3023
3024 #[tokio::test]
3025 async fn test_duplicate_custom_verification_template() {
3026 let state = make_state();
3027 let svc = SesV2Service::new(state);
3028
3029 let body = r#"{
3030 "TemplateName": "dup-tmpl",
3031 "FromEmailAddress": "a@b.com",
3032 "TemplateSubject": "s",
3033 "TemplateContent": "c",
3034 "SuccessRedirectionURL": "https://ok",
3035 "FailureRedirectionURL": "https://fail"
3036 }"#;
3037
3038 let req = make_request(
3039 Method::POST,
3040 "/v2/email/custom-verification-email-templates",
3041 body,
3042 );
3043 svc.handle(req).await.unwrap();
3044
3045 let req = make_request(
3046 Method::POST,
3047 "/v2/email/custom-verification-email-templates",
3048 body,
3049 );
3050 let resp = svc.handle(req).await.unwrap();
3051 assert_eq!(resp.status, StatusCode::CONFLICT);
3052 }
3053
3054 #[tokio::test]
3055 async fn test_send_custom_verification_email() {
3056 let state = make_state();
3057 let svc = SesV2Service::new(state.clone());
3058
3059 let req = make_request(
3061 Method::POST,
3062 "/v2/email/custom-verification-email-templates",
3063 r#"{
3064 "TemplateName": "verify",
3065 "FromEmailAddress": "a@b.com",
3066 "TemplateSubject": "Verify",
3067 "TemplateContent": "content",
3068 "SuccessRedirectionURL": "https://ok",
3069 "FailureRedirectionURL": "https://fail"
3070 }"#,
3071 );
3072 svc.handle(req).await.unwrap();
3073
3074 let req = make_request(
3076 Method::POST,
3077 "/v2/email/outbound-custom-verification-emails",
3078 r#"{"EmailAddress": "user@example.com", "TemplateName": "verify"}"#,
3079 );
3080 let resp = svc.handle(req).await.unwrap();
3081 assert_eq!(resp.status, StatusCode::OK);
3082 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3083 assert!(body["MessageId"].as_str().is_some());
3084
3085 let s = state.read();
3087 assert_eq!(s.sent_emails.len(), 1);
3088 assert_eq!(s.sent_emails[0].to, vec!["user@example.com"]);
3089 }
3090
3091 #[tokio::test]
3092 async fn test_send_custom_verification_email_template_not_found() {
3093 let state = make_state();
3094 let svc = SesV2Service::new(state);
3095
3096 let req = make_request(
3097 Method::POST,
3098 "/v2/email/outbound-custom-verification-emails",
3099 r#"{"EmailAddress": "user@example.com", "TemplateName": "nonexistent"}"#,
3100 );
3101 let resp = svc.handle(req).await.unwrap();
3102 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3103 }
3104
3105 #[tokio::test]
3108 async fn test_render_email_template() {
3109 let state = make_state();
3110 let svc = SesV2Service::new(state);
3111
3112 let req = make_request(
3114 Method::POST,
3115 "/v2/email/templates",
3116 r#"{
3117 "TemplateName": "greet",
3118 "TemplateContent": {
3119 "Subject": "Hello {{name}}",
3120 "Html": "<h1>Welcome, {{name}}!</h1><p>Your code is {{code}}.</p>",
3121 "Text": "Welcome, {{name}}! Your code is {{code}}."
3122 }
3123 }"#,
3124 );
3125 svc.handle(req).await.unwrap();
3126
3127 let req = make_request(
3129 Method::POST,
3130 "/v2/email/templates/greet/render",
3131 r#"{"TemplateData": "{\"name\": \"Alice\", \"code\": \"1234\"}"}"#,
3132 );
3133 let resp = svc.handle(req).await.unwrap();
3134 assert_eq!(resp.status, StatusCode::OK);
3135 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3136 let rendered = body["RenderedTemplate"].as_str().unwrap();
3137 assert!(rendered.contains("Subject: Hello Alice"));
3138 assert!(rendered.contains("Welcome, Alice!"));
3139 assert!(rendered.contains("Your code is 1234."));
3140 }
3141
3142 #[tokio::test]
3143 async fn test_render_email_template_not_found() {
3144 let state = make_state();
3145 let svc = SesV2Service::new(state);
3146
3147 let req = make_request(
3148 Method::POST,
3149 "/v2/email/templates/nonexistent/render",
3150 r#"{"TemplateData": "{}"}"#,
3151 );
3152 let resp = svc.handle(req).await.unwrap();
3153 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3154 }
3155
3156 #[tokio::test]
3157 async fn test_render_email_template_missing_data() {
3158 let state = make_state();
3159 let svc = SesV2Service::new(state);
3160
3161 let req = make_request(
3163 Method::POST,
3164 "/v2/email/templates",
3165 r#"{"TemplateName": "t1", "TemplateContent": {"Subject": "Hi"}}"#,
3166 );
3167 svc.handle(req).await.unwrap();
3168
3169 let req = make_request(Method::POST, "/v2/email/templates/t1/render", r#"{}"#);
3170 let resp = svc.handle(req).await.unwrap();
3171 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
3172 }
3173
3174 #[tokio::test]
3177 async fn test_dedicated_ip_pool_lifecycle() {
3178 let state = make_state();
3179 let svc = SesV2Service::new(state);
3180
3181 let req = make_request(
3183 Method::POST,
3184 "/v2/email/dedicated-ip-pools",
3185 r#"{"PoolName": "my-pool", "ScalingMode": "STANDARD"}"#,
3186 );
3187 let resp = svc.handle(req).await.unwrap();
3188 assert_eq!(resp.status, StatusCode::OK);
3189
3190 let req = make_request(Method::GET, "/v2/email/dedicated-ip-pools", "");
3192 let resp = svc.handle(req).await.unwrap();
3193 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3194 assert_eq!(body["DedicatedIpPools"].as_array().unwrap().len(), 1);
3195
3196 let req = make_request(
3198 Method::POST,
3199 "/v2/email/dedicated-ip-pools",
3200 r#"{"PoolName": "my-pool"}"#,
3201 );
3202 let resp = svc.handle(req).await.unwrap();
3203 assert_eq!(resp.status, StatusCode::CONFLICT);
3204
3205 let req = make_request(Method::DELETE, "/v2/email/dedicated-ip-pools/my-pool", "");
3207 let resp = svc.handle(req).await.unwrap();
3208 assert_eq!(resp.status, StatusCode::OK);
3209
3210 let req = make_request(Method::DELETE, "/v2/email/dedicated-ip-pools/my-pool", "");
3212 let resp = svc.handle(req).await.unwrap();
3213 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3214 }
3215
3216 #[tokio::test]
3217 async fn test_managed_pool_generates_ips() {
3218 let state = make_state();
3219 let svc = SesV2Service::new(state);
3220
3221 let req = make_request(
3223 Method::POST,
3224 "/v2/email/dedicated-ip-pools",
3225 r#"{"PoolName": "managed-pool", "ScalingMode": "MANAGED"}"#,
3226 );
3227 let resp = svc.handle(req).await.unwrap();
3228 assert_eq!(resp.status, StatusCode::OK);
3229
3230 let req = make_request_with_query(
3232 Method::GET,
3233 "/v2/email/dedicated-ips",
3234 "",
3235 "PoolName=managed-pool",
3236 {
3237 let mut m = HashMap::new();
3238 m.insert("PoolName".to_string(), "managed-pool".to_string());
3239 m
3240 },
3241 );
3242 let resp = svc.handle(req).await.unwrap();
3243 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3244 let ips = body["DedicatedIps"].as_array().unwrap();
3245 assert_eq!(ips.len(), 3);
3246 assert_eq!(ips[0]["WarmupStatus"], "NOT_APPLICABLE");
3247 assert_eq!(ips[0]["WarmupPercentage"], -1);
3248 }
3249
3250 #[tokio::test]
3251 async fn test_dedicated_ip_operations() {
3252 let state = make_state();
3253 let svc = SesV2Service::new(state);
3254
3255 let req = make_request(
3257 Method::POST,
3258 "/v2/email/dedicated-ip-pools",
3259 r#"{"PoolName": "pool-a", "ScalingMode": "MANAGED"}"#,
3260 );
3261 svc.handle(req).await.unwrap();
3262
3263 let req = make_request(
3264 Method::POST,
3265 "/v2/email/dedicated-ip-pools",
3266 r#"{"PoolName": "pool-b", "ScalingMode": "STANDARD"}"#,
3267 );
3268 svc.handle(req).await.unwrap();
3269
3270 let req = make_request(Method::GET, "/v2/email/dedicated-ips/198.51.100.1", "");
3272 let resp = svc.handle(req).await.unwrap();
3273 assert_eq!(resp.status, StatusCode::OK);
3274 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3275 assert_eq!(body["DedicatedIp"]["PoolName"], "pool-a");
3276
3277 let req = make_request(
3279 Method::PUT,
3280 "/v2/email/dedicated-ips/198.51.100.1/pool",
3281 r#"{"DestinationPoolName": "pool-b"}"#,
3282 );
3283 let resp = svc.handle(req).await.unwrap();
3284 assert_eq!(resp.status, StatusCode::OK);
3285
3286 let req = make_request(Method::GET, "/v2/email/dedicated-ips/198.51.100.1", "");
3288 let resp = svc.handle(req).await.unwrap();
3289 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3290 assert_eq!(body["DedicatedIp"]["PoolName"], "pool-b");
3291
3292 let req = make_request(
3294 Method::PUT,
3295 "/v2/email/dedicated-ips/198.51.100.1/warmup",
3296 r#"{"WarmupPercentage": 50}"#,
3297 );
3298 let resp = svc.handle(req).await.unwrap();
3299 assert_eq!(resp.status, StatusCode::OK);
3300
3301 let req = make_request(Method::GET, "/v2/email/dedicated-ips/198.51.100.1", "");
3302 let resp = svc.handle(req).await.unwrap();
3303 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3304 assert_eq!(body["DedicatedIp"]["WarmupPercentage"], 50);
3305 assert_eq!(body["DedicatedIp"]["WarmupStatus"], "IN_PROGRESS");
3306
3307 let req = make_request(Method::GET, "/v2/email/dedicated-ips/1.2.3.4", "");
3309 let resp = svc.handle(req).await.unwrap();
3310 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3311 }
3312
3313 #[tokio::test]
3314 async fn test_pool_scaling_attributes() {
3315 let state = make_state();
3316 let svc = SesV2Service::new(state);
3317
3318 let req = make_request(
3319 Method::POST,
3320 "/v2/email/dedicated-ip-pools",
3321 r#"{"PoolName": "scalable", "ScalingMode": "STANDARD"}"#,
3322 );
3323 svc.handle(req).await.unwrap();
3324
3325 let req = make_request(
3327 Method::PUT,
3328 "/v2/email/dedicated-ip-pools/scalable/scaling",
3329 r#"{"ScalingMode": "MANAGED"}"#,
3330 );
3331 let resp = svc.handle(req).await.unwrap();
3332 assert_eq!(resp.status, StatusCode::OK);
3333
3334 let req = make_request(
3336 Method::PUT,
3337 "/v2/email/dedicated-ip-pools/scalable/scaling",
3338 r#"{"ScalingMode": "STANDARD"}"#,
3339 );
3340 let resp = svc.handle(req).await.unwrap();
3341 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
3342 }
3343
3344 #[tokio::test]
3345 async fn test_account_dedicated_ip_warmup() {
3346 let state = make_state();
3347 let svc = SesV2Service::new(state);
3348
3349 let req = make_request(
3350 Method::PUT,
3351 "/v2/email/account/dedicated-ips/warmup",
3352 r#"{"AutoWarmupEnabled": true}"#,
3353 );
3354 let resp = svc.handle(req).await.unwrap();
3355 assert_eq!(resp.status, StatusCode::OK);
3356
3357 let req = make_request(Method::GET, "/v2/email/account", "");
3358 let resp = svc.handle(req).await.unwrap();
3359 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3360 assert_eq!(body["DedicatedIpAutoWarmupEnabled"], true);
3361 }
3362
3363 #[tokio::test]
3366 async fn test_multi_region_endpoint_lifecycle() {
3367 let state = make_state();
3368 let svc = SesV2Service::new(state);
3369
3370 let req = make_request(
3372 Method::POST,
3373 "/v2/email/multi-region-endpoints",
3374 r#"{"EndpointName": "global-ep", "Details": {"RoutesDetails": [{"Region": "us-west-2"}]}}"#,
3375 );
3376 let resp = svc.handle(req).await.unwrap();
3377 assert_eq!(resp.status, StatusCode::OK);
3378 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3379 assert_eq!(body["Status"], "READY");
3380 assert!(body["EndpointId"].as_str().is_some());
3381
3382 let req = make_request(
3384 Method::GET,
3385 "/v2/email/multi-region-endpoints/global-ep",
3386 "",
3387 );
3388 let resp = svc.handle(req).await.unwrap();
3389 assert_eq!(resp.status, StatusCode::OK);
3390 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3391 assert_eq!(body["EndpointName"], "global-ep");
3392 assert_eq!(body["Status"], "READY");
3393 let routes = body["Routes"].as_array().unwrap();
3394 assert!(!routes.is_empty());
3395
3396 let req = make_request(Method::GET, "/v2/email/multi-region-endpoints", "");
3398 let resp = svc.handle(req).await.unwrap();
3399 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3400 assert_eq!(body["MultiRegionEndpoints"].as_array().unwrap().len(), 1);
3401
3402 let req = make_request(
3404 Method::POST,
3405 "/v2/email/multi-region-endpoints",
3406 r#"{"EndpointName": "global-ep", "Details": {"RoutesDetails": [{"Region": "eu-west-1"}]}}"#,
3407 );
3408 let resp = svc.handle(req).await.unwrap();
3409 assert_eq!(resp.status, StatusCode::CONFLICT);
3410
3411 let req = make_request(
3413 Method::DELETE,
3414 "/v2/email/multi-region-endpoints/global-ep",
3415 "",
3416 );
3417 let resp = svc.handle(req).await.unwrap();
3418 assert_eq!(resp.status, StatusCode::OK);
3419 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3420 assert_eq!(body["Status"], "DELETING");
3421
3422 let req = make_request(
3424 Method::GET,
3425 "/v2/email/multi-region-endpoints/global-ep",
3426 "",
3427 );
3428 let resp = svc.handle(req).await.unwrap();
3429 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3430 }
3431
3432 #[tokio::test]
3435 async fn test_account_details() {
3436 let state = make_state();
3437 let svc = SesV2Service::new(state);
3438
3439 let req = make_request(
3440 Method::POST,
3441 "/v2/email/account/details",
3442 r#"{"MailType": "TRANSACTIONAL", "WebsiteURL": "https://example.com", "UseCaseDescription": "Testing"}"#,
3443 );
3444 let resp = svc.handle(req).await.unwrap();
3445 assert_eq!(resp.status, StatusCode::OK);
3446
3447 let req = make_request(Method::GET, "/v2/email/account", "");
3448 let resp = svc.handle(req).await.unwrap();
3449 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3450 assert_eq!(body["Details"]["MailType"], "TRANSACTIONAL");
3451 assert_eq!(body["Details"]["WebsiteURL"], "https://example.com");
3452 assert_eq!(body["Details"]["UseCaseDescription"], "Testing");
3453 }
3454
3455 #[tokio::test]
3456 async fn test_account_sending_attributes() {
3457 let state = make_state();
3458 let svc = SesV2Service::new(state);
3459
3460 let req = make_request(
3462 Method::PUT,
3463 "/v2/email/account/sending",
3464 r#"{"SendingEnabled": false}"#,
3465 );
3466 let resp = svc.handle(req).await.unwrap();
3467 assert_eq!(resp.status, StatusCode::OK);
3468
3469 let req = make_request(Method::GET, "/v2/email/account", "");
3470 let resp = svc.handle(req).await.unwrap();
3471 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3472 assert_eq!(body["SendingEnabled"], false);
3473
3474 let req = make_request(
3476 Method::PUT,
3477 "/v2/email/account/sending",
3478 r#"{"SendingEnabled": true}"#,
3479 );
3480 svc.handle(req).await.unwrap();
3481
3482 let req = make_request(Method::GET, "/v2/email/account", "");
3483 let resp = svc.handle(req).await.unwrap();
3484 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3485 assert_eq!(body["SendingEnabled"], true);
3486 }
3487
3488 #[tokio::test]
3489 async fn test_account_suppression_attributes() {
3490 let state = make_state();
3491 let svc = SesV2Service::new(state);
3492
3493 let req = make_request(
3494 Method::PUT,
3495 "/v2/email/account/suppression",
3496 r#"{"SuppressedReasons": ["BOUNCE", "COMPLAINT"]}"#,
3497 );
3498 let resp = svc.handle(req).await.unwrap();
3499 assert_eq!(resp.status, StatusCode::OK);
3500
3501 let req = make_request(Method::GET, "/v2/email/account", "");
3502 let resp = svc.handle(req).await.unwrap();
3503 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3504 let reasons = body["SuppressionAttributes"]["SuppressedReasons"]
3505 .as_array()
3506 .unwrap();
3507 assert_eq!(reasons.len(), 2);
3508 }
3509
3510 #[tokio::test]
3511 async fn test_account_vdm_attributes() {
3512 let state = make_state();
3513 let svc = SesV2Service::new(state);
3514
3515 let req = make_request(
3516 Method::PUT,
3517 "/v2/email/account/vdm",
3518 r#"{"VdmAttributes": {"VdmEnabled": "ENABLED", "DashboardAttributes": {"EngagementMetrics": "ENABLED"}}}"#,
3519 );
3520 let resp = svc.handle(req).await.unwrap();
3521 assert_eq!(resp.status, StatusCode::OK);
3522
3523 let req = make_request(Method::GET, "/v2/email/account", "");
3524 let resp = svc.handle(req).await.unwrap();
3525 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3526 assert_eq!(body["VdmAttributes"]["VdmEnabled"], "ENABLED");
3527 }
3528
3529 #[tokio::test]
3530 async fn test_import_job_lifecycle() {
3531 let state = make_state();
3532 let svc = SesV2Service::new(state);
3533
3534 let req = make_request(
3536 Method::POST,
3537 "/v2/email/import-jobs",
3538 r#"{
3539 "ImportDestination": {
3540 "SuppressionListDestination": {"SuppressionListImportAction": "PUT"}
3541 },
3542 "ImportDataSource": {
3543 "S3Url": "s3://bucket/file.csv",
3544 "DataFormat": "CSV"
3545 }
3546 }"#,
3547 );
3548 let resp = svc.handle(req).await.unwrap();
3549 assert_eq!(resp.status, StatusCode::OK);
3550 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3551 let job_id = body["JobId"].as_str().unwrap().to_string();
3552
3553 let req = make_request(
3555 Method::GET,
3556 &format!("/v2/email/import-jobs/{}", job_id),
3557 "",
3558 );
3559 let resp = svc.handle(req).await.unwrap();
3560 assert_eq!(resp.status, StatusCode::OK);
3561 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3562 assert_eq!(body["JobId"], job_id);
3563 assert_eq!(body["JobStatus"], "COMPLETED");
3564
3565 let req = make_request(Method::POST, "/v2/email/import-jobs/list", "{}");
3567 let resp = svc.handle(req).await.unwrap();
3568 assert_eq!(resp.status, StatusCode::OK);
3569 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3570 assert_eq!(body["ImportJobs"].as_array().unwrap().len(), 1);
3571
3572 let req = make_request(Method::GET, "/v2/email/import-jobs/nonexistent", "");
3574 let resp = svc.handle(req).await.unwrap();
3575 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3576 }
3577
3578 #[tokio::test]
3579 async fn test_export_job_lifecycle() {
3580 let state = make_state();
3581 let svc = SesV2Service::new(state);
3582
3583 let req = make_request(
3585 Method::POST,
3586 "/v2/email/export-jobs",
3587 r#"{
3588 "ExportDataSource": {
3589 "MetricsDataSource": {
3590 "Dimensions": {},
3591 "Namespace": "VDM",
3592 "Metrics": []
3593 }
3594 },
3595 "ExportDestination": {
3596 "DataFormat": "CSV",
3597 "S3Url": "s3://bucket/export"
3598 }
3599 }"#,
3600 );
3601 let resp = svc.handle(req).await.unwrap();
3602 assert_eq!(resp.status, StatusCode::OK);
3603 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3604 let job_id = body["JobId"].as_str().unwrap().to_string();
3605
3606 let req = make_request(
3608 Method::GET,
3609 &format!("/v2/email/export-jobs/{}", job_id),
3610 "",
3611 );
3612 let resp = svc.handle(req).await.unwrap();
3613 assert_eq!(resp.status, StatusCode::OK);
3614 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3615 assert_eq!(body["JobId"], job_id);
3616 assert_eq!(body["JobStatus"], "COMPLETED");
3617 assert_eq!(body["ExportSourceType"], "METRICS_DATA");
3618
3619 let req = make_request(Method::POST, "/v2/email/list-export-jobs", "{}");
3621 let resp = svc.handle(req).await.unwrap();
3622 assert_eq!(resp.status, StatusCode::OK);
3623 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3624 assert_eq!(body["ExportJobs"].as_array().unwrap().len(), 1);
3625
3626 let req = make_request(
3628 Method::PUT,
3629 &format!("/v2/email/export-jobs/{}/cancel", job_id),
3630 "",
3631 );
3632 let resp = svc.handle(req).await.unwrap();
3633 assert_eq!(resp.status, StatusCode::CONFLICT);
3634 }
3635
3636 #[tokio::test]
3637 async fn test_tenant_lifecycle() {
3638 let state = make_state();
3639 let svc = SesV2Service::new(state);
3640
3641 let req = make_request(
3643 Method::POST,
3644 "/v2/email/tenants",
3645 r#"{"TenantName": "my-tenant"}"#,
3646 );
3647 let resp = svc.handle(req).await.unwrap();
3648 assert_eq!(resp.status, StatusCode::OK);
3649 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3650 assert_eq!(body["TenantName"], "my-tenant");
3651 assert!(body["TenantId"].as_str().is_some());
3652 assert_eq!(body["SendingStatus"], "ENABLED");
3653
3654 let req = make_request(
3656 Method::POST,
3657 "/v2/email/tenants/get",
3658 r#"{"TenantName": "my-tenant"}"#,
3659 );
3660 let resp = svc.handle(req).await.unwrap();
3661 assert_eq!(resp.status, StatusCode::OK);
3662 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3663 assert_eq!(body["Tenant"]["TenantName"], "my-tenant");
3664
3665 let req = make_request(Method::POST, "/v2/email/tenants/list", "{}");
3667 let resp = svc.handle(req).await.unwrap();
3668 assert_eq!(resp.status, StatusCode::OK);
3669 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3670 assert_eq!(body["Tenants"].as_array().unwrap().len(), 1);
3671
3672 let req = make_request(
3674 Method::POST,
3675 "/v2/email/tenants/resources",
3676 r#"{"TenantName": "my-tenant", "ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com"}"#,
3677 );
3678 let resp = svc.handle(req).await.unwrap();
3679 assert_eq!(resp.status, StatusCode::OK);
3680
3681 let req = make_request(
3683 Method::POST,
3684 "/v2/email/tenants/resources/list",
3685 r#"{"TenantName": "my-tenant"}"#,
3686 );
3687 let resp = svc.handle(req).await.unwrap();
3688 assert_eq!(resp.status, StatusCode::OK);
3689 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3690 assert_eq!(body["TenantResources"].as_array().unwrap().len(), 1);
3691
3692 let req = make_request(
3694 Method::POST,
3695 "/v2/email/resources/tenants/list",
3696 r#"{"ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com"}"#,
3697 );
3698 let resp = svc.handle(req).await.unwrap();
3699 assert_eq!(resp.status, StatusCode::OK);
3700 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3701 assert_eq!(body["ResourceTenants"].as_array().unwrap().len(), 1);
3702
3703 let req = make_request(
3705 Method::POST,
3706 "/v2/email/tenants/resources/delete",
3707 r#"{"TenantName": "my-tenant", "ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com"}"#,
3708 );
3709 let resp = svc.handle(req).await.unwrap();
3710 assert_eq!(resp.status, StatusCode::OK);
3711
3712 let req = make_request(
3714 Method::POST,
3715 "/v2/email/tenants/resources/list",
3716 r#"{"TenantName": "my-tenant"}"#,
3717 );
3718 let resp = svc.handle(req).await.unwrap();
3719 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3720 assert!(body["TenantResources"].as_array().unwrap().is_empty());
3721
3722 let req = make_request(
3724 Method::POST,
3725 "/v2/email/tenants/delete",
3726 r#"{"TenantName": "my-tenant"}"#,
3727 );
3728 let resp = svc.handle(req).await.unwrap();
3729 assert_eq!(resp.status, StatusCode::OK);
3730
3731 let req = make_request(
3733 Method::POST,
3734 "/v2/email/tenants/get",
3735 r#"{"TenantName": "my-tenant"}"#,
3736 );
3737 let resp = svc.handle(req).await.unwrap();
3738 assert_eq!(resp.status, StatusCode::NOT_FOUND);
3739 }
3740
3741 #[tokio::test]
3742 async fn test_reputation_entity() {
3743 let state = make_state();
3744 let svc = SesV2Service::new(state);
3745
3746 let req = make_request(
3748 Method::GET,
3749 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com",
3750 "",
3751 );
3752 let resp = svc.handle(req).await.unwrap();
3753 assert_eq!(resp.status, StatusCode::OK);
3754 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3755 assert_eq!(
3756 body["ReputationEntity"]["SendingStatusAggregate"],
3757 "ENABLED"
3758 );
3759
3760 let req = make_request(
3762 Method::PUT,
3763 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com/customer-managed-status",
3764 r#"{"SendingStatus": "DISABLED"}"#,
3765 );
3766 let resp = svc.handle(req).await.unwrap();
3767 assert_eq!(resp.status, StatusCode::OK);
3768
3769 let req = make_request(
3771 Method::PUT,
3772 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com/policy",
3773 r#"{"ReputationEntityPolicy": "arn:aws:ses:us-east-1:123456789012:policy/my-policy"}"#,
3774 );
3775 let resp = svc.handle(req).await.unwrap();
3776 assert_eq!(resp.status, StatusCode::OK);
3777
3778 let req = make_request(
3780 Method::GET,
3781 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com",
3782 "",
3783 );
3784 let resp = svc.handle(req).await.unwrap();
3785 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3786 assert_eq!(
3787 body["ReputationEntity"]["CustomerManagedStatus"]["SendingStatus"],
3788 "DISABLED"
3789 );
3790
3791 let req = make_request(Method::POST, "/v2/email/reputation/entities", "{}");
3793 let resp = svc.handle(req).await.unwrap();
3794 assert_eq!(resp.status, StatusCode::OK);
3795 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3796 assert_eq!(body["ReputationEntities"].as_array().unwrap().len(), 1);
3797 }
3798
3799 #[tokio::test]
3800 async fn test_batch_get_metric_data() {
3801 let state = make_state();
3802 let svc = SesV2Service::new(state);
3803
3804 let req = make_request(
3805 Method::POST,
3806 "/v2/email/metrics/batch",
3807 r#"{
3808 "Queries": [
3809 {
3810 "Id": "q1",
3811 "Namespace": "VDM",
3812 "Metric": "SEND",
3813 "StartDate": "2024-01-01T00:00:00Z",
3814 "EndDate": "2024-01-02T00:00:00Z"
3815 }
3816 ]
3817 }"#,
3818 );
3819 let resp = svc.handle(req).await.unwrap();
3820 assert_eq!(resp.status, StatusCode::OK);
3821 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3822 assert_eq!(body["Results"].as_array().unwrap().len(), 1);
3823 assert_eq!(body["Results"][0]["Id"], "q1");
3824 assert!(body["Errors"].as_array().unwrap().is_empty());
3825 }
3826
3827 #[tokio::test]
3828 async fn test_duplicate_tenant() {
3829 let state = make_state();
3830 let svc = SesV2Service::new(state);
3831
3832 let req = make_request(
3833 Method::POST,
3834 "/v2/email/tenants",
3835 r#"{"TenantName": "dup"}"#,
3836 );
3837 svc.handle(req).await.unwrap();
3838
3839 let req = make_request(
3840 Method::POST,
3841 "/v2/email/tenants",
3842 r#"{"TenantName": "dup"}"#,
3843 );
3844 let resp = svc.handle(req).await.unwrap();
3845 assert_eq!(resp.status, StatusCode::CONFLICT);
3846 }
3847}