1pub mod normalizer;
2
3use serde_json::{Value, json};
4
5pub const RAW_SNAPSHOT_SCHEMA_VERSION: &str = "1.0";
6pub const FIGMA_API_VERSION: &str = "v1";
7pub const DEFAULT_FIGMA_API_BASE_URL: &str = "https://api.figma.com";
8
9#[derive(Debug, thiserror::Error)]
10pub enum FetchClientError {
11 #[error("invalid fetch request: {0}")]
12 InvalidRequest(String),
13 #[error("invalid fixture json: {0}")]
14 InvalidFixtureJson(#[from] serde_json::Error),
15 #[error("figma api unauthorized")]
16 Unauthorized,
17 #[error("figma api returned non-success status {status}: {message}")]
18 HttpStatus { status: u16, message: String },
19 #[error("invalid figma api response: {0}")]
20 InvalidApiResponse(String),
21 #[error("http transport error: {0}")]
22 HttpTransport(String),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct FetchNodesRequest {
28 pub file_key: String,
29 pub node_id: String,
30}
31
32impl FetchNodesRequest {
33 pub fn new(file_key: String, node_id: String) -> Result<Self, FetchClientError> {
34 let file_key = file_key.trim().to_string();
35 if file_key.is_empty() {
36 return Err(FetchClientError::InvalidRequest(
37 "file_key is required".to_string(),
38 ));
39 }
40
41 let node_id = node_id.trim().to_string();
42 if node_id.is_empty() {
43 return Err(FetchClientError::InvalidRequest(
44 "node_id is required".to_string(),
45 ));
46 }
47
48 Ok(Self { file_key, node_id })
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct LiveFetchRequest {
54 pub fetch: FetchNodesRequest,
55 pub figma_token: String,
56 pub api_base_url: Option<String>,
57}
58
59impl LiveFetchRequest {
60 pub fn new(
61 file_key: String,
62 node_id: String,
63 figma_token: String,
64 api_base_url: Option<String>,
65 ) -> Result<Self, FetchClientError> {
66 let fetch = FetchNodesRequest::new(file_key, node_id)?;
67
68 let figma_token = figma_token.trim().to_string();
69 if figma_token.is_empty() {
70 return Err(FetchClientError::InvalidRequest(
71 "figma_token is required for live fetch".to_string(),
72 ));
73 }
74
75 let api_base_url = api_base_url
76 .map(|value| value.trim().to_string())
77 .filter(|value| !value.is_empty());
78
79 Ok(Self {
80 fetch,
81 figma_token,
82 api_base_url,
83 })
84 }
85
86 pub fn api_base_url(&self) -> &str {
87 self.api_base_url
88 .as_deref()
89 .unwrap_or(DEFAULT_FIGMA_API_BASE_URL)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct LiveScreenshotRequest {
95 pub fetch: FetchNodesRequest,
96 pub figma_token: String,
97 pub api_base_url: Option<String>,
98}
99
100impl LiveScreenshotRequest {
101 pub fn new(
102 file_key: String,
103 node_id: String,
104 figma_token: String,
105 api_base_url: Option<String>,
106 ) -> Result<Self, FetchClientError> {
107 let fetch = FetchNodesRequest::new(file_key, node_id)?;
108
109 let figma_token = figma_token.trim().to_string();
110 if figma_token.is_empty() {
111 return Err(FetchClientError::InvalidRequest(
112 "figma_token is required for screenshot fetch".to_string(),
113 ));
114 }
115
116 let api_base_url = api_base_url
117 .map(|value| value.trim().to_string())
118 .filter(|value| !value.is_empty());
119
120 Ok(Self {
121 fetch,
122 figma_token,
123 api_base_url,
124 })
125 }
126
127 pub fn api_base_url(&self) -> &str {
128 self.api_base_url
129 .as_deref()
130 .unwrap_or(DEFAULT_FIGMA_API_BASE_URL)
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
135#[serde(deny_unknown_fields)]
136pub struct NodeScreenshot {
137 pub node_id: String,
138 pub image_url: String,
139 pub format: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
143#[serde(deny_unknown_fields)]
144pub struct RawSnapshotSource {
145 pub file_key: String,
146 pub node_id: String,
147 pub figma_api_version: String,
148}
149
150#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
151#[serde(deny_unknown_fields)]
152pub struct RawFigmaSnapshot {
153 pub snapshot_version: String,
154 pub source: RawSnapshotSource,
155 pub payload: Value,
156}
157
158pub fn fetch_snapshot_from_fixture(
159 request: &FetchNodesRequest,
160 fixture_json: &str,
161) -> Result<RawFigmaSnapshot, FetchClientError> {
162 let payload: Value = serde_json::from_str(fixture_json)?;
163 Ok(RawFigmaSnapshot {
164 snapshot_version: RAW_SNAPSHOT_SCHEMA_VERSION.to_string(),
165 source: RawSnapshotSource {
166 file_key: request.file_key.clone(),
167 node_id: request.node_id.clone(),
168 figma_api_version: FIGMA_API_VERSION.to_string(),
169 },
170 payload,
171 })
172}
173
174pub fn fetch_snapshot_live(
175 request: &LiveFetchRequest,
176) -> Result<RawFigmaSnapshot, FetchClientError> {
177 fetch_snapshot_live_with_base_url(
178 &request.fetch,
179 request.figma_token.as_str(),
180 request.api_base_url(),
181 )
182}
183
184pub fn fetch_node_screenshot_live(
185 request: &LiveScreenshotRequest,
186) -> Result<NodeScreenshot, FetchClientError> {
187 fetch_node_screenshot_live_with_base_url(
188 &request.fetch,
189 request.figma_token.as_str(),
190 request.api_base_url(),
191 )
192}
193
194pub fn fetch_snapshot_live_with_base_url(
195 request: &FetchNodesRequest,
196 figma_token: &str,
197 api_base_url: &str,
198) -> Result<RawFigmaSnapshot, FetchClientError> {
199 let figma_token = figma_token.trim();
200 if figma_token.is_empty() {
201 return Err(FetchClientError::InvalidRequest(
202 "figma_token is required for live fetch".to_string(),
203 ));
204 }
205 let api_base_url = api_base_url.trim();
206 if api_base_url.is_empty() {
207 return Err(FetchClientError::InvalidRequest(
208 "api_base_url is required for live fetch".to_string(),
209 ));
210 }
211
212 let api_url = format!(
213 "{}/v1/files/{}/nodes",
214 api_base_url.trim_end_matches('/'),
215 request.file_key
216 );
217
218 let response = reqwest::blocking::Client::new()
219 .get(api_url)
220 .header("X-Figma-Token", figma_token)
221 .query(&[("ids", request.node_id.as_str())])
222 .send()
223 .map_err(|err| FetchClientError::HttpTransport(err.to_string()))?;
224
225 let status = response.status();
226 if status == reqwest::StatusCode::UNAUTHORIZED {
227 return Err(FetchClientError::Unauthorized);
228 }
229 if !status.is_success() {
230 let body = response
231 .text()
232 .unwrap_or_else(|_| "response body unavailable".to_string());
233 return Err(FetchClientError::HttpStatus {
234 status: status.as_u16(),
235 message: body,
236 });
237 }
238
239 let payload = response
240 .json::<Value>()
241 .map_err(|err| FetchClientError::InvalidApiResponse(err.to_string()))?;
242 build_snapshot_from_live_nodes_payload(request, payload)
243}
244
245pub fn fetch_node_screenshot_live_with_base_url(
246 request: &FetchNodesRequest,
247 figma_token: &str,
248 api_base_url: &str,
249) -> Result<NodeScreenshot, FetchClientError> {
250 let figma_token = figma_token.trim();
251 if figma_token.is_empty() {
252 return Err(FetchClientError::InvalidRequest(
253 "figma_token is required for screenshot fetch".to_string(),
254 ));
255 }
256 let api_base_url = api_base_url.trim();
257 if api_base_url.is_empty() {
258 return Err(FetchClientError::InvalidRequest(
259 "api_base_url is required for screenshot fetch".to_string(),
260 ));
261 }
262
263 let api_url = format!(
264 "{}/v1/images/{}",
265 api_base_url.trim_end_matches('/'),
266 request.file_key
267 );
268
269 let response = reqwest::blocking::Client::new()
270 .get(api_url)
271 .header("X-Figma-Token", figma_token)
272 .query(&[("ids", request.node_id.as_str()), ("format", "png")])
273 .send()
274 .map_err(|err| FetchClientError::HttpTransport(err.to_string()))?;
275
276 let status = response.status();
277 if status == reqwest::StatusCode::UNAUTHORIZED {
278 return Err(FetchClientError::Unauthorized);
279 }
280 if !status.is_success() {
281 let body = response
282 .text()
283 .unwrap_or_else(|_| "response body unavailable".to_string());
284 return Err(FetchClientError::HttpStatus {
285 status: status.as_u16(),
286 message: body,
287 });
288 }
289
290 let payload = response
291 .json::<Value>()
292 .map_err(|err| FetchClientError::InvalidApiResponse(err.to_string()))?;
293 build_node_screenshot_from_payload(request, payload)
294}
295
296fn build_snapshot_from_live_nodes_payload(
297 request: &FetchNodesRequest,
298 payload: Value,
299) -> Result<RawFigmaSnapshot, FetchClientError> {
300 let document = payload
301 .get("nodes")
302 .and_then(Value::as_object)
303 .and_then(|nodes| nodes.get(request.node_id.as_str()))
304 .and_then(Value::as_object)
305 .and_then(|node| node.get("document"))
306 .cloned()
307 .ok_or_else(|| {
308 FetchClientError::InvalidApiResponse(format!(
309 "missing nodes.{}.document in figma response",
310 request.node_id
311 ))
312 })?;
313
314 Ok(RawFigmaSnapshot {
315 snapshot_version: RAW_SNAPSHOT_SCHEMA_VERSION.to_string(),
316 source: RawSnapshotSource {
317 file_key: request.file_key.clone(),
318 node_id: request.node_id.clone(),
319 figma_api_version: FIGMA_API_VERSION.to_string(),
320 },
321 payload: json!({
322 "document": document
323 }),
324 })
325}
326
327fn build_node_screenshot_from_payload(
328 request: &FetchNodesRequest,
329 payload: Value,
330) -> Result<NodeScreenshot, FetchClientError> {
331 let image_url = payload
332 .get("images")
333 .and_then(Value::as_object)
334 .and_then(|images| images.get(request.node_id.as_str()))
335 .and_then(Value::as_str)
336 .ok_or_else(|| {
337 FetchClientError::InvalidApiResponse(format!(
338 "missing images.{} in figma response",
339 request.node_id
340 ))
341 })?;
342
343 Ok(NodeScreenshot {
344 node_id: request.node_id.clone(),
345 image_url: image_url.to_string(),
346 format: "png".to_string(),
347 })
348}
349
350#[cfg(test)]
351mod tests {
352 use serde_json::json;
353 use std::io::{Read, Write};
354
355 #[test]
356 fn fetch_nodes_request_rejects_missing_file_key() {
357 let err = super::FetchNodesRequest::new("".to_string(), "123:456".to_string())
358 .expect_err("empty file key should be rejected");
359 assert_eq!(
360 err.to_string(),
361 "invalid fetch request: file_key is required"
362 );
363 }
364
365 #[test]
366 fn fetch_snapshot_from_fixture_preserves_source_and_payload() {
367 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
368 .expect("request should be valid");
369
370 let fixture = r#"{
371 "document": {
372 "id": "123:456",
373 "name": "Root Frame"
374 }
375 }"#;
376
377 let snapshot = super::fetch_snapshot_from_fixture(&request, fixture)
378 .expect("fixture payload should parse");
379
380 assert_eq!(
381 snapshot.snapshot_version,
382 super::RAW_SNAPSHOT_SCHEMA_VERSION
383 );
384 assert_eq!(snapshot.source.file_key, "abc123");
385 assert_eq!(snapshot.source.node_id, "123:456");
386 assert_eq!(snapshot.source.figma_api_version, super::FIGMA_API_VERSION);
387 assert_eq!(
388 snapshot.payload,
389 json!({
390 "document": {
391 "id": "123:456",
392 "name": "Root Frame"
393 }
394 })
395 );
396 }
397
398 #[test]
399 fn fetch_snapshot_from_fixture_reports_invalid_json() {
400 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
401 .expect("request should be valid");
402
403 let err = super::fetch_snapshot_from_fixture(&request, "{")
404 .expect_err("malformed fixture should fail");
405 assert!(err.to_string().starts_with("invalid fixture json:"));
406 }
407
408 #[test]
409 fn raw_snapshot_contract_round_trip() {
410 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
411 .expect("request should be valid");
412 let snapshot = super::fetch_snapshot_from_fixture(
413 &request,
414 r#"{"document":{"id":"123:456","name":"Root Frame"}}"#,
415 )
416 .expect("fixture payload should parse");
417
418 let encoded = serde_json::to_string(&snapshot).expect("snapshot should serialize");
419 let decoded: super::RawFigmaSnapshot =
420 serde_json::from_str(&encoded).expect("snapshot should deserialize");
421
422 assert_eq!(decoded, snapshot);
423 }
424
425 #[test]
426 fn live_fetch_request_rejects_missing_figma_token() {
427 let err = super::LiveFetchRequest::new(
428 "abc123".to_string(),
429 "123:456".to_string(),
430 "".to_string(),
431 None,
432 )
433 .expect_err("empty figma token should be rejected");
434
435 assert_eq!(
436 err.to_string(),
437 "invalid fetch request: figma_token is required for live fetch"
438 );
439 }
440
441 #[test]
442 fn live_fetch_request_allows_explicit_api_base_url_override() {
443 let request = super::LiveFetchRequest::new(
444 "abc123".to_string(),
445 "123:456".to_string(),
446 "secret-token".to_string(),
447 Some("http://127.0.0.1:9999".to_string()),
448 )
449 .expect("live fetch request should be valid");
450
451 assert_eq!(request.fetch.file_key, "abc123");
452 assert_eq!(request.fetch.node_id, "123:456");
453 assert_eq!(request.figma_token, "secret-token");
454 assert_eq!(
455 request.api_base_url,
456 Some("http://127.0.0.1:9999".to_string())
457 );
458 }
459
460 #[test]
461 fn live_fetch_request_uses_default_figma_api_base_url() {
462 let request = super::LiveFetchRequest::new(
463 "abc123".to_string(),
464 "123:456".to_string(),
465 "secret-token".to_string(),
466 None,
467 )
468 .expect("live fetch request should be valid");
469
470 assert_eq!(request.api_base_url(), super::DEFAULT_FIGMA_API_BASE_URL);
471 }
472
473 #[test]
474 fn fetch_client_error_contract_includes_live_transport_variants() {
475 let unauthorized = super::FetchClientError::Unauthorized;
476 assert_eq!(unauthorized.to_string(), "figma api unauthorized");
477
478 let http_status = super::FetchClientError::HttpStatus {
479 status: 404,
480 message: "Not Found".to_string(),
481 };
482 assert_eq!(
483 http_status.to_string(),
484 "figma api returned non-success status 404: Not Found"
485 );
486 }
487
488 #[test]
489 fn build_snapshot_from_live_nodes_payload_extracts_requested_document() {
490 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
491 .expect("request should be valid");
492 let payload = serde_json::json!({
493 "nodes": {
494 "123:456": {
495 "document": {
496 "id": "123:456",
497 "name": "Live Root"
498 }
499 }
500 }
501 });
502
503 let snapshot = super::build_snapshot_from_live_nodes_payload(&request, payload)
504 .expect("valid payload");
505
506 assert_eq!(snapshot.source.file_key, "abc123");
507 assert_eq!(snapshot.source.node_id, "123:456");
508 assert_eq!(
509 snapshot.payload,
510 serde_json::json!({
511 "document": {
512 "id": "123:456",
513 "name": "Live Root"
514 }
515 })
516 );
517 }
518
519 #[test]
520 fn build_snapshot_from_live_nodes_payload_requires_document_for_node() {
521 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
522 .expect("request should be valid");
523 let payload = serde_json::json!({
524 "nodes": {
525 "123:456": {}
526 }
527 });
528
529 let err = super::build_snapshot_from_live_nodes_payload(&request, payload)
530 .expect_err("payload without document should fail");
531 assert_eq!(
532 err.to_string(),
533 "invalid figma api response: missing nodes.123:456.document in figma response"
534 );
535 }
536
537 #[test]
538 fn fetch_snapshot_live_rejects_missing_figma_token() {
539 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
540 .expect("request should be valid");
541 let err = super::fetch_snapshot_live_with_base_url(&request, "", "http://127.0.0.1:9")
542 .expect_err("empty token should fail");
543
544 assert_eq!(
545 err.to_string(),
546 "invalid fetch request: figma_token is required for live fetch"
547 );
548 }
549
550 #[test]
551 fn fetch_snapshot_live_with_base_url_sends_auth_header_and_maps_success() {
552 let (base_url, request_rx, server_thread) = match start_single_response_server(
553 "200 OK",
554 r#"{
555 "nodes": {
556 "123:456": {
557 "document": {
558 "id": "123:456",
559 "name": "Live Root"
560 }
561 }
562 }
563 }"#,
564 ) {
565 Ok(server) => server,
566 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
567 eprintln!("skipping live transport test: local socket bind not permitted");
568 return;
569 }
570 Err(err) => panic!("mock server should bind: {err}"),
571 };
572 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
573 .expect("request should be valid");
574
575 let snapshot =
576 super::fetch_snapshot_live_with_base_url(&request, "secret-token", &base_url)
577 .expect("live fetch should succeed");
578 let raw_request = request_rx
579 .recv_timeout(std::time::Duration::from_secs(2))
580 .expect("mock server should receive request");
581 server_thread.join().expect("server thread should finish");
582
583 let lower_request = raw_request.to_ascii_lowercase();
584 assert!(raw_request.starts_with("GET /v1/files/abc123/nodes?ids=123%3A456 HTTP/1.1"));
585 assert!(lower_request.contains("x-figma-token: secret-token"));
586 assert_eq!(
587 snapshot.payload,
588 serde_json::json!({
589 "document": {
590 "id": "123:456",
591 "name": "Live Root"
592 }
593 })
594 );
595 }
596
597 #[test]
598 fn fetch_snapshot_live_maps_unauthorized_status() {
599 let (base_url, _request_rx, server_thread) =
600 match start_single_response_server("401 Unauthorized", "Unauthorized") {
601 Ok(server) => server,
602 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
603 eprintln!("skipping live transport test: local socket bind not permitted");
604 return;
605 }
606 Err(err) => panic!("mock server should bind: {err}"),
607 };
608 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
609 .expect("request should be valid");
610
611 let err = super::fetch_snapshot_live_with_base_url(&request, "secret-token", &base_url)
612 .expect_err("unauthorized response should fail");
613 server_thread.join().expect("server thread should finish");
614
615 assert_eq!(err.to_string(), "figma api unauthorized");
616 }
617
618 #[test]
619 fn fetch_snapshot_live_maps_non_success_status_with_body() {
620 let (base_url, _request_rx, server_thread) =
621 match start_single_response_server("404 Not Found", "No file") {
622 Ok(server) => server,
623 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
624 eprintln!("skipping live transport test: local socket bind not permitted");
625 return;
626 }
627 Err(err) => panic!("mock server should bind: {err}"),
628 };
629 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
630 .expect("request should be valid");
631
632 let err = super::fetch_snapshot_live_with_base_url(&request, "secret-token", &base_url)
633 .expect_err("404 response should fail");
634 server_thread.join().expect("server thread should finish");
635
636 assert_eq!(
637 err.to_string(),
638 "figma api returned non-success status 404: No file"
639 );
640 }
641
642 #[test]
643 fn fetch_node_screenshot_live_with_base_url_requests_images_endpoint() {
644 let (base_url, request_rx, server_thread) = match start_single_response_server(
645 "200 OK",
646 r#"{
647 "images": {
648 "123:456": "https://cdn.example.com/image.png"
649 }
650 }"#,
651 ) {
652 Ok(server) => server,
653 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
654 eprintln!("skipping live transport test: local socket bind not permitted");
655 return;
656 }
657 Err(err) => panic!("mock server should bind: {err}"),
658 };
659 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
660 .expect("request should be valid");
661
662 let screenshot =
663 super::fetch_node_screenshot_live_with_base_url(&request, "secret-token", &base_url)
664 .expect("screenshot fetch should succeed");
665 let raw_request = request_rx
666 .recv_timeout(std::time::Duration::from_secs(2))
667 .expect("mock server should receive request");
668 server_thread.join().expect("server thread should finish");
669
670 let lower_request = raw_request.to_ascii_lowercase();
671 assert!(raw_request.starts_with("GET /v1/images/abc123?ids=123%3A456&format=png HTTP/1.1"));
672 assert!(lower_request.contains("x-figma-token: secret-token"));
673 assert_eq!(screenshot.node_id, "123:456");
674 assert_eq!(screenshot.image_url, "https://cdn.example.com/image.png");
675 assert_eq!(screenshot.format, "png");
676 }
677
678 #[test]
679 fn fetch_node_screenshot_live_with_base_url_reports_missing_image_ref() {
680 let (base_url, _request_rx, server_thread) =
681 match start_single_response_server("200 OK", r#"{"images":{}}"#) {
682 Ok(server) => server,
683 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => {
684 eprintln!("skipping live transport test: local socket bind not permitted");
685 return;
686 }
687 Err(err) => panic!("mock server should bind: {err}"),
688 };
689 let request = super::FetchNodesRequest::new("abc123".to_string(), "123:456".to_string())
690 .expect("request should be valid");
691
692 let err =
693 super::fetch_node_screenshot_live_with_base_url(&request, "secret-token", &base_url)
694 .expect_err("missing image ref should fail");
695 server_thread.join().expect("server thread should finish");
696
697 assert_eq!(
698 err.to_string(),
699 "invalid figma api response: missing images.123:456 in figma response"
700 );
701 }
702
703 fn start_single_response_server(
704 status_line: &str,
705 body: &str,
706 ) -> Result<
707 (
708 String,
709 std::sync::mpsc::Receiver<String>,
710 std::thread::JoinHandle<()>,
711 ),
712 std::io::Error,
713 > {
714 let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
715 let address = listener
716 .local_addr()
717 .expect("mock server should expose local address");
718 let (request_tx, request_rx) = std::sync::mpsc::channel::<String>();
719 let status_line = status_line.to_string();
720 let body = body.to_string();
721
722 let server_thread = std::thread::spawn(move || {
723 let (mut stream, _) = listener
724 .accept()
725 .expect("mock server should accept one request");
726 stream
727 .set_read_timeout(Some(std::time::Duration::from_secs(2)))
728 .expect("mock server should set read timeout");
729
730 let mut request_bytes = Vec::new();
731 let mut buffer = [0_u8; 4096];
732 loop {
733 let bytes_read = stream
734 .read(&mut buffer)
735 .expect("mock server should read request bytes");
736 if bytes_read == 0 {
737 break;
738 }
739 request_bytes.extend_from_slice(&buffer[..bytes_read]);
740 if request_bytes.windows(4).any(|window| window == b"\r\n\r\n") {
741 break;
742 }
743 }
744
745 let request = String::from_utf8_lossy(&request_bytes).to_string();
746 let _ = request_tx.send(request);
747
748 let response = format!(
749 "HTTP/1.1 {status_line}\r\nContent-Type: application/json\r\nContent-Length: {content_length}\r\nConnection: close\r\n\r\n{body}",
750 content_length = body.len()
751 );
752 stream
753 .write_all(response.as_bytes())
754 .expect("mock server should write response");
755 stream.flush().expect("mock server should flush response");
756 });
757
758 Ok((format!("http://{address}"), request_rx, server_thread))
759 }
760}