1use std::collections::HashMap;
8
9use jsdet_core::observation::Value;
10
11#[derive(Debug, Clone)]
13pub struct FakeResponse {
14 pub status: u16,
15 pub status_text: String,
16 pub headers: HashMap<String, String>,
17 pub body: String,
18 pub content_type: String,
19}
20
21impl Default for FakeResponse {
22 fn default() -> Self {
23 Self {
24 status: 200,
25 status_text: "OK".into(),
26 headers: HashMap::new(),
27 body: String::new(),
28 content_type: "text/html".into(),
29 }
30 }
31}
32
33impl FakeResponse {
34 pub fn ok(body: &str) -> Self {
35 Self {
36 body: body.into(),
37 ..Self::default()
38 }
39 }
40
41 pub fn json(body: &str) -> Self {
42 Self {
43 body: body.into(),
44 content_type: "application/json".into(),
45 ..Self::default()
46 }
47 }
48
49 pub fn not_found() -> Self {
50 Self {
51 status: 404,
52 status_text: "Not Found".into(),
53 body: "Not Found".into(),
54 ..Self::default()
55 }
56 }
57
58 pub fn error() -> Self {
59 Self {
60 status: 500,
61 status_text: "Internal Server Error".into(),
62 body: "Internal Server Error".into(),
63 ..Self::default()
64 }
65 }
66
67 pub fn to_fetch_response_json(&self) -> String {
69 let headers_json = serde_json::to_string(&self.headers).unwrap_or_default();
70 format!(
71 r#"{{"ok":{},"status":{},"statusText":"{}","headers":{},"body":"{}","type":"basic","url":"","redirected":false}}"#,
72 self.status >= 200 && self.status < 300,
73 self.status,
74 self.status_text,
75 headers_json,
76 self.body.replace('"', "\\\""),
77 )
78 }
79}
80
81#[derive(Debug, Default)]
88pub struct ResponseRegistry {
89 exact: HashMap<String, FakeResponse>,
91 prefix: Vec<(String, FakeResponse)>,
93 fallback: Option<FakeResponse>,
95}
96
97impl ResponseRegistry {
98 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn add_exact(&mut self, url: impl Into<String>, response: FakeResponse) {
104 self.exact.insert(url.into(), response);
105 }
106
107 pub fn add_prefix(&mut self, prefix: impl Into<String>, response: FakeResponse) {
109 self.prefix.push((prefix.into(), response));
110 }
111
112 pub fn set_fallback(&mut self, response: FakeResponse) {
114 self.fallback = Some(response);
115 }
116
117 pub fn get(&self, url: &str) -> FakeResponse {
119 if let Some(r) = self.exact.get(url) {
120 return r.clone();
121 }
122 for (prefix, response) in &self.prefix {
123 if url.starts_with(prefix.as_str()) {
124 return response.clone();
125 }
126 }
127 self.fallback
128 .clone()
129 .unwrap_or_else(FakeResponse::not_found)
130 }
131}
132
133pub fn handle_fetch(url: &str, _method: &str, registry: &ResponseRegistry) -> Value {
135 let response = registry.get(url);
136 Value::json(response.to_fetch_response_json())
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn exact_url_match() {
145 let mut reg = ResponseRegistry::new();
146 reg.add_exact(
147 "https://api.example.com/data",
148 FakeResponse::json(r#"{"key":"val"}"#),
149 );
150 let resp = reg.get("https://api.example.com/data");
151 assert_eq!(resp.status, 200);
152 assert!(resp.body.contains("key"));
153 }
154
155 #[test]
156 fn prefix_match() {
157 let mut reg = ResponseRegistry::new();
158 reg.add_prefix("https://cdn.", FakeResponse::ok("cdn content"));
159 let resp = reg.get("https://cdn.example.com/lib.js");
160 assert_eq!(resp.body, "cdn content");
161 }
162
163 #[test]
164 fn fallback_when_no_match() {
165 let reg = ResponseRegistry::new();
166 let resp = reg.get("https://unknown.com");
167 assert_eq!(resp.status, 404);
168 }
169}