sendra_core/script/
run.rs1use rhai::{Dynamic, Scope};
5
6use crate::{Request, Response, SendraError};
7
8use super::engine::{capture, failure_message};
9use super::marshal::{request_from_dynamic, request_map, response_map};
10use super::{Hook, Script, ScriptOutcome, ScriptOutput};
11
12pub fn run_pre_request(
28 script: &Script,
29 request: &Request,
30) -> (Result<Request, SendraError>, ScriptOutput) {
31 debug_assert_eq!(script.hook, Hook::PreRequest);
32
33 let mut scope = Scope::new();
34 scope.push("request", request_map(request));
35
36 let (result, output) = capture(|engine| engine.run_ast_with_scope(&mut scope, &script.ast));
37
38 let result = result
39 .map_err(|err| SendraError::ScriptFailed {
40 hook: Hook::PreRequest,
41 message: failure_message(&err),
42 })
43 .and_then(|()| {
44 let left_behind = scope.get_value::<Dynamic>("request").expect(
45 "`request` was pushed into the scope and a script cannot remove a variable",
46 );
47
48 request_from_dynamic(request, left_behind)
49 });
50
51 (result, output)
52}
53
54pub fn run_post_request(script: &Script, response: &Response) -> (ScriptOutcome, ScriptOutput) {
62 debug_assert_eq!(script.hook, Hook::PostRequest);
63
64 let mut scope = Scope::new();
65 scope.push_constant("response", response_map(response));
66
67 let (result, output) = capture(|engine| engine.run_ast_with_scope(&mut scope, &script.ast));
68
69 let outcome = match result {
70 Ok(()) => ScriptOutcome::Passed,
71 Err(err) => ScriptOutcome::Failed {
72 message: failure_message(&err),
73 },
74 };
75
76 (outcome, output)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 use crate::Method;
84
85 use super::super::test_support::{
86 response, run_post, run_pre, with_post_request, with_pre_request,
87 };
88
89 #[test]
92 fn a_pre_request_script_can_add_a_header() {
93 let request = with_pre_request(r#"request.headers["X-Signature"] = "abc";"#);
94 let sent = run_pre(&request).expect("the script should run");
95
96 assert_eq!(sent.header("X-Signature"), Some("abc"));
97 assert_eq!(sent.header("Accept"), Some("application/json"));
99 assert_eq!(sent.url, "https://example.com/orders");
100 assert_eq!(sent.method, Method::Post);
101 assert_eq!(sent.body.as_deref(), Some(r#"{"id":1}"#));
102 }
103
104 #[test]
105 fn a_pre_request_script_can_modify_and_remove_a_header() {
106 let request = with_pre_request(
107 "request.headers[\"Accept\"] = \"text/plain\";\nrequest.headers.remove(\"Nope\");",
108 );
109 let sent = run_pre(&request).expect("the script should run");
110 assert_eq!(sent.header("Accept"), Some("text/plain"));
111
112 let request = with_pre_request(r#"request.headers.remove("Accept");"#);
115 let sent = run_pre(&request).expect("the script should run");
116 assert!(sent.headers.is_empty(), "{:?}", sent.headers);
117 }
118
119 #[test]
120 fn a_pre_request_script_can_rewrite_the_url_and_the_body() {
121 let request =
122 with_pre_request("request.url = request.url + \"?dry_run=1\";\nrequest.body = \"{}\";");
123 let sent = run_pre(&request).expect("the script should run");
124
125 assert_eq!(sent.url, "https://example.com/orders?dry_run=1");
126 assert_eq!(sent.body.as_deref(), Some("{}"));
127 }
128
129 #[test]
130 fn a_pre_request_script_can_clear_the_body() {
131 let request = with_pre_request("request.body = ();");
133 assert_eq!(run_pre(&request).expect("the script should run").body, None);
134
135 let request = with_pre_request(r#"request.body = "";"#);
136 assert_eq!(
137 run_pre(&request)
138 .expect("the script should run")
139 .body
140 .as_deref(),
141 Some("")
142 );
143 }
144
145 #[test]
146 fn a_pre_request_script_can_read_the_request_it_was_given() {
147 let request = with_pre_request(
149 r#"request.headers["X-Seen"] = request.method + " " + request.url + " " + request.body.len();"#,
150 );
151 let sent = run_pre(&request).expect("the script should run");
152
153 assert_eq!(
154 sent.header("X-Seen"),
155 Some("POST https://example.com/orders 8")
156 );
157 }
158
159 #[test]
160 fn a_pre_request_script_that_throws_is_a_typed_error_not_a_panic() {
161 let request = with_pre_request(r#"throw "no signing key";"#);
162 let err = run_pre(&request).expect_err("a throw stops the request");
163
164 assert!(
165 matches!(
166 &err,
167 SendraError::ScriptFailed {
168 hook: Hook::PreRequest,
169 message
170 } if message == "no signing key"
171 ),
172 "{err:?}"
173 );
174 }
175
176 #[test]
179 fn a_post_request_script_can_read_the_response_and_pass() {
180 let request = with_post_request(
181 "if response.status != 201 { throw \"expected 201\"; }\n\
182 if !response.body.contains(\"ada\") { throw \"expected ada\"; }\n\
183 if response.status_text != \"Created\" { throw \"expected Created\"; }\n\
184 if response.elapsed_ms < 0 { throw \"time ran backwards\"; }",
185 );
186
187 assert_eq!(run_post(&request, &response()), ScriptOutcome::Passed);
188 }
189
190 #[test]
191 fn a_post_request_script_sees_every_header_including_repeats() {
192 let request = with_post_request(
195 "let cookies = response.headers.filter(|h| h.name == \"set-cookie\");\n\
196 if cookies.len() != 2 { throw \"expected 2 set-cookie headers, got \" + cookies.len(); }\n\
197 let ct = response.headers.find(|h| h.name == \"content-type\");\n\
198 if ct == () || !ct.value.contains(\"json\") { throw \"expected JSON\"; }",
199 );
200
201 assert_eq!(run_post(&request, &response()), ScriptOutcome::Passed);
202 }
203
204 #[test]
205 fn a_post_request_script_can_fail_explicitly() {
206 let request = with_post_request(
207 r#"if response.status != 200 { throw "expected 200, got " + response.status; }"#,
208 );
209
210 assert_eq!(
211 run_post(&request, &response()),
212 ScriptOutcome::Failed {
213 message: "expected 200, got 201".to_string()
214 }
215 );
216 }
217
218 #[test]
219 fn a_thrown_message_is_reported_verbatim() {
220 let request = with_post_request(r#"throw "the order id was missing";"#);
223 let outcome = run_post(&request, &response());
224
225 assert_eq!(outcome.failure(), Some("the order id was missing"));
226 assert!(!outcome.passed());
227 }
228
229 #[test]
230 fn a_bug_in_a_post_request_script_keeps_its_position() {
231 let request = with_post_request("response.body.no_such_method();");
234 let outcome = run_post(&request, &response());
235
236 let message = outcome.failure().expect("a bug is still a failure");
237 assert!(message.contains("no_such_method"), "{message}");
238 assert!(message.contains("line"), "{message}");
239 }
240
241 #[test]
242 fn a_post_request_script_cannot_modify_the_response() {
243 let request = with_post_request("response.status = 200;");
246 let outcome = run_post(&request, &response());
247
248 assert!(!outcome.passed(), "assigning to the response must fail");
249 let message = outcome.failure().unwrap();
250 assert!(message.to_lowercase().contains("constant"), "{message}");
251 }
252}