Skip to main content

fakecloud_ses/service/
mod.rs

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    /// Attach a delivery context for cross-service event fanout.
33    pub fn with_delivery(mut self, ctx: SesDeliveryContext) -> Self {
34        self.delivery_ctx = Some(ctx);
35        self
36    }
37
38    /// Determine the action from the HTTP method and path segments.
39    /// SES v2 uses REST-style routing with base path /v2/email/:
40    ///   GET    /v2/email/account                         -> GetAccount
41    ///   POST   /v2/email/identities                      -> CreateEmailIdentity
42    ///   GET    /v2/email/identities                      -> ListEmailIdentities
43    ///   GET    /v2/email/identities/{id}                 -> GetEmailIdentity
44    ///   DELETE /v2/email/identities/{id}                 -> DeleteEmailIdentity
45    ///   POST   /v2/email/configuration-sets              -> CreateConfigurationSet
46    ///   GET    /v2/email/configuration-sets              -> ListConfigurationSets
47    ///   GET    /v2/email/configuration-sets/{name}       -> GetConfigurationSet
48    ///   DELETE /v2/email/configuration-sets/{name}       -> DeleteConfigurationSet
49    ///   POST   /v2/email/templates                       -> CreateEmailTemplate
50    ///   GET    /v2/email/templates                       -> ListEmailTemplates
51    ///   GET    /v2/email/templates/{name}                -> GetEmailTemplate
52    ///   PUT    /v2/email/templates/{name}                -> UpdateEmailTemplate
53    ///   DELETE /v2/email/templates/{name}                -> DeleteEmailTemplate
54    ///   POST   /v2/email/outbound-emails                 -> SendEmail
55    ///   POST   /v2/email/outbound-bulk-emails            -> SendBulkEmail
56    ///   POST   /v2/email/tags                            -> TagResource
57    ///   DELETE /v2/email/tags                            -> UntagResource
58    ///   GET    /v2/email/tags                            -> ListTagsForResource
59    ///   POST   /v2/email/contact-lists                   -> CreateContactList
60    ///   GET    /v2/email/contact-lists                   -> ListContactLists
61    ///   GET    /v2/email/contact-lists/{name}            -> GetContactList
62    ///   PUT    /v2/email/contact-lists/{name}            -> UpdateContactList
63    ///   DELETE /v2/email/contact-lists/{name}            -> DeleteContactList
64    ///   POST   /v2/email/contact-lists/{name}/contacts   -> CreateContact
65    ///   GET    /v2/email/contact-lists/{name}/contacts   -> ListContacts
66    ///   GET    /v2/email/contact-lists/{name}/contacts/{email} -> GetContact
67    ///   PUT    /v2/email/contact-lists/{name}/contacts/{email} -> UpdateContact
68    ///   DELETE /v2/email/contact-lists/{name}/contacts/{email} -> DeleteContact
69    ///   PUT    /v2/email/suppression/addresses            -> PutSuppressedDestination
70    ///   GET    /v2/email/suppression/addresses            -> ListSuppressedDestinations
71    ///   GET    /v2/email/suppression/addresses/{email}    -> GetSuppressedDestination
72    ///   DELETE /v2/email/suppression/addresses/{email}    -> DeleteSuppressedDestination
73    ///   POST   /v2/email/configuration-sets/{name}/event-destinations -> CreateConfigurationSetEventDestination
74    ///   GET    /v2/email/configuration-sets/{name}/event-destinations -> GetConfigurationSetEventDestinations
75    ///   PUT    /v2/email/configuration-sets/{name}/event-destinations/{dest} -> UpdateConfigurationSetEventDestination
76    ///   DELETE /v2/email/configuration-sets/{name}/event-destinations/{dest} -> DeleteConfigurationSetEventDestination
77    ///   POST   /v2/email/identities/{id}/policies/{policy} -> CreateEmailIdentityPolicy
78    ///   GET    /v2/email/identities/{id}/policies         -> GetEmailIdentityPolicies
79    ///   PUT    /v2/email/identities/{id}/policies/{policy} -> UpdateEmailIdentityPolicy
80    ///   DELETE /v2/email/identities/{id}/policies/{policy} -> DeleteEmailIdentityPolicy
81    ///   PUT    /v2/email/identities/{id}/dkim              -> PutEmailIdentityDkimAttributes
82    ///   PUT    /v2/email/identities/{id}/dkim/signing      -> PutEmailIdentityDkimSigningAttributes
83    ///   PUT    /v2/email/identities/{id}/feedback          -> PutEmailIdentityFeedbackAttributes
84    ///   PUT    /v2/email/identities/{id}/mail-from         -> PutEmailIdentityMailFromAttributes
85    ///   PUT    /v2/email/identities/{id}/configuration-set -> PutEmailIdentityConfigurationSetAttributes
86    ///   PUT    /v2/email/configuration-sets/{name}/sending             -> PutConfigurationSetSendingOptions
87    ///   PUT    /v2/email/configuration-sets/{name}/delivery-options    -> PutConfigurationSetDeliveryOptions
88    ///   PUT    /v2/email/configuration-sets/{name}/tracking-options    -> PutConfigurationSetTrackingOptions
89    ///   PUT    /v2/email/configuration-sets/{name}/suppression-options -> PutConfigurationSetSuppressionOptions
90    ///   PUT    /v2/email/configuration-sets/{name}/reputation-options  -> PutConfigurationSetReputationOptions
91    ///   PUT    /v2/email/configuration-sets/{name}/vdm-options         -> PutConfigurationSetVdmOptions
92    ///   PUT    /v2/email/configuration-sets/{name}/archiving-options   -> PutConfigurationSetArchivingOptions
93    ///   POST   /v2/email/custom-verification-email-templates           -> CreateCustomVerificationEmailTemplate
94    ///   GET    /v2/email/custom-verification-email-templates            -> ListCustomVerificationEmailTemplates
95    ///   GET    /v2/email/custom-verification-email-templates/{name}     -> GetCustomVerificationEmailTemplate
96    ///   PUT    /v2/email/custom-verification-email-templates/{name}     -> UpdateCustomVerificationEmailTemplate
97    ///   DELETE /v2/email/custom-verification-email-templates/{name}     -> DeleteCustomVerificationEmailTemplate
98    ///   POST   /v2/email/outbound-custom-verification-emails            -> SendCustomVerificationEmail
99    ///   POST   /v2/email/templates/{name}/render                        -> TestRenderEmailTemplate
100    ///   POST   /v2/email/import-jobs                                     -> CreateImportJob
101    ///   POST   /v2/email/import-jobs/list                                -> ListImportJobs
102    ///   GET    /v2/email/import-jobs/{id}                                -> GetImportJob
103    ///   POST   /v2/email/export-jobs                                     -> CreateExportJob
104    ///   POST   /v2/email/list-export-jobs                                -> ListExportJobs
105    ///   PUT    /v2/email/export-jobs/{id}/cancel                         -> CancelExportJob
106    ///   GET    /v2/email/export-jobs/{id}                                -> GetExportJob
107    ///   POST   /v2/email/tenants                                         -> CreateTenant
108    ///   POST   /v2/email/tenants/list                                    -> ListTenants
109    ///   POST   /v2/email/tenants/get                                     -> GetTenant
110    ///   POST   /v2/email/tenants/delete                                  -> DeleteTenant
111    ///   POST   /v2/email/tenants/resources                               -> CreateTenantResourceAssociation
112    ///   POST   /v2/email/tenants/resources/delete                        -> DeleteTenantResourceAssociation
113    ///   POST   /v2/email/tenants/resources/list                          -> ListTenantResources
114    ///   POST   /v2/email/resources/tenants/list                          -> ListResourceTenants
115    ///   POST   /v2/email/reputation/entities                             -> ListReputationEntities
116    ///   PUT    /v2/email/reputation/entities/{type}/{ref}/customer-managed-status -> UpdateReputationEntityCustomerManagedStatus
117    ///   PUT    /v2/email/reputation/entities/{type}/{ref}/policy          -> UpdateReputationEntityPolicy
118    ///   GET    /v2/email/reputation/entities/{type}/{ref}                 -> GetReputationEntity
119    ///   POST   /v2/email/metrics/batch                                   -> BatchGetMetricData
120    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
195/// URL-decode a path segment (e.g. `test%40example.com` -> `test@example.com`).
196fn 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        // SDK sends POST .../contacts/list for ListContacts
352        (&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        // Route v1 Query protocol requests to the v1 module.
651        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            // NOTE: SES v1 receipt rule/filter actions are implemented (see v1.rs)
913            // but excluded from the conformance audit because there is no SES v1
914            // Smithy model (only sesv2.json exists) to generate checksums from.
915        ]
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        // Create identity
975        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        // List identities
987        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        // Get identity
993        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        // Delete identity
1001        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        // Verify deleted
1010        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        // Create template
1057        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        // Get template
1066        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        // Update template
1073        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        // Verify update
1082        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        // List templates
1088        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        // Delete template
1094        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        // Verify deleted
1099        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        // Verify stored
1134        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        // Create
1160        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        // Get
1169        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        // List
1176        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        // Delete
1182        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        // Verify deleted
1187        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    // --- Contact List tests ---
1475
1476    #[tokio::test]
1477    async fn test_contact_list_lifecycle() {
1478        let state = make_state();
1479        let svc = SesV2Service::new(state);
1480
1481        // Create contact list with topics
1482        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        // Get contact list
1502        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        // List contact lists
1514        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        // Update contact list
1521        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        // Verify update
1546        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        // Delete contact list
1553        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        // Verify deleted
1558        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    // --- Contact tests ---
1595
1596    #[tokio::test]
1597    async fn test_contact_lifecycle() {
1598        let state = make_state();
1599        let svc = SesV2Service::new(state);
1600
1601        // Create contact list first
1602        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        // Create contact
1620        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        // Get contact
1635        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        // List contacts
1655        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        // Update contact
1666        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        // Verify update
1680        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        // Delete contact
1691        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        // Verify deleted
1700        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        // Create list and contact
1778        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        // Delete the contact list
1793        let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
1794        svc.handle(req).await.unwrap();
1795
1796        // Verify contacts map is cleaned up
1797        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        // Create an identity
1807        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        // Tag it
1815        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        // List tags
1824        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        // Create an identity
1849        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        // Tag it
1859        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        // Untag - remove "env"
1869        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        // Verify only "team" remains
1878        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        // Delete identity
1919        let req = make_request(
1920            Method::DELETE,
1921            "/v2/email/identities/test%40example.com",
1922            "",
1923        );
1924        svc.handle(req).await.unwrap();
1925
1926        // Tags should be gone
1927        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        // Delete config set
1952        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        // Delete contact list
1980        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    // --- Suppression List tests ---
1992
1993    #[tokio::test]
1994    async fn test_suppressed_destination_lifecycle() {
1995        let state = make_state();
1996        let svc = SesV2Service::new(state);
1997
1998        // Put suppressed destination
1999        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        // Get suppressed destination
2008        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        // List suppressed destinations
2026        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        // Delete suppressed destination
2038        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        // Verify deleted
2047        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        // Put with BOUNCE
2099        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        // Put again with COMPLAINT (upsert)
2107        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    // --- Event Destination tests ---
2139
2140    #[tokio::test]
2141    async fn test_event_destination_lifecycle() {
2142        let state = make_state();
2143        let svc = SesV2Service::new(state);
2144
2145        // Create config set first
2146        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        // Create event destination
2154        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        // Get event destinations
2170        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        // Update event destination
2189        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        // Verify update
2204        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        // Delete event destination
2216        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        // Verify deleted
2225        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    // --- Email Identity Policy tests ---
2357
2358    #[tokio::test]
2359    async fn test_identity_policy_lifecycle() {
2360        let state = make_state();
2361        let svc = SesV2Service::new(state);
2362
2363        // Create identity first
2364        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        // Create policy
2372        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        // Get policies
2385        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        // Update policy
2397        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        // Verify update
2410        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        // Delete policy
2420        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        // Verify deleted
2429        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    // --- Identity Attribute tests ---
2554
2555    #[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        // Create identity first
2561        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        // Disable DKIM signing
2569        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        // Verify via GetEmailIdentity
2578        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        // Verify stored
2622        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    // --- Configuration Set Options tests ---
2717
2718    #[tokio::test]
2719    async fn test_put_configuration_set_sending_options() {
2720        let state = make_state();
2721        let svc = SesV2Service::new(state);
2722
2723        // Create config set
2724        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        // Disable sending
2732        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        // Verify
2741        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    // --- Custom Verification Email Template tests ---
2932
2933    #[tokio::test]
2934    async fn test_custom_verification_email_template_lifecycle() {
2935        let state = make_state();
2936        let svc = SesV2Service::new(state);
2937
2938        // Create
2939        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        // Get
2955        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        // List
2971        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        // Update
2987        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        // Verify update
2996        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        // Delete
3006        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        // Verify deleted
3015        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        // Create template first
3060        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        // Send
3075        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        // Verify stored in sent_emails
3086        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    // --- TestRenderEmailTemplate tests ---
3106
3107    #[tokio::test]
3108    async fn test_render_email_template() {
3109        let state = make_state();
3110        let svc = SesV2Service::new(state);
3111
3112        // Create template
3113        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        // Render
3128        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        // Create template
3162        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    // ── Dedicated IP Pool tests ─────────────────────────────────────────
3175
3176    #[tokio::test]
3177    async fn test_dedicated_ip_pool_lifecycle() {
3178        let state = make_state();
3179        let svc = SesV2Service::new(state);
3180
3181        // Create pool
3182        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        // List pools
3191        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        // Duplicate
3197        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        // Delete pool
3206        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        // Delete non-existent
3211        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        // Create managed pool
3222        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        // List dedicated IPs filtered by pool
3231        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        // Create two pools
3256        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        // Get a specific IP
3271        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        // Move IP to pool-b
3278        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        // Verify it moved
3287        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        // Set warmup
3293        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        // Non-existent IP
3308        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        // Change to MANAGED
3326        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        // Cannot change from MANAGED to STANDARD
3335        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    // ── Multi-region Endpoint tests ─────────────────────────────────────
3364
3365    #[tokio::test]
3366    async fn test_multi_region_endpoint_lifecycle() {
3367        let state = make_state();
3368        let svc = SesV2Service::new(state);
3369
3370        // Create
3371        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        // Get
3383        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        // List
3397        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        // Duplicate
3403        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        // Delete
3412        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        // Get after delete
3423        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    // ── Account Settings tests ──────────────────────────────────────────
3433
3434    #[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        // Disable sending
3461        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        // Re-enable
3475        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        // Create import job
3535        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        // Get import job
3554        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        // List import jobs
3566        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        // Get non-existent job
3573        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        // Create export job
3584        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        // Get export job
3607        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        // List export jobs
3620        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        // Cancel — should fail since already COMPLETED
3627        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        // Create tenant
3642        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        // Get tenant
3655        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        // List tenants
3666        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        // Create resource association
3673        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        // List tenant resources
3682        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        // List resource tenants
3693        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        // Delete resource association
3704        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        // Verify association is gone
3713        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        // Delete tenant
3723        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        // Verify deleted
3732        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        // Get default reputation entity (auto-created)
3747        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        // Update customer managed status
3761        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        // Update policy
3770        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        // Verify via get
3779        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        // List reputation entities
3792        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}