1use crate::configuration_overrides_types::JSONOverridesRecord;
8use crate::environment_matching::matches_user_environment;
9use crate::{
10 error::Error, JSONDefaultEnginesRecord, JSONEngineBase, JSONEngineMethod, JSONEngineRecord,
11 JSONEngineUrl, JSONEngineUrls, JSONEngineVariant, JSONSearchConfigurationRecords,
12 RefinedSearchConfig, SearchEngineDefinition, SearchEngineUrl, SearchEngineUrls,
13 SearchUserEnvironment,
14};
15use crate::{sort_helpers, JSONAvailableLocalesRecord, JSONEngineOrdersRecord};
16use remote_settings::RemoteSettingsRecord;
17use std::collections::HashSet;
18
19impl Default for SearchEngineUrl {
20 fn default() -> Self {
21 Self {
22 base: Default::default(),
23 method: JSONEngineMethod::default().as_str().to_string(),
24 params: Default::default(),
25 search_term_param_name: Default::default(),
26 display_name: Default::default(),
27 is_new_until: Default::default(),
28 exclude_partner_code_from_telemetry: Default::default(),
29 accepted_content_types: Default::default(),
30 }
31 }
32}
33
34impl SearchEngineUrl {
35 fn merge(&mut self, user_environment: &SearchUserEnvironment, preferred: &JSONEngineUrl) {
36 if let Some(base) = &preferred.base {
37 self.base = base.clone();
38 }
39 if let Some(method) = &preferred.method {
40 self.method = method.as_str().to_string();
41 }
42 if let Some(params) = &preferred.params {
43 self.params = params.clone();
44 }
45 if let Some(search_term_param_name) = &preferred.search_term_param_name {
46 self.search_term_param_name = Some(search_term_param_name.clone());
47 }
48 if let Some(display_name_map) = &preferred.display_name_map {
49 self.display_name = display_name_map
50 .get(&user_environment.locale)
51 .or_else(|| display_name_map.get("default"))
52 .cloned();
53 }
54 if let Some(is_new_until) = &preferred.is_new_until {
55 self.is_new_until = Some(is_new_until.clone());
56 }
57 if let Some(accepted_content_types) = &preferred.accepted_content_types {
58 self.accepted_content_types = Some(accepted_content_types.clone());
59 }
60 self.exclude_partner_code_from_telemetry = preferred.exclude_partner_code_from_telemetry;
61 }
62}
63
64impl SearchEngineUrls {
65 fn merge(&mut self, user_environment: &SearchUserEnvironment, preferred: &JSONEngineUrls) {
66 if let Some(search_url) = &preferred.search {
67 self.search.merge(user_environment, search_url);
68 }
69 if let Some(suggestions) = &preferred.suggestions {
70 self.suggestions
71 .get_or_insert_with(Default::default)
72 .merge(user_environment, suggestions);
73 }
74 if let Some(trending) = &preferred.trending {
75 self.trending
76 .get_or_insert_with(Default::default)
77 .merge(user_environment, trending);
78 }
79 if let Some(search_form) = &preferred.search_form {
80 self.search_form
81 .get_or_insert_with(Default::default)
82 .merge(user_environment, search_form);
83 }
84 if let Some(visual_search) = &preferred.visual_search {
85 self.visual_search
86 .get_or_insert_with(Default::default)
87 .merge(user_environment, visual_search);
88 }
89 }
90}
91
92impl SearchEngineDefinition {
93 fn merge_variant(
94 &mut self,
95 user_environment: &SearchUserEnvironment,
96 variant: &JSONEngineVariant,
97 ) {
98 if !self.optional {
99 self.optional = variant.optional;
100 }
101 if let Some(partner_code) = &variant.partner_code {
102 self.partner_code = partner_code.clone();
103 }
104 if let Some(telemetry_suffix) = &variant.telemetry_suffix {
105 self.telemetry_suffix = telemetry_suffix.clone();
106 }
107 if let Some(urls) = &variant.urls {
108 self.urls.merge(user_environment, urls);
109 }
110 if let Some(is_new_until) = &variant.is_new_until {
111 self.is_new_until = Some(is_new_until.clone());
112 }
113 }
114
115 fn merge_override(
116 &mut self,
117 user_environment: &SearchUserEnvironment,
118 override_record: &JSONOverridesRecord,
119 ) {
120 self.partner_code = override_record.partner_code.clone();
121 self.urls.merge(user_environment, &override_record.urls);
122 self.click_url = Some(override_record.click_url.clone());
123
124 if let Some(telemetry_suffix) = &override_record.telemetry_suffix {
125 self.telemetry_suffix = telemetry_suffix.clone();
126 }
127 }
128
129 pub(crate) fn from_configuration_details(
130 user_environment: &SearchUserEnvironment,
131 identifier: &str,
132 base: JSONEngineBase,
133 variant: &JSONEngineVariant,
134 sub_variant: &Option<JSONEngineVariant>,
135 ) -> SearchEngineDefinition {
136 let mut engine_definition = SearchEngineDefinition {
137 aliases: base.aliases.unwrap_or_default(),
138 charset: base.charset.unwrap_or_else(|| "UTF-8".to_string()),
139 classification: base.classification,
140 identifier: identifier.to_string(),
141 name: base.name,
142 optional: variant.optional,
143 order_hint: None,
144 partner_code: base.partner_code.unwrap_or_default(),
145 telemetry_suffix: String::new(),
146 urls: SearchEngineUrls::default(),
147 click_url: None,
148 is_new_until: None,
149 };
150
151 engine_definition.urls.merge(user_environment, &base.urls);
152 engine_definition.merge_variant(user_environment, variant);
153 if let Some(sub_variant) = sub_variant {
154 engine_definition.merge_variant(user_environment, sub_variant);
155 }
156
157 engine_definition
158 }
159}
160
161pub(crate) struct FilterRecordsResult {
162 engines: Vec<SearchEngineDefinition>,
163 default_engines_record: Option<JSONDefaultEnginesRecord>,
164 engine_orders_record: Option<JSONEngineOrdersRecord>,
165}
166
167pub(crate) trait Filter {
168 fn filter_records(
169 &self,
170 user_environment: &mut SearchUserEnvironment,
171 overrides: Option<Vec<JSONOverridesRecord>>,
172 ) -> Result<FilterRecordsResult, Error>;
173}
174
175fn apply_overrides(
176 user_environment: &SearchUserEnvironment,
177 engines: &mut [SearchEngineDefinition],
178 overrides: &[JSONOverridesRecord],
179) {
180 for override_record in overrides {
181 for engine in engines.iter_mut() {
182 if engine.identifier == override_record.identifier {
183 engine.merge_override(user_environment, override_record);
184 }
185 }
186 }
187}
188
189fn negotiate_languages(user_environment: &mut SearchUserEnvironment, available_locales: &[String]) {
190 let user_locale = user_environment.locale.to_lowercase();
191
192 let available_locales_set: HashSet<String> = available_locales
193 .iter()
194 .map(|locale| locale.to_lowercase())
195 .collect();
196
197 if available_locales_set.contains(&user_locale) {
198 return;
199 }
200 if user_locale.starts_with("en-") {
201 user_environment.locale = "en-us".to_string();
202 return;
203 }
204 if let Some(index) = user_locale.find('-') {
205 let base_locale = &user_locale[..index];
206 if available_locales_set.contains(base_locale) {
207 user_environment.locale = base_locale.to_string();
208 }
209 }
210}
211
212impl Filter for Vec<RemoteSettingsRecord> {
213 fn filter_records(
214 &self,
215 user_environment: &mut SearchUserEnvironment,
216 overrides: Option<Vec<JSONOverridesRecord>>,
217 ) -> Result<FilterRecordsResult, Error> {
218 let mut available_locales = Vec::new();
219 for record in self {
220 if let Some(val) = record.fields.get("recordType") {
221 if *val == "availableLocales" {
222 let stringified = serde_json::to_string(&record.fields)?;
223 let locales_record: Option<JSONAvailableLocalesRecord> =
224 serde_json::from_str(&stringified)?;
225 available_locales = locales_record.unwrap().locales;
226 }
227 }
228 }
229 negotiate_languages(user_environment, &available_locales);
230
231 let mut engines = Vec::new();
232 let mut default_engines_record = None;
233 let mut engine_orders_record = None;
234
235 for record in self {
236 let stringified = serde_json::to_string(&record.fields)?;
239 match record.fields.get("recordType") {
240 Some(val) if *val == "engine" => {
241 let engine_config: Option<JSONEngineRecord> =
242 serde_json::from_str(&stringified)?;
243 if let Some(engine_config) = engine_config {
244 let result =
245 maybe_extract_engine_config(user_environment, Box::new(engine_config));
246 engines.extend(result);
247 }
248 }
249 Some(val) if *val == "defaultEngines" => {
250 default_engines_record = serde_json::from_str(&stringified)?;
251 }
252 Some(val) if *val == "engineOrders" => {
253 engine_orders_record = serde_json::from_str(&stringified)?;
254 }
255 Some(val) if *val == "availableLocales" => {
256 }
258 Some(_val) => {}
261 None => {}
262 }
263 }
264
265 if let Some(overrides_data) = &overrides {
266 apply_overrides(user_environment, &mut engines, overrides_data);
267 }
268
269 Ok(FilterRecordsResult {
270 engines,
271 default_engines_record,
272 engine_orders_record,
273 })
274 }
275}
276
277impl Filter for Vec<JSONSearchConfigurationRecords> {
278 fn filter_records(
279 &self,
280 user_environment: &mut SearchUserEnvironment,
281 overrides: Option<Vec<JSONOverridesRecord>>,
282 ) -> Result<FilterRecordsResult, Error> {
283 let mut available_locales = Vec::new();
284 for record in self {
285 if let JSONSearchConfigurationRecords::AvailableLocales(locales_record) = record {
286 available_locales = locales_record.locales.clone();
287 }
288 }
289 negotiate_languages(user_environment, &available_locales);
290
291 let mut engines = Vec::new();
292 let mut default_engines_record = None;
293 let mut engine_orders_record = None;
294
295 for record in self {
296 match record {
297 JSONSearchConfigurationRecords::Engine(engine) => {
298 let result = maybe_extract_engine_config(user_environment, engine.clone());
299 engines.extend(result);
300 }
301 JSONSearchConfigurationRecords::DefaultEngines(default_engines) => {
302 default_engines_record = Some(default_engines);
303 }
304 JSONSearchConfigurationRecords::EngineOrders(engine_orders) => {
305 engine_orders_record = Some(engine_orders)
306 }
307 JSONSearchConfigurationRecords::AvailableLocales(_) => {
308 }
310 JSONSearchConfigurationRecords::Unknown => {
311 }
313 }
314 }
315
316 if let Some(overrides_data) = &overrides {
317 apply_overrides(user_environment, &mut engines, overrides_data);
318 }
319
320 Ok(FilterRecordsResult {
321 engines,
322 default_engines_record: default_engines_record.cloned(),
323 engine_orders_record: engine_orders_record.cloned(),
324 })
325 }
326}
327
328pub(crate) fn filter_engine_configuration_impl(
329 user_environment: SearchUserEnvironment,
330 configuration: &impl Filter,
331 overrides: Option<Vec<JSONOverridesRecord>>,
332) -> Result<RefinedSearchConfig, Error> {
333 let mut user_environment = user_environment.clone();
334 user_environment.locale = user_environment.locale.to_lowercase();
335 user_environment.region = user_environment.region.to_lowercase();
336 user_environment.version = user_environment.version.to_lowercase();
337
338 let filtered_result = configuration.filter_records(&mut user_environment, overrides);
339
340 filtered_result.map(|result| {
341 let (default_engine_id, default_private_engine_id) = determine_default_engines(
342 &result.engines,
343 result.default_engines_record,
344 &user_environment,
345 );
346
347 let mut engines = result.engines.clone();
348
349 if let Some(orders_record) = result.engine_orders_record {
350 for order_data in &orders_record.orders {
351 if matches_user_environment(&order_data.environment, &user_environment) {
352 sort_helpers::set_engine_order(&mut engines, &order_data.order);
353 }
354 }
355 }
356
357 engines.sort_by(|a, b| {
358 sort_helpers::sort(
359 default_engine_id.as_ref(),
360 default_private_engine_id.as_ref(),
361 a,
362 b,
363 )
364 });
365
366 RefinedSearchConfig {
367 engines,
368 app_default_engine_id: default_engine_id,
369 app_private_default_engine_id: default_private_engine_id,
370 }
371 })
372}
373
374fn maybe_extract_engine_config(
375 user_environment: &SearchUserEnvironment,
376 record: Box<JSONEngineRecord>,
377) -> Option<SearchEngineDefinition> {
378 let JSONEngineRecord {
379 identifier,
380 variants,
381 base,
382 } = *record;
383 let matching_variant = variants
384 .into_iter()
385 .rev()
386 .find(|r| matches_user_environment(&r.environment, user_environment));
387
388 let mut matching_sub_variant = None;
389 if let Some(variant) = &matching_variant {
390 matching_sub_variant = variant
391 .sub_variants
392 .iter()
393 .rev()
394 .find(|r| matches_user_environment(&r.environment, user_environment))
395 .cloned();
396 }
397
398 matching_variant.map(|variant| {
399 SearchEngineDefinition::from_configuration_details(
400 user_environment,
401 &identifier,
402 base,
403 &variant,
404 &matching_sub_variant,
405 )
406 })
407}
408
409fn determine_default_engines(
410 engines: &[SearchEngineDefinition],
411 default_engines_record: Option<JSONDefaultEnginesRecord>,
412 user_environment: &SearchUserEnvironment,
413) -> (Option<String>, Option<String>) {
414 match default_engines_record {
415 None => (None, None),
416 Some(record) => {
417 let mut default_engine_id = None;
418 let mut default_engine_private_id = None;
419
420 let specific_default = record
421 .specific_defaults
422 .into_iter()
423 .rev()
424 .find(|r| matches_user_environment(&r.environment, user_environment));
425
426 if let Some(specific_default) = specific_default {
427 if let Some(engine_id) =
430 find_engine_id_with_match(engines, specific_default.default)
431 {
432 default_engine_id.replace(engine_id);
433 }
434 if let Some(private_engine_id) =
435 find_engine_id_with_match(engines, specific_default.default_private)
436 {
437 default_engine_private_id.replace(private_engine_id);
438 }
439 }
440
441 (
442 default_engine_id.or_else(|| find_engine_id(engines, record.global_default)),
451 default_engine_private_id
452 .or_else(|| find_engine_id(engines, record.global_default_private)),
453 )
454 }
455 }
456}
457
458fn find_engine_id(engines: &[SearchEngineDefinition], engine_id: String) -> Option<String> {
459 if engine_id.is_empty() {
460 return None;
461 }
462 match engines.iter().any(|e| e.identifier == engine_id) {
463 true => Some(engine_id.clone()),
464 false => None,
465 }
466}
467
468fn find_engine_id_with_match(
469 engines: &[SearchEngineDefinition],
470 engine_id_match: String,
471) -> Option<String> {
472 if engine_id_match.is_empty() {
473 return None;
474 }
475 if let Some(match_no_star) = engine_id_match.strip_suffix('*') {
476 return engines
477 .iter()
478 .find(|e| e.identifier.starts_with(match_no_star))
479 .map(|e| e.identifier.clone());
480 }
481
482 engines
483 .iter()
484 .find(|e| e.identifier == engine_id_match)
485 .map(|e| e.identifier.clone())
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use crate::*;
492 use once_cell::sync::Lazy;
493 use std::{collections::HashMap, vec};
494
495 #[test]
496 fn test_default_search_engine_url() {
497 assert_eq!(
498 SearchEngineUrl::default(),
499 SearchEngineUrl {
500 base: "".to_string(),
501 method: "GET".to_string(),
502 params: Vec::new(),
503 search_term_param_name: None,
504 display_name: None,
505 is_new_until: None,
506 exclude_partner_code_from_telemetry: false,
507 accepted_content_types: None,
508 },
509 );
510 }
511
512 #[test]
513 fn test_default_search_engine_urls() {
514 assert_eq!(
515 SearchEngineUrls::default(),
516 SearchEngineUrls {
517 search: SearchEngineUrl::default(),
518 suggestions: None,
519 trending: None,
520 search_form: None,
521 visual_search: None,
522 },
523 );
524 }
525
526 #[test]
527 fn test_merge_override() {
528 let mut test_engine = SearchEngineDefinition {
529 identifier: "test".to_string(),
530 partner_code: "partner-code".to_string(),
531 telemetry_suffix: "original-telemetry-suffix".to_string(),
532 ..Default::default()
533 };
534
535 let override_record = JSONOverridesRecord {
536 identifier: "test".to_string(),
537 partner_code: "override-partner-code".to_string(),
538 click_url: "https://example.com/click-url".to_string(),
539 telemetry_suffix: None,
540 urls: JSONEngineUrls {
541 search: Some(JSONEngineUrl {
542 base: Some("https://example.com/override-search".to_string()),
543 ..Default::default()
544 }),
545 ..Default::default()
546 },
547 };
548
549 test_engine.merge_override(
550 &SearchUserEnvironment {
551 locale: "fi".into(),
552 ..Default::default()
553 },
554 &override_record,
555 );
556
557 assert_eq!(
558 test_engine.partner_code, "override-partner-code",
559 "Should override the partner code"
560 );
561 assert_eq!(
562 test_engine.click_url,
563 Some("https://example.com/click-url".to_string()),
564 "Should override the click url"
565 );
566 assert_eq!(
567 test_engine.urls.search.base, "https://example.com/override-search",
568 "Should override search url"
569 );
570 assert_eq!(
571 test_engine.telemetry_suffix, "original-telemetry-suffix",
572 "Should not override telemetry suffix when telemetry suffix is supplied as None"
573 );
574 }
575
576 #[test]
577 fn test_merge_override_locale_match() {
578 let mut test_engine = SearchEngineDefinition {
579 identifier: "test".to_string(),
580 partner_code: "partner-code".to_string(),
581 telemetry_suffix: "original-telemetry-suffix".to_string(),
582 ..Default::default()
583 };
584
585 let override_record = JSONOverridesRecord {
586 identifier: "test".to_string(),
587 partner_code: "override-partner-code".to_string(),
588 click_url: "https://example.com/click-url".to_string(),
589 telemetry_suffix: None,
590 urls: JSONEngineUrls {
591 search: Some(JSONEngineUrl {
592 base: Some("https://example.com/override-search".to_string()),
593 display_name_map: Some(HashMap::from([
594 ("default".to_string(), "My Display Name".to_string()),
596 ("en-GB".to_string(), "en-GB Display Name".to_string()),
598 ])),
599 ..Default::default()
600 }),
601 ..Default::default()
602 },
603 };
604
605 test_engine.merge_override(
606 &SearchUserEnvironment {
607 locale: "en-GB".into(),
609 ..Default::default()
610 },
611 &override_record,
612 );
613
614 assert_eq!(
615 test_engine.urls.search.display_name,
616 Some("en-GB Display Name".to_string()),
617 "Should override display name with en-GB version"
618 );
619 }
620
621 static ENGINES_LIST: Lazy<Vec<SearchEngineDefinition>> = Lazy::new(|| {
622 vec![
623 SearchEngineDefinition {
624 identifier: "engine1".to_string(),
625 name: "Test".to_string(),
626 urls: SearchEngineUrls {
627 search: SearchEngineUrl {
628 base: "https://example.com".to_string(),
629 ..Default::default()
630 },
631 ..Default::default()
632 },
633 ..Default::default()
634 },
635 SearchEngineDefinition {
636 identifier: "engine2".to_string(),
637 name: "Test 2".to_string(),
638 urls: SearchEngineUrls {
639 search: SearchEngineUrl {
640 base: "https://example.com/2".to_string(),
641 ..Default::default()
642 },
643 ..Default::default()
644 },
645 ..Default::default()
646 },
647 SearchEngineDefinition {
648 identifier: "engine3".to_string(),
649 name: "Test 3".to_string(),
650 urls: SearchEngineUrls {
651 search: SearchEngineUrl {
652 base: "https://example.com/3".to_string(),
653 ..Default::default()
654 },
655 ..Default::default()
656 },
657 ..Default::default()
658 },
659 SearchEngineDefinition {
660 identifier: "engine4wildcardmatch".to_string(),
661 name: "Test 4".to_string(),
662 urls: SearchEngineUrls {
663 search: SearchEngineUrl {
664 base: "https://example.com/4".to_string(),
665 ..Default::default()
666 },
667 ..Default::default()
668 },
669 ..Default::default()
670 },
671 ]
672 });
673
674 #[test]
675 fn test_determine_default_engines_returns_global_default() {
676 let (default_engine_id, default_engine_private_id) = determine_default_engines(
677 &ENGINES_LIST,
678 Some(JSONDefaultEnginesRecord {
679 global_default: "engine2".to_string(),
680 global_default_private: String::new(),
681 specific_defaults: Vec::new(),
682 }),
683 &SearchUserEnvironment {
684 locale: "fi".into(),
685 ..Default::default()
686 },
687 );
688
689 assert_eq!(
690 default_engine_id.unwrap(),
691 "engine2",
692 "Should have returned the global default engine"
693 );
694 assert!(
695 default_engine_private_id.is_none(),
696 "Should not have returned an id for the private engine"
697 );
698
699 let (default_engine_id, default_engine_private_id) = determine_default_engines(
700 &ENGINES_LIST,
701 Some(JSONDefaultEnginesRecord {
702 global_default: "engine2".to_string(),
703 global_default_private: String::new(),
704 specific_defaults: vec![JSONSpecificDefaultRecord {
705 default: "engine1".to_string(),
706 default_private: String::new(),
707 environment: JSONVariantEnvironment {
708 locales: vec!["en-GB".to_string()],
709 ..Default::default()
710 },
711 }],
712 }),
713 &SearchUserEnvironment {
714 locale: "fi".into(),
715 ..Default::default()
716 },
717 );
718
719 assert_eq!(
720 default_engine_id.unwrap(),
721 "engine2",
722 "Should have returned the global default engine when no specific defaults environments match"
723 );
724 assert!(
725 default_engine_private_id.is_none(),
726 "Should not have returned an id for the private engine"
727 );
728
729 let (default_engine_id, default_engine_private_id) = determine_default_engines(
730 &ENGINES_LIST,
731 Some(JSONDefaultEnginesRecord {
732 global_default: "engine2".to_string(),
733 global_default_private: String::new(),
734 specific_defaults: vec![JSONSpecificDefaultRecord {
735 default: "engine1".to_string(),
736 default_private: String::new(),
737 environment: JSONVariantEnvironment {
738 locales: vec!["fi".to_string()],
739 ..Default::default()
740 },
741 }],
742 }),
743 &SearchUserEnvironment {
744 locale: "fi".into(),
745 ..Default::default()
746 },
747 );
748
749 assert_eq!(
750 default_engine_id.unwrap(),
751 "engine1",
752 "Should have returned the specific default when environments match"
753 );
754 assert!(
755 default_engine_private_id.is_none(),
756 "Should not have returned an id for the private engine"
757 );
758
759 let (default_engine_id, default_engine_private_id) = determine_default_engines(
760 &ENGINES_LIST,
761 Some(JSONDefaultEnginesRecord {
762 global_default: "engine2".to_string(),
763 global_default_private: String::new(),
764 specific_defaults: vec![JSONSpecificDefaultRecord {
765 default: "engine4*".to_string(),
766 default_private: String::new(),
767 environment: JSONVariantEnvironment {
768 locales: vec!["fi".to_string()],
769 ..Default::default()
770 },
771 }],
772 }),
773 &SearchUserEnvironment {
774 locale: "fi".into(),
775 ..Default::default()
776 },
777 );
778
779 assert_eq!(
780 default_engine_id.unwrap(),
781 "engine4wildcardmatch",
782 "Should have returned the specific default when using a wildcard match"
783 );
784 assert!(
785 default_engine_private_id.is_none(),
786 "Should not have returned an id for the private engine"
787 );
788
789 let (default_engine_id, default_engine_private_id) = determine_default_engines(
790 &ENGINES_LIST,
791 Some(JSONDefaultEnginesRecord {
792 global_default: "engine2".to_string(),
793 global_default_private: String::new(),
794 specific_defaults: vec![
795 JSONSpecificDefaultRecord {
796 default: "engine4*".to_string(),
797 default_private: String::new(),
798 environment: JSONVariantEnvironment {
799 locales: vec!["fi".to_string()],
800 ..Default::default()
801 },
802 },
803 JSONSpecificDefaultRecord {
804 default: "engine3".to_string(),
805 default_private: String::new(),
806 environment: JSONVariantEnvironment {
807 locales: vec!["fi".to_string()],
808 ..Default::default()
809 },
810 },
811 ],
812 }),
813 &SearchUserEnvironment {
814 locale: "fi".into(),
815 ..Default::default()
816 },
817 );
818
819 assert_eq!(
820 default_engine_id.unwrap(),
821 "engine3",
822 "Should have returned the last specific default when multiple environments match"
823 );
824 assert!(
825 default_engine_private_id.is_none(),
826 "Should not have returned an id for the private engine"
827 );
828 }
829
830 #[test]
831 fn test_determine_default_engines_returns_global_default_private() {
832 let (default_engine_id, default_engine_private_id) = determine_default_engines(
833 &ENGINES_LIST,
834 Some(JSONDefaultEnginesRecord {
835 global_default: "engine2".to_string(),
836 global_default_private: "engine3".to_string(),
837 specific_defaults: Vec::new(),
838 }),
839 &SearchUserEnvironment {
840 ..Default::default()
841 },
842 );
843
844 assert_eq!(
845 default_engine_id.unwrap(),
846 "engine2",
847 "Should have returned the global default engine"
848 );
849 assert_eq!(
850 default_engine_private_id.unwrap(),
851 "engine3",
852 "Should have returned the global default engine for private mode"
853 );
854
855 let (default_engine_id, default_engine_private_id) = determine_default_engines(
856 &ENGINES_LIST,
857 Some(JSONDefaultEnginesRecord {
858 global_default: "engine2".to_string(),
859 global_default_private: "engine3".to_string(),
860 specific_defaults: vec![JSONSpecificDefaultRecord {
861 default: String::new(),
862 default_private: "engine1".to_string(),
863 environment: JSONVariantEnvironment {
864 locales: vec!["en-GB".to_string()],
865 ..Default::default()
866 },
867 }],
868 }),
869 &SearchUserEnvironment {
870 locale: "fi".into(),
871 ..Default::default()
872 },
873 );
874
875 assert_eq!(
876 default_engine_id.unwrap(),
877 "engine2",
878 "Should have returned the global default engine when no specific defaults environments match"
879 );
880 assert_eq!(
881 default_engine_private_id.unwrap(),
882 "engine3",
883 "Should have returned the global default engine for private mode when no specific defaults environments match"
884 );
885
886 let (default_engine_id, default_engine_private_id) = determine_default_engines(
887 &ENGINES_LIST,
888 Some(JSONDefaultEnginesRecord {
889 global_default: "engine2".to_string(),
890 global_default_private: "engine3".to_string(),
891 specific_defaults: vec![JSONSpecificDefaultRecord {
892 default: String::new(),
893 default_private: "engine1".to_string(),
894 environment: JSONVariantEnvironment {
895 locales: vec!["fi".to_string()],
896 ..Default::default()
897 },
898 }],
899 }),
900 &SearchUserEnvironment {
901 locale: "fi".into(),
902 ..Default::default()
903 },
904 );
905
906 assert_eq!(
907 default_engine_id.unwrap(),
908 "engine2",
909 "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)."
910 );
911 assert_eq!(
912 default_engine_private_id.unwrap(),
913 "engine1",
914 "Should have returned the specific default engine for private mode when environments match"
915 );
916
917 let (default_engine_id, default_engine_private_id) = determine_default_engines(
918 &ENGINES_LIST,
919 Some(JSONDefaultEnginesRecord {
920 global_default: "engine2".to_string(),
921 global_default_private: String::new(),
922 specific_defaults: vec![JSONSpecificDefaultRecord {
923 default: String::new(),
924 default_private: "engine4*".to_string(),
925 environment: JSONVariantEnvironment {
926 locales: vec!["fi".to_string()],
927 ..Default::default()
928 },
929 }],
930 }),
931 &SearchUserEnvironment {
932 locale: "fi".into(),
933 ..Default::default()
934 },
935 );
936
937 assert_eq!(
938 default_engine_id.unwrap(),
939 "engine2",
940 "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)"
941 );
942 assert_eq!(
943 default_engine_private_id.unwrap(),
944 "engine4wildcardmatch",
945 "Should have returned the specific default for private mode when using a wildcard match"
946 );
947 }
948
949 #[test]
950 fn test_locale_matched_exactly() {
951 let mut user_env = SearchUserEnvironment {
952 locale: "en-CA".into(),
953 ..Default::default()
954 };
955 negotiate_languages(&mut user_env, &["en-CA".to_string(), "fr".to_string()]);
956 assert_eq!(
957 user_env.locale, "en-CA",
958 "Should return user locale unchanged if in available locales"
959 );
960 }
961
962 #[test]
963 fn test_locale_fallback_to_base_locale() {
964 let mut user_env = SearchUserEnvironment {
965 locale: "de-AT".into(),
966 ..Default::default()
967 };
968 negotiate_languages(&mut user_env, &["de".to_string()]);
969 assert_eq!(
970 user_env.locale, "de",
971 "Should fallback to base locale if base is in available locales"
972 );
973 }
974
975 static ENGLISH_LOCALES: &[&str] = &["en-AU", "en-IE", "en-RU", "en-ZA"];
976
977 #[test]
978 fn test_english_locales_fallbacks_to_en_us() {
979 for user_locale in ENGLISH_LOCALES {
980 let mut user_env = SearchUserEnvironment {
981 locale: user_locale.to_string(),
982 ..Default::default()
983 };
984 negotiate_languages(&mut user_env, &["en-US".to_string()]);
985 assert_eq!(
986 user_env.locale, "en-us",
987 "Should remap {} to en-us when en-us is available",
988 user_locale
989 );
990 }
991 }
992
993 #[test]
994 fn test_locale_unmatched() {
995 let mut user_env = SearchUserEnvironment {
996 locale: "fr-CA".into(),
997 ..Default::default()
998 };
999 negotiate_languages(&mut user_env, &["de".to_string(), "en-US".to_string()]);
1000 assert_eq!(
1001 user_env.locale, "fr-CA",
1002 "Should leave locale unchanged if no match or english locale fallback is not found"
1003 );
1004 }
1005}
1006
1007#[cfg(test)]
1008mod from_configuration_details_tests {
1009 use crate::test_helpers::{
1010 ExpectedEngineFromJSONBase, JSON_ENGINE_BASE, JSON_ENGINE_SUBVARIANT, JSON_ENGINE_VARIANT,
1011 };
1012 use crate::*;
1013 use once_cell::sync::Lazy;
1014
1015 #[test]
1016 fn test_fallsback_to_defaults() {
1017 let result = SearchEngineDefinition::from_configuration_details(
1021 &SearchUserEnvironment {
1022 locale: "fi".into(),
1023 ..Default::default()
1024 },
1025 "test",
1026 JSONEngineBase {
1027 aliases: None,
1028 charset: None,
1029 classification: SearchEngineClassification::General,
1030 name: "Test".to_string(),
1031 partner_code: None,
1032 urls: JSONEngineUrls {
1033 search: Some(JSONEngineUrl {
1034 base: Some("https://example.com".to_string()),
1035 ..Default::default()
1036 }),
1037 suggestions: None,
1038 trending: None,
1039 search_form: None,
1040 visual_search: None,
1041 },
1042 },
1043 &JSONEngineVariant {
1044 environment: JSONVariantEnvironment {
1045 all_regions_and_locales: true,
1046 ..Default::default()
1047 },
1048 is_new_until: None,
1049 optional: false,
1050 partner_code: None,
1051 telemetry_suffix: None,
1052 urls: None,
1053 sub_variants: vec![],
1054 },
1055 &None,
1056 );
1057
1058 assert_eq!(
1059 result,
1060 SearchEngineDefinition {
1061 aliases: Vec::new(),
1062 charset: "UTF-8".to_string(),
1063 classification: SearchEngineClassification::General,
1064 identifier: "test".to_string(),
1065 is_new_until: None,
1066 partner_code: String::new(),
1067 name: "Test".to_string(),
1068 optional: false,
1069 order_hint: None,
1070 telemetry_suffix: String::new(),
1071 urls: SearchEngineUrls {
1072 search: SearchEngineUrl {
1073 base: "https://example.com".to_string(),
1074 ..Default::default()
1075 },
1076 suggestions: None,
1077 trending: None,
1078 search_form: None,
1079 visual_search: None,
1080 },
1081 click_url: None
1082 }
1083 )
1084 }
1085
1086 #[test]
1087 fn test_uses_base_values_only() {
1088 let result = SearchEngineDefinition::from_configuration_details(
1089 &SearchUserEnvironment {
1090 locale: "fi".into(),
1091 ..Default::default()
1092 },
1093 "test",
1094 Lazy::force(&JSON_ENGINE_BASE).clone(),
1095 &JSONEngineVariant {
1096 environment: JSONVariantEnvironment {
1097 all_regions_and_locales: true,
1098 ..Default::default()
1099 },
1100 is_new_until: None,
1101 optional: false,
1102 partner_code: None,
1103 telemetry_suffix: None,
1104 urls: None,
1105 sub_variants: vec![],
1106 },
1107 &None,
1108 );
1109 assert_eq!(
1110 result,
1111 ExpectedEngineFromJSONBase::new("test", "Test").build()
1112 );
1113 }
1114
1115 #[test]
1116 fn test_uses_locale_specific_visual_display_name() {
1117 let result = SearchEngineDefinition::from_configuration_details(
1118 &SearchUserEnvironment {
1119 locale: "en-GB".into(),
1120 ..Default::default()
1121 },
1122 "test",
1123 Lazy::force(&JSON_ENGINE_BASE).clone(),
1124 &JSONEngineVariant {
1125 environment: JSONVariantEnvironment {
1126 all_regions_and_locales: true,
1127 ..Default::default()
1128 },
1129 is_new_until: None,
1130 optional: false,
1131 partner_code: None,
1132 telemetry_suffix: None,
1133 urls: None,
1134 sub_variants: vec![],
1135 },
1136 &None,
1137 );
1138
1139 assert_eq!(
1140 result,
1141 ExpectedEngineFromJSONBase::new("test", "Test")
1142 .visual_search_display_name("Visual Search en-GB")
1143 .build()
1144 );
1145 }
1146
1147 #[test]
1148 fn test_merges_variants() {
1149 let result = SearchEngineDefinition::from_configuration_details(
1150 &SearchUserEnvironment {
1151 locale: "fi".into(),
1152 ..Default::default()
1153 },
1154 "test",
1155 Lazy::force(&JSON_ENGINE_BASE).clone(),
1156 &JSON_ENGINE_VARIANT,
1157 &None,
1158 );
1159
1160 assert_eq!(
1161 result,
1162 ExpectedEngineFromJSONBase::new("test", "Test")
1163 .variant_is_new_until("2063-04-05")
1164 .variant_optional(true)
1165 .variant_partner_code("trek")
1166 .variant_telemetry_suffix("star")
1167 .variant_search_url(
1168 "https://example.com/variant",
1169 "GET",
1170 "variant",
1171 "test variant",
1172 "ship",
1173 )
1174 .variant_suggestions_url(
1175 "https://example.com/suggestions-variant",
1176 "GET",
1177 "suggest-variant",
1178 "sugg test variant",
1179 "variant",
1180 )
1181 .variant_trending_url(
1182 "https://example.com/trending-variant",
1183 "GET",
1184 "trend-variant",
1185 "trend test variant",
1186 "trend",
1187 true,
1188 )
1189 .variant_search_form_url(
1190 "https://example.com/search_form",
1191 "GET",
1192 "search-form-name",
1193 "search-form-value",
1194 )
1195 .variant_visual_search_url(
1196 "https://example.com/visual-search-variant",
1197 "visual-search-variant-name",
1198 "visual-search-variant-value",
1199 "url_variant",
1200 "Visual Search Variant",
1201 "2096-02-02",
1202 )
1203 .build()
1204 );
1205 }
1206
1207 #[test]
1208 fn test_merges_variant_and_uses_locale_specific_visual_search_display_name() {
1209 let result = SearchEngineDefinition::from_configuration_details(
1210 &SearchUserEnvironment {
1211 locale: "en-GB".into(),
1212 ..Default::default()
1213 },
1214 "test",
1215 Lazy::force(&JSON_ENGINE_BASE).clone(),
1216 &JSON_ENGINE_VARIANT,
1217 &None,
1218 );
1219
1220 assert_eq!(
1221 result,
1222 ExpectedEngineFromJSONBase::new("test", "Test")
1223 .variant_is_new_until("2063-04-05")
1224 .variant_optional(true)
1225 .variant_partner_code("trek")
1226 .variant_telemetry_suffix("star")
1227 .variant_search_url(
1228 "https://example.com/variant",
1229 "GET",
1230 "variant",
1231 "test variant",
1232 "ship",
1233 )
1234 .variant_suggestions_url(
1235 "https://example.com/suggestions-variant",
1236 "GET",
1237 "suggest-variant",
1238 "sugg test variant",
1239 "variant",
1240 )
1241 .variant_trending_url(
1242 "https://example.com/trending-variant",
1243 "GET",
1244 "trend-variant",
1245 "trend test variant",
1246 "trend",
1247 true,
1248 )
1249 .variant_search_form_url(
1250 "https://example.com/search_form",
1251 "GET",
1252 "search-form-name",
1253 "search-form-value",
1254 )
1255 .variant_visual_search_url(
1256 "https://example.com/visual-search-variant",
1257 "visual-search-variant-name",
1258 "visual-search-variant-value",
1259 "url_variant",
1260 "Visual Search Variant en-GB",
1262 "2096-02-02",
1263 )
1264 .build()
1265 );
1266 }
1267
1268 #[test]
1269 fn test_merges_sub_variants() {
1270 let result = SearchEngineDefinition::from_configuration_details(
1271 &SearchUserEnvironment {
1272 locale: "fi".into(),
1273 ..Default::default()
1274 },
1275 "test",
1276 Lazy::force(&JSON_ENGINE_BASE).clone(),
1277 &JSON_ENGINE_VARIANT,
1278 &Some(JSON_ENGINE_SUBVARIANT.clone()),
1279 );
1280
1281 assert_eq!(
1282 result,
1283 ExpectedEngineFromJSONBase::new("test", "Test")
1284 .variant_is_new_until("2063-04-05")
1285 .variant_optional(true)
1286 .subvariant_partner_code("trek2")
1287 .subvariant_telemetry_suffix("star2")
1288 .subvariant_search_url(
1289 "https://example.com/subvariant",
1290 "GET",
1291 "subvariant",
1292 "test subvariant",
1293 "shuttle",
1294 )
1295 .subvariant_suggestions_url(
1296 "https://example.com/suggestions-subvariant",
1297 "GET",
1298 "suggest-subvariant",
1299 "sugg test subvariant",
1300 "subvariant",
1301 true,
1302 )
1303 .subvariant_trending_url(
1304 "https://example.com/trending-subvariant",
1305 "GET",
1306 "trend-subvariant",
1307 "trend test subvariant",
1308 "subtrend",
1309 )
1310 .subvariant_search_form_url(
1311 "https://example.com/search-form-subvariant",
1312 "GET",
1313 "search-form-subvariant",
1314 "search form subvariant",
1315 )
1316 .subvariant_visual_search_url(
1317 "https://example.com/visual-search-subvariant",
1318 "visual-search-subvariant-name",
1319 "visual-search-subvariant-value",
1320 "url_subvariant",
1321 "Visual Search Subvariant",
1322 "2097-03-03",
1323 )
1324 .build()
1325 );
1326 }
1327
1328 #[test]
1329 fn test_merges_subvariant_and_uses_locale_specific_visual_search_display_name() {
1330 let result = SearchEngineDefinition::from_configuration_details(
1331 &SearchUserEnvironment {
1332 locale: "en-GB".into(),
1333 ..Default::default()
1334 },
1335 "test",
1336 Lazy::force(&JSON_ENGINE_BASE).clone(),
1337 &JSON_ENGINE_VARIANT,
1338 &Some(JSON_ENGINE_SUBVARIANT.clone()),
1339 );
1340
1341 assert_eq!(
1342 result,
1343 ExpectedEngineFromJSONBase::new("test", "Test")
1344 .variant_is_new_until("2063-04-05")
1345 .variant_optional(true)
1346 .subvariant_partner_code("trek2")
1347 .subvariant_telemetry_suffix("star2")
1348 .subvariant_search_url(
1349 "https://example.com/subvariant",
1350 "GET",
1351 "subvariant",
1352 "test subvariant",
1353 "shuttle",
1354 )
1355 .subvariant_suggestions_url(
1356 "https://example.com/suggestions-subvariant",
1357 "GET",
1358 "suggest-subvariant",
1359 "sugg test subvariant",
1360 "subvariant",
1361 true,
1362 )
1363 .subvariant_trending_url(
1364 "https://example.com/trending-subvariant",
1365 "GET",
1366 "trend-subvariant",
1367 "trend test subvariant",
1368 "subtrend",
1369 )
1370 .subvariant_search_form_url(
1371 "https://example.com/search-form-subvariant",
1372 "GET",
1373 "search-form-subvariant",
1374 "search form subvariant",
1375 )
1376 .subvariant_visual_search_url(
1377 "https://example.com/visual-search-subvariant",
1378 "visual-search-subvariant-name",
1379 "visual-search-subvariant-value",
1380 "url_subvariant",
1381 "Visual Search Subvariant en-GB",
1383 "2097-03-03",
1384 )
1385 .build()
1386 );
1387 }
1388}