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::{RemoteSettingsConfig2, 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 = RemoteSettingsConfig2 {
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 "/v1/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 "signature": {
918 "x5u": "fake",
919 "signature": "fake",
920 },
921 },
922 "timestamp": 1000,
923 "changes": [
924 EngineRecord::minimal("test", "Test")
925 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d7")
926 .schema(1001)
927 .last_modified(1000)
928 .build(),
929 EngineRecord::minimal("distro-default", "Distribution Default")
930 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d8")
931 .schema(1002)
932 .last_modified(1000)
933 .build(),
934 EngineRecord::minimal("private-default-FR", "Private default FR")
935 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d9")
936 .schema(1003)
937 .last_modified(1000)
938 .build(),
939 {
940 "recordType": "defaultEngines",
941 "globalDefault": "test",
942 "specificDefaults": [{
943 "environment": {
944 "distributions": ["test-distro"],
945 },
946 "default": "distro-default"
947 }, {
948 "environment": {
949 "regions": ["fr"]
950 },
951 "defaultPrivate": "private-default-FR"
952 }],
953 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0e0",
954 "schema": 1004,
955 "last_modified": 1000,
956 }
957 ]
958 })
959 .to_string()
960 }
961
962 fn response_body_changes() -> String {
963 json!({
964 "timestamp": 1000,
965 "changes": [
966 {
967 "collection": "search-config-v2",
968 "bucket": "main",
969 "last_modified": 1000,
970 }
971 ],
972 })
973 .to_string()
974 }
975
976 fn response_body_locales() -> String {
977 json!({
978 "metadata": {
979 "id": "search-config-v2",
980 "last_modified": 1000,
981 "bucket": "main",
982 "signature": {
983 "x5u": "fake",
984 "signature": "fake",
985 },
986 },
987 "timestamp": 1000,
988 "changes": [
989 EngineRecord::minimal("engine-de", "German Engine")
990 .override_variants(
991 Variant::new()
992 .locales(&["de"])
993 )
994 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d7")
995 .schema(1001)
996 .last_modified(1000)
997 .build(),
998 EngineRecord::minimal("engine-en-us", "English US Engine")
999 .override_variants(
1000 Variant::new()
1001 .locales(&["en-US"])
1002 )
1003 .id("c5dcd1da-7126-4abb-846b-ec85b0d4d0d8")
1004 .schema(1002)
1005 .last_modified(1000)
1006 .build(),
1007 {
1008 "recordType": "availableLocales",
1009 "locales": ["de", "en-US"],
1010 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0e0",
1011 "schema": 1004,
1012 "last_modified": 1000,
1013 }
1014 ]
1015 })
1016 .to_string()
1017 }
1018
1019 fn response_body_overrides() -> String {
1020 let mut engine = test_helpers::overrides_engine();
1021 engine["identifier"] = json!("test");
1022 engine["id"] = json!("c5dcd1da-7126-4abb-846b-ec85b0d4d0d7");
1023 engine["schema"] = json!(1001);
1024 engine["last_modified"] = json!(1000);
1025
1026 json!({
1027 "metadata": {
1028 "id": "search-config-overrides-v2",
1029 "last_modified": 1000,
1030 "bucket": "main",
1031 "signature": {
1032 "x5u": "fake",
1033 "signature": "fake",
1034 },
1035 },
1036 "timestamp": 1000,
1037 "changes": [ engine ]
1038 })
1039 .to_string()
1040 }
1041
1042 #[test]
1043 fn test_remote_settings_empty_search_config_records_throws_error() {
1044 let changes_mock = mock_changes_endpoint();
1045 let m = mock(
1046 "GET",
1047 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
1048 )
1049 .with_body(
1050 json!({
1051 "metadata": {
1052 "id": "search-config-v2",
1053 "last_modified": 1000,
1054 "bucket": "main",
1055 "signature": {
1056 "x5u": "fake",
1057 "signature": "fake",
1058 },
1059 },
1060 "timestamp": 1000,
1061 "changes": [
1062 ]})
1063 .to_string(),
1064 )
1065 .with_status(200)
1066 .with_header("content-type", "application/json")
1067 .with_header("etag", "\"1000\"")
1068 .create();
1069
1070 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
1071
1072 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1073 distribution_id: "test-distro".to_string(),
1074 ..Default::default()
1075 });
1076 assert!(
1077 result.is_err(),
1078 "Should throw an error when a configuration has not been specified before filtering"
1079 );
1080 assert!(result
1081 .unwrap_err()
1082 .to_string()
1083 .contains("No search config v2 records received from remote settings"));
1084 changes_mock.expect(1).assert();
1085 m.expect(1).assert();
1086 }
1087
1088 #[test]
1089 fn test_remote_settings_search_config_records_is_none_throws_error() {
1090 let changes_mock = mock_changes_endpoint();
1091 let m1 = mock(
1092 "GET",
1093 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
1094 )
1095 .with_body(response_body())
1096 .with_status(501)
1097 .with_header("content-type", "application/json")
1098 .with_header("etag", "\"1000\"")
1099 .create();
1100
1101 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_MISSING);
1102
1103 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1104 distribution_id: "test-distro".to_string(),
1105 ..Default::default()
1106 });
1107 assert!(
1108 result.is_err(),
1109 "Should throw an error when a configuration has not been specified before filtering"
1110 );
1111 assert!(result
1112 .unwrap_err()
1113 .to_string()
1114 .contains("No search config v2 records received from remote settings"));
1115 changes_mock.expect(1).assert();
1116 m1.expect(1).assert();
1117 }
1118
1119 #[test]
1120 fn test_remote_settings_empty_search_config_overrides_filtered_without_error() {
1121 let changes_mock = mock_changes_endpoint();
1122 let m1 = mock(
1123 "GET",
1124 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
1125 )
1126 .with_body(response_body())
1127 .with_status(200)
1128 .with_header("content-type", "application/json")
1129 .with_header("etag", "\"1000\"")
1130 .create();
1131
1132 let m2 = mock(
1133 "GET",
1134 "/v1/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
1135 )
1136 .with_body(
1137 json!({
1138 "metadata": {
1139 "id": "search-config-overrides-v2",
1140 "last_modified": 1000,
1141 "bucket": "main",
1142 "signature": {
1143 "x5u": "fake",
1144 "signature": "fake",
1145 },
1146 },
1147 "timestamp": 1000,
1148 "changes": [
1149 ]})
1150 .to_string(),
1151 )
1152 .with_status(200)
1153 .with_header("content-type", "application/json")
1154 .with_header("etag", "\"1000\"")
1155 .create();
1156
1157 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_PRESENT);
1158
1159 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1160 distribution_id: "test-distro".to_string(),
1161 ..Default::default()
1162 });
1163 assert!(
1164 result.is_ok(),
1165 "Should have filtered the configuration using an empty search config overrides without causing an error. {:?}",
1166 result
1167 );
1168 changes_mock.expect(1).assert();
1169 m1.expect(1).assert();
1170 m2.expect(1).assert();
1171 }
1172
1173 #[test]
1174 fn test_remote_settings_search_config_overrides_records_is_none_throws_error() {
1175 let changes_mock = mock_changes_endpoint();
1176 let m1 = mock(
1177 "GET",
1178 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
1179 )
1180 .with_body(response_body())
1181 .with_status(200)
1182 .with_header("content-type", "application/json")
1183 .with_header("etag", "\"1000\"")
1184 .create();
1185
1186 let m2 = mock(
1187 "GET",
1188 "/v1/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
1189 )
1190 .with_body(response_body_overrides())
1191 .with_status(501)
1192 .with_header("content-type", "application/json")
1193 .with_header("etag", "\"1000\"")
1194 .create();
1195
1196 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_MISSING);
1197
1198 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1199 distribution_id: "test-distro".to_string(),
1200 ..Default::default()
1201 });
1202 assert!(
1203 result.is_err(),
1204 "Should throw an error when a configuration overrides has not been specified before filtering"
1205 );
1206 assert!(result
1207 .unwrap_err()
1208 .to_string()
1209 .contains("No search config overrides v2 records received from remote settings"));
1210 changes_mock.expect(1).assert();
1211 m1.expect(1).assert();
1212 m2.expect(1).assert();
1213 }
1214
1215 #[test]
1216 fn test_filter_with_remote_settings_overrides() {
1217 let changes_mock = mock_changes_endpoint();
1218 let m1 = mock(
1219 "GET",
1220 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
1221 )
1222 .with_body(response_body())
1223 .with_status(200)
1224 .with_header("content-type", "application/json")
1225 .with_header("etag", "\"1000\"")
1226 .create();
1227
1228 let m2 = mock(
1229 "GET",
1230 "/v1/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
1231 )
1232 .with_body(response_body_overrides())
1233 .with_status(200)
1234 .with_header("content-type", "application/json")
1235 .with_header("etag", "\"1000\"")
1236 .create();
1237
1238 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_PRESENT);
1239
1240 let override_test_engine = ExpectedEngine::minimal("test", "Test")
1241 .partner_code("overrides-partner-code")
1242 .click_url("https://example.com/click-url")
1243 .telemetry_suffix("overrides-telemetry-suffix")
1244 .search_base("https://example.com/search-overrides")
1245 .search_term_param_name("search")
1246 .search_params(vec![SearchUrlParam {
1247 name: "overrides-name".to_string(),
1248 value: Some("overrides-value".to_string()),
1249 enterprise_value: None,
1250 experiment_config: None,
1251 }])
1252 .build();
1253
1254 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1255 ..Default::default()
1256 });
1257
1258 assert!(
1259 result.is_ok(),
1260 "Should have filtered the configuration without error. {:?}",
1261 result
1262 );
1263 assert_eq!(
1264 result.unwrap().engines[0],
1265 override_test_engine.clone(),
1266 "Should have applied the overrides to the matching engine"
1267 );
1268 changes_mock.expect(1).assert();
1269 m1.expect(1).assert();
1270 m2.expect(1).assert();
1271 }
1272
1273 #[test]
1274 fn test_filter_with_remote_settings() {
1275 let changes_mock = mock_changes_endpoint();
1276
1277 let m = mock(
1278 "GET",
1279 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
1280 )
1281 .with_body(response_body())
1282 .with_status(200)
1283 .with_header("content-type", "application/json")
1284 .with_header("etag", "\"1000\"")
1285 .create();
1286
1287 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
1288
1289 let test_engine = ExpectedEngine::minimal("test", "Test").build();
1290 let private_default_fr_engine =
1291 ExpectedEngine::minimal("private-default-FR", "Private default FR").build();
1292 let distro_default_engine =
1293 ExpectedEngine::minimal("distro-default", "Distribution Default").build();
1294
1295 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1296 distribution_id: "test-distro".to_string(),
1297 ..Default::default()
1298 });
1299 assert!(
1300 result.is_ok(),
1301 "Should have filtered the configuration without error. {:?}",
1302 result
1303 );
1304 assert_eq!(
1305 result.unwrap(),
1306 RefinedSearchConfig {
1307 engines: vec![
1308 distro_default_engine.clone(),
1309 private_default_fr_engine.clone(),
1310 test_engine.clone(),
1311 ],
1312 app_default_engine_id: Some("distro-default".to_string()),
1313 app_private_default_engine_id: None
1314 },
1315 "Should have selected the default engine for the matching specific default"
1316 );
1317
1318 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1319 region: "fr".into(),
1320 distribution_id: String::new(),
1321 ..Default::default()
1322 });
1323 assert!(
1324 result.is_ok(),
1325 "Should have filtered the configuration without error. {:?}",
1326 result
1327 );
1328 assert_eq!(
1329 result.unwrap(),
1330 RefinedSearchConfig {
1331 engines: vec![
1332 test_engine.clone(),
1333 private_default_fr_engine.clone(),
1334 distro_default_engine.clone(),
1335 ],
1336 app_default_engine_id: Some("test".to_string()),
1337 app_private_default_engine_id: Some("private-default-FR".to_string())
1338 },
1339 "Should have selected the private default engine for the matching specific default"
1340 );
1341 changes_mock.expect(1).assert();
1342 m.expect(1).assert();
1343 }
1344
1345 #[test]
1346 fn test_filter_with_remote_settings_negotiate_locales() {
1347 let changes_mock = mock_changes_endpoint();
1348 let m = mock(
1349 "GET",
1350 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
1351 )
1352 .with_body(response_body_locales())
1353 .with_status(200)
1354 .with_header("content-type", "application/json")
1355 .with_header("etag", "\"1000\"")
1356 .create();
1357
1358 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
1359
1360 let result_de = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1361 locale: "de-AT".into(),
1362 ..Default::default()
1363 });
1364 assert!(
1365 result_de.is_ok(),
1366 "Should have filtered the configuration without error. {:?}",
1367 result_de
1368 );
1369
1370 assert_eq!(
1371 result_de.unwrap(),
1372 RefinedSearchConfig {
1373 engines: vec![ExpectedEngine::minimal("engine-de", "German Engine").build()],
1374 app_default_engine_id: None,
1375 app_private_default_engine_id: None,
1376 },
1377 "Should have selected the de engine when given de-AT which is not an available locale"
1378 );
1379
1380 let result_en = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1381 locale: "en-AU".to_string(),
1382 ..Default::default()
1383 });
1384 assert_eq!(
1385 result_en.unwrap(),
1386 RefinedSearchConfig {
1387 engines: vec![ExpectedEngine::minimal("engine-en-us", "English US Engine").build(),],
1388 app_default_engine_id: None,
1389 app_private_default_engine_id: None,
1390 },
1391 "Should have selected the en-us engine when given another english locale we don't support"
1392 );
1393 changes_mock.expect(1).assert();
1394 m.expect(1).assert();
1395 }
1396
1397 #[test]
1398 fn test_configuration_overrides_applied() {
1399 let selector = Arc::new(SearchEngineSelector::new());
1400
1401 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
1402 json!({
1403 "data": [
1404 test_helpers::overrides_engine(),
1405 { "identifier": "distro-default",
1407 "partnerCode": "distro-overrides-partner-code",
1408 "clickUrl": "https://example.com/click-url-distro",
1409 "urls": {
1410 "search": {
1411 "base": "https://example.com/search-distro",
1412 },
1413 },
1414 }
1415 ]
1416 })
1417 .to_string(),
1418 );
1419 let config_result = Arc::clone(&selector).set_search_config(
1420 json!({
1421 "data": [
1422 EngineRecord::minimal("overrides-engine", "Overrides Engine")
1423 .build(),
1424 EngineRecord::minimal("distro-default", "Distribution Default")
1425 .override_variants(Variant::new()
1426 .all_regions_and_locales()
1427 .telemetry_suffix("distro-telemetry-suffix"))
1428 .build(),
1429 ]
1430 })
1431 .to_string(),
1432 );
1433 config_result.expect("Should have set the configuration successfully");
1434 config_overrides_result.expect("Should have set the configuration overrides successfully");
1435
1436 let override_test_engine = ExpectedEngine::minimal("overrides-engine", "Overrides Engine")
1437 .partner_code("overrides-partner-code")
1438 .click_url("https://example.com/click-url")
1439 .telemetry_suffix("overrides-telemetry-suffix")
1440 .search_base("https://example.com/search-overrides")
1441 .search_params(vec![SearchUrlParam {
1442 name: "overrides-name".to_string(),
1443 value: Some("overrides-value".to_string()),
1444 enterprise_value: None,
1445 experiment_config: None,
1446 }])
1447 .build();
1448
1449 let override_distro_default_engine =
1450 ExpectedEngine::minimal("distro-default", "Distribution Default")
1451 .partner_code("distro-overrides-partner-code")
1452 .click_url("https://example.com/click-url-distro")
1453 .search_base("https://example.com/search-distro")
1454 .telemetry_suffix("distro-telemetry-suffix")
1455 .build();
1456
1457 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1458 ..Default::default()
1459 });
1460 assert!(
1461 result.is_ok(),
1462 "Should have filtered the configuration without error. {:?}",
1463 result
1464 );
1465
1466 assert_eq!(
1467 result.unwrap(),
1468 RefinedSearchConfig {
1469 engines: vec![
1470 override_distro_default_engine.clone(),
1471 override_test_engine.clone(),
1472 ],
1473 app_default_engine_id: None,
1474 app_private_default_engine_id: None
1475 },
1476 "Should have applied the overrides to the matching engine."
1477 );
1478 }
1479
1480 #[test]
1481 fn test_filter_engine_configuration_negotiate_locales() {
1482 let selector = Arc::new(SearchEngineSelector::new());
1483 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
1484 json!({ "data": [test_helpers::overrides_engine()] }).to_string(),
1485 );
1486
1487 let config_result = Arc::clone(&selector).set_search_config(
1488 json!({
1489 "data": [
1490 {
1491 "recordType": "availableLocales",
1492 "locales": ["de", "en-US"]
1493 },
1494 EngineRecord::minimal("engine-de", "German Engine")
1495 .override_variants(Variant::new()
1496 .locales(&["de"]))
1497 .build(),
1498 EngineRecord::minimal("engine-en-us", "English US Engine")
1499 .override_variants(Variant::new()
1500 .locales(&["en-US"]))
1501 .build(),
1502 ]
1503 })
1504 .to_string(),
1505 );
1506 config_result.expect("Should have set the configuration successfully");
1507 config_overrides_result.expect("Should have set the configuration overrides successfully");
1508
1509 let result_de = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1510 locale: "de-AT".into(),
1511 ..Default::default()
1512 });
1513 assert!(
1514 result_de.is_ok(),
1515 "Should have filtered the configuration without error. {:?}",
1516 result_de
1517 );
1518
1519 assert_eq!(
1520 result_de.unwrap(),
1521 RefinedSearchConfig {
1522 engines: vec![ExpectedEngine::minimal("engine-de", "German Engine").build(),],
1523 app_default_engine_id: None,
1524 app_private_default_engine_id: None,
1525 },
1526 "Should have selected the de engine when given de-AT which is not an available locale"
1527 );
1528
1529 let result_en = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1530 locale: "en-AU".to_string(),
1531 ..Default::default()
1532 });
1533 assert_eq!(
1534 result_en.unwrap(),
1535 RefinedSearchConfig {
1536 engines: vec![ExpectedEngine::minimal("engine-en-us", "English US Engine").build(),],
1537 app_default_engine_id: None,
1538 app_private_default_engine_id: None,
1539 },
1540 "Should have selected the en-us engine when given another english locale we don't support"
1541 );
1542 }
1543}