Skip to main content

fakecloud_cloudfront/
functions_service.rs

1//! Handlers for CloudFront Batch 3 resources: Functions, Public Keys,
2//! Key Groups, Key Value Stores, Origin Access Identities (legacy),
3//! Monitoring Subscriptions.
4
5use base64::Engine;
6use chrono::Utc;
7use http::header::ETAG;
8use http::{HeaderMap, StatusCode};
9use uuid::Uuid;
10
11use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
12
13use crate::functions::{
14    CloudFrontOriginAccessIdentityConfig, FunctionConfig, ImportSource, KeyGroupConfig,
15    MonitoringSubscriptionBody, PublicKeyConfig, StoredFunction, StoredKeyGroup,
16    StoredKeyValueStore, StoredMonitoringSubscription, StoredOriginAccessIdentity, StoredPublicKey,
17};
18use crate::policies::{
19    not_found, precondition_failed, require_if_match, rfc3339, route_id, xml_with_etag,
20};
21use crate::router::Route;
22use crate::service::{
23    aws_error, esc, generate_id_with_prefix, invalid_argument, xml_response, CloudFrontService,
24    DEFAULT_ACCOUNT,
25};
26use crate::xml_io;
27
28const NS: &str = crate::NAMESPACE;
29const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
30
31// ─── CloudFront Functions ─────────────────────────────────────────────
32
33impl CloudFrontService {
34    pub(crate) fn create_function(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
35        // Body shape: <CreateFunctionRequest><Name/><FunctionConfig/><FunctionCode/></CreateFunctionRequest>
36        let parsed: CreateFunctionRequest = xml_io::from_xml_root(&req.body)
37            .map_err(|e| invalid_argument(format!("invalid CreateFunctionRequest XML: {e}")))?;
38        if parsed.name.is_empty() {
39            return Err(invalid_argument("CreateFunctionRequest.Name is required"));
40        }
41
42        let mut state = self.state.write();
43        let account = state
44            .accounts
45            .entry(DEFAULT_ACCOUNT.to_string())
46            .or_default();
47        if account.functions.contains_key(&parsed.name) {
48            return Err(aws_error(
49                StatusCode::CONFLICT,
50                "FunctionAlreadyExists",
51                format!("Function {} already exists", parsed.name),
52            ));
53        }
54        let now = Utc::now();
55        let etag = generate_id_with_prefix("E");
56        let function_arn = format!(
57            "arn:aws:cloudfront::{}:function/{}",
58            DEFAULT_ACCOUNT, parsed.name
59        );
60        let stored = StoredFunction {
61            name: parsed.name.clone(),
62            etag: etag.clone(),
63            status: "UNPUBLISHED".to_string(),
64            stage: "DEVELOPMENT".to_string(),
65            function_arn: function_arn.clone(),
66            created_time: now,
67            last_modified_time: now,
68            config: parsed.function_config,
69            function_code: parsed.function_code,
70            // No published snapshot until PublishFunction is called.
71            live_function_code: None,
72        };
73        account
74            .functions
75            .insert(parsed.name.clone(), stored.clone());
76        drop(state);
77
78        let body = render_function_summary(&stored, "CreateFunctionResult");
79        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, None))
80    }
81
82    pub(crate) fn describe_function(
83        &self,
84        req: &AwsRequest,
85        route: &Route,
86    ) -> Result<AwsResponse, AwsServiceError> {
87        let name = route_id(route, "Function")?;
88        let stage = parse_stage_query(&req.raw_query);
89        let state = self.state.read();
90        let f = state
91            .accounts
92            .get(DEFAULT_ACCOUNT)
93            .and_then(|a| a.functions.get(&name).cloned())
94            .ok_or_else(|| not_found("Function", &name))?;
95        drop(state);
96        let view = stage_view(&f, &stage);
97        let body = render_function_summary(&view, "DescribeFunctionResult");
98        Ok(xml_with_etag(StatusCode::OK, body, &view.etag, None))
99    }
100
101    pub(crate) fn get_function(
102        &self,
103        req: &AwsRequest,
104        route: &Route,
105    ) -> Result<AwsResponse, AwsServiceError> {
106        let name = route_id(route, "Function")?;
107        let stage = parse_stage_query(&req.raw_query);
108        let state = self.state.read();
109        let f = state
110            .accounts
111            .get(DEFAULT_ACCOUNT)
112            .and_then(|a| a.functions.get(&name).cloned())
113            .ok_or_else(|| not_found("Function", &name))?;
114        drop(state);
115        let view = stage_view(&f, &stage);
116        let mut headers = HeaderMap::new();
117        headers.insert(
118            ETAG,
119            view.etag
120                .parse()
121                .unwrap_or_else(|_| http::HeaderValue::from_static("")),
122        );
123        let bytes = base64::engine::general_purpose::STANDARD
124            .decode(view.function_code.as_bytes())
125            .unwrap_or_default();
126        Ok(AwsResponse {
127            status: StatusCode::OK,
128            headers,
129            content_type: "application/octet-stream".to_string(),
130            body: fakecloud_core::service::ResponseBody::Bytes(bytes::Bytes::from(bytes)),
131        })
132    }
133
134    pub(crate) fn update_function(
135        &self,
136        req: &AwsRequest,
137        route: &Route,
138    ) -> Result<AwsResponse, AwsServiceError> {
139        let name = route_id(route, "Function")?;
140        let if_match = require_if_match(req)?;
141        let parsed: UpdateFunctionRequest = xml_io::from_xml_root(&req.body)
142            .map_err(|e| invalid_argument(format!("invalid UpdateFunctionRequest XML: {e}")))?;
143        let mut state = self.state.write();
144        let account = state
145            .accounts
146            .get_mut(DEFAULT_ACCOUNT)
147            .ok_or_else(|| not_found("Function", &name))?;
148        let f = account
149            .functions
150            .get_mut(&name)
151            .ok_or_else(|| not_found("Function", &name))?;
152        if f.etag != if_match {
153            return Err(precondition_failed());
154        }
155        f.config = parsed.function_config;
156        f.function_code = parsed.function_code;
157        f.etag = generate_id_with_prefix("E");
158        f.last_modified_time = Utc::now();
159        f.status = "UNPUBLISHED".to_string();
160        f.stage = "DEVELOPMENT".to_string();
161        let snap = f.clone();
162        drop(state);
163        let body = render_function_summary(&snap, "UpdateFunctionResult");
164        // SDK has a known typo on UpdateFunctionOutput: it deserializes
165        // the etag from header `ETtag`, not `ETag`. Send both so any
166        // SDK version can read it.
167        let mut headers = HeaderMap::new();
168        if let Ok(v) = http::HeaderValue::from_str(&snap.etag) {
169            headers.insert(ETAG, v.clone());
170            headers.insert("ETtag", v);
171        }
172        Ok(xml_response(StatusCode::OK, body, headers))
173    }
174
175    pub(crate) fn delete_function(
176        &self,
177        req: &AwsRequest,
178        route: &Route,
179    ) -> Result<AwsResponse, AwsServiceError> {
180        let name = route_id(route, "Function")?;
181        let if_match = require_if_match(req)?;
182        let mut state = self.state.write();
183        let account = state
184            .accounts
185            .get_mut(DEFAULT_ACCOUNT)
186            .ok_or_else(|| not_found("Function", &name))?;
187        let f = account
188            .functions
189            .get(&name)
190            .ok_or_else(|| not_found("Function", &name))?;
191        if f.etag != if_match {
192            return Err(precondition_failed());
193        }
194        account.functions.remove(&name);
195        drop(state);
196        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
197    }
198
199    pub(crate) fn list_functions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
200        let stage = validate_stage_query(&req.raw_query)?;
201        let state = self.state.read();
202        let mut items: Vec<StoredFunction> = state
203            .accounts
204            .get(DEFAULT_ACCOUNT)
205            .map(|a| a.functions.values().cloned().collect())
206            .unwrap_or_default();
207        drop(state);
208        items.sort_by(|a, b| a.name.cmp(&b.name));
209
210        let mut body = String::with_capacity(512);
211        body.push_str(XML_DECL);
212        body.push_str(&format!("<FunctionList xmlns=\"{NS}\">"));
213        body.push_str("<Marker></Marker>");
214        body.push_str("<MaxItems>100</MaxItems>");
215        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
216        body.push_str("<Items>");
217        for f in &items {
218            let view = stage_view(f, &stage);
219            body.push_str(&render_function_summary_inner(&view));
220        }
221        body.push_str("</Items>");
222        body.push_str("</FunctionList>");
223        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
224    }
225
226    pub(crate) fn publish_function(
227        &self,
228        req: &AwsRequest,
229        route: &Route,
230    ) -> Result<AwsResponse, AwsServiceError> {
231        let name = route_id(route, "Function")?;
232        let if_match = require_if_match(req)?;
233        let mut state = self.state.write();
234        let account = state
235            .accounts
236            .get_mut(DEFAULT_ACCOUNT)
237            .ok_or_else(|| not_found("Function", &name))?;
238        let f = account
239            .functions
240            .get_mut(&name)
241            .ok_or_else(|| not_found("Function", &name))?;
242        if f.etag != if_match {
243            return Err(precondition_failed());
244        }
245        f.status = "DEPLOYED".to_string();
246        f.stage = "LIVE".to_string();
247        f.last_modified_time = Utc::now();
248        // Freeze the current development code as the LIVE snapshot.
249        // Subsequent UpdateFunction calls mutate `function_code` but
250        // leave this alone, so `TestFunction(Stage=LIVE)` keeps running
251        // the published version until the next Publish.
252        f.live_function_code = Some(f.function_code.clone());
253        let snap = f.clone();
254        drop(state);
255        let body = render_function_summary(&snap, "PublishFunctionResult");
256        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
257    }
258
259    pub(crate) fn test_function(
260        &self,
261        req: &AwsRequest,
262        route: &Route,
263    ) -> Result<AwsResponse, AwsServiceError> {
264        let name = route_id(route, "Function")?;
265        let if_match = require_if_match(req)?;
266        let parsed: TestFunctionRequest = xml_io::from_xml_root(&req.body)
267            .map_err(|e| invalid_argument(format!("invalid TestFunctionRequest XML: {e}")))?;
268        let event_bytes = base64::engine::general_purpose::STANDARD
269            .decode(parsed.event_object.trim().as_bytes())
270            .map_err(|e| invalid_argument(format!("EventObject is not valid base64: {e}")))?;
271
272        let state = self.state.read();
273        let f = state
274            .accounts
275            .get(DEFAULT_ACCOUNT)
276            .and_then(|a| a.functions.get(&name).cloned())
277            .ok_or_else(|| {
278                aws_error(
279                    StatusCode::NOT_FOUND,
280                    "NoSuchFunctionExists",
281                    format!("The specified function does not exist: {name}"),
282                )
283            })?;
284        drop(state);
285        if f.etag != if_match {
286            return Err(precondition_failed());
287        }
288
289        // AWS lets callers pick which version to run. DEVELOPMENT (the
290        // default) is the latest CreateFunction / UpdateFunction body;
291        // LIVE is the snapshot taken at PublishFunction. Falling back to
292        // DEVELOPMENT when no snapshot exists matches the AWS error
293        // shape ("function not yet published") less closely, but is
294        // strictly nicer for tests against unpublished functions.
295        let stage = parsed.stage.as_deref().unwrap_or("DEVELOPMENT");
296        let source_b64 = if stage.eq_ignore_ascii_case("LIVE") {
297            f.live_function_code.as_deref().unwrap_or(&f.function_code)
298        } else {
299            f.function_code.as_str()
300        };
301        let code_bytes = base64::engine::general_purpose::STANDARD
302            .decode(source_b64.as_bytes())
303            .unwrap_or_else(|_| source_b64.as_bytes().to_vec());
304        let code = String::from_utf8(code_bytes)
305            .map_err(|e| invalid_argument(format!("function code is not valid UTF-8: {e}")))?;
306        let exec = crate::js_runtime::run_handler(&code, &event_bytes);
307
308        let mut body = String::with_capacity(1024);
309        body.push_str(XML_DECL);
310        body.push_str(&format!("<TestResult xmlns=\"{NS}\">"));
311        body.push_str(&render_function_summary_inner(&f));
312        body.push_str(&format!(
313            "<ComputeUtilization>{}</ComputeUtilization>",
314            exec.compute_utilization
315        ));
316        body.push_str("<FunctionExecutionLogs>");
317        for line in &exec.logs {
318            body.push_str(&format!("<member>{}</member>", esc(line)));
319        }
320        body.push_str("</FunctionExecutionLogs>");
321        body.push_str(&format!(
322            "<FunctionErrorMessage>{}</FunctionErrorMessage>",
323            esc(exec.error.as_deref().unwrap_or(""))
324        ));
325        body.push_str(&format!(
326            "<FunctionOutput>{}</FunctionOutput>",
327            esc(exec.output.as_deref().unwrap_or(""))
328        ));
329        body.push_str("</TestResult>");
330        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
331    }
332}
333
334// ─── Public Keys ──────────────────────────────────────────────────────
335
336impl CloudFrontService {
337    pub(crate) fn create_public_key(
338        &self,
339        req: &AwsRequest,
340    ) -> Result<AwsResponse, AwsServiceError> {
341        let cfg: PublicKeyConfig = xml_io::from_xml_root(&req.body)
342            .map_err(|e| invalid_argument(format!("invalid PublicKeyConfig XML: {e}")))?;
343        if cfg.name.is_empty() {
344            return Err(invalid_argument("PublicKeyConfig.Name is required"));
345        }
346        if cfg.encoded_key.is_empty() {
347            return Err(invalid_argument("PublicKeyConfig.EncodedKey is required"));
348        }
349        let mut state = self.state.write();
350        let account = state
351            .accounts
352            .entry(DEFAULT_ACCOUNT.to_string())
353            .or_default();
354        if let Some(existing) = account
355            .public_keys
356            .values()
357            .find(|p| p.config.caller_reference == cfg.caller_reference)
358        {
359            return Err(aws_error(
360                StatusCode::CONFLICT,
361                "PublicKeyAlreadyExists",
362                format!(
363                    "PublicKey with same CallerReference exists: {}",
364                    existing.id
365                ),
366            ));
367        }
368        let id = generate_id_with_prefix("K");
369        let etag = generate_id_with_prefix("E");
370        let stored = StoredPublicKey {
371            id: id.clone(),
372            etag: etag.clone(),
373            created_time: Utc::now(),
374            config: cfg,
375        };
376        account.public_keys.insert(id.clone(), stored.clone());
377        drop(state);
378        let body = render_public_key(&stored, "PublicKey");
379        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
380    }
381
382    pub(crate) fn get_public_key(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
383        let id = route_id(route, "PublicKey")?;
384        let state = self.state.read();
385        let p = state
386            .accounts
387            .get(DEFAULT_ACCOUNT)
388            .and_then(|a| a.public_keys.get(&id).cloned())
389            .ok_or_else(|| not_found("PublicKey", &id))?;
390        drop(state);
391        let body = render_public_key(&p, "PublicKey");
392        Ok(xml_with_etag(StatusCode::OK, body, &p.etag, None))
393    }
394
395    pub(crate) fn get_public_key_config(
396        &self,
397        route: &Route,
398    ) -> Result<AwsResponse, AwsServiceError> {
399        let id = route_id(route, "PublicKey")?;
400        let state = self.state.read();
401        let p = state
402            .accounts
403            .get(DEFAULT_ACCOUNT)
404            .and_then(|a| a.public_keys.get(&id).cloned())
405            .ok_or_else(|| not_found("PublicKey", &id))?;
406        drop(state);
407        let body = config_xml("PublicKeyConfig", &p.config)?;
408        Ok(xml_with_etag(StatusCode::OK, body, &p.etag, None))
409    }
410
411    pub(crate) fn update_public_key(
412        &self,
413        req: &AwsRequest,
414        route: &Route,
415    ) -> Result<AwsResponse, AwsServiceError> {
416        let id = route_id(route, "PublicKey")?;
417        let if_match = require_if_match(req)?;
418        let cfg: PublicKeyConfig = xml_io::from_xml_root(&req.body)
419            .map_err(|e| invalid_argument(format!("invalid PublicKeyConfig XML: {e}")))?;
420        if cfg.name.is_empty() {
421            return Err(invalid_argument("PublicKeyConfig.Name is required"));
422        }
423        let mut state = self.state.write();
424        let account = state
425            .accounts
426            .get_mut(DEFAULT_ACCOUNT)
427            .ok_or_else(|| not_found("PublicKey", &id))?;
428        let p = account
429            .public_keys
430            .get_mut(&id)
431            .ok_or_else(|| not_found("PublicKey", &id))?;
432        if p.etag != if_match {
433            return Err(precondition_failed());
434        }
435        // CallerReference is immutable per AWS.
436        if p.config.caller_reference != cfg.caller_reference {
437            return Err(invalid_argument(
438                "CallerReference cannot change on UpdatePublicKey",
439            ));
440        }
441        p.config = cfg;
442        p.etag = generate_id_with_prefix("E");
443        let snap = p.clone();
444        drop(state);
445        let body = render_public_key(&snap, "PublicKey");
446        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
447    }
448
449    pub(crate) fn delete_public_key(
450        &self,
451        req: &AwsRequest,
452        route: &Route,
453    ) -> Result<AwsResponse, AwsServiceError> {
454        let id = route_id(route, "PublicKey")?;
455        let if_match = require_if_match(req)?;
456        let mut state = self.state.write();
457        let account = state
458            .accounts
459            .get_mut(DEFAULT_ACCOUNT)
460            .ok_or_else(|| not_found("PublicKey", &id))?;
461        let p = account
462            .public_keys
463            .get(&id)
464            .ok_or_else(|| not_found("PublicKey", &id))?;
465        if p.etag != if_match {
466            return Err(precondition_failed());
467        }
468        account.public_keys.remove(&id);
469        drop(state);
470        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
471    }
472
473    pub(crate) fn list_public_keys(
474        &self,
475        _req: &AwsRequest,
476    ) -> Result<AwsResponse, AwsServiceError> {
477        let state = self.state.read();
478        let mut items: Vec<StoredPublicKey> = state
479            .accounts
480            .get(DEFAULT_ACCOUNT)
481            .map(|a| a.public_keys.values().cloned().collect())
482            .unwrap_or_default();
483        drop(state);
484        items.sort_by(|a, b| a.id.cmp(&b.id));
485
486        let mut body = String::with_capacity(512);
487        body.push_str(XML_DECL);
488        body.push_str(&format!("<PublicKeyList xmlns=\"{NS}\">"));
489        body.push_str("<Marker></Marker>");
490        body.push_str("<MaxItems>100</MaxItems>");
491        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
492        body.push_str("<Items>");
493        for p in &items {
494            body.push_str("<PublicKeySummary>");
495            body.push_str(&format!("<Id>{}</Id>", esc(&p.id)));
496            body.push_str(&format!("<Name>{}</Name>", esc(&p.config.name)));
497            body.push_str(&format!(
498                "<CreatedTime>{}</CreatedTime>",
499                rfc3339(&p.created_time)
500            ));
501            body.push_str(&format!(
502                "<EncodedKey>{}</EncodedKey>",
503                esc(&p.config.encoded_key)
504            ));
505            if let Some(c) = &p.config.comment {
506                body.push_str(&format!("<Comment>{}</Comment>", esc(c)));
507            }
508            body.push_str("</PublicKeySummary>");
509        }
510        body.push_str("</Items>");
511        body.push_str("</PublicKeyList>");
512        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
513    }
514}
515
516// ─── Key Groups ───────────────────────────────────────────────────────
517
518impl CloudFrontService {
519    pub(crate) fn create_key_group(
520        &self,
521        req: &AwsRequest,
522    ) -> Result<AwsResponse, AwsServiceError> {
523        let cfg: KeyGroupConfig = xml_io::from_xml_root(&req.body)
524            .map_err(|e| invalid_argument(format!("invalid KeyGroupConfig XML: {e}")))?;
525        if cfg.name.is_empty() {
526            return Err(invalid_argument("KeyGroupConfig.Name is required"));
527        }
528        let mut state = self.state.write();
529        let account = state
530            .accounts
531            .entry(DEFAULT_ACCOUNT.to_string())
532            .or_default();
533        let id = generate_id_with_prefix("K");
534        let etag = generate_id_with_prefix("E");
535        let stored = StoredKeyGroup {
536            id: id.clone(),
537            etag: etag.clone(),
538            last_modified_time: Utc::now(),
539            config: cfg,
540        };
541        account.key_groups.insert(id.clone(), stored.clone());
542        drop(state);
543        let body = render_key_group(&stored, "KeyGroup");
544        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
545    }
546
547    pub(crate) fn get_key_group(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
548        let id = route_id(route, "KeyGroup")?;
549        let state = self.state.read();
550        let g = state
551            .accounts
552            .get(DEFAULT_ACCOUNT)
553            .and_then(|a| a.key_groups.get(&id).cloned())
554            .ok_or_else(|| not_found("KeyGroup", &id))?;
555        drop(state);
556        let body = render_key_group(&g, "KeyGroup");
557        Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
558    }
559
560    pub(crate) fn get_key_group_config(
561        &self,
562        route: &Route,
563    ) -> Result<AwsResponse, AwsServiceError> {
564        let id = route_id(route, "KeyGroup")?;
565        let state = self.state.read();
566        let g = state
567            .accounts
568            .get(DEFAULT_ACCOUNT)
569            .and_then(|a| a.key_groups.get(&id).cloned())
570            .ok_or_else(|| not_found("KeyGroup", &id))?;
571        drop(state);
572        let body = config_xml("KeyGroupConfig", &g.config)?;
573        Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
574    }
575
576    pub(crate) fn update_key_group(
577        &self,
578        req: &AwsRequest,
579        route: &Route,
580    ) -> Result<AwsResponse, AwsServiceError> {
581        let id = route_id(route, "KeyGroup")?;
582        let if_match = require_if_match(req)?;
583        let cfg: KeyGroupConfig = xml_io::from_xml_root(&req.body)
584            .map_err(|e| invalid_argument(format!("invalid KeyGroupConfig XML: {e}")))?;
585        if cfg.name.is_empty() {
586            return Err(invalid_argument("KeyGroupConfig.Name is required"));
587        }
588        let mut state = self.state.write();
589        let account = state
590            .accounts
591            .get_mut(DEFAULT_ACCOUNT)
592            .ok_or_else(|| not_found("KeyGroup", &id))?;
593        let g = account
594            .key_groups
595            .get_mut(&id)
596            .ok_or_else(|| not_found("KeyGroup", &id))?;
597        if g.etag != if_match {
598            return Err(precondition_failed());
599        }
600        g.config = cfg;
601        g.etag = generate_id_with_prefix("E");
602        g.last_modified_time = Utc::now();
603        let snap = g.clone();
604        drop(state);
605        let body = render_key_group(&snap, "KeyGroup");
606        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
607    }
608
609    pub(crate) fn delete_key_group(
610        &self,
611        req: &AwsRequest,
612        route: &Route,
613    ) -> Result<AwsResponse, AwsServiceError> {
614        let id = route_id(route, "KeyGroup")?;
615        let if_match = require_if_match(req)?;
616        let mut state = self.state.write();
617        let account = state
618            .accounts
619            .get_mut(DEFAULT_ACCOUNT)
620            .ok_or_else(|| not_found("KeyGroup", &id))?;
621        let g = account
622            .key_groups
623            .get(&id)
624            .ok_or_else(|| not_found("KeyGroup", &id))?;
625        if g.etag != if_match {
626            return Err(precondition_failed());
627        }
628        account.key_groups.remove(&id);
629        drop(state);
630        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
631    }
632
633    pub(crate) fn list_key_groups(
634        &self,
635        _req: &AwsRequest,
636    ) -> Result<AwsResponse, AwsServiceError> {
637        let state = self.state.read();
638        let mut items: Vec<StoredKeyGroup> = state
639            .accounts
640            .get(DEFAULT_ACCOUNT)
641            .map(|a| a.key_groups.values().cloned().collect())
642            .unwrap_or_default();
643        drop(state);
644        items.sort_by(|a, b| a.config.name.cmp(&b.config.name));
645
646        let mut body = String::with_capacity(512);
647        body.push_str(XML_DECL);
648        body.push_str(&format!("<KeyGroupList xmlns=\"{NS}\">"));
649        body.push_str("<Marker></Marker>");
650        body.push_str("<MaxItems>100</MaxItems>");
651        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
652        body.push_str("<Items>");
653        for g in &items {
654            body.push_str("<KeyGroupSummary>");
655            body.push_str("<KeyGroup>");
656            push_key_group_inner(&mut body, g);
657            body.push_str("</KeyGroup>");
658            body.push_str("</KeyGroupSummary>");
659        }
660        body.push_str("</Items>");
661        body.push_str("</KeyGroupList>");
662        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
663    }
664}
665
666// ─── Key Value Stores ─────────────────────────────────────────────────
667
668impl CloudFrontService {
669    pub(crate) fn create_key_value_store(
670        &self,
671        req: &AwsRequest,
672    ) -> Result<AwsResponse, AwsServiceError> {
673        let parsed: CreateKeyValueStoreRequest = xml_io::from_xml_root(&req.body)
674            .map_err(|e| invalid_argument(format!("invalid CreateKeyValueStore XML: {e}")))?;
675        if parsed.name.is_empty() {
676            return Err(invalid_argument("Name is required"));
677        }
678        let mut state = self.state.write();
679        let account = state
680            .accounts
681            .entry(DEFAULT_ACCOUNT.to_string())
682            .or_default();
683        if account.key_value_stores.contains_key(&parsed.name) {
684            return Err(aws_error(
685                StatusCode::CONFLICT,
686                "EntityAlreadyExists",
687                format!("KeyValueStore {} already exists", parsed.name),
688            ));
689        }
690        let now = Utc::now();
691        let id = Uuid::new_v4().to_string();
692        let etag = generate_id_with_prefix("E");
693        let arn = format!(
694            "arn:aws:cloudfront::{}:key-value-store/{}",
695            DEFAULT_ACCOUNT, id
696        );
697        let stored = StoredKeyValueStore {
698            name: parsed.name.clone(),
699            id,
700            etag: etag.clone(),
701            arn,
702            status: "READY".to_string(),
703            created_time: now,
704            last_modified_time: now,
705            comment: parsed.comment,
706            import_source: parsed.import_source,
707        };
708        account
709            .key_value_stores
710            .insert(parsed.name.clone(), stored.clone());
711        drop(state);
712        let body = render_key_value_store(&stored, "CreateKeyValueStoreResult");
713        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, None))
714    }
715
716    pub(crate) fn describe_key_value_store(
717        &self,
718        route: &Route,
719    ) -> Result<AwsResponse, AwsServiceError> {
720        let name = route_id(route, "KeyValueStore")?;
721        let state = self.state.read();
722        let kvs = state
723            .accounts
724            .get(DEFAULT_ACCOUNT)
725            .and_then(|a| a.key_value_stores.get(&name).cloned())
726            .ok_or_else(|| not_found("KeyValueStore", &name))?;
727        drop(state);
728        let body = render_key_value_store(&kvs, "DescribeKeyValueStoreResult");
729        Ok(xml_with_etag(StatusCode::OK, body, &kvs.etag, None))
730    }
731
732    pub(crate) fn update_key_value_store(
733        &self,
734        req: &AwsRequest,
735        route: &Route,
736    ) -> Result<AwsResponse, AwsServiceError> {
737        let name = route_id(route, "KeyValueStore")?;
738        let if_match = require_if_match(req)?;
739        let parsed: UpdateKeyValueStoreRequest = xml_io::from_xml_root(&req.body)
740            .map_err(|e| invalid_argument(format!("invalid UpdateKeyValueStore XML: {e}")))?;
741        let mut state = self.state.write();
742        let account = state
743            .accounts
744            .get_mut(DEFAULT_ACCOUNT)
745            .ok_or_else(|| not_found("KeyValueStore", &name))?;
746        let kvs = account
747            .key_value_stores
748            .get_mut(&name)
749            .ok_or_else(|| not_found("KeyValueStore", &name))?;
750        if kvs.etag != if_match {
751            return Err(precondition_failed());
752        }
753        kvs.comment = Some(parsed.comment);
754        kvs.etag = generate_id_with_prefix("E");
755        kvs.last_modified_time = Utc::now();
756        let snap = kvs.clone();
757        drop(state);
758        let body = render_key_value_store(&snap, "UpdateKeyValueStoreResult");
759        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
760    }
761
762    pub(crate) fn delete_key_value_store(
763        &self,
764        req: &AwsRequest,
765        route: &Route,
766    ) -> Result<AwsResponse, AwsServiceError> {
767        let name = route_id(route, "KeyValueStore")?;
768        let if_match = require_if_match(req)?;
769        let mut state = self.state.write();
770        let account = state
771            .accounts
772            .get_mut(DEFAULT_ACCOUNT)
773            .ok_or_else(|| not_found("KeyValueStore", &name))?;
774        let kvs = account
775            .key_value_stores
776            .get(&name)
777            .ok_or_else(|| not_found("KeyValueStore", &name))?;
778        if kvs.etag != if_match {
779            return Err(precondition_failed());
780        }
781        account.key_value_stores.remove(&name);
782        drop(state);
783        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
784    }
785
786    pub(crate) fn list_key_value_stores(
787        &self,
788        _req: &AwsRequest,
789    ) -> Result<AwsResponse, AwsServiceError> {
790        let state = self.state.read();
791        let mut items: Vec<StoredKeyValueStore> = state
792            .accounts
793            .get(DEFAULT_ACCOUNT)
794            .map(|a| a.key_value_stores.values().cloned().collect())
795            .unwrap_or_default();
796        drop(state);
797        items.sort_by(|a, b| a.name.cmp(&b.name));
798
799        let mut body = String::with_capacity(512);
800        body.push_str(XML_DECL);
801        body.push_str(&format!("<KeyValueStoreList xmlns=\"{NS}\">"));
802        body.push_str("<NextMarker></NextMarker>");
803        body.push_str("<MaxItems>100</MaxItems>");
804        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
805        body.push_str("<Items>");
806        for kvs in &items {
807            body.push_str("<KeyValueStore>");
808            push_kvs_inner(&mut body, kvs);
809            body.push_str("</KeyValueStore>");
810        }
811        body.push_str("</Items>");
812        body.push_str("</KeyValueStoreList>");
813        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
814    }
815}
816
817// ─── Origin Access Identities (legacy) ────────────────────────────────
818
819impl CloudFrontService {
820    pub(crate) fn create_oai(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
821        let cfg: CloudFrontOriginAccessIdentityConfig = xml_io::from_xml_root(&req.body)
822            .map_err(|e| invalid_argument(format!("invalid OAI config XML: {e}")))?;
823        if cfg.caller_reference.is_empty() {
824            return Err(invalid_argument("CallerReference is required"));
825        }
826        let mut state = self.state.write();
827        let account = state
828            .accounts
829            .entry(DEFAULT_ACCOUNT.to_string())
830            .or_default();
831        if let Some(existing) = account
832            .origin_access_identities
833            .values()
834            .find(|o| o.config.caller_reference == cfg.caller_reference)
835        {
836            return Err(aws_error(
837                StatusCode::CONFLICT,
838                "CloudFrontOriginAccessIdentityAlreadyExists",
839                format!("OAI with same CallerReference exists: {}", existing.id),
840            ));
841        }
842        let id = format!(
843            "E{}",
844            Uuid::new_v4()
845                .simple()
846                .to_string()
847                .to_uppercase()
848                .chars()
849                .take(13)
850                .collect::<String>()
851        );
852        let etag = generate_id_with_prefix("E");
853        let canonical = Uuid::new_v4().simple().to_string();
854        let stored = StoredOriginAccessIdentity {
855            id: id.clone(),
856            etag: etag.clone(),
857            s3_canonical_user_id: canonical,
858            config: cfg,
859        };
860        account
861            .origin_access_identities
862            .insert(id.clone(), stored.clone());
863        drop(state);
864        let body = render_oai(&stored, "CloudFrontOriginAccessIdentity");
865        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
866    }
867
868    pub(crate) fn get_oai(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
869        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
870        let state = self.state.read();
871        let oai = state
872            .accounts
873            .get(DEFAULT_ACCOUNT)
874            .and_then(|a| a.origin_access_identities.get(&id).cloned())
875            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
876        drop(state);
877        let body = render_oai(&oai, "CloudFrontOriginAccessIdentity");
878        Ok(xml_with_etag(StatusCode::OK, body, &oai.etag, None))
879    }
880
881    pub(crate) fn get_oai_config(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
882        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
883        let state = self.state.read();
884        let oai = state
885            .accounts
886            .get(DEFAULT_ACCOUNT)
887            .and_then(|a| a.origin_access_identities.get(&id).cloned())
888            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
889        drop(state);
890        let body = config_xml("CloudFrontOriginAccessIdentityConfig", &oai.config)?;
891        Ok(xml_with_etag(StatusCode::OK, body, &oai.etag, None))
892    }
893
894    pub(crate) fn update_oai(
895        &self,
896        req: &AwsRequest,
897        route: &Route,
898    ) -> Result<AwsResponse, AwsServiceError> {
899        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
900        let if_match = require_if_match(req)?;
901        let cfg: CloudFrontOriginAccessIdentityConfig = xml_io::from_xml_root(&req.body)
902            .map_err(|e| invalid_argument(format!("invalid OAI config XML: {e}")))?;
903        let mut state = self.state.write();
904        let account = state
905            .accounts
906            .get_mut(DEFAULT_ACCOUNT)
907            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
908        let oai = account
909            .origin_access_identities
910            .get_mut(&id)
911            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
912        if oai.etag != if_match {
913            return Err(precondition_failed());
914        }
915        if oai.config.caller_reference != cfg.caller_reference {
916            return Err(invalid_argument(
917                "CallerReference cannot change on UpdateCloudFrontOriginAccessIdentity",
918            ));
919        }
920        oai.config = cfg;
921        oai.etag = generate_id_with_prefix("E");
922        let snap = oai.clone();
923        drop(state);
924        let body = render_oai(&snap, "CloudFrontOriginAccessIdentity");
925        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
926    }
927
928    pub(crate) fn delete_oai(
929        &self,
930        req: &AwsRequest,
931        route: &Route,
932    ) -> Result<AwsResponse, AwsServiceError> {
933        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
934        let if_match = require_if_match(req)?;
935        let mut state = self.state.write();
936        let account = state
937            .accounts
938            .get_mut(DEFAULT_ACCOUNT)
939            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
940        let oai = account
941            .origin_access_identities
942            .get(&id)
943            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
944        if oai.etag != if_match {
945            return Err(precondition_failed());
946        }
947        account.origin_access_identities.remove(&id);
948        drop(state);
949        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
950    }
951
952    pub(crate) fn list_oai(&self, _req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
953        let state = self.state.read();
954        let mut items: Vec<StoredOriginAccessIdentity> = state
955            .accounts
956            .get(DEFAULT_ACCOUNT)
957            .map(|a| a.origin_access_identities.values().cloned().collect())
958            .unwrap_or_default();
959        drop(state);
960        items.sort_by(|a, b| a.id.cmp(&b.id));
961
962        let mut body = String::with_capacity(512);
963        body.push_str(XML_DECL);
964        body.push_str(&format!(
965            "<CloudFrontOriginAccessIdentityList xmlns=\"{NS}\">"
966        ));
967        body.push_str("<Marker></Marker>");
968        body.push_str("<MaxItems>100</MaxItems>");
969        body.push_str("<IsTruncated>false</IsTruncated>");
970        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
971        body.push_str("<Items>");
972        for oai in &items {
973            body.push_str("<CloudFrontOriginAccessIdentitySummary>");
974            body.push_str(&format!("<Id>{}</Id>", esc(&oai.id)));
975            body.push_str(&format!(
976                "<S3CanonicalUserId>{}</S3CanonicalUserId>",
977                esc(&oai.s3_canonical_user_id)
978            ));
979            body.push_str(&format!("<Comment>{}</Comment>", esc(&oai.config.comment)));
980            body.push_str("</CloudFrontOriginAccessIdentitySummary>");
981        }
982        body.push_str("</Items>");
983        body.push_str("</CloudFrontOriginAccessIdentityList>");
984        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
985    }
986}
987
988// ─── Monitoring Subscriptions ─────────────────────────────────────────
989
990impl CloudFrontService {
991    pub(crate) fn create_monitoring_subscription(
992        &self,
993        req: &AwsRequest,
994        route: &Route,
995    ) -> Result<AwsResponse, AwsServiceError> {
996        let dist_id = route_id(route, "Distribution")?;
997        // Check the distribution exists before parsing the body. Synthetic
998        // probes hit this op against a placeholder distribution ID; surfacing
999        // NoSuchDistribution (declared in the Smithy errors list) before any
1000        // XML-parse InvalidArgument keeps the conformance probe honest.
1001        {
1002            let state = self.state.read();
1003            let has_dist = state
1004                .accounts
1005                .get(DEFAULT_ACCOUNT)
1006                .is_some_and(|a| a.distributions.contains_key(&dist_id));
1007            if !has_dist {
1008                return Err(not_found("Distribution", &dist_id));
1009            }
1010        }
1011        let parsed: MonitoringSubscriptionBody = xml_io::from_xml_root(&req.body)
1012            .map_err(|e| invalid_argument(format!("invalid MonitoringSubscription XML: {e}")))?;
1013        let mut state = self.state.write();
1014        let account = state
1015            .accounts
1016            .entry(DEFAULT_ACCOUNT.to_string())
1017            .or_default();
1018        if !account.distributions.contains_key(&dist_id) {
1019            return Err(not_found("Distribution", &dist_id));
1020        }
1021        let stored = StoredMonitoringSubscription {
1022            distribution_id: dist_id.clone(),
1023            config: parsed.realtime_metrics_subscription_config,
1024        };
1025        account
1026            .monitoring_subscriptions
1027            .insert(dist_id.clone(), stored.clone());
1028        drop(state);
1029        let body = render_monitoring(&stored);
1030        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
1031    }
1032
1033    pub(crate) fn get_monitoring_subscription(
1034        &self,
1035        route: &Route,
1036    ) -> Result<AwsResponse, AwsServiceError> {
1037        let dist_id = route_id(route, "Distribution")?;
1038        let state = self.state.read();
1039        let m = state
1040            .accounts
1041            .get(DEFAULT_ACCOUNT)
1042            .and_then(|a| a.monitoring_subscriptions.get(&dist_id).cloned())
1043            .ok_or_else(|| {
1044                aws_error(
1045                    StatusCode::NOT_FOUND,
1046                    "NoSuchMonitoringSubscription",
1047                    format!("No monitoring subscription for distribution {dist_id}"),
1048                )
1049            })?;
1050        drop(state);
1051        let body = render_monitoring(&m);
1052        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
1053    }
1054
1055    pub(crate) fn delete_monitoring_subscription(
1056        &self,
1057        route: &Route,
1058    ) -> Result<AwsResponse, AwsServiceError> {
1059        let dist_id = route_id(route, "Distribution")?;
1060        let mut state = self.state.write();
1061        let account = state
1062            .accounts
1063            .get_mut(DEFAULT_ACCOUNT)
1064            .ok_or_else(|| not_found("Distribution", &dist_id))?;
1065        if account.monitoring_subscriptions.remove(&dist_id).is_none() {
1066            return Err(aws_error(
1067                StatusCode::NOT_FOUND,
1068                "NoSuchMonitoringSubscription",
1069                format!("No monitoring subscription for distribution {dist_id}"),
1070            ));
1071        }
1072        drop(state);
1073        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
1074    }
1075}
1076
1077// ─── Helpers ──────────────────────────────────────────────────────────
1078
1079#[derive(Debug, serde::Deserialize)]
1080#[serde(rename_all = "PascalCase")]
1081struct CreateFunctionRequest {
1082    name: String,
1083    function_config: FunctionConfig,
1084    /// Base64-encoded source.
1085    function_code: String,
1086}
1087
1088#[derive(Debug, serde::Deserialize)]
1089#[serde(rename_all = "PascalCase")]
1090struct UpdateFunctionRequest {
1091    function_config: FunctionConfig,
1092    function_code: String,
1093}
1094
1095#[derive(Debug, serde::Deserialize)]
1096#[serde(rename_all = "PascalCase")]
1097struct TestFunctionRequest {
1098    #[serde(default)]
1099    event_object: String,
1100    #[serde(default)]
1101    stage: Option<String>,
1102}
1103
1104#[derive(Debug, serde::Deserialize)]
1105#[serde(rename_all = "PascalCase")]
1106struct CreateKeyValueStoreRequest {
1107    name: String,
1108    #[serde(default)]
1109    comment: Option<String>,
1110    #[serde(default)]
1111    import_source: Option<ImportSource>,
1112}
1113
1114#[derive(Debug, serde::Deserialize)]
1115#[serde(rename_all = "PascalCase")]
1116struct UpdateKeyValueStoreRequest {
1117    comment: String,
1118}
1119
1120fn config_xml<T: serde::Serialize>(root: &str, cfg: &T) -> Result<String, AwsServiceError> {
1121    let inner = quick_xml::se::to_string_with_root(root, cfg).map_err(|e| {
1122        aws_error(
1123            StatusCode::INTERNAL_SERVER_ERROR,
1124            "InternalError",
1125            format!("xml encode failed: {e}"),
1126        )
1127    })?;
1128    let stamped = inner.replacen(
1129        &format!("<{root}>"),
1130        &format!("<{root} xmlns=\"{NS}\">", NS = crate::NAMESPACE),
1131        1,
1132    );
1133    Ok(format!("{XML_DECL}{stamped}"))
1134}
1135
1136fn parse_stage_query(query: &str) -> Option<String> {
1137    use std::collections::HashMap;
1138    let pairs: HashMap<&str, &str> = query.split('&').filter_map(|p| p.split_once('=')).collect();
1139    pairs.get("Stage").map(|s| s.to_string())
1140}
1141
1142/// Validate the `Stage` query against the FunctionStage enum
1143/// (DEVELOPMENT | LIVE). Returns InvalidArgument for unknown values; absent
1144/// or valid is OK.
1145pub(crate) fn validate_stage_query(
1146    query: &str,
1147) -> Result<Option<String>, fakecloud_core::service::AwsServiceError> {
1148    let stage = parse_stage_query(query);
1149    if let Some(ref v) = stage {
1150        if v != "DEVELOPMENT" && v != "LIVE" {
1151            return Err(crate::service::invalid_argument(format!(
1152                "Stage must be one of 'DEVELOPMENT' or 'LIVE', got '{v}'"
1153            )));
1154        }
1155    }
1156    Ok(stage)
1157}
1158
1159fn stage_view(f: &StoredFunction, stage: &Option<String>) -> StoredFunction {
1160    let mut clone = f.clone();
1161    if stage.as_deref() == Some("LIVE") {
1162        clone.stage = "LIVE".into();
1163        // GetFunction/TestFunction against the LIVE stage must serve the
1164        // code frozen at PublishFunction, not the mutable DEVELOPMENT
1165        // working copy. If the function was never published there is no
1166        // LIVE snapshot, so fall back to the development code unchanged.
1167        if let Some(live) = &f.live_function_code {
1168            clone.function_code = live.clone();
1169        }
1170    }
1171    clone
1172}
1173
1174fn render_function_summary(f: &StoredFunction, _root: &str) -> String {
1175    // CloudFront returns FunctionSummary as the root for Create/Describe/
1176    // Update/Publish — there is no operation-specific wrapper element.
1177    let mut out = String::with_capacity(512);
1178    out.push_str(XML_DECL);
1179    out.push_str(&render_function_summary_inner_with_ns(f));
1180    out
1181}
1182
1183fn render_function_summary_inner_with_ns(f: &StoredFunction) -> String {
1184    let mut out = String::with_capacity(512);
1185    out.push_str(&format!("<FunctionSummary xmlns=\"{NS}\">"));
1186    out.push_str(&render_function_summary_body(f));
1187    out.push_str("</FunctionSummary>");
1188    out
1189}
1190
1191fn render_function_summary_inner(f: &StoredFunction) -> String {
1192    let mut out = String::with_capacity(512);
1193    out.push_str("<FunctionSummary>");
1194    out.push_str(&render_function_summary_body(f));
1195    out.push_str("</FunctionSummary>");
1196    out
1197}
1198
1199fn render_function_summary_body(f: &StoredFunction) -> String {
1200    let mut out = String::with_capacity(512);
1201    out.push_str(&format!("<Name>{}</Name>", esc(&f.name)));
1202    out.push_str(&format!("<Status>{}</Status>", esc(&f.status)));
1203    out.push_str("<FunctionConfig>");
1204    if let Some(c) = &f.config.comment {
1205        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
1206    } else {
1207        out.push_str("<Comment></Comment>");
1208    }
1209    out.push_str(&format!("<Runtime>{}</Runtime>", esc(&f.config.runtime)));
1210    if let Some(kvsa) = &f.config.key_value_store_associations {
1211        out.push_str("<KeyValueStoreAssociations>");
1212        out.push_str(&format!("<Quantity>{}</Quantity>", kvsa.quantity));
1213        if let Some(items) = &kvsa.items {
1214            out.push_str("<Items>");
1215            for a in &items.key_value_store_association {
1216                out.push_str("<KeyValueStoreAssociation>");
1217                out.push_str(&format!(
1218                    "<KeyValueStoreARN>{}</KeyValueStoreARN>",
1219                    esc(&a.key_value_store_arn)
1220                ));
1221                out.push_str("</KeyValueStoreAssociation>");
1222            }
1223            out.push_str("</Items>");
1224        }
1225        out.push_str("</KeyValueStoreAssociations>");
1226    }
1227    out.push_str("</FunctionConfig>");
1228    out.push_str("<FunctionMetadata>");
1229    out.push_str(&format!(
1230        "<FunctionARN>{}</FunctionARN>",
1231        esc(&f.function_arn)
1232    ));
1233    out.push_str(&format!("<Stage>{}</Stage>", esc(&f.stage)));
1234    out.push_str(&format!(
1235        "<CreatedTime>{}</CreatedTime>",
1236        rfc3339(&f.created_time)
1237    ));
1238    out.push_str(&format!(
1239        "<LastModifiedTime>{}</LastModifiedTime>",
1240        rfc3339(&f.last_modified_time)
1241    ));
1242    out.push_str("</FunctionMetadata>");
1243    out
1244}
1245
1246fn render_public_key(p: &StoredPublicKey, root: &str) -> String {
1247    let mut out = String::with_capacity(512);
1248    out.push_str(XML_DECL);
1249    out.push_str(&format!("<{root} xmlns=\"{NS}\">"));
1250    out.push_str(&format!("<Id>{}</Id>", esc(&p.id)));
1251    out.push_str(&format!(
1252        "<CreatedTime>{}</CreatedTime>",
1253        rfc3339(&p.created_time)
1254    ));
1255    out.push_str("<PublicKeyConfig>");
1256    out.push_str(&format!(
1257        "<CallerReference>{}</CallerReference>",
1258        esc(&p.config.caller_reference)
1259    ));
1260    out.push_str(&format!("<Name>{}</Name>", esc(&p.config.name)));
1261    out.push_str(&format!(
1262        "<EncodedKey>{}</EncodedKey>",
1263        esc(&p.config.encoded_key)
1264    ));
1265    if let Some(c) = &p.config.comment {
1266        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
1267    }
1268    out.push_str("</PublicKeyConfig>");
1269    out.push_str(&format!("</{root}>"));
1270    out
1271}
1272
1273fn push_key_group_inner(out: &mut String, g: &StoredKeyGroup) {
1274    out.push_str(&format!("<Id>{}</Id>", esc(&g.id)));
1275    out.push_str(&format!(
1276        "<LastModifiedTime>{}</LastModifiedTime>",
1277        rfc3339(&g.last_modified_time)
1278    ));
1279    out.push_str("<KeyGroupConfig>");
1280    out.push_str(&format!("<Name>{}</Name>", esc(&g.config.name)));
1281    out.push_str("<Items>");
1282    for k in &g.config.items.public_key {
1283        out.push_str(&format!("<PublicKey>{}</PublicKey>", esc(k)));
1284    }
1285    out.push_str("</Items>");
1286    if let Some(c) = &g.config.comment {
1287        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
1288    }
1289    out.push_str("</KeyGroupConfig>");
1290}
1291
1292fn render_key_group(g: &StoredKeyGroup, root: &str) -> String {
1293    let mut out = String::with_capacity(512);
1294    out.push_str(XML_DECL);
1295    out.push_str(&format!("<{root} xmlns=\"{NS}\">"));
1296    push_key_group_inner(&mut out, g);
1297    out.push_str(&format!("</{root}>"));
1298    out
1299}
1300
1301fn push_kvs_inner(out: &mut String, kvs: &StoredKeyValueStore) {
1302    out.push_str(&format!("<Name>{}</Name>", esc(&kvs.name)));
1303    out.push_str(&format!("<Id>{}</Id>", esc(&kvs.id)));
1304    out.push_str(&format!(
1305        "<Comment>{}</Comment>",
1306        esc(kvs.comment.as_deref().unwrap_or(""))
1307    ));
1308    out.push_str(&format!("<ARN>{}</ARN>", esc(&kvs.arn)));
1309    out.push_str(&format!("<Status>{}</Status>", esc(&kvs.status)));
1310    out.push_str(&format!(
1311        "<LastModifiedTime>{}</LastModifiedTime>",
1312        rfc3339(&kvs.last_modified_time)
1313    ));
1314}
1315
1316fn render_key_value_store(kvs: &StoredKeyValueStore, _root: &str) -> String {
1317    // SDK expects KeyValueStore as root for Create/Describe/Update.
1318    let mut out = String::with_capacity(512);
1319    out.push_str(XML_DECL);
1320    out.push_str(&format!("<KeyValueStore xmlns=\"{NS}\">"));
1321    push_kvs_inner(&mut out, kvs);
1322    out.push_str("</KeyValueStore>");
1323    out
1324}
1325
1326fn render_oai(oai: &StoredOriginAccessIdentity, root: &str) -> String {
1327    let mut out = String::with_capacity(512);
1328    out.push_str(XML_DECL);
1329    out.push_str(&format!("<{root} xmlns=\"{NS}\">"));
1330    out.push_str(&format!("<Id>{}</Id>", esc(&oai.id)));
1331    out.push_str(&format!(
1332        "<S3CanonicalUserId>{}</S3CanonicalUserId>",
1333        esc(&oai.s3_canonical_user_id)
1334    ));
1335    out.push_str("<CloudFrontOriginAccessIdentityConfig>");
1336    out.push_str(&format!(
1337        "<CallerReference>{}</CallerReference>",
1338        esc(&oai.config.caller_reference)
1339    ));
1340    out.push_str(&format!("<Comment>{}</Comment>", esc(&oai.config.comment)));
1341    out.push_str("</CloudFrontOriginAccessIdentityConfig>");
1342    out.push_str(&format!("</{root}>"));
1343    out
1344}
1345
1346fn render_monitoring(m: &StoredMonitoringSubscription) -> String {
1347    let mut out = String::with_capacity(256);
1348    out.push_str(XML_DECL);
1349    out.push_str(&format!("<MonitoringSubscription xmlns=\"{NS}\">"));
1350    out.push_str("<RealtimeMetricsSubscriptionConfig>");
1351    out.push_str(&format!(
1352        "<RealtimeMetricsSubscriptionStatus>{}</RealtimeMetricsSubscriptionStatus>",
1353        esc(&m.config.realtime_metrics_subscription_status)
1354    ));
1355    out.push_str("</RealtimeMetricsSubscriptionConfig>");
1356    out.push_str("</MonitoringSubscription>");
1357    out
1358}
1359
1360#[cfg(test)]
1361mod tests {
1362    use super::*;
1363    use crate::service::CloudFrontService;
1364    use crate::state::CloudFrontAccounts;
1365    use bytes::Bytes;
1366    use fakecloud_core::service::AwsService;
1367    use http::HeaderValue;
1368    use parking_lot::RwLock;
1369    use std::sync::Arc;
1370
1371    fn svc() -> CloudFrontService {
1372        CloudFrontService::new(Arc::new(RwLock::new(CloudFrontAccounts::new())))
1373    }
1374
1375    fn req(method: http::Method, path: &str, body: &str, if_match: Option<&str>) -> AwsRequest {
1376        let mut headers = HeaderMap::new();
1377        if let Some(v) = if_match {
1378            headers.insert(http::header::IF_MATCH, HeaderValue::from_str(v).unwrap());
1379        }
1380        AwsRequest {
1381            service: "cloudfront".into(),
1382            action: String::new(),
1383            region: "us-east-1".into(),
1384            account_id: DEFAULT_ACCOUNT.into(),
1385            request_id: uuid::Uuid::new_v4().to_string(),
1386            headers,
1387            query_params: std::collections::HashMap::new(),
1388            body_stream: parking_lot::Mutex::new(None),
1389            body: Bytes::from(body.to_string()),
1390            path_segments: path
1391                .split('/')
1392                .filter(|s| !s.is_empty())
1393                .map(String::from)
1394                .collect(),
1395            raw_path: path.into(),
1396            raw_query: String::new(),
1397            method,
1398            is_query_protocol: false,
1399            access_key_id: None,
1400            principal: None,
1401        }
1402    }
1403
1404    async fn create_function(svc: &CloudFrontService, name: &str, code: &str) -> String {
1405        let code_b64 = base64::engine::general_purpose::STANDARD.encode(code.as_bytes());
1406        let body = format!(
1407            r#"<?xml version="1.0"?>
1408<CreateFunctionRequest xmlns="{NS}">
1409  <Name>{name}</Name>
1410  <FunctionConfig>
1411    <Comment>t</Comment>
1412    <Runtime>cloudfront-js-2.0</Runtime>
1413  </FunctionConfig>
1414  <FunctionCode>{code_b64}</FunctionCode>
1415</CreateFunctionRequest>"#
1416        );
1417        let resp = svc
1418            .handle(req(http::Method::POST, "/2020-05-31/function", &body, None))
1419            .await
1420            .unwrap();
1421        assert_eq!(resp.status, StatusCode::CREATED);
1422        resp.headers
1423            .get(http::header::ETAG)
1424            .unwrap()
1425            .to_str()
1426            .unwrap()
1427            .to_string()
1428    }
1429
1430    fn test_function_request_xml(event_json: &str) -> String {
1431        test_function_request_xml_with_stage(event_json, "DEVELOPMENT")
1432    }
1433
1434    fn test_function_request_xml_with_stage(event_json: &str, stage: &str) -> String {
1435        let event_b64 = base64::engine::general_purpose::STANDARD.encode(event_json.as_bytes());
1436        format!(
1437            r#"<?xml version="1.0"?>
1438<TestFunctionRequest xmlns="{NS}">
1439  <Stage>{stage}</Stage>
1440  <EventObject>{event_b64}</EventObject>
1441</TestFunctionRequest>"#
1442        )
1443    }
1444
1445    async fn update_function(
1446        svc: &CloudFrontService,
1447        name: &str,
1448        code: &str,
1449        if_match: &str,
1450    ) -> String {
1451        let code_b64 = base64::engine::general_purpose::STANDARD.encode(code.as_bytes());
1452        let body = format!(
1453            r#"<?xml version="1.0"?>
1454<UpdateFunctionRequest xmlns="{NS}">
1455  <FunctionConfig>
1456    <Comment>t</Comment>
1457    <Runtime>cloudfront-js-2.0</Runtime>
1458  </FunctionConfig>
1459  <FunctionCode>{code_b64}</FunctionCode>
1460</UpdateFunctionRequest>"#
1461        );
1462        let resp = svc
1463            .handle(req(
1464                http::Method::PUT,
1465                &format!("/2020-05-31/function/{name}"),
1466                &body,
1467                Some(if_match),
1468            ))
1469            .await
1470            .unwrap();
1471        assert_eq!(resp.status, StatusCode::OK);
1472        resp.headers
1473            .get(http::header::ETAG)
1474            .unwrap()
1475            .to_str()
1476            .unwrap()
1477            .to_string()
1478    }
1479
1480    async fn publish_function(svc: &CloudFrontService, name: &str, if_match: &str) -> String {
1481        let resp = svc
1482            .handle(req(
1483                http::Method::POST,
1484                &format!("/2020-05-31/function/{name}/publish"),
1485                "",
1486                Some(if_match),
1487            ))
1488            .await
1489            .unwrap();
1490        assert_eq!(resp.status, StatusCode::OK);
1491        resp.headers
1492            .get(http::header::ETAG)
1493            .unwrap()
1494            .to_str()
1495            .unwrap()
1496            .to_string()
1497    }
1498
1499    #[tokio::test]
1500    async fn test_function_executes_handler_and_returns_result() {
1501        let svc = svc();
1502        let etag = create_function(
1503            &svc,
1504            "fn-ok",
1505            r#"function handler(event) { event.headers.x = "y"; return event; }"#,
1506        )
1507        .await;
1508        let body = test_function_request_xml(r#"{"headers":{}}"#);
1509        let resp = svc
1510            .handle(req(
1511                http::Method::POST,
1512                "/2020-05-31/function/fn-ok/test",
1513                &body,
1514                Some(&etag),
1515            ))
1516            .await
1517            .unwrap();
1518        assert_eq!(resp.status, StatusCode::OK);
1519        let xml = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1520        assert!(
1521            xml.contains("&quot;x&quot;:&quot;y&quot;"),
1522            "expected x:y in FunctionOutput, got {xml}"
1523        );
1524        assert!(xml.contains("<FunctionErrorMessage></FunctionErrorMessage>"));
1525    }
1526
1527    #[tokio::test]
1528    async fn test_function_propagates_js_error_into_message() {
1529        let svc = svc();
1530        let etag = create_function(
1531            &svc,
1532            "fn-err",
1533            r#"function handler() { throw new Error("boom"); }"#,
1534        )
1535        .await;
1536        let body = test_function_request_xml("{}");
1537        let resp = svc
1538            .handle(req(
1539                http::Method::POST,
1540                "/2020-05-31/function/fn-err/test",
1541                &body,
1542                Some(&etag),
1543            ))
1544            .await
1545            .unwrap();
1546        assert_eq!(resp.status, StatusCode::OK);
1547        let xml = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1548        assert!(
1549            xml.contains("boom"),
1550            "expected boom in error msg, got {xml}"
1551        );
1552        assert!(xml.contains("<FunctionOutput></FunctionOutput>"));
1553    }
1554
1555    #[tokio::test]
1556    async fn test_function_unknown_name_returns_error() {
1557        let svc = svc();
1558        let body = test_function_request_xml("{}");
1559        let err = match svc
1560            .handle(req(
1561                http::Method::POST,
1562                "/2020-05-31/function/missing/test",
1563                &body,
1564                Some("E0"),
1565            ))
1566            .await
1567        {
1568            Err(e) => e,
1569            Ok(_) => panic!("expected NoSuchFunctionExists, got Ok"),
1570        };
1571        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1572        assert_eq!(err.code(), "NoSuchFunctionExists");
1573    }
1574
1575    #[tokio::test]
1576    async fn test_function_modifies_aws_request_shape() {
1577        // Mirrors the canonical CloudFront Functions example from the
1578        // AWS docs: handler rewrites a request header and returns the
1579        // request, fakecloud passes the JSON shape straight through.
1580        let svc = svc();
1581        let etag = create_function(
1582            &svc,
1583            "fn-aws-shape",
1584            r#"function handler(event) { event.request.headers["x-foo"] = {value: "bar"}; return event.request; }"#,
1585        )
1586        .await;
1587        let body = test_function_request_xml(
1588            r#"{"version":"1.0","context":{},"viewer":{},"request":{"method":"GET","uri":"/","querystring":{},"headers":{},"cookies":{}}}"#,
1589        );
1590        let resp = svc
1591            .handle(req(
1592                http::Method::POST,
1593                "/2020-05-31/function/fn-aws-shape/test",
1594                &body,
1595                Some(&etag),
1596            ))
1597            .await
1598            .unwrap();
1599        assert_eq!(resp.status, StatusCode::OK);
1600        let xml = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1601        assert!(
1602            xml.contains("x-foo"),
1603            "expected request header rewrite in output, got {xml}"
1604        );
1605        assert!(
1606            xml.contains("bar"),
1607            "expected header value in output, got {xml}"
1608        );
1609        assert!(
1610            xml.contains("<FunctionErrorMessage></FunctionErrorMessage>"),
1611            "expected empty error, got {xml}"
1612        );
1613    }
1614
1615    #[tokio::test]
1616    async fn test_function_logs_error_and_marks_compute_over_100() {
1617        let svc = svc();
1618        let etag = create_function(
1619            &svc,
1620            "fn-throws",
1621            r#"function handler() { throw new Error("kaboom"); }"#,
1622        )
1623        .await;
1624        let body = test_function_request_xml("{}");
1625        let resp = svc
1626            .handle(req(
1627                http::Method::POST,
1628                "/2020-05-31/function/fn-throws/test",
1629                &body,
1630                Some(&etag),
1631            ))
1632            .await
1633            .unwrap();
1634        assert_eq!(resp.status, StatusCode::OK);
1635        let xml = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1636        // Error appears in both the dedicated message and in the logs
1637        assert!(xml.contains("kaboom"), "expected kaboom in body, got {xml}");
1638        assert!(
1639            xml.contains("<FunctionExecutionLogs>")
1640                && xml.contains("<member>ERROR: ")
1641                && xml.contains("kaboom"),
1642            "expected error log line, got {xml}"
1643        );
1644        // ComputeUtilization is rendered as a plain integer; on failure
1645        // we saturate past 100.
1646        let cu_open = xml.find("<ComputeUtilization>").unwrap() + "<ComputeUtilization>".len();
1647        let cu_close = xml.find("</ComputeUtilization>").unwrap();
1648        let pct: u32 = xml[cu_open..cu_close].parse().unwrap();
1649        assert!(pct > 100, "expected pct > 100 on error, got {pct}");
1650    }
1651
1652    #[tokio::test]
1653    async fn test_function_stage_selects_published_or_development_code() {
1654        // Publish freezes a copy of the code; subsequent UpdateFunction
1655        // mutates DEVELOPMENT but leaves LIVE pinned to the published
1656        // snapshot. Each stage's TestFunction must run the matching
1657        // version.
1658        let svc = svc();
1659        let etag =
1660            create_function(&svc, "fn-stage", r#"function handler() { return "v1"; }"#).await;
1661        let pub_etag = publish_function(&svc, "fn-stage", &etag).await;
1662        let _new_etag = update_function(
1663            &svc,
1664            "fn-stage",
1665            r#"function handler() { return "v2"; }"#,
1666            &pub_etag,
1667        )
1668        .await;
1669
1670        // DEVELOPMENT runs v2 (the latest update body).
1671        let dev_body = test_function_request_xml_with_stage("{}", "DEVELOPMENT");
1672        let resp = svc
1673            .handle(req(
1674                http::Method::POST,
1675                "/2020-05-31/function/fn-stage/test",
1676                &dev_body,
1677                Some("E_NOT_MATCHING"),
1678            ))
1679            .await;
1680        // We deliberately allow stale If-Match here so we exercise the
1681        // stage path; the precondition fires before we get to JS, so
1682        // grab the latest etag and retry.
1683        assert!(resp.is_err(), "stale If-Match must be rejected");
1684        let described = svc
1685            .handle(req(
1686                http::Method::GET,
1687                "/2020-05-31/function/fn-stage",
1688                "",
1689                None,
1690            ))
1691            .await
1692            .unwrap();
1693        let live_etag = described
1694            .headers
1695            .get(http::header::ETAG)
1696            .unwrap()
1697            .to_str()
1698            .unwrap()
1699            .to_string();
1700
1701        let dev_resp = svc
1702            .handle(req(
1703                http::Method::POST,
1704                "/2020-05-31/function/fn-stage/test",
1705                &dev_body,
1706                Some(&live_etag),
1707            ))
1708            .await
1709            .unwrap();
1710        let dev_xml = std::str::from_utf8(dev_resp.body.expect_bytes()).unwrap();
1711        assert!(
1712            dev_xml.contains("&quot;v2&quot;"),
1713            "DEVELOPMENT should run latest update (v2), got {dev_xml}"
1714        );
1715
1716        // LIVE runs v1 (the published snapshot, not affected by the
1717        // post-publish update).
1718        let live_body = test_function_request_xml_with_stage("{}", "LIVE");
1719        let live_resp = svc
1720            .handle(req(
1721                http::Method::POST,
1722                "/2020-05-31/function/fn-stage/test",
1723                &live_body,
1724                Some(&live_etag),
1725            ))
1726            .await
1727            .unwrap();
1728        let live_xml = std::str::from_utf8(live_resp.body.expect_bytes()).unwrap();
1729        assert!(
1730            live_xml.contains("&quot;v1&quot;"),
1731            "LIVE should run published snapshot (v1), got {live_xml}"
1732        );
1733    }
1734
1735    #[tokio::test]
1736    async fn test_function_infinite_loop_is_killed() {
1737        let svc = svc();
1738        let etag = create_function(&svc, "fn-loop", r#"function handler() { while(1){} }"#).await;
1739        let body = test_function_request_xml("{}");
1740        let resp = svc
1741            .handle(req(
1742                http::Method::POST,
1743                "/2020-05-31/function/fn-loop/test",
1744                &body,
1745                Some(&etag),
1746            ))
1747            .await
1748            .unwrap();
1749        assert_eq!(resp.status, StatusCode::OK);
1750        let xml = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
1751        assert!(
1752            xml.contains("<FunctionOutput></FunctionOutput>"),
1753            "expected empty output, got {xml}"
1754        );
1755        assert!(
1756            xml.contains("ERROR:") && xml.contains("limit"),
1757            "expected timeout/limit error in logs, got {xml}"
1758        );
1759        let cu_open = xml.find("<ComputeUtilization>").unwrap() + "<ComputeUtilization>".len();
1760        let cu_close = xml.find("</ComputeUtilization>").unwrap();
1761        let pct: u32 = xml[cu_open..cu_close].parse().unwrap();
1762        assert!(pct > 100, "expected pct > 100 after kill, got {pct}");
1763    }
1764
1765    fn req_q(method: http::Method, path: &str, query: &str) -> AwsRequest {
1766        let mut r = req(method, path, "", None);
1767        r.raw_query = query.to_string();
1768        r
1769    }
1770
1771    #[tokio::test]
1772    async fn get_function_live_stage_returns_published_code_not_dev() {
1773        // Finding #3: after PublishFunction freezes the LIVE snapshot, a
1774        // subsequent UpdateFunction mutates DEVELOPMENT only. GetFunction
1775        // must serve DEV working copy for DEVELOPMENT and the frozen
1776        // snapshot for LIVE.
1777        let svc = svc();
1778        let e1 = create_function(&svc, "stage-fn", "CODE_V1").await;
1779        let e2 = publish_function(&svc, "stage-fn", &e1).await;
1780        update_function(&svc, "stage-fn", "CODE_V2", &e2).await;
1781
1782        let live = svc
1783            .handle(req_q(
1784                http::Method::GET,
1785                "/2020-05-31/function/stage-fn",
1786                "Stage=LIVE",
1787            ))
1788            .await
1789            .unwrap();
1790        let live_code = String::from_utf8(live.body.expect_bytes().to_vec()).unwrap();
1791        assert_eq!(live_code, "CODE_V1", "LIVE stage must serve published code");
1792
1793        let dev = svc
1794            .handle(req_q(
1795                http::Method::GET,
1796                "/2020-05-31/function/stage-fn",
1797                "Stage=DEVELOPMENT",
1798            ))
1799            .await
1800            .unwrap();
1801        let dev_code = String::from_utf8(dev.body.expect_bytes().to_vec()).unwrap();
1802        assert_eq!(dev_code, "CODE_V2", "DEVELOPMENT stage serves working copy");
1803    }
1804
1805    #[tokio::test]
1806    async fn function_config_round_trips_key_value_store_associations() {
1807        // Finding #4: KeyValueStoreAssociations survive Create and are echoed
1808        // on Describe.
1809        let svc = svc();
1810        let arn = "arn:aws:cloudfront::123456789012:key-value-store/kvs-1";
1811        let code_b64 = base64::engine::general_purpose::STANDARD.encode(b"CODE");
1812        let body = format!(
1813            r#"<?xml version="1.0"?>
1814<CreateFunctionRequest xmlns="{NS}">
1815  <Name>kvs-fn</Name>
1816  <FunctionConfig>
1817    <Comment>t</Comment>
1818    <Runtime>cloudfront-js-2.0</Runtime>
1819    <KeyValueStoreAssociations>
1820      <Quantity>1</Quantity>
1821      <Items>
1822        <KeyValueStoreAssociation>
1823          <KeyValueStoreARN>{arn}</KeyValueStoreARN>
1824        </KeyValueStoreAssociation>
1825      </Items>
1826    </KeyValueStoreAssociations>
1827  </FunctionConfig>
1828  <FunctionCode>{code_b64}</FunctionCode>
1829</CreateFunctionRequest>"#
1830        );
1831        let create = svc
1832            .handle(req(http::Method::POST, "/2020-05-31/function", &body, None))
1833            .await
1834            .unwrap();
1835        assert_eq!(create.status, StatusCode::CREATED);
1836        let create_xml = std::str::from_utf8(create.body.expect_bytes()).unwrap();
1837        assert!(
1838            create_xml.contains(&format!("<KeyValueStoreARN>{arn}</KeyValueStoreARN>")),
1839            "create response dropped KVS association: {create_xml}"
1840        );
1841
1842        let describe = svc
1843            .handle(req(
1844                http::Method::GET,
1845                "/2020-05-31/function/kvs-fn/describe",
1846                "",
1847                None,
1848            ))
1849            .await
1850            .unwrap();
1851        let describe_xml = std::str::from_utf8(describe.body.expect_bytes()).unwrap();
1852        assert!(
1853            describe_xml.contains(&format!("<KeyValueStoreARN>{arn}</KeyValueStoreARN>")),
1854            "describe response dropped KVS association: {describe_xml}"
1855        );
1856        assert!(describe_xml.contains("<Quantity>1</Quantity>"));
1857    }
1858}