1use std::collections::BTreeMap;
9
10use crate::assertions::{Assertions, NotAssertions};
11use crate::{
12 ApiKeyAuth, Auth, BasicAuth, Collection, Document, MultipartPart, OAuthAuth, Request,
13 SendraError,
14};
15
16use super::Environment;
17
18const TEMPLATE_OPEN: &str = "{{";
20const TEMPLATE_CLOSE: &str = "}}";
21
22impl Environment {
23 pub fn apply(&self, request: &Request) -> Result<Request, SendraError> {
48 let mut headers = Vec::with_capacity(request.headers.len());
49 for (name, value) in &request.headers {
50 let name = self.expand_templates(name)?;
51 let value = self.expand_templates(value)?;
52 headers.push((name, value));
53 }
54
55 let query: Vec<(String, String)> = request
62 .query
63 .iter()
64 .map(|(name, value)| Ok((self.expand_templates(name)?, self.expand_templates(value)?)))
65 .collect::<Result<_, SendraError>>()?;
66
67 let auth = match &request.auth {
74 Some(auth) => Some(self.substitute_auth(auth)?),
75 None => match &self.auth {
76 Some(auth) => {
77 let auth = self.substitute_auth(auth)?;
78 if let Some(reason) = auth.collision_reason(&headers, &query) {
85 return Err(SendraError::InvalidRequest { reason });
86 }
87 Some(auth)
88 }
89 None => None,
90 },
91 };
92
93 Ok(Request {
94 name: request.name.clone(),
98 method: request.method,
99 url: self.expand_templates(&request.url)?,
100 headers,
101 query,
102 body: request
103 .body
104 .as_deref()
105 .map(|body| self.expand_templates(body))
106 .transpose()?,
107 json: request
113 .json
114 .as_ref()
115 .map(|value| self.expand_json(value))
116 .transpose()?,
117 body_file: request
121 .body_file
122 .as_deref()
123 .map(|path| self.expand_templates(path))
124 .transpose()?,
125 form: request
126 .form
127 .iter()
128 .map(|(name, value)| {
129 Ok((self.expand_templates(name)?, self.expand_templates(value)?))
130 })
131 .collect::<Result<_, SendraError>>()?,
132 multipart: request
133 .multipart
134 .iter()
135 .map(|part| {
136 Ok(MultipartPart {
137 name: self.expand_templates(&part.name)?,
138 value: part
139 .value
140 .as_deref()
141 .map(|value| self.expand_templates(value))
142 .transpose()?,
143 path: part
146 .path
147 .as_deref()
148 .map(|path| self.expand_templates(path))
149 .transpose()?,
150 })
151 })
152 .collect::<Result<_, SendraError>>()?,
153 auth,
154 assertions: request
155 .assertions
156 .as_ref()
157 .map(|assertions| self.apply_assertions(assertions))
158 .transpose()?,
159 pre_request: request.pre_request.clone(),
171 post_request: request.post_request.clone(),
172 capture: request.capture.clone(),
182 retry: request.retry,
186 })
187 }
188
189 fn apply_assertions(&self, assertions: &Assertions) -> Result<Assertions, SendraError> {
204 Ok(Assertions {
205 status: assertions.status,
206 status_in: assertions.status_in.clone(),
207 headers: self.apply_assertion_headers(&assertions.headers)?,
208 body_contains: assertions
209 .body_contains
210 .as_deref()
211 .map(|body| self.expand_templates(body))
212 .transpose()?,
213 body_matches: assertions
214 .body_matches
215 .as_deref()
216 .map(|pattern| self.expand_templates(pattern))
217 .transpose()?,
218 elapsed_ms_under: assertions.elapsed_ms_under,
219 json: self.apply_assertion_json(&assertions.json)?,
220 not: assertions
221 .not
222 .as_ref()
223 .map(|not| self.apply_not_assertions(not))
224 .transpose()?,
225 })
226 }
227
228 fn apply_not_assertions(&self, not: &NotAssertions) -> Result<NotAssertions, SendraError> {
231 Ok(NotAssertions {
232 status: not.status,
233 status_in: not.status_in.clone(),
234 headers: self.apply_assertion_headers(¬.headers)?,
235 body_contains: not
236 .body_contains
237 .as_deref()
238 .map(|body| self.expand_templates(body))
239 .transpose()?,
240 body_matches: not
241 .body_matches
242 .as_deref()
243 .map(|pattern| self.expand_templates(pattern))
244 .transpose()?,
245 elapsed_ms_under: not.elapsed_ms_under,
246 json: self.apply_assertion_json(¬.json)?,
247 })
248 }
249
250 fn apply_assertion_headers(
251 &self,
252 headers: &BTreeMap<String, Option<String>>,
253 ) -> Result<BTreeMap<String, Option<String>>, SendraError> {
254 let mut expanded = BTreeMap::new();
255 for (name, expected) in headers {
256 let expected = expected
257 .as_deref()
258 .map(|value| self.expand_templates(value))
259 .transpose()?;
260 expanded.insert(name.clone(), expected);
261 }
262 Ok(expanded)
263 }
264
265 fn apply_assertion_json(
266 &self,
267 json: &BTreeMap<String, serde_json::Value>,
268 ) -> Result<BTreeMap<String, serde_json::Value>, SendraError> {
269 let mut expanded = BTreeMap::new();
270 for (path, expected) in json {
271 expanded.insert(path.clone(), self.expand_json(expected)?);
272 }
273 Ok(expanded)
274 }
275
276 fn expand_json(&self, value: &serde_json::Value) -> Result<serde_json::Value, SendraError> {
281 use serde_json::Value;
282 Ok(match value {
283 Value::String(text) => Value::String(self.expand_templates(text)?),
284 Value::Array(items) => Value::Array(
285 items
286 .iter()
287 .map(|item| self.expand_json(item))
288 .collect::<Result<_, _>>()?,
289 ),
290 Value::Object(fields) => Value::Object(
291 fields
292 .iter()
293 .map(|(key, value)| Ok((key.clone(), self.expand_json(value)?)))
294 .collect::<Result<_, SendraError>>()?,
295 ),
296 other => other.clone(),
297 })
298 }
299
300 pub fn apply_collection(&self, collection: &Collection) -> Result<Collection, SendraError> {
314 Ok(Collection {
315 name: collection.name.clone(),
316 requests: collection
317 .requests
318 .iter()
319 .map(|request| self.apply(request))
320 .collect::<Result<_, _>>()?,
321 })
322 }
323
324 pub fn apply_document(&self, document: &Document) -> Result<Document, SendraError> {
326 Ok(match document {
327 Document::Single(request) => Document::Single(self.apply(request)?),
328 Document::Collection(collection) => {
329 Document::Collection(self.apply_collection(collection)?)
330 }
331 })
332 }
333
334 fn expand_templates(&self, text: &str) -> Result<String, SendraError> {
336 super::expand(text, TEMPLATE_OPEN, TEMPLATE_CLOSE, |name| {
337 self.lookup(name)
338 })
339 }
340
341 fn substitute_auth(&self, auth: &Auth) -> Result<Auth, SendraError> {
348 Ok(Auth {
349 bearer: auth
350 .bearer
351 .as_deref()
352 .map(|token| self.expand_templates(token))
353 .transpose()?,
354 basic: auth
355 .basic
356 .as_ref()
357 .map(|basic| -> Result<BasicAuth, SendraError> {
358 Ok(BasicAuth {
359 user: self.expand_templates(&basic.user)?,
360 pass: self.expand_templates(&basic.pass)?,
361 })
362 })
363 .transpose()?,
364 api_key: auth
365 .api_key
366 .as_ref()
367 .map(|api_key| -> Result<ApiKeyAuth, SendraError> {
368 Ok(ApiKeyAuth {
369 r#in: api_key.r#in,
370 name: self.expand_templates(&api_key.name)?,
371 value: self.expand_templates(&api_key.value)?,
372 })
373 })
374 .transpose()?,
375 oauth: auth
376 .oauth
377 .as_ref()
378 .map(|oauth| -> Result<OAuthAuth, SendraError> {
379 Ok(OAuthAuth {
380 grant_type: oauth.grant_type,
381 token_url: self.expand_templates(&oauth.token_url)?,
382 client_id: self.expand_templates(&oauth.client_id)?,
383 client_secret: oauth
384 .client_secret
385 .as_deref()
386 .map(|value| self.expand_templates(value))
387 .transpose()?,
388 scope: oauth
389 .scope
390 .as_deref()
391 .map(|value| self.expand_templates(value))
392 .transpose()?,
393 username: oauth
394 .username
395 .as_deref()
396 .map(|value| self.expand_templates(value))
397 .transpose()?,
398 password: oauth
399 .password
400 .as_deref()
401 .map(|value| self.expand_templates(value))
402 .transpose()?,
403 authorization_url: oauth
404 .authorization_url
405 .as_deref()
406 .map(|value| self.expand_templates(value))
407 .transpose()?,
408 redirect_uri: oauth
409 .redirect_uri
410 .as_deref()
411 .map(|value| self.expand_templates(value))
412 .transpose()?,
413 })
414 })
415 .transpose()?,
416 })
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::super::test_helpers::environment;
423 use super::*;
424
425 use crate::Method;
426
427 const TEMPLATED: &str = "\
429name: Templated
430method: POST
431url: '{{base_url}}/users/{{user_id}}'
432headers:
433 Authorization: 'Bearer {{api_key}}'
434 '{{header_name}}': fixed-value
435body: '{\"host\": \"{{base_url}}\"}'
436";
437
438 #[test]
439 fn substitutes_url_headers_and_body() {
440 let request = Request::from_yaml_str(TEMPLATED).unwrap();
441 let environment = environment(
442 &[
443 ("base_url", "https://staging.example.com"),
444 ("user_id", "42"),
445 ("api_key", "s3cret"),
446 ("header_name", "X-Tenant"),
447 ],
448 &[],
449 );
450
451 let applied = environment.apply(&request).expect("every variable is set");
452
453 assert_eq!(applied.url, "https://staging.example.com/users/42");
454 assert_eq!(applied.header("Authorization"), Some("Bearer s3cret"));
455 assert_eq!(applied.header("X-Tenant"), Some("fixed-value"));
457 assert_eq!(
458 applied.body.as_deref(),
459 Some("{\"host\": \"https://staging.example.com\"}")
460 );
461 assert_eq!(applied.name.as_deref(), Some("Templated"));
463 assert_eq!(applied.method, Method::Post);
464 }
465
466 #[test]
467 fn substitution_reaches_json_form_and_multipart_values_but_not_a_body_files_content() {
468 let yaml = "\
469method: POST
470url: https://example.com
471";
472 let json_request = Request::from_yaml_str(&format!(
477 "{yaml}json:\n tenant: '{{{{tenant}}}}'\n nested:\n id: '{{{{tenant}}}}'\n"
478 ))
479 .unwrap();
480 let form_request =
481 Request::from_yaml_str(&format!("{yaml}form:\n tenant: '{{{{tenant}}}}'\n")).unwrap();
482 let multipart_request = Request::from_yaml_str(&format!(
483 "{yaml}multipart:\n - name: '{{{{tenant}}}}'\n value: '{{{{tenant}}}}'\n"
484 ))
485 .unwrap();
486
487 let environment = environment(&[("tenant", "acme")], &[]);
488
489 let applied_json = environment.apply(&json_request).expect("tenant is set");
490 assert_eq!(
491 applied_json.json,
492 Some(serde_json::json!({"tenant": "acme", "nested": {"id": "acme"}}))
493 );
494
495 let applied_form = environment.apply(&form_request).expect("tenant is set");
496 assert_eq!(
497 applied_form.form,
498 vec![("tenant".to_string(), "acme".to_string())]
499 );
500
501 let applied_multipart = environment
502 .apply(&multipart_request)
503 .expect("tenant is set");
504 assert_eq!(applied_multipart.multipart[0].name, "acme");
505 assert_eq!(
506 applied_multipart.multipart[0].value.as_deref(),
507 Some("acme")
508 );
509
510 let body_file_request =
512 Request::from_yaml_str(&format!("{yaml}body_file: './{{{{tenant}}}}.json'\n")).unwrap();
513 let applied_body_file = environment
514 .apply(&body_file_request)
515 .expect("tenant is set");
516 assert_eq!(applied_body_file.body_file.as_deref(), Some("./acme.json"));
517
518 let dir = tempfile::tempdir().unwrap();
524 std::fs::write(dir.path().join("acme.json"), "{{tenant}}").unwrap();
525 let resolved = applied_body_file
526 .resolve_body(dir.path())
527 .expect("the file is there");
528 assert_eq!(
529 resolved.body.as_deref(),
530 Some("{{tenant}}"),
531 "a placeholder inside the file's content must not be substituted"
532 );
533 }
534
535 #[test]
536 fn substitution_preserves_header_order_including_a_repeated_name() {
537 let yaml = "\
538method: GET
539url: https://example.com
540headers:
541 Accept: application/json
542 X-Forwarded-For:
543 - '{{first}}'
544 - '{{second}}'
545 X-Tenant: '{{tenant}}'
546";
547 let request = Request::from_yaml_str(yaml).unwrap();
548 let environment = environment(
549 &[
550 ("first", "1.2.3.4"),
551 ("second", "5.6.7.8"),
552 ("tenant", "acme"),
553 ],
554 &[],
555 );
556
557 let applied = environment.apply(&request).expect("every variable is set");
558
559 assert_eq!(
560 applied.headers,
561 vec![
562 ("Accept".to_string(), "application/json".to_string()),
563 ("X-Forwarded-For".to_string(), "1.2.3.4".to_string()),
564 ("X-Forwarded-For".to_string(), "5.6.7.8".to_string()),
565 ("X-Tenant".to_string(), "acme".to_string()),
566 ],
567 "order among distinct names and among a repeated name must both survive"
568 );
569 }
570
571 const TEMPLATED_ASSERTIONS: &str = "\
574method: GET
575url: '{{base_url}}/users/{{user_id}}'
576assertions:
577 status: 200
578 headers:
579 x-tenant: '{{tenant}}'
580 content-type:
581 body_contains: '{{tenant}}'
582 json:
583 $.id: '{{user_id}}'
584 $.nested:
585 tenant: '{{tenant}}'
586 tags: ['{{tenant}}', literal]
587";
588
589 #[test]
590 fn substitutes_the_values_in_an_assertions_block() {
591 let request = Request::from_yaml_str(TEMPLATED_ASSERTIONS).unwrap();
592 let environment = environment(
593 &[
594 ("base_url", "https://staging.example.com"),
595 ("user_id", "42"),
596 ("tenant", "acme"),
597 ],
598 &[],
599 );
600
601 let assertions = environment
602 .apply(&request)
603 .expect("every variable is set")
604 .assertions
605 .expect("the block survives substitution");
606
607 assert_eq!(assertions.status, Some(200));
608 assert_eq!(
609 assertions.headers.get("x-tenant"),
610 Some(&Some("acme".to_string()))
611 );
612 assert_eq!(assertions.headers.get("content-type"), Some(&None));
614 assert_eq!(assertions.body_contains.as_deref(), Some("acme"));
615 assert_eq!(assertions.json["$.id"], serde_json::json!("42"));
617 assert_eq!(
618 assertions.json["$.nested"],
619 serde_json::json!({"tenant": "acme", "tags": ["acme", "literal"]})
620 );
621 }
622
623 #[test]
624 fn script_source_is_not_substituted() {
625 let request = Request::from_yaml_str(
637 "\
638method: GET
639url: 'https://example.com/{{tenant}}'
640pre_request: |
641 request.headers[\"X-Tenant\"] = \"{{tenant}}\";
642 request.headers[\"X-Secret\"] = \"${SECRET}\";
643post_request: |
644 if response.body != \"{{tenant}}\" { throw \"{{tenant}}\"; }
645",
646 )
647 .unwrap();
648 let environment = environment(&[("tenant", "acme")], &[]);
649
650 let applied = environment.apply(&request).unwrap();
651
652 assert_eq!(applied.url, "https://example.com/acme");
655
656 assert_eq!(applied.pre_request, request.pre_request);
658 assert_eq!(applied.post_request, request.post_request);
659
660 assert!(applied
665 .pre_request
666 .as_deref()
667 .unwrap()
668 .contains("${SECRET}"));
669 assert!(applied
670 .pre_request
671 .as_deref()
672 .unwrap()
673 .contains("{{tenant}}"));
674 }
675
676 #[test]
677 fn assertion_keys_are_not_substituted() {
678 let request = Request::from_yaml_str(
684 "\
685method: GET
686url: https://example.com
687assertions:
688 headers:
689 '{{header_name}}': fixed
690 json:
691 '$.{{field}}': 1
692 $.obj:
693 '{{key}}': 2
694",
695 )
696 .unwrap();
697 let environment = environment(
698 &[("header_name", "X-Tenant"), ("field", "id"), ("key", "k")],
699 &[],
700 );
701
702 let assertions = environment.apply(&request).unwrap().assertions.unwrap();
703
704 assert!(assertions.headers.contains_key("{{header_name}}"));
705 assert!(assertions.json.contains_key("$.{{field}}"));
706 assert_eq!(assertions.json["$.obj"], serde_json::json!({"{{key}}": 2}));
707 }
708
709 #[test]
710 fn a_missing_variable_in_an_assertion_fails_the_request_like_any_other() {
711 let request = Request::from_yaml_str(
715 "method: GET\nurl: https://example.com\nassertions:\n body_contains: '{{nope}}'\n",
716 )
717 .unwrap();
718
719 let err = environment(&[("tenant", "acme")], &[])
720 .apply(&request)
721 .expect_err("`nope` is not defined");
722
723 assert!(
724 matches!(err, SendraError::VariableNotFound { .. }),
725 "got {err:?}"
726 );
727 }
728
729 #[test]
730 fn a_request_with_no_placeholders_is_unchanged() {
731 let request =
734 Request::from_yaml_str("method: GET\nurl: https://example.com/a\nbody: 'plain'\n")
735 .unwrap();
736 let applied = Environment::default().apply(&request).unwrap();
737 assert_eq!(applied, request);
738 }
739
740 #[test]
741 fn substitution_works_inside_a_collection() {
742 let yaml = "\
743name: Example API
744requests:
745 - name: List users
746 method: GET
747 url: '{{base_url}}/users'
748 - name: Create user
749 method: POST
750 url: '{{base_url}}/users'
751 headers:
752 Authorization: 'Bearer {{api_key}}'
753 body: '{\"name\": \"ada\"}'
754";
755 let document = Document::from_yaml_str(yaml).unwrap();
756 let environment = environment(
757 &[("base_url", "https://staging.example.com")],
758 &[("API_KEY", "s3cret")],
759 );
760 let environment = Environment {
762 variables: {
763 let mut variables = environment.variables.clone();
764 variables.insert("api_key".to_string(), "${API_KEY}".to_string());
765 variables
766 },
767 ..environment
768 };
769
770 let Document::Collection(applied) = environment.apply_document(&document).unwrap() else {
771 panic!("a collection must stay a collection");
772 };
773
774 assert_eq!(applied.name.as_deref(), Some("Example API"));
776 assert_eq!(applied.names(), vec!["List users", "Create user"]);
777 assert_eq!(applied.requests[0].url, "https://staging.example.com/users");
778 assert_eq!(applied.requests[1].url, "https://staging.example.com/users");
779 assert_eq!(
780 applied.requests[1].header("Authorization"),
781 Some("Bearer s3cret")
782 );
783 assert_eq!(
785 applied.requests[1].body.as_deref(),
786 Some("{\"name\": \"ada\"}")
787 );
788 }
789
790 #[test]
791 fn applying_to_a_whole_document_is_all_or_nothing() {
792 let yaml = "\
798requests:
799 - name: Fine
800 method: GET
801 url: '{{base_url}}/a'
802 - name: Broken
803 method: GET
804 url: '{{missing}}/b'
805";
806 let document = Document::from_yaml_str(yaml).unwrap();
807 let environment = environment(&[("base_url", "https://example.com")], &[]);
808
809 let err = environment
810 .apply_document(&document)
811 .expect_err("the second request references nothing");
812 assert!(
813 matches!(&err, SendraError::VariableNotFound { name, .. } if name == "missing"),
814 "got {err:?}"
815 );
816 }
817
818 #[test]
819 fn a_value_is_not_rescanned_for_placeholders() {
820 let request = Request::from_yaml_str("method: GET\nurl: '{{a}}'\n").unwrap();
823 let environment = environment(&[("a", "literal-{{b}}"), ("b", "never-used")], &[]);
824
825 let applied = environment.apply(&request).unwrap();
826 assert_eq!(applied.url, "literal-{{b}}");
827 }
828
829 #[test]
830 fn whitespace_inside_a_placeholder_is_ignored() {
831 let request = Request::from_yaml_str("method: GET\nurl: '{{ base_url }}/x'\n").unwrap();
832 let environment = environment(&[("base_url", "https://example.com")], &[]);
833 assert_eq!(
834 environment.apply(&request).unwrap().url,
835 "https://example.com/x"
836 );
837 }
838
839 #[test]
840 fn text_that_only_looks_like_a_placeholder_is_left_alone() {
841 for url in ["https://example.com/{{unclosed", "https://example.com/{{}}"] {
845 let request = Request::from_yaml_str(&format!("method: GET\nurl: '{url}'\n")).unwrap();
846 let applied = Environment::default()
847 .apply(&request)
848 .unwrap_or_else(|e| panic!("{url} should not error: {e}"));
849 assert_eq!(applied.url, url);
850 }
851 }
852
853 #[test]
854 fn two_header_names_resolving_to_the_same_name_after_substitution_keeps_both() {
855 let yaml = "\
862method: GET
863url: https://example.com
864headers:
865 '{{name}}': from-template
866 X-Key: from-literal
867";
868 let request = Request::from_yaml_str(yaml).unwrap();
869 let environment = environment(&[("name", "X-Key")], &[]);
870
871 let applied = environment
872 .apply(&request)
873 .expect("a post-substitution collision is legal, not an error");
874 assert_eq!(
875 applied.headers,
876 vec![
877 ("X-Key".to_string(), "from-template".to_string()),
878 ("X-Key".to_string(), "from-literal".to_string()),
879 ]
880 );
881 }
882
883 #[test]
884 fn the_capture_block_is_carried_through_substitution_untouched() {
885 let request = Request::from_yaml_str(
889 "method: GET
890url: '{{base_url}}'
891capture:
892 token: '$.{{field}}'
893",
894 )
895 .unwrap();
896 let environment = environment(&[("base_url", "https://example.com")], &[]);
897
898 let applied = environment
899 .apply(&request)
900 .expect("`{{field}}` is inside the capture block, which is not substituted");
901 assert_eq!(
902 applied.capture, request.capture,
903 "the block goes through verbatim"
904 );
905 assert_eq!(
906 applied.capture.as_ref().unwrap().entries()["token"],
907 crate::CaptureSource::JsonPath("$.{{field}}".to_string())
908 );
909 }
910
911 #[test]
914 fn environment_level_auth_applies_when_the_request_sets_none_of_its_own() {
915 let environment = Environment::from_yaml_str(
916 "base_url: https://example.com\nauth:\n bearer: env-token\n",
917 )
918 .unwrap();
919 let request = Request::from_yaml_str("method: GET\nurl: '{{base_url}}'\n").unwrap();
920
921 let applied = environment.apply(&request).expect("resolves");
922 let auth = applied.auth.expect("the environment default was filled in");
923 assert_eq!(auth.bearer.as_deref(), Some("env-token"));
924 }
925
926 #[test]
927 fn a_requests_own_auth_fully_replaces_the_environments_default_not_merges() {
928 let environment = Environment::from_yaml_str("auth:\n bearer: env-token\n").unwrap();
929 let request = Request::from_yaml_str(
930 "method: GET\nurl: https://example.com\nauth:\n basic:\n user: a\n pass: b\n",
931 )
932 .unwrap();
933
934 let applied = environment.apply(&request).expect("resolves");
935 let auth = applied.auth.expect("the request's own auth survives");
936 assert!(auth.bearer.is_none());
939 assert_eq!(auth.basic.map(|basic| basic.user), Some("a".to_string()));
940 }
941
942 #[test]
943 fn environment_level_auth_substitutes_against_its_own_environments_variables() {
944 let environment =
945 Environment::from_yaml_str("token: s3cret\nauth:\n bearer: '{{token}}'\n").unwrap();
946 let request = Request::from_yaml_str("method: GET\nurl: https://example.com\n").unwrap();
947
948 let applied = environment.apply(&request).expect("`token` resolves");
949 assert_eq!(
950 applied.auth.and_then(|auth| auth.bearer),
951 Some("s3cret".to_string())
952 );
953 }
954
955 #[test]
956 fn environment_level_auth_header_colliding_with_an_explicit_header_is_rejected() {
957 let environment = Environment::from_yaml_str("auth:\n bearer: env-token\n").unwrap();
958 let request = Request::from_yaml_str(
959 "method: GET\nurl: https://example.com\nheaders:\n Authorization: hand-written\n",
960 )
961 .unwrap();
962
963 let err = environment
964 .apply(&request)
965 .expect_err("the environment default would collide with the explicit header");
966 assert!(
967 matches!(&err, SendraError::InvalidRequest { reason } if reason.contains("Authorization")),
968 "got {err:?}"
969 );
970 }
971
972 #[test]
973 fn environment_level_api_key_in_query_form_resolves_end_to_end() {
974 let environment = Environment::from_yaml_str(
975 "auth:\n api_key:\n in: query\n name: api_key\n value: s3cret\n",
976 )
977 .unwrap();
978 let request =
979 Request::from_yaml_str("method: GET\nurl: https://example.com/search\n").unwrap();
980
981 let resolved = environment
982 .apply(&request)
983 .and_then(|request| request.resolve_auth())
984 .and_then(|request| request.resolve_query())
985 .expect("resolves end to end");
986 assert_eq!(resolved.url, "https://example.com/search?api_key=s3cret");
987 }
988
989 #[test]
990 fn a_malformed_environment_level_auth_block_is_a_typed_error_at_parse_time() {
991 let err =
992 Environment::from_yaml_str("auth:\n bearer: x\n basic:\n user: a\n pass: b\n")
993 .expect_err("bearer and basic together must be rejected");
994 assert!(
995 matches!(&err, SendraError::InvalidEnvironment { reason, .. } if reason.contains("bearer") && reason.contains("basic")),
996 "got {err:?}"
997 );
998 }
999}