1use crate::configuration_overrides_types::JSONOverridesRecord;
8use crate::configuration_overrides_types::JSONSearchConfigurationOverrides;
9use crate::filter::filter_engine_configuration_impl;
10use crate::{
11 error::Error, JSONSearchConfiguration, RefinedSearchConfig, SearchApiResult,
12 SearchUserEnvironment,
13};
14use error_support::handle_error;
15use parking_lot::Mutex;
16use remote_settings::{RemoteSettingsClient, RemoteSettingsService};
17use std::sync::Arc;
18
19#[derive(Default)]
20pub(crate) struct SearchEngineSelectorInner {
21 configuration: Option<JSONSearchConfiguration>,
22 configuration_overrides: Option<JSONSearchConfigurationOverrides>,
23 search_config_client: Option<Arc<RemoteSettingsClient>>,
24 search_config_overrides_client: Option<Arc<RemoteSettingsClient>>,
25}
26
27#[derive(Default, uniffi::Object)]
31pub struct SearchEngineSelector(Mutex<SearchEngineSelectorInner>);
32
33#[uniffi::export]
34impl SearchEngineSelector {
35 #[uniffi::constructor]
36 pub fn new() -> Self {
37 Self(Mutex::default())
38 }
39
40 pub fn use_remote_settings_server(
50 self: Arc<Self>,
51 service: &Arc<RemoteSettingsService>,
52 apply_engine_overrides: bool,
53 ) {
54 let mut inner = self.0.lock();
55 inner.search_config_client = Some(service.make_client("search-config-v2".to_string()));
56
57 if apply_engine_overrides {
58 inner.search_config_overrides_client =
59 Some(service.make_client("search-config-overrides-v2".to_string()));
60 }
61 }
62
63 #[handle_error(Error)]
69 pub fn set_search_config(self: Arc<Self>, configuration: String) -> SearchApiResult<()> {
70 if configuration.is_empty() {
71 return Err(Error::SearchConfigNotSpecified);
72 }
73 self.0.lock().configuration = serde_json::from_str(&configuration)?;
74 Ok(())
75 }
76
77 #[handle_error(Error)]
78 pub fn set_config_overrides(self: Arc<Self>, overrides: String) -> SearchApiResult<()> {
79 if overrides.is_empty() {
80 return Err(Error::SearchConfigOverridesNotSpecified);
81 }
82 self.0.lock().configuration_overrides = serde_json::from_str(&overrides)?;
83 Ok(())
84 }
85
86 pub fn clear_search_config(self: Arc<Self>) {}
90
91 #[handle_error(Error)]
95 pub fn filter_engine_configuration(
96 self: Arc<Self>,
97 user_environment: SearchUserEnvironment,
98 ) -> SearchApiResult<RefinedSearchConfig> {
99 let inner = self.0.lock();
100 if let Some(client) = &inner.search_config_client {
101 let records = client.get_records(false);
107
108 if let Some(records) = records {
109 if records.is_empty() {
110 return Err(Error::SearchConfigNoRecords);
111 }
112
113 if let Some(overrides_client) = &inner.search_config_overrides_client {
114 let overrides_records = overrides_client.get_records(false);
115
116 if let Some(overrides_records) = overrides_records {
117 if overrides_records.is_empty() {
118 return filter_engine_configuration_impl(
119 user_environment,
120 &records,
121 None,
122 );
123 }
124 let stringified = serde_json::to_string(&overrides_records)?;
127 let json_overrides: Vec<JSONOverridesRecord> =
128 serde_json::from_str(&stringified)?;
129
130 return filter_engine_configuration_impl(
131 user_environment,
132 &records,
133 Some(json_overrides),
134 );
135 } else {
136 return Err(Error::SearchConfigOverridesNoRecords);
137 }
138 }
139
140 return filter_engine_configuration_impl(user_environment, &records, None);
141 } else {
142 return Err(Error::SearchConfigNoRecords);
143 }
144 }
145 let config = match &inner.configuration {
146 None => return Err(Error::SearchConfigNotSpecified),
147 Some(configuration) => configuration.data.clone(),
148 };
149
150 let config_overrides = match &inner.configuration_overrides {
151 None => return Err(Error::SearchConfigOverridesNotSpecified),
152 Some(overrides) => overrides.data.clone(),
153 };
154 return filter_engine_configuration_impl(user_environment, &config, Some(config_overrides));
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::test_helpers::{EngineRecord, ExpectedEngine, SubVariant, Variant};
162 use crate::{test_helpers, types::*, SearchApiError};
163 use mockito::mock;
164 use remote_settings::{RemoteSettingsConfig, RemoteSettingsContext, RemoteSettingsServer};
165 use serde_json::json;
166
167 #[test]
168 fn test_set_config_should_allow_basic_config() {
169 let selector = Arc::new(SearchEngineSelector::new());
170
171 let config = json!({
172 "data": [
173 EngineRecord::full("test1", "Test 1").build(),
174 {
175 "recordType": "defaultEngines",
176 "globalDefault": "test"
177 }
178 ]
179 });
180
181 let config_result = Arc::clone(&selector).set_search_config(config.to_string());
182 config_result.expect("Should have set the configuration successfully");
183 }
184
185 #[test]
186 fn test_set_config_should_allow_extra_fields() {
187 let selector = Arc::new(SearchEngineSelector::new());
188
189 let mut engine = EngineRecord::minimal("test", "Test").build();
190 engine["base"]["urls"]["search"]["extraField1"] = json!(true);
191 engine["base"]["extraField2"] = json!("123");
192 engine["extraField3"] = json!(["foo"]);
193
194 let config_result = Arc::clone(&selector).set_search_config(
195 json!({
196 "data": [
197 engine,
198 {
199 "recordType": "defaultEngines",
200 "globalDefault": "test",
201 "extraField4": {
202 "subField1": true
203 }
204 }
205 ]
206 })
207 .to_string(),
208 );
209 config_result.expect("Should have set the configuration successfully with extra fields");
210 }
211
212 #[test]
213 fn test_set_config_should_ignore_unknown_record_types() {
214 let selector = Arc::new(SearchEngineSelector::new());
215 let config = json!({
216 "data": [
217 EngineRecord::full("test1", "Test 1").build(),
218 {
219 "recordType": "defaultEngines",
220 "globalDefault": "test"
221 },
222 {
223 "recordType": "unknown"
224 }
225 ]
226 });
227 let config_result = Arc::clone(&selector).set_search_config(config.to_string());
228
229 config_result
230 .expect("Should have set the configuration successfully with unknown record types.");
231 }
232
233 #[test]
234 fn test_filter_engine_configuration_throws_without_config() {
235 let selector = Arc::new(SearchEngineSelector::new());
236
237 let result = selector.filter_engine_configuration(SearchUserEnvironment {
238 ..Default::default()
239 });
240
241 assert!(
242 result.is_err(),
243 "Should throw an error when a configuration has not been specified before filtering"
244 );
245 assert!(result
246 .unwrap_err()
247 .to_string()
248 .contains("Search configuration not specified"))
249 }
250
251 #[test]
252 fn test_filter_engine_configuration_throws_without_config_overrides() {
253 let selector = Arc::new(SearchEngineSelector::new());
254 let _ = Arc::clone(&selector).set_search_config(
255 json!({
256 "data": [
257 EngineRecord::full("test1", "Test 1").build(),
258 ]
259 })
260 .to_string(),
261 );
262
263 let result = selector.filter_engine_configuration(SearchUserEnvironment {
264 ..Default::default()
265 });
266
267 assert!(
268 result.is_err(),
269 "Should throw an error when a configuration overrides has not been specified before filtering"
270 );
271
272 assert!(result
273 .unwrap_err()
274 .to_string()
275 .contains("Search configuration overrides not specified"))
276 }
277
278 #[test]
279 fn test_filter_engine_configuration_returns_basic_engines() {
280 let selector = Arc::new(SearchEngineSelector::new());
281 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
282 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
283 );
284
285 let config_result = Arc::clone(&selector).set_search_config(
286 json!({
287 "data": [
288 EngineRecord::full("test1", "Test 1").build(),
289 EngineRecord::minimal("test2", "Test 2").build(),
290 {
291 "recordType": "defaultEngines",
292 "globalDefault": "test1",
293 "globalDefaultPrivate": "test2"
294 }
295 ]
296 })
297 .to_string(),
298 );
299 config_result.expect("Should have set the configuration successfully");
300 config_overrides_result.expect("Should have set the configuration overrides successfully");
301
302 let result = selector.filter_engine_configuration(SearchUserEnvironment {
303 ..Default::default()
304 });
305
306 assert!(
307 result.is_ok(),
308 "Should have filtered the configuration without error. {:?}",
309 result
310 );
311 assert_eq!(
312 result.unwrap(),
313 RefinedSearchConfig {
314 engines: vec!(
315 ExpectedEngine::full("test1", "Test 1").build(),
316 ExpectedEngine::minimal("test2", "Test 2").build(),
317 ),
318 app_default_engine_id: Some("test1".to_string()),
319 app_private_default_engine_id: Some("test2".to_string())
320 }
321 )
322 }
323
324 #[test]
325 fn test_filter_engine_configuration_handles_basic_variants() {
326 let selector = Arc::new(SearchEngineSelector::new());
327 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
328 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
329 );
330
331 let config_result = Arc::clone(&selector).set_search_config(
332 json!({
333 "data": [
334 EngineRecord::full("test1", "Test 1")
335 .add_variant(
336 Variant::new()
337 .regions(&["FR"])
338 .urls(json!({
339 "search": {
340 "method": "POST",
341 "params": [{
342 "name": "mission",
343 "value": "ongoing"
344 }]
345 }
346 }))
347 )
348 .build(),
349 EngineRecord::minimal("test2", "Test 2")
350 .add_variant(
351 Variant::new()
352 .optional(true)
353 .partner_code("ship")
354 .telemetry_suffix("E")
355 )
356 .build(),
357 {
358 "recordType": "defaultEngines",
359 "globalDefault": "test1",
360 "globalDefaultPrivate": "test2"
361 }
362 ]
363 })
364 .to_string(),
365 );
366 config_result.expect("Should have set the configuration successfully");
367 config_overrides_result.expect("Should have set the configuration overrides successfully");
368
369 let result = selector.filter_engine_configuration(SearchUserEnvironment {
370 region: "FR".into(),
371 ..Default::default()
372 });
373
374 assert!(
375 result.is_ok(),
376 "Should have filtered the configuration without error. {:?}",
377 result
378 );
379
380 let expected_1 = ExpectedEngine::full("test1", "Test 1")
381 .search_method("POST")
382 .search_params(vec![SearchUrlParam {
383 name: "mission".to_string(),
384 value: Some("ongoing".to_string()),
385 enterprise_value: None,
386 experiment_config: None,
387 }])
388 .build();
389
390 let expected_2 = ExpectedEngine::minimal("test2", "Test 2")
391 .optional(true)
392 .partner_code("ship")
393 .telemetry_suffix("E")
394 .build();
395
396 assert_eq!(
397 result.unwrap(),
398 RefinedSearchConfig {
399 engines: vec!(expected_1, expected_2),
400 app_default_engine_id: Some("test1".to_string()),
401 app_private_default_engine_id: Some("test2".to_string())
402 }
403 );
404 }
405
406 #[test]
407 fn test_filter_engine_configuration_handles_basic_subvariants() {
408 let selector = Arc::new(SearchEngineSelector::new());
409 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
410 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
411 );
412
413 let config_result = Arc::clone(&selector).set_search_config(
414 json!({
415 "data": [
416 EngineRecord::full("test1", "Test 1")
417 .add_variant(
418 Variant::new()
419 .regions(&["FR"])
420 .add_subvariant(
421 SubVariant::new()
422 .locales(&["fr"])
423 .partner_code("fr-partner-code")
424 .telemetry_suffix("fr-telemetry-suffix"),
425 )
426 .add_subvariant(
427 SubVariant::new()
428 .locales(&["en-CA"])
429 .urls(json!({
430 "search": {
431 "method": "GET",
432 "params": [{
433 "name": "en-ca-param-name",
434 "enterpriseValue": "en-ca-param-value"
435 }]
436 }
437 })),
438 )
439 )
440 .build(),
441 {
442 "recordType": "defaultEngines",
443 "globalDefault": "test1"
444 },
445 {
446 "recordType": "availableLocales",
447 "locales": ["en-CA", "fr"]
448 }
449 ]
450 })
451 .to_string(),
452 );
453 config_result.expect("Should have set the configuration successfully");
454 config_overrides_result.expect("Should have set the configuration overrides successfully");
455
456 let mut result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
457 region: "FR".into(),
458 locale: "fr".into(),
459 ..Default::default()
460 });
461
462 assert!(
463 result.is_ok(),
464 "Should have filtered the configuration without error. {:?}",
465 result
466 );
467
468 let expected_1 = ExpectedEngine::full("test1", "Test 1")
469 .partner_code("fr-partner-code")
470 .telemetry_suffix("fr-telemetry-suffix")
471 .build();
472
473 assert_eq!(
474 result.unwrap(),
475 RefinedSearchConfig {
476 engines: vec!(expected_1),
477 app_default_engine_id: Some("test1".to_string()),
478 app_private_default_engine_id: None
479 },
480 "Should have correctly matched and merged the fr locale sub-variant."
481 );
482
483 result = selector.filter_engine_configuration(SearchUserEnvironment {
484 region: "FR".into(),
485 locale: "en-CA".into(),
486 ..Default::default()
487 });
488
489 assert!(
490 result.is_ok(),
491 "Should have filtered the configuration without error. {:?}",
492 result
493 );
494
495 let expected_2 = ExpectedEngine::full("test1", "Test 1")
496 .search_params(vec![SearchUrlParam {
497 name: "en-ca-param-name".to_string(),
498 value: None,
499 enterprise_value: Some("en-ca-param-value".to_string()),
500 experiment_config: None,
501 }])
502 .build();
503
504 assert_eq!(
505 result.unwrap(),
506 RefinedSearchConfig {
507 engines: vec!(expected_2),
508 app_default_engine_id: Some("test1".to_string()),
509 app_private_default_engine_id: None
510 },
511 "Should have correctly matched and merged the en-CA locale sub-variant."
512 );
513 }
514
515 #[test]
516 fn test_filter_engine_configuration_handles_environments() {
517 let selector = Arc::new(SearchEngineSelector::new());
518 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
519 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
520 );
521
522 let config_result = Arc::clone(&selector).set_search_config(
523 json!({
524 "data": [
525 EngineRecord::full("test1", "Test 1").build(),
526 EngineRecord::full("test2", "Test 2")
527 .override_variants(
528 Variant::new()
529 .applications(&["firefox-android", "focus-ios"])
530 )
531 .build(),
532 EngineRecord::full("test3", "Test 3")
533 .override_variants(
534 Variant::new()
535 .distributions(&["starship"])
536 )
537 .build(),
538 {
539 "recordType": "defaultEngines",
540 "globalDefault": "test1",
541 }
542 ]
543 })
544 .to_string(),
545 );
546 config_result.expect("Should have set the configuration successfully");
547 config_overrides_result.expect("Should have set the configuration overrides successfully");
548
549 let mut result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
550 distribution_id: String::new(),
551 app_name: SearchApplicationName::Firefox,
552 ..Default::default()
553 });
554
555 assert!(
556 result.is_ok(),
557 "Should have filtered the configuration without error. {:?}",
558 result
559 );
560
561 assert_eq!(
562 result.unwrap(),
563 RefinedSearchConfig {
564 engines: vec!(ExpectedEngine::full("test1", "Test 1").build()),
565 app_default_engine_id: Some("test1".to_string()),
566 app_private_default_engine_id: None
567 }, "Should have selected test1 for all matching locales, as the environments do not match for the other two"
568 );
569
570 result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
571 distribution_id: String::new(),
572 app_name: SearchApplicationName::FocusIos,
573 ..Default::default()
574 });
575
576 assert!(
577 result.is_ok(),
578 "Should have filtered the configuration without error. {:?}",
579 result
580 );
581
582 let expected_1 = ExpectedEngine::full("test1", "Test 1").build();
583 let expected_2 = ExpectedEngine::full("test2", "Test 2").build();
584 assert_eq!(
585 result.unwrap(),
586 RefinedSearchConfig {
587 engines: vec!(expected_1, expected_2),
588 app_default_engine_id: Some("test1".to_string()),
589 app_private_default_engine_id: None
590 },
591 "Should have selected test1 for all matching locales and test2 for matching Focus IOS"
592 );
593
594 result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
595 distribution_id: "starship".to_string(),
596 app_name: SearchApplicationName::Firefox,
597 ..Default::default()
598 });
599
600 assert!(
601 result.is_ok(),
602 "Should have filtered the configuration without error. {:?}",
603 result
604 );
605
606 let expected_1 = ExpectedEngine::full("test1", "Test 1").build();
607 let expected_3 = ExpectedEngine::full("test3", "Test 3").build();
608 assert_eq!(
609 result.unwrap(),
610 RefinedSearchConfig {
611 engines: vec!(expected_1, expected_3),
612 app_default_engine_id: Some("test1".to_string()),
613 app_private_default_engine_id: None
614 }, "Should have selected test1 for all matching locales and test3 for matching the distribution id"
615 );
616 }
617
618 #[test]
619 fn test_set_config_should_handle_default_engines() {
620 let selector = Arc::new(SearchEngineSelector::new());
621 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
622 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
623 );
624
625 let config_result = Arc::clone(&selector).set_search_config(
626 json!({
627 "data": [
628 EngineRecord::minimal("test", "Test").build(),
629 EngineRecord::minimal("distro-default", "Distribution Default").build(),
630 EngineRecord::minimal("private-default-FR", "Private default FR").build(),
631 {
632 "recordType": "defaultEngines",
633 "globalDefault": "test",
634 "specificDefaults": [{
635 "environment": {
636 "distributions": ["test-distro"],
637 },
638 "default": "distro-default"
639 }, {
640 "environment": {
641 "regions": ["fr"]
642 },
643 "defaultPrivate": "private-default-FR"
644 }]
645 }
646 ]
647 })
648 .to_string(),
649 );
650 config_result.expect("Should have set the configuration successfully");
651 config_overrides_result.expect("Should have set the configuration overrides successfully");
652
653 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
654 distribution_id: "test-distro".to_string(),
655 ..Default::default()
656 });
657 assert!(
658 result.is_ok(),
659 "Should have filtered the configuration without error. {:?}",
660 result
661 );
662
663 assert_eq!(
664 result.unwrap(),
665 RefinedSearchConfig {
666 engines: vec![
667 ExpectedEngine::minimal("distro-default", "Distribution Default").build(),
668 ExpectedEngine::minimal("private-default-FR", "Private default FR").build(),
669 ExpectedEngine::minimal("test", "Test").build(),
670 ],
671 app_default_engine_id: Some("distro-default".to_string()),
672 app_private_default_engine_id: None
673 },
674 "Should have selected the distro-default engine for the matching specific default"
675 );
676
677 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
678 region: "fr".into(),
679 distribution_id: String::new(),
680 ..Default::default()
681 });
682 assert!(
683 result.is_ok(),
684 "Should have filtered the configuration without error. {:?}",
685 result
686 );
687
688 assert_eq!(
689 result.unwrap(),
690 RefinedSearchConfig {
691 engines: vec![
692 ExpectedEngine::minimal("test", "Test").build(),
693 ExpectedEngine::minimal("private-default-FR", "Private default FR").build(),
694 ExpectedEngine::minimal("distro-default", "Distribution Default").build(),
695 ],
696 app_default_engine_id: Some("test".to_string()),
697 app_private_default_engine_id: Some("private-default-FR".to_string())
698 },
699 "Should have selected the private default engine for the matching specific default"
700 );
701 }
702
703 #[test]
704 fn test_filter_engine_orders() {
705 let selector = Arc::new(SearchEngineSelector::new());
706 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
707 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
708 );
709
710 let engine_order_config = Arc::clone(&selector).set_search_config(
711 json!({
712 "data": [
713 EngineRecord::minimal("after-defaults", "after-defaults").build(),
714 EngineRecord::minimal("b-engine", "first alphabetical").build(),
715 EngineRecord::minimal("a-engine", "last alphabetical").build(),
716 EngineRecord::minimal("default-engine", "default-engine").build(),
717 EngineRecord::minimal("default-private-engine", "default-privite-engine").build(),
718 {
719 "recordType": "defaultEngines",
720 "globalDefault": "default-engine",
721 "globalDefaultPrivate": "default-private-engine",
722 },
723 {
724 "recordType": "engineOrders",
725 "orders": [
726 {
727 "environment": {
728 "locales": ["en-CA"],
729 "regions": ["CA"],
730 },
731 "order": ["after-defaults"],
732 },
733 ],
734 },
735 {
736 "recordType": "availableLocales",
737 "locales": ["en-CA", "fr"]
738 }
739 ]
740 })
741 .to_string(),
742 );
743 engine_order_config.expect("Should have set the configuration successfully");
744 config_overrides_result.expect("Should have set the configuration overrides successfully");
745
746 fn assert_actual_engines_equals_expected(
747 result: Result<RefinedSearchConfig, SearchApiError>,
748 expected_engine_orders: Vec<String>,
749 message: &str,
750 ) {
751 assert!(
752 result.is_ok(),
753 "Should have filtered the configuration without error. {:?}",
754 result
755 );
756
757 let refined_config = result.unwrap();
758 let actual_engine_orders: Vec<String> = refined_config
759 .engines
760 .into_iter()
761 .map(|e| e.identifier)
762 .collect();
763
764 assert_eq!(actual_engine_orders, expected_engine_orders, "{}", message);
765 }
766
767 assert_actual_engines_equals_expected(
768 Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
769 locale: "en-CA".into(),
770 region: "CA".into(),
771 ..Default::default()
772 }),
773 vec![
774 "default-engine".to_string(),
775 "default-private-engine".to_string(),
776 "after-defaults".to_string(),
777 "b-engine".to_string(),
778 "a-engine".to_string(),
779 ],
780 "Should order the default engine first, default private engine second, and the rest of the engines based on order hint then alphabetically by name."
781 );
782
783 let starts_with_wiki_config = Arc::clone(&selector).set_search_config(
784 json!({
785 "data": [
786 EngineRecord::minimal("wiki-ca", "wiki-ca")
787 .override_variants(
788 Variant::new()
789 .locales(&["en-CA"])
790 .regions(&["CA"])
791 )
792 .build(),
793 EngineRecord::minimal("wiki-uk", "wiki-uk")
794 .override_variants(
795 Variant::new()
796 .locales(&["en-GB"])
797 .regions(&["GB"])
798 )
799 .build(),
800 EngineRecord::minimal("engine-1", "engine-1").build(),
801 EngineRecord::minimal("engine-2", "engine-2").build(),
802 {
803 "recordType": "engineOrders",
804 "orders": [
805 {
806 "environment": {
807 "locales": ["en-CA"],
808 "regions": ["CA"],
809 },
810 "order": ["wiki*", "engine-1", "engine-2"],
811 },
812 {
813 "environment": {
814 "locales": ["en-GB"],
815 "regions": ["GB"],
816 },
817 "order": ["wiki*", "engine-1", "engine-2"],
818 },
819 ],
820 },
821 {
822 "recordType": "availableLocales",
823 "locales": ["en-CA", "en-GB", "fr"]
824 }
825
826 ]
827 })
828 .to_string(),
829 );
830 starts_with_wiki_config.expect("Should have set the configuration successfully");
831
832 assert_actual_engines_equals_expected(
833 Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
834 locale: "en-CA".into(),
835 region: "CA".into(),
836 ..Default::default()
837 }),
838 vec![
839 "wiki-ca".to_string(),
840 "engine-1".to_string(),
841 "engine-2".to_string(),
842 ],
843 "Should list the wiki-ca engine and other engines in correct orders with the en-CA and CA locale region environment."
844 );
845
846 assert_actual_engines_equals_expected(
847 Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
848 locale: "en-GB".into(),
849 region: "GB".into(),
850 ..Default::default()
851 }),
852 vec![
853 "wiki-uk".to_string(),
854 "engine-1".to_string(),
855 "engine-2".to_string(),
856 ],
857 "Should list the wiki-uk engine and other engines in correct orders with the en-GB and GB locale region environment."
858 );
859 }
860
861 const APPLY_OVERRIDES: bool = true;
862 const DO_NOT_APPLY_OVERRIDES: bool = false;
863 const RECORDS_MISSING: bool = false;
864 const RECORDS_PRESENT: bool = true;
865
866 fn setup_remote_settings_test(
867 should_apply_overrides: bool,
868 expect_sync_successful: bool,
869 ) -> Arc<SearchEngineSelector> {
870 error_support::init_for_tests();
871 viaduct_dev::init_backend_dev();
872
873 let config = RemoteSettingsConfig {
874 server: Some(RemoteSettingsServer::Custom {
875 url: mockito::server_url(),
876 }),
877 bucket_name: Some(String::from("main")),
878 app_context: Some(RemoteSettingsContext::default()),
879 };
880 let service = Arc::new(RemoteSettingsService::new(String::from(":memory:"), config));
881
882 let selector = Arc::new(SearchEngineSelector::new());
883
884 Arc::clone(&selector).use_remote_settings_server(&service, should_apply_overrides);
885 let sync_result = Arc::clone(&service).sync();
886 assert!(
887 if expect_sync_successful {
888 sync_result.is_ok()
889 } else {
890 sync_result.is_err()
891 },
892 "Should have completed the sync successfully. {:?}",
893 sync_result
894 );
895
896 selector
897 }
898
899 fn mock_changes_endpoint() -> mockito::Mock {
900 mock(
901 "GET",
902 "/v2/buckets/monitor/collections/changes/changeset?_expected=0",
903 )
904 .with_body(response_body_changes())
905 .with_status(200)
906 .with_header("content-type", "application/json")
907 .with_header("etag", "\"1000\"")
908 .create()
909 }
910
911 fn response_body() -> String {
912 json!({
913 "metadata": {
914 "id": "search-config-v2",
915 "last_modified": 1000,
916 "bucket": "main",
917 "signatures": [{
918 "x5u": "fake",
919 "signature": "fake",
920 "mode": "fake",
921 }],
922 },
923 "timestamp": 1000,
924 "changes": [
925 EngineRecord::minimal("test", "Test")
926 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d7")
927 .schema(1001)
928 .last_modified(1000)
929 .build(),
930 EngineRecord::minimal("distro-default", "Distribution Default")
931 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d8")
932 .schema(1002)
933 .last_modified(1000)
934 .build(),
935 EngineRecord::minimal("private-default-FR", "Private default FR")
936 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d9")
937 .schema(1003)
938 .last_modified(1000)
939 .build(),
940 {
941 "recordType": "defaultEngines",
942 "globalDefault": "test",
943 "specificDefaults": [{
944 "environment": {
945 "distributions": ["test-distro"],
946 },
947 "default": "distro-default"
948 }, {
949 "environment": {
950 "regions": ["fr"]
951 },
952 "defaultPrivate": "private-default-FR"
953 }],
954 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0e0",
955 "schema": 1004,
956 "last_modified": 1000,
957 }
958 ]
959 })
960 .to_string()
961 }
962
963 fn response_body_changes() -> String {
964 json!({
965 "timestamp": 1000,
966 "changes": [
967 {
968 "collection": "search-config-v2",
969 "bucket": "main",
970 "last_modified": 1000,
971 }
972 ],
973 })
974 .to_string()
975 }
976
977 fn response_body_locales() -> String {
978 json!({
979 "metadata": {
980 "id": "search-config-v2",
981 "last_modified": 1000,
982 "bucket": "main",
983 "signatures": [{
984 "x5u": "fake",
985 "signature": "fake",
986 "mode": "fake",
987 }],
988 },
989 "timestamp": 1000,
990 "changes": [
991 EngineRecord::minimal("engine-de", "German Engine")
992 .override_variants(
993 Variant::new()
994 .locales(&["de"])
995 )
996 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d7")
997 .schema(1001)
998 .last_modified(1000)
999 .build(),
1000 EngineRecord::minimal("engine-en-us", "English US Engine")
1001 .override_variants(
1002 Variant::new()
1003 .locales(&["en-US"])
1004 )
1005 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d8")
1006 .schema(1002)
1007 .last_modified(1000)
1008 .build(),
1009 {
1010 "recordType": "availableLocales",
1011 "locales": ["de", "en-US"],
1012 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0e0",
1013 "schema": 1004,
1014 "last_modified": 1000,
1015 }
1016 ]
1017 })
1018 .to_string()
1019 }
1020
1021 fn response_body_overrides() -> String {
1022 let mut engine = test_helpers::overrides_engine();
1023 engine["identifier"] = json!("test");
1024 engine["id"] = json!("c5dcd1da-7126-4abb-846b-ec85b0d4d0d7");
1025 engine["schema"] = json!(1001);
1026 engine["last_modified"] = json!(1000);
1027
1028 json!({
1029 "metadata": {
1030 "id": "search-config-overrides-v2",
1031 "last_modified": 1000,
1032 "bucket": "main",
1033 "signatures": [{
1034 "x5u": "fake",
1035 "signature": "fake",
1036 "mode": "fake",
1037 }],
1038 },
1039 "timestamp": 1000,
1040 "changes": [ engine ]
1041 })
1042 .to_string()
1043 }
1044
1045 #[test]
1046 fn test_remote_settings_empty_search_config_records_throws_error() {
1047 let changes_mock = mock_changes_endpoint();
1048 let m = mock(
1049 "GET",
1050 "/v2/buckets/main/collections/search-config-v2/changeset?_expected=0",
1051 )
1052 .with_body(
1053 json!({
1054 "metadata": {
1055 "id": "search-config-v2",
1056 "last_modified": 1000,
1057 "bucket": "main",
1058 "signatures": [{
1059 "x5u": "fake",
1060 "signature": "fake",
1061 "mode": "fake",
1062 }],
1063 },
1064 "timestamp": 1000,
1065 "changes": [
1066 ]})
1067 .to_string(),
1068 )
1069 .with_status(200)
1070 .with_header("content-type", "application/json")
1071 .with_header("etag", "\"1000\"")
1072 .create();
1073
1074 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
1075
1076 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1077 distribution_id: "test-distro".to_string(),
1078 ..Default::default()
1079 });
1080 assert!(
1081 result.is_err(),
1082 "Should throw an error when a configuration has not been specified before filtering"
1083 );
1084 assert!(result
1085 .unwrap_err()
1086 .to_string()
1087 .contains("No search config v2 records received from remote settings"));
1088 changes_mock.expect(1).assert();
1089 m.expect(1).assert();
1090 }
1091
1092 #[test]
1093 fn test_remote_settings_search_config_records_is_none_throws_error() {
1094 let changes_mock = mock_changes_endpoint();
1095 let m1 = mock(
1096 "GET",
1097 "/v2/buckets/main/collections/search-config-v2/changeset?_expected=0",
1098 )
1099 .with_body(response_body())
1100 .with_status(501)
1101 .with_header("content-type", "application/json")
1102 .with_header("etag", "\"1000\"")
1103 .create();
1104
1105 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_MISSING);
1106
1107 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1108 distribution_id: "test-distro".to_string(),
1109 ..Default::default()
1110 });
1111 assert!(
1112 result.is_err(),
1113 "Should throw an error when a configuration has not been specified before filtering"
1114 );
1115 assert!(result
1116 .unwrap_err()
1117 .to_string()
1118 .contains("No search config v2 records received from remote settings"));
1119 changes_mock.expect(1).assert();
1120 m1.expect(1).assert();
1121 }
1122
1123 #[test]
1124 fn test_remote_settings_empty_search_config_overrides_filtered_without_error() {
1125 let changes_mock = mock_changes_endpoint();
1126 let m1 = mock(
1127 "GET",
1128 "/v2/buckets/main/collections/search-config-v2/changeset?_expected=0",
1129 )
1130 .with_body(response_body())
1131 .with_status(200)
1132 .with_header("content-type", "application/json")
1133 .with_header("etag", "\"1000\"")
1134 .create();
1135
1136 let m2 = mock(
1137 "GET",
1138 "/v2/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
1139 )
1140 .with_body(
1141 json!({
1142 "metadata": {
1143 "id": "search-config-overrides-v2",
1144 "last_modified": 1000,
1145 "bucket": "main",
1146 "signatures": [{
1147 "x5u": "fake",
1148 "signature": "fake",
1149 "mode": "fake",
1150 }],
1151 },
1152 "timestamp": 1000,
1153 "changes": [
1154 ]})
1155 .to_string(),
1156 )
1157 .with_status(200)
1158 .with_header("content-type", "application/json")
1159 .with_header("etag", "\"1000\"")
1160 .create();
1161
1162 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_PRESENT);
1163
1164 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1165 distribution_id: "test-distro".to_string(),
1166 ..Default::default()
1167 });
1168 assert!(
1169 result.is_ok(),
1170 "Should have filtered the configuration using an empty search config overrides without causing an error. {:?}",
1171 result
1172 );
1173 changes_mock.expect(1).assert();
1174 m1.expect(1).assert();
1175 m2.expect(1).assert();
1176 }
1177
1178 #[test]
1179 fn test_remote_settings_search_config_overrides_records_is_none_throws_error() {
1180 let changes_mock = mock_changes_endpoint();
1181 let m1 = mock(
1182 "GET",
1183 "/v2/buckets/main/collections/search-config-v2/changeset?_expected=0",
1184 )
1185 .with_body(response_body())
1186 .with_status(200)
1187 .with_header("content-type", "application/json")
1188 .with_header("etag", "\"1000\"")
1189 .create();
1190
1191 let m2 = mock(
1192 "GET",
1193 "/v2/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
1194 )
1195 .with_body(response_body_overrides())
1196 .with_status(501)
1197 .with_header("content-type", "application/json")
1198 .with_header("etag", "\"1000\"")
1199 .create();
1200
1201 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_MISSING);
1202
1203 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1204 distribution_id: "test-distro".to_string(),
1205 ..Default::default()
1206 });
1207 assert!(
1208 result.is_err(),
1209 "Should throw an error when a configuration overrides has not been specified before filtering"
1210 );
1211 assert!(result
1212 .unwrap_err()
1213 .to_string()
1214 .contains("No search config overrides v2 records received from remote settings"));
1215 changes_mock.expect(1).assert();
1216 m1.expect(1).assert();
1217 m2.expect(1).assert();
1218 }
1219
1220 #[test]
1221 fn test_filter_with_remote_settings_overrides() {
1222 let changes_mock = mock_changes_endpoint();
1223 let m1 = mock(
1224 "GET",
1225 "/v2/buckets/main/collections/search-config-v2/changeset?_expected=0",
1226 )
1227 .with_body(response_body())
1228 .with_status(200)
1229 .with_header("content-type", "application/json")
1230 .with_header("etag", "\"1000\"")
1231 .create();
1232
1233 let m2 = mock(
1234 "GET",
1235 "/v2/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
1236 )
1237 .with_body(response_body_overrides())
1238 .with_status(200)
1239 .with_header("content-type", "application/json")
1240 .with_header("etag", "\"1000\"")
1241 .create();
1242
1243 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_PRESENT);
1244
1245 let override_test_engine = ExpectedEngine::minimal("test", "Test")
1246 .partner_code("overrides-partner-code")
1247 .click_url("https://example.com/click-url")
1248 .telemetry_suffix("overrides-telemetry-suffix")
1249 .search_base("https://example.com/search-overrides")
1250 .search_term_param_name("search")
1251 .search_params(vec![SearchUrlParam {
1252 name: "overrides-name".to_string(),
1253 value: Some("overrides-value".to_string()),
1254 enterprise_value: None,
1255 experiment_config: None,
1256 }])
1257 .build();
1258
1259 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1260 ..Default::default()
1261 });
1262
1263 assert!(
1264 result.is_ok(),
1265 "Should have filtered the configuration without error. {:?}",
1266 result
1267 );
1268 assert_eq!(
1269 result.unwrap().engines[0],
1270 override_test_engine.clone(),
1271 "Should have applied the overrides to the matching engine"
1272 );
1273 changes_mock.expect(1).assert();
1274 m1.expect(1).assert();
1275 m2.expect(1).assert();
1276 }
1277
1278 #[test]
1279 fn test_filter_with_remote_settings() {
1280 let changes_mock = mock_changes_endpoint();
1281
1282 let m = mock(
1283 "GET",
1284 "/v2/buckets/main/collections/search-config-v2/changeset?_expected=0",
1285 )
1286 .with_body(response_body())
1287 .with_status(200)
1288 .with_header("content-type", "application/json")
1289 .with_header("etag", "\"1000\"")
1290 .create();
1291
1292 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
1293
1294 let test_engine = ExpectedEngine::minimal("test", "Test").build();
1295 let private_default_fr_engine =
1296 ExpectedEngine::minimal("private-default-FR", "Private default FR").build();
1297 let distro_default_engine =
1298 ExpectedEngine::minimal("distro-default", "Distribution Default").build();
1299
1300 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1301 distribution_id: "test-distro".to_string(),
1302 ..Default::default()
1303 });
1304 assert!(
1305 result.is_ok(),
1306 "Should have filtered the configuration without error. {:?}",
1307 result
1308 );
1309 assert_eq!(
1310 result.unwrap(),
1311 RefinedSearchConfig {
1312 engines: vec![
1313 distro_default_engine.clone(),
1314 private_default_fr_engine.clone(),
1315 test_engine.clone(),
1316 ],
1317 app_default_engine_id: Some("distro-default".to_string()),
1318 app_private_default_engine_id: None
1319 },
1320 "Should have selected the default engine for the matching specific default"
1321 );
1322
1323 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1324 region: "fr".into(),
1325 distribution_id: String::new(),
1326 ..Default::default()
1327 });
1328 assert!(
1329 result.is_ok(),
1330 "Should have filtered the configuration without error. {:?}",
1331 result
1332 );
1333 assert_eq!(
1334 result.unwrap(),
1335 RefinedSearchConfig {
1336 engines: vec![
1337 test_engine.clone(),
1338 private_default_fr_engine.clone(),
1339 distro_default_engine.clone(),
1340 ],
1341 app_default_engine_id: Some("test".to_string()),
1342 app_private_default_engine_id: Some("private-default-FR".to_string())
1343 },
1344 "Should have selected the private default engine for the matching specific default"
1345 );
1346 changes_mock.expect(1).assert();
1347 m.expect(1).assert();
1348 }
1349
1350 #[test]
1351 fn test_filter_with_remote_settings_negotiate_locales() {
1352 let changes_mock = mock_changes_endpoint();
1353 let m = mock(
1354 "GET",
1355 "/v2/buckets/main/collections/search-config-v2/changeset?_expected=0",
1356 )
1357 .with_body(response_body_locales())
1358 .with_status(200)
1359 .with_header("content-type", "application/json")
1360 .with_header("etag", "\"1000\"")
1361 .create();
1362
1363 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
1364
1365 let result_de = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1366 locale: "de-AT".into(),
1367 ..Default::default()
1368 });
1369 assert!(
1370 result_de.is_ok(),
1371 "Should have filtered the configuration without error. {:?}",
1372 result_de
1373 );
1374
1375 assert_eq!(
1376 result_de.unwrap(),
1377 RefinedSearchConfig {
1378 engines: vec![ExpectedEngine::minimal("engine-de", "German Engine").build()],
1379 app_default_engine_id: None,
1380 app_private_default_engine_id: None,
1381 },
1382 "Should have selected the de engine when given de-AT which is not an available locale"
1383 );
1384
1385 let result_en = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1386 locale: "en-AU".to_string(),
1387 ..Default::default()
1388 });
1389 assert_eq!(
1390 result_en.unwrap(),
1391 RefinedSearchConfig {
1392 engines: vec![ExpectedEngine::minimal("engine-en-us", "English US Engine").build(),],
1393 app_default_engine_id: None,
1394 app_private_default_engine_id: None,
1395 },
1396 "Should have selected the en-us engine when given another english locale we don't support"
1397 );
1398 changes_mock.expect(1).assert();
1399 m.expect(1).assert();
1400 }
1401
1402 #[test]
1403 fn test_configuration_overrides_applied() {
1404 let selector = Arc::new(SearchEngineSelector::new());
1405
1406 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
1407 json!({
1408 "data": [
1409 test_helpers::overrides_engine(),
1410 { "identifier": "distro-default",
1412 "partnerCode": "distro-overrides-partner-code",
1413 "clickUrl": "https://example.com/click-url-distro",
1414 "urls": {
1415 "search": {
1416 "base": "https://example.com/search-distro",
1417 },
1418 },
1419 }
1420 ]
1421 })
1422 .to_string(),
1423 );
1424 let config_result = Arc::clone(&selector).set_search_config(
1425 json!({
1426 "data": [
1427 EngineRecord::minimal("overrides-engine", "Overrides Engine")
1428 .build(),
1429 EngineRecord::minimal("distro-default", "Distribution Default")
1430 .override_variants(Variant::new()
1431 .all_regions_and_locales()
1432 .telemetry_suffix("distro-telemetry-suffix"))
1433 .build(),
1434 ]
1435 })
1436 .to_string(),
1437 );
1438 config_result.expect("Should have set the configuration successfully");
1439 config_overrides_result.expect("Should have set the configuration overrides successfully");
1440
1441 let override_test_engine = ExpectedEngine::minimal("overrides-engine", "Overrides Engine")
1442 .partner_code("overrides-partner-code")
1443 .click_url("https://example.com/click-url")
1444 .telemetry_suffix("overrides-telemetry-suffix")
1445 .search_base("https://example.com/search-overrides")
1446 .search_params(vec![SearchUrlParam {
1447 name: "overrides-name".to_string(),
1448 value: Some("overrides-value".to_string()),
1449 enterprise_value: None,
1450 experiment_config: None,
1451 }])
1452 .build();
1453
1454 let override_distro_default_engine =
1455 ExpectedEngine::minimal("distro-default", "Distribution Default")
1456 .partner_code("distro-overrides-partner-code")
1457 .click_url("https://example.com/click-url-distro")
1458 .search_base("https://example.com/search-distro")
1459 .telemetry_suffix("distro-telemetry-suffix")
1460 .build();
1461
1462 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1463 ..Default::default()
1464 });
1465 assert!(
1466 result.is_ok(),
1467 "Should have filtered the configuration without error. {:?}",
1468 result
1469 );
1470
1471 assert_eq!(
1472 result.unwrap(),
1473 RefinedSearchConfig {
1474 engines: vec![
1475 override_distro_default_engine.clone(),
1476 override_test_engine.clone(),
1477 ],
1478 app_default_engine_id: None,
1479 app_private_default_engine_id: None
1480 },
1481 "Should have applied the overrides to the matching engine."
1482 );
1483 }
1484
1485 #[test]
1486 fn test_filter_engine_configuration_negotiate_locales() {
1487 let selector = Arc::new(SearchEngineSelector::new());
1488 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
1489 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
1490 );
1491
1492 let config_result = Arc::clone(&selector).set_search_config(
1493 json!({
1494 "data": [
1495 {
1496 "recordType": "availableLocales",
1497 "locales": ["de", "en-US"]
1498 },
1499 EngineRecord::minimal("engine-de", "German Engine")
1500 .override_variants(Variant::new()
1501 .locales(&["de"]))
1502 .build(),
1503 EngineRecord::minimal("engine-en-us", "English US Engine")
1504 .override_variants(Variant::new()
1505 .locales(&["en-US"]))
1506 .build(),
1507 ]
1508 })
1509 .to_string(),
1510 );
1511 config_result.expect("Should have set the configuration successfully");
1512 config_overrides_result.expect("Should have set the configuration overrides successfully");
1513
1514 let result_de = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1515 locale: "de-AT".into(),
1516 ..Default::default()
1517 });
1518 assert!(
1519 result_de.is_ok(),
1520 "Should have filtered the configuration without error. {:?}",
1521 result_de
1522 );
1523
1524 assert_eq!(
1525 result_de.unwrap(),
1526 RefinedSearchConfig {
1527 engines: vec![ExpectedEngine::minimal("engine-de", "German Engine").build(),],
1528 app_default_engine_id: None,
1529 app_private_default_engine_id: None,
1530 },
1531 "Should have selected the de engine when given de-AT which is not an available locale"
1532 );
1533
1534 let result_en = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1535 locale: "en-AU".to_string(),
1536 ..Default::default()
1537 });
1538 assert_eq!(
1539 result_en.unwrap(),
1540 RefinedSearchConfig {
1541 engines: vec![ExpectedEngine::minimal("engine-en-us", "English US Engine").build(),],
1542 app_default_engine_id: None,
1543 app_private_default_engine_id: None,
1544 },
1545 "Should have selected the en-us engine when given another english locale we don't support"
1546 );
1547 }
1548}