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::{types::*, SearchApiError};
162 use mockito::mock;
163 use pretty_assertions::assert_eq;
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_result = Arc::clone(&selector).set_search_config(
172 json!({
173 "data": [
174 {
175 "recordType": "engine",
176 "identifier": "test",
177 "base": {
178 "name": "Test",
179 "classification": "general",
180 "urls": {
181 "search": {
182 "base": "https://example.com",
183 "method": "GET"
184 }
185 }
186 },
187 "variants": [{
188 "environment": {
189 "allRegionsAndLocales": true,
190 "excludedRegions": []
191 }
192 }],
193 },
194 {
195 "recordType": "defaultEngines",
196 "globalDefault": "test"
197 }
198 ]
199 })
200 .to_string(),
201 );
202 assert!(
203 config_result.is_ok(),
204 "Should have set the configuration successfully. {:?}",
205 config_result
206 );
207 }
208
209 #[test]
210 fn test_set_config_should_allow_extra_fields() {
211 let selector = Arc::new(SearchEngineSelector::new());
212
213 let config_result = Arc::clone(&selector).set_search_config(
214 json!({
215 "data": [
216 {
217 "recordType": "engine",
218 "identifier": "test",
219 "base": {
220 "name": "Test",
221 "classification": "general",
222 "urls": {
223 "search": {
224 "base": "https://example.com",
225 "method": "GET",
226 "extraField1": true
227 }
228 },
229 "extraField2": "123"
230 },
231 "variants": [{
232 "environment": {
233 "allRegionsAndLocales": true
234 }
235 }],
236 "extraField3": ["foo"]
237 },
238 {
239 "recordType": "defaultEngines",
240 "globalDefault": "test",
241 "extraField4": {
242 "subField1": true
243 }
244 }
245 ]
246 })
247 .to_string(),
248 );
249 assert!(
250 config_result.is_ok(),
251 "Should have set the configuration successfully with extra fields. {:?}",
252 config_result
253 );
254 }
255
256 #[test]
257 fn test_set_config_should_ignore_unknown_record_types() {
258 let selector = Arc::new(SearchEngineSelector::new());
259
260 let config_result = Arc::clone(&selector).set_search_config(
261 json!({
262 "data": [
263 {
264 "recordType": "engine",
265 "identifier": "test",
266 "base": {
267 "name": "Test",
268 "classification": "general",
269 "urls": {
270 "search": {
271 "base": "https://example.com",
272 "method": "GET"
273 }
274 }
275 },
276 "variants": [{
277 "environment": {
278 "allRegionsAndLocales": true
279 }
280 }],
281 },
282 {
283 "recordType": "defaultEngines",
284 "globalDefault": "test"
285 },
286 {
287 "recordType": "unknown"
288 }
289 ]
290 })
291 .to_string(),
292 );
293 assert!(
294 config_result.is_ok(),
295 "Should have set the configuration successfully with unknown record types. {:?}",
296 config_result
297 );
298 }
299
300 #[test]
301 fn test_filter_engine_configuration_throws_without_config() {
302 let selector = Arc::new(SearchEngineSelector::new());
303
304 let result = selector.filter_engine_configuration(SearchUserEnvironment {
305 ..Default::default()
306 });
307
308 assert!(
309 result.is_err(),
310 "Should throw an error when a configuration has not been specified before filtering"
311 );
312 assert!(result
313 .unwrap_err()
314 .to_string()
315 .contains("Search configuration not specified"))
316 }
317
318 #[test]
319 fn test_filter_engine_configuration_throws_without_config_overrides() {
320 let selector = Arc::new(SearchEngineSelector::new());
321 let _ = Arc::clone(&selector).set_search_config(
322 json!({
323 "data": [
324 {
325 "recordType": "engine",
326 "identifier": "test",
327 "base": {
328 "name": "Test",
329 "classification": "general",
330 "urls": {
331 "search": {
332 "base": "https://example.com",
333 "method": "GET",
334 }
335 }
336 },
337 "variants": [{
338 "environment": {
339 "allRegionsAndLocales": true
340 }
341 }],
342 },
343 ]
344 })
345 .to_string(),
346 );
347
348 let result = selector.filter_engine_configuration(SearchUserEnvironment {
349 ..Default::default()
350 });
351
352 assert!(
353 result.is_err(),
354 "Should throw an error when a configuration overrides has not been specified before filtering"
355 );
356
357 assert!(result
358 .unwrap_err()
359 .to_string()
360 .contains("Search configuration overrides not specified"))
361 }
362
363 #[test]
364 fn test_filter_engine_configuration_returns_basic_engines() {
365 let selector = Arc::new(SearchEngineSelector::new());
366
367 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
368 json!({
369 "data": [
370 {
371 "identifier": "overrides-engine",
372 "partnerCode": "overrides-partner-code",
373 "clickUrl": "https://example.com/click-url",
374 "telemetrySuffix": "overrides-telemetry-suffix",
375 "urls": {
376 "search": {
377 "base": "https://example.com/search-overrides",
378 "method": "GET",
379 "params": []
380 }
381 }
382 }
383 ]
384 })
385 .to_string(),
386 );
387 let config_result = Arc::clone(&selector).set_search_config(
388 json!({
389 "data": [
390 {
391 "recordType": "engine",
392 "identifier": "test1",
393 "base": {
394 "name": "Test 1",
395 "classification": "general",
396 "urls": {
397 "search": {
398 "base": "https://example.com/1",
399 "method": "GET",
400 "params": [{
401 "name": "search-name",
402 "enterpriseValue": "enterprise-value",
403 }],
404 "searchTermParamName": "q"
405 },
406 "suggestions": {
407 "base": "https://example.com/suggestions",
408 "method": "POST",
409 "params": [{
410 "name": "suggestion-name",
411 "value": "suggestion-value",
412 }],
413 "searchTermParamName": "suggest"
414 },
415 "trending": {
416 "base": "https://example.com/trending",
417 "method": "GET",
418 "params": [{
419 "name": "trending-name",
420 "experimentConfig": "trending-experiment-value",
421 }]
422 },
423 "searchForm": {
424 "base": "https://example.com/search-form",
425 "method": "GET",
426 "params": [{
427 "name": "search-form-name",
428 "value": "search-form-value",
429 }]
430 }
431 }
432 },
433 "variants": [{
434 "environment": {
435 "allRegionsAndLocales": true
436 }
437 }],
438 },
439 {
440 "recordType": "engine",
441 "identifier": "test2",
442 "base": {
443 "name": "Test 2",
444 "urls": {
446 "search": {
447 "base": "https://example.com/2",
448 "method": "GET",
449 "searchTermParamName": "search"
450 }
451 }
452 },
453 "variants": [{
454 "environment": {
455 "allRegionsAndLocales": true
456 }
457 }],
458 },
459 {
460 "recordType": "defaultEngines",
461 "globalDefault": "test1",
462 "globalDefaultPrivate": "test2"
463 }
464 ]
465 })
466 .to_string(),
467 );
468 assert!(
469 config_result.is_ok(),
470 "Should have set the configuration successfully. {:?}",
471 config_result
472 );
473 assert!(
474 config_overrides_result.is_ok(),
475 "Should have set the configuration overrides successfully. {:?}",
476 config_overrides_result
477 );
478
479 let result = selector.filter_engine_configuration(SearchUserEnvironment {
480 ..Default::default()
481 });
482
483 assert!(
484 result.is_ok(),
485 "Should have filtered the configuration without error. {:?}",
486 result
487 );
488 assert_eq!(
489 result.unwrap(),
490 RefinedSearchConfig {
491 engines: vec!(
492 SearchEngineDefinition {
493 charset: "UTF-8".to_string(),
494 classification: SearchEngineClassification::General,
495 identifier: "test1".to_string(),
496 name: "Test 1".to_string(),
497 urls: SearchEngineUrls {
498 search: SearchEngineUrl {
499 base: "https://example.com/1".to_string(),
500 method: "GET".to_string(),
501 params: vec![SearchUrlParam {
502 name: "search-name".to_string(),
503 value: None,
504 enterprise_value: Some("enterprise-value".to_string()),
505 experiment_config: None
506 }],
507 search_term_param_name: Some("q".to_string())
508 },
509 suggestions: Some(SearchEngineUrl {
510 base: "https://example.com/suggestions".to_string(),
511 method: "POST".to_string(),
512 params: vec![SearchUrlParam {
513 name: "suggestion-name".to_string(),
514 value: Some("suggestion-value".to_string()),
515 enterprise_value: None,
516 experiment_config: None
517 }],
518 search_term_param_name: Some("suggest".to_string())
519 }),
520 trending: Some(SearchEngineUrl {
521 base: "https://example.com/trending".to_string(),
522 method: "GET".to_string(),
523 params: vec![SearchUrlParam {
524 name: "trending-name".to_string(),
525 value: None,
526 enterprise_value: None,
527 experiment_config: Some(
528 "trending-experiment-value".to_string()
529 )
530 }],
531 search_term_param_name: None
532 }),
533 search_form: Some(SearchEngineUrl {
534 base: "https://example.com/search-form".to_string(),
535 method: "GET".to_string(),
536 params: vec![SearchUrlParam {
537 name: "search-form-name".to_string(),
538 value: Some("search-form-value".to_string()),
539 experiment_config: None,
540 enterprise_value: None,
541 }],
542 search_term_param_name: None,
543 }),
544 },
545 ..Default::default()
546 },
547 SearchEngineDefinition {
548 aliases: Vec::new(),
549 charset: "UTF-8".to_string(),
550 classification: SearchEngineClassification::Unknown,
551 identifier: "test2".to_string(),
552 is_new_until: None,
553 name: "Test 2".to_string(),
554 optional: false,
555 order_hint: None,
556 partner_code: String::new(),
557 telemetry_suffix: String::new(),
558 urls: SearchEngineUrls {
559 search: SearchEngineUrl {
560 base: "https://example.com/2".to_string(),
561 method: "GET".to_string(),
562 params: Vec::new(),
563 search_term_param_name: Some("search".to_string())
564 },
565 suggestions: None,
566 trending: None,
567 search_form: None
568 },
569 click_url: None,
570 }
571 ),
572 app_default_engine_id: Some("test1".to_string()),
573 app_private_default_engine_id: Some("test2".to_string())
574 }
575 )
576 }
577
578 #[test]
579 fn test_filter_engine_configuration_handles_basic_variants() {
580 let selector = Arc::new(SearchEngineSelector::new());
581
582 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
583 json!({
584 "data": [
585 {
586 "identifier": "overrides-engine",
587 "partnerCode": "overrides-partner-code",
588 "clickUrl": "https://example.com/click-url",
589 "telemetrySuffix": "overrides-telemetry-suffix",
590 "urls": {
591 "search": {
592 "base": "https://example.com/search-overrides",
593 "method": "GET",
594 "params": []
595 }
596 }
597 }
598 ]
599 })
600 .to_string(),
601 );
602 let config_result = Arc::clone(&selector).set_search_config(
603 json!({
604 "data": [
605 {
606 "recordType": "engine",
607 "identifier": "test1",
608 "base": {
609 "name": "Test 1",
610 "classification": "general",
611 "partnerCode": "star",
612 "urls": {
613 "search": {
614 "base": "https://example.com/1",
615 "method": "GET",
616 "searchTermParamName": "q"
617 },
618 "suggestions": {
619 "base": "https://example.com/suggestions",
620 "method": "POST",
621 "params": [{
622 "name": "type",
623 "value": "space",
624 }],
625 "searchTermParamName": "suggest"
626 },
627 "trending": {
628 "base": "https://example.com/trending",
629 "method": "GET",
630 "params": [{
631 "name": "area",
632 "experimentConfig": "area-param",
633 }]
634 },
635 "searchForm": {
636 "base": "https://example.com/search-form",
637 "method": "GET",
638 "params": [{
639 "name": "search-form-name",
640 "value": "search-form-value",
641 }]
642 }
643 }
644 },
645 "variants": [{
646 "environment": {
647 "allRegionsAndLocales": true
648 },
649 },
650 {
651 "environment": {
652 "regions": ["FR"]
653 },
654 "urls": {
655 "search": {
656 "method": "POST",
657 "params": [{
658 "name": "mission",
659 "value": "ongoing"
660 }]
661 }
662 }
663 }],
664 },
665 {
666 "recordType": "engine",
667 "identifier": "test2",
668 "base": {
669 "name": "Test 2",
670 "classification": "general",
671 "urls": {
672 "search": {
673 "base": "https://example.com/2",
674 "method": "GET",
675 "searchTermParamName": "search"
676 }
677 }
678 },
679 "variants": [{
680 "environment": {
681 "allRegionsAndLocales": true
682 },
683 "partnerCode": "ship",
684 "telemetrySuffix": "E",
685 "optional": true
686 }],
687 },
688 {
689 "recordType": "defaultEngines",
690 "globalDefault": "test1",
691 "globalDefaultPrivate": "test2"
692 }
693 ]
694 })
695 .to_string(),
696 );
697 assert!(
698 config_result.is_ok(),
699 "Should have set the configuration successfully. {:?}",
700 config_result
701 );
702 assert!(
703 config_overrides_result.is_ok(),
704 "Should have set the configuration overrides successfully. {:?}",
705 config_overrides_result
706 );
707
708 let result = selector.filter_engine_configuration(SearchUserEnvironment {
709 region: "FR".into(),
710 ..Default::default()
711 });
712
713 assert!(
714 result.is_ok(),
715 "Should have filtered the configuration without error. {:?}",
716 result
717 );
718 assert_eq!(
719 result.unwrap(),
720 RefinedSearchConfig {
721 engines: vec!(
722 SearchEngineDefinition {
723 charset: "UTF-8".to_string(),
724 classification: SearchEngineClassification::General,
725 identifier: "test1".to_string(),
726 name: "Test 1".to_string(),
727 partner_code: "star".to_string(),
728 urls: SearchEngineUrls {
729 search: SearchEngineUrl {
730 base: "https://example.com/1".to_string(),
731 method: "POST".to_string(),
732 params: vec![SearchUrlParam {
733 name: "mission".to_string(),
734 value: Some("ongoing".to_string()),
735 enterprise_value: None,
736 experiment_config: None
737 }],
738 search_term_param_name: Some("q".to_string())
739 },
740 suggestions: Some(SearchEngineUrl {
741 base: "https://example.com/suggestions".to_string(),
742 method: "POST".to_string(),
743 params: vec![SearchUrlParam {
744 name: "type".to_string(),
745 value: Some("space".to_string()),
746 enterprise_value: None,
747 experiment_config: None
748 }],
749 search_term_param_name: Some("suggest".to_string())
750 }),
751 trending: Some(SearchEngineUrl {
752 base: "https://example.com/trending".to_string(),
753 method: "GET".to_string(),
754 params: vec![SearchUrlParam {
755 name: "area".to_string(),
756 value: None,
757 enterprise_value: None,
758 experiment_config: Some("area-param".to_string())
759 }],
760 search_term_param_name: None
761 }),
762 search_form: Some(SearchEngineUrl {
763 base: "https://example.com/search-form".to_string(),
764 method: "GET".to_string(),
765 params: vec![SearchUrlParam {
766 name: "search-form-name".to_string(),
767 value: Some("search-form-value".to_string()),
768 enterprise_value: None,
769 experiment_config: None,
770 }],
771 search_term_param_name: None,
772 }),
773 },
774 ..Default::default()
775 },
776 SearchEngineDefinition {
777 charset: "UTF-8".to_string(),
778 classification: SearchEngineClassification::General,
779 identifier: "test2".to_string(),
780 name: "Test 2".to_string(),
781 optional: true,
782 partner_code: "ship".to_string(),
783 telemetry_suffix: "E".to_string(),
784 urls: SearchEngineUrls {
785 search: SearchEngineUrl {
786 base: "https://example.com/2".to_string(),
787 method: "GET".to_string(),
788 params: Vec::new(),
789 search_term_param_name: Some("search".to_string())
790 },
791 ..Default::default()
792 },
793 ..Default::default()
794 }
795 ),
796 app_default_engine_id: Some("test1".to_string()),
797 app_private_default_engine_id: Some("test2".to_string())
798 }
799 )
800 }
801
802 #[test]
803 fn test_filter_engine_configuration_handles_basic_subvariants() {
804 let selector = Arc::new(SearchEngineSelector::new());
805
806 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
807 json!({
808 "data": [
809 {
810 "identifier": "overrides-engine",
811 "partnerCode": "overrides-partner-code",
812 "clickUrl": "https://example.com/click-url",
813 "telemetrySuffix": "overrides-telemetry-suffix",
814 "urls": {
815 "search": {
816 "base": "https://example.com/search-overrides",
817 "method": "GET",
818 "params": []
819 }
820 }
821 }
822 ]
823 })
824 .to_string(),
825 );
826 let config_result = Arc::clone(&selector).set_search_config(
827 json!({
828 "data": [
829 {
830 "recordType": "engine",
831 "identifier": "test1",
832 "base": {
833 "name": "Test 1",
834 "partnerCode": "star",
835 "urls": {
836 "search": {
837 "base": "https://example.com/1",
838 "method": "GET",
839 "searchTermParamName": "q"
840 },
841 "suggestions": {
842 "base": "https://example.com/suggestions",
843 "method": "POST",
844 "params": [{
845 "name": "type",
846 "value": "space",
847 }],
848 "searchTermParamName": "suggest"
849 },
850 "trending": {
851 "base": "https://example.com/trending",
852 "method": "GET",
853 "params": [{
854 "name": "area",
855 "experimentConfig": "area-param",
856 }]
857 },
858 "searchForm": {
859 "base": "https://example.com/search-form",
860 "method": "GET",
861 "params": [{
862 "name": "search-form-name",
863 "value": "search-form-value",
864 }]
865 }
866 }
867 },
868 "variants": [{
869 "environment": {
870 "allRegionsAndLocales": true
871 },
872 },
873 {
874 "environment": {
875 "regions": ["FR"]
876 },
877 "urls": {
878 "search": {
879 "method": "POST",
880 "params": [{
881 "name": "variant-param-name",
882 "value": "variant-param-value"
883 }]
884 }
885 },
886 "subVariants": [
887 {
888 "environment": {
889 "locales": ["fr"]
890 },
891 "partnerCode": "fr-partner-code",
892 "telemetrySuffix": "fr-telemetry-suffix"
893 },
894 {
895 "environment": {
896 "locales": ["en-CA"]
897 },
898 "urls": {
899 "search": {
900 "method": "GET",
901 "params": [{
902 "name": "en-ca-param-name",
903 "value": "en-ca-param-value"
904 }]
905 }
906 },
907 }
908 ]
909 }],
910 },
911 {
912 "recordType": "defaultEngines",
913 "globalDefault": "test1"
914 },
915 {
916 "recordType": "availableLocales",
917 "locales": ["en-CA", "fr"]
918 }
919 ]
920 })
921 .to_string(),
922 );
923 assert!(
924 config_result.is_ok(),
925 "Should have set the configuration successfully. {:?}",
926 config_result
927 );
928 assert!(
929 config_overrides_result.is_ok(),
930 "Should have set the configuration overrides successfully. {:?}",
931 config_overrides_result
932 );
933
934 let mut result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
935 region: "FR".into(),
936 locale: "fr".into(),
937 ..Default::default()
938 });
939
940 assert!(
941 result.is_ok(),
942 "Should have filtered the configuration without error. {:?}",
943 result
944 );
945 assert_eq!(
946 result.unwrap(),
947 RefinedSearchConfig {
948 engines: vec!(SearchEngineDefinition {
949 charset: "UTF-8".to_string(),
950 identifier: "test1".to_string(),
951 name: "Test 1".to_string(),
952 partner_code: "fr-partner-code".to_string(),
953 telemetry_suffix: "fr-telemetry-suffix".to_string(),
954 urls: SearchEngineUrls {
955 search: SearchEngineUrl {
956 base: "https://example.com/1".to_string(),
957 method: "POST".to_string(),
958 params: vec![SearchUrlParam {
959 name: "variant-param-name".to_string(),
960 value: Some("variant-param-value".to_string()),
961 enterprise_value: None,
962 experiment_config: None
963 }],
964 search_term_param_name: Some("q".to_string())
965 },
966 suggestions: Some(SearchEngineUrl {
967 base: "https://example.com/suggestions".to_string(),
968 method: "POST".to_string(),
969 params: vec![SearchUrlParam {
970 name: "type".to_string(),
971 value: Some("space".to_string()),
972 enterprise_value: None,
973 experiment_config: None
974 }],
975 search_term_param_name: Some("suggest".to_string())
976 }),
977 trending: Some(SearchEngineUrl {
978 base: "https://example.com/trending".to_string(),
979 method: "GET".to_string(),
980 params: vec![SearchUrlParam {
981 name: "area".to_string(),
982 value: None,
983 enterprise_value: None,
984 experiment_config: Some("area-param".to_string())
985 }],
986 search_term_param_name: None
987 }),
988 search_form: Some(SearchEngineUrl {
989 base: "https://example.com/search-form".to_string(),
990 method: "GET".to_string(),
991 params: vec![SearchUrlParam {
992 name: "search-form-name".to_string(),
993 value: Some("search-form-value".to_string()),
994 enterprise_value: None,
995 experiment_config: None,
996 }],
997 search_term_param_name: None,
998 }),
999 },
1000 ..Default::default()
1001 }),
1002 app_default_engine_id: Some("test1".to_string()),
1003 app_private_default_engine_id: None
1004 },
1005 "Should have correctly matched and merged the fr locale sub-variant."
1006 );
1007
1008 result = selector.filter_engine_configuration(SearchUserEnvironment {
1009 region: "FR".into(),
1010 locale: "en-CA".into(),
1011 ..Default::default()
1012 });
1013
1014 assert!(
1015 result.is_ok(),
1016 "Should have filtered the configuration without error. {:?}",
1017 result
1018 );
1019 assert_eq!(
1020 result.unwrap(),
1021 RefinedSearchConfig {
1022 engines: vec!(SearchEngineDefinition {
1023 charset: "UTF-8".to_string(),
1024 identifier: "test1".to_string(),
1025 name: "Test 1".to_string(),
1026 partner_code: "star".to_string(),
1027 urls: SearchEngineUrls {
1028 search: SearchEngineUrl {
1029 base: "https://example.com/1".to_string(),
1030 method: "GET".to_string(),
1031 params: vec![SearchUrlParam {
1032 name: "en-ca-param-name".to_string(),
1033 value: Some("en-ca-param-value".to_string()),
1034 enterprise_value: None,
1035 experiment_config: None
1036 }],
1037 search_term_param_name: Some("q".to_string())
1038 },
1039 suggestions: Some(SearchEngineUrl {
1040 base: "https://example.com/suggestions".to_string(),
1041 method: "POST".to_string(),
1042 params: vec![SearchUrlParam {
1043 name: "type".to_string(),
1044 value: Some("space".to_string()),
1045 enterprise_value: None,
1046 experiment_config: None
1047 }],
1048 search_term_param_name: Some("suggest".to_string())
1049 }),
1050 trending: Some(SearchEngineUrl {
1051 base: "https://example.com/trending".to_string(),
1052 method: "GET".to_string(),
1053 params: vec![SearchUrlParam {
1054 name: "area".to_string(),
1055 value: None,
1056 enterprise_value: None,
1057 experiment_config: Some("area-param".to_string())
1058 }],
1059 search_term_param_name: None
1060 }),
1061 search_form: Some(SearchEngineUrl {
1062 base: "https://example.com/search-form".to_string(),
1063 method: "GET".to_string(),
1064 params: vec![SearchUrlParam {
1065 name: "search-form-name".to_string(),
1066 value: Some("search-form-value".to_string()),
1067 enterprise_value: None,
1068 experiment_config: None,
1069 }],
1070 search_term_param_name: None,
1071 }),
1072 },
1073 ..Default::default()
1074 }),
1075 app_default_engine_id: Some("test1".to_string()),
1076 app_private_default_engine_id: None
1077 },
1078 "Should have correctly matched and merged the en-CA locale sub-variant."
1079 );
1080 }
1081
1082 #[test]
1083 fn test_filter_engine_configuration_handles_environments() {
1084 let selector = Arc::new(SearchEngineSelector::new());
1085
1086 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
1087 json!({
1088 "data": [
1089 {
1090 "identifier": "overrides-engine",
1091 "partnerCode": "overrides-partner-code",
1092 "clickUrl": "https://example.com/click-url",
1093 "telemetrySuffix": "overrides-telemetry-suffix",
1094 "urls": {
1095 "search": {
1096 "base": "https://example.com/search-overrides",
1097 "method": "GET",
1098 "params": []
1099 }
1100 }
1101 }
1102 ]
1103 })
1104 .to_string(),
1105 );
1106 let config_result = Arc::clone(&selector).set_search_config(
1107 json!({
1108 "data": [
1109 {
1110 "recordType": "engine",
1111 "identifier": "test1",
1112 "base": {
1113 "name": "Test 1",
1114 "classification": "general",
1115 "urls": {
1116 "search": {
1117 "base": "https://example.com/1",
1118 "method": "GET",
1119 "searchTermParamName": "q"
1120 }
1121 }
1122 },
1123 "variants": [{
1124 "environment": {
1125 "allRegionsAndLocales": true
1126 }
1127 }],
1128 },
1129 {
1130 "recordType": "engine",
1131 "identifier": "test2",
1132 "base": {
1133 "name": "Test 2",
1134 "classification": "general",
1135 "urls": {
1136 "search": {
1137 "base": "https://example.com/2",
1138 "method": "GET",
1139 "searchTermParamName": "search"
1140 }
1141 }
1142 },
1143 "variants": [{
1144 "environment": {
1145 "applications": ["firefox-android", "focus-ios"]
1146 }
1147 }],
1148 },
1149 {
1150 "recordType": "engine",
1151 "identifier": "test3",
1152 "base": {
1153 "name": "Test 3",
1154 "classification": "general",
1155 "urls": {
1156 "search": {
1157 "base": "https://example.com/3",
1158 "method": "GET",
1159 "searchTermParamName": "trek"
1160 }
1161 }
1162 },
1163 "variants": [{
1164 "environment": {
1165 "distributions": ["starship"]
1166 }
1167 }],
1168 },
1169 {
1170 "recordType": "defaultEngines",
1171 "globalDefault": "test1",
1172 }
1173 ]
1174 })
1175 .to_string(),
1176 );
1177 assert!(
1178 config_result.is_ok(),
1179 "Should have set the configuration successfully. {:?}",
1180 config_result
1181 );
1182 assert!(
1183 config_overrides_result.is_ok(),
1184 "Should have set the configuration overrides successfully. {:?}",
1185 config_overrides_result
1186 );
1187
1188 let mut result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1189 distribution_id: String::new(),
1190 app_name: SearchApplicationName::Firefox,
1191 ..Default::default()
1192 });
1193
1194 assert!(
1195 result.is_ok(),
1196 "Should have filtered the configuration without error. {:?}",
1197 result
1198 );
1199 assert_eq!(
1200 result.unwrap(),
1201 RefinedSearchConfig {
1202 engines: vec!(
1203 SearchEngineDefinition {
1204 charset: "UTF-8".to_string(),
1205 classification: SearchEngineClassification::General,
1206 identifier: "test1".to_string(),
1207 name: "Test 1".to_string(),
1208 urls: SearchEngineUrls {
1209 search: SearchEngineUrl {
1210 base: "https://example.com/1".to_string(),
1211 method: "GET".to_string(),
1212 params: Vec::new(),
1213 search_term_param_name: Some("q".to_string())
1214 },
1215 ..Default::default()
1216 },
1217 ..Default::default()
1218 },
1219 ),
1220 app_default_engine_id: Some("test1".to_string()),
1221 app_private_default_engine_id: None
1222 }, "Should have selected test1 for all matching locales, as the environments do not match for the other two"
1223 );
1224
1225 result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1226 distribution_id: String::new(),
1227 app_name: SearchApplicationName::FocusIos,
1228 ..Default::default()
1229 });
1230
1231 assert!(
1232 result.is_ok(),
1233 "Should have filtered the configuration without error. {:?}",
1234 result
1235 );
1236 assert_eq!(
1237 result.unwrap(),
1238 RefinedSearchConfig {
1239 engines: vec!(
1240 SearchEngineDefinition {
1241 charset: "UTF-8".to_string(),
1242 classification: SearchEngineClassification::General,
1243 identifier: "test1".to_string(),
1244 name: "Test 1".to_string(),
1245 urls: SearchEngineUrls {
1246 search: SearchEngineUrl {
1247 base: "https://example.com/1".to_string(),
1248 method: "GET".to_string(),
1249 params: Vec::new(),
1250 search_term_param_name: Some("q".to_string())
1251 },
1252 ..Default::default()
1253 },
1254 ..Default::default()
1255 },
1256 SearchEngineDefinition {
1257 charset: "UTF-8".to_string(),
1258 classification: SearchEngineClassification::General,
1259 identifier: "test2".to_string(),
1260 name: "Test 2".to_string(),
1261 urls: SearchEngineUrls {
1262 search: SearchEngineUrl {
1263 base: "https://example.com/2".to_string(),
1264 method: "GET".to_string(),
1265 params: Vec::new(),
1266 search_term_param_name: Some("search".to_string())
1267 },
1268 ..Default::default()
1269 },
1270 ..Default::default()
1271 },
1272 ),
1273 app_default_engine_id: Some("test1".to_string()),
1274 app_private_default_engine_id: None
1275 },
1276 "Should have selected test1 for all matching locales and test2 for matching Focus IOS"
1277 );
1278
1279 result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1280 distribution_id: "starship".to_string(),
1281 app_name: SearchApplicationName::Firefox,
1282 ..Default::default()
1283 });
1284
1285 assert!(
1286 result.is_ok(),
1287 "Should have filtered the configuration without error. {:?}",
1288 result
1289 );
1290 assert_eq!(
1291 result.unwrap(),
1292 RefinedSearchConfig {
1293 engines: vec!(
1294 SearchEngineDefinition {
1295 charset: "UTF-8".to_string(),
1296 classification: SearchEngineClassification::General,
1297 identifier: "test1".to_string(),
1298 name: "Test 1".to_string(),
1299 urls: SearchEngineUrls {
1300 search: SearchEngineUrl {
1301 base: "https://example.com/1".to_string(),
1302 method: "GET".to_string(),
1303 params: Vec::new(),
1304 search_term_param_name: Some("q".to_string())
1305 },
1306 ..Default::default()
1307 },
1308 ..Default::default()
1309 },
1310 SearchEngineDefinition {
1311 charset: "UTF-8".to_string(),
1312 classification: SearchEngineClassification::General,
1313 identifier: "test3".to_string(),
1314 name: "Test 3".to_string(),
1315 urls: SearchEngineUrls {
1316 search: SearchEngineUrl {
1317 base: "https://example.com/3".to_string(),
1318 method: "GET".to_string(),
1319 params: Vec::new(),
1320 search_term_param_name: Some("trek".to_string())
1321 },
1322 ..Default::default()
1323 },
1324 ..Default::default()
1325 },
1326 ),
1327 app_default_engine_id: Some("test1".to_string()),
1328 app_private_default_engine_id: None
1329 }, "Should have selected test1 for all matching locales and test3 for matching the distribution id"
1330 );
1331 }
1332
1333 #[test]
1334 fn test_set_config_should_handle_default_engines() {
1335 let selector = Arc::new(SearchEngineSelector::new());
1336
1337 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
1338 json!({
1339 "data": [
1340 {
1341 "identifier": "overrides-engine",
1342 "partnerCode": "overrides-partner-code",
1343 "clickUrl": "https://example.com/click-url",
1344 "telemetrySuffix": "overrides-telemetry-suffix",
1345 "urls": {
1346 "search": {
1347 "base": "https://example.com/search-overrides",
1348 "method": "GET",
1349 "params": []
1350 }
1351 }
1352 }
1353 ]
1354 })
1355 .to_string(),
1356 );
1357 let config_result = Arc::clone(&selector).set_search_config(
1358 json!({
1359 "data": [
1360 {
1361 "recordType": "engine",
1362 "identifier": "test",
1363 "base": {
1364 "name": "Test",
1365 "classification": "general",
1366 "urls": {
1367 "search": {
1368 "base": "https://example.com",
1369 "method": "GET",
1370 }
1371 }
1372 },
1373 "variants": [{
1374 "environment": {
1375 "allRegionsAndLocales": true
1376 }
1377 }],
1378 },
1379 {
1380 "recordType": "engine",
1381 "identifier": "distro-default",
1382 "base": {
1383 "name": "Distribution Default",
1384 "classification": "general",
1385 "urls": {
1386 "search": {
1387 "base": "https://example.com",
1388 "method": "GET"
1389 }
1390 }
1391 },
1392 "variants": [{
1393 "environment": {
1394 "allRegionsAndLocales": true
1395 }
1396 }],
1397 },
1398 {
1399 "recordType": "engine",
1400 "identifier": "private-default-FR",
1401 "base": {
1402 "name": "Private default FR",
1403 "classification": "general",
1404 "urls": {
1405 "search": {
1406 "base": "https://example.com",
1407 "method": "GET"
1408 }
1409 }
1410 },
1411 "variants": [{
1412 "environment": {
1413 "allRegionsAndLocales": true,
1414 }
1415 }],
1416 },
1417 {
1418 "recordType": "defaultEngines",
1419 "globalDefault": "test",
1420 "specificDefaults": [{
1421 "environment": {
1422 "distributions": ["test-distro"],
1423 },
1424 "default": "distro-default"
1425 }, {
1426 "environment": {
1427 "regions": ["fr"]
1428 },
1429 "defaultPrivate": "private-default-FR"
1430 }]
1431 }
1432 ]
1433 })
1434 .to_string(),
1435 );
1436 assert!(
1437 config_result.is_ok(),
1438 "Should have set the configuration successfully. {:?}",
1439 config_result
1440 );
1441 assert!(
1442 config_overrides_result.is_ok(),
1443 "Should have set the configuration overrides successfully. {:?}",
1444 config_overrides_result
1445 );
1446
1447 let test_engine = SearchEngineDefinition {
1448 charset: "UTF-8".to_string(),
1449 classification: SearchEngineClassification::General,
1450 identifier: "test".to_string(),
1451 name: "Test".to_string(),
1452 urls: SearchEngineUrls {
1453 search: SearchEngineUrl {
1454 base: "https://example.com".to_string(),
1455 method: "GET".to_string(),
1456 params: Vec::new(),
1457 search_term_param_name: None,
1458 },
1459 ..Default::default()
1460 },
1461 ..Default::default()
1462 };
1463 let distro_default_engine = SearchEngineDefinition {
1464 charset: "UTF-8".to_string(),
1465 classification: SearchEngineClassification::General,
1466 identifier: "distro-default".to_string(),
1467 name: "Distribution Default".to_string(),
1468 urls: SearchEngineUrls {
1469 search: SearchEngineUrl {
1470 base: "https://example.com".to_string(),
1471 method: "GET".to_string(),
1472 params: Vec::new(),
1473 search_term_param_name: None,
1474 },
1475 ..Default::default()
1476 },
1477 ..Default::default()
1478 };
1479 let private_default_fr_engine = SearchEngineDefinition {
1480 charset: "UTF-8".to_string(),
1481 classification: SearchEngineClassification::General,
1482 identifier: "private-default-FR".to_string(),
1483 name: "Private default FR".to_string(),
1484 urls: SearchEngineUrls {
1485 search: SearchEngineUrl {
1486 base: "https://example.com".to_string(),
1487 method: "GET".to_string(),
1488 params: Vec::new(),
1489 search_term_param_name: None,
1490 },
1491 ..Default::default()
1492 },
1493 ..Default::default()
1494 };
1495
1496 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1497 distribution_id: "test-distro".to_string(),
1498 ..Default::default()
1499 });
1500 assert!(
1501 result.is_ok(),
1502 "Should have filtered the configuration without error. {:?}",
1503 result
1504 );
1505 assert_eq!(
1506 result.unwrap(),
1507 RefinedSearchConfig {
1508 engines: vec![
1509 distro_default_engine.clone(),
1510 private_default_fr_engine.clone(),
1511 test_engine.clone(),
1512 ],
1513 app_default_engine_id: Some("distro-default".to_string()),
1514 app_private_default_engine_id: None
1515 },
1516 "Should have selected the default engine for the matching specific default"
1517 );
1518
1519 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1520 region: "fr".into(),
1521 distribution_id: String::new(),
1522 ..Default::default()
1523 });
1524 assert!(
1525 result.is_ok(),
1526 "Should have filtered the configuration without error. {:?}",
1527 result
1528 );
1529 assert_eq!(
1530 result.unwrap(),
1531 RefinedSearchConfig {
1532 engines: vec![
1533 test_engine,
1534 private_default_fr_engine,
1535 distro_default_engine,
1536 ],
1537 app_default_engine_id: Some("test".to_string()),
1538 app_private_default_engine_id: Some("private-default-FR".to_string())
1539 },
1540 "Should have selected the private default engine for the matching specific default"
1541 );
1542 }
1543
1544 #[test]
1545 fn test_filter_engine_orders() {
1546 let selector = Arc::new(SearchEngineSelector::new());
1547
1548 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
1549 json!({
1550 "data": [
1551 {
1552 "identifier": "overrides-engine",
1553 "partnerCode": "overrides-partner-code",
1554 "clickUrl": "https://example.com/click-url",
1555 "telemetrySuffix": "overrides-telemetry-suffix",
1556 "urls": {
1557 "search": {
1558 "base": "https://example.com/search-overrides",
1559 "method": "GET",
1560 "params": []
1561 }
1562 }
1563 }
1564 ]
1565 })
1566 .to_string(),
1567 );
1568 let engine_order_config = Arc::clone(&selector).set_search_config(
1569 json!({
1570 "data": [
1571 {
1572 "recordType": "engine",
1573 "identifier": "after-defaults",
1574 "base": {
1575 "name": "after-defaults",
1576 "classification": "general",
1577 "urls": {
1578 "search": {
1579 "base": "https://example.com",
1580 "method": "GET"
1581 }
1582 }
1583 },
1584 "variants": [{
1585 "environment": {
1586 "allRegionsAndLocales": true,
1587 }
1588 }],
1589 },
1590 {
1591 "recordType": "engine",
1592 "identifier": "b-engine",
1593 "base": {
1594 "name": "First Alphabetical",
1595 "classification": "general",
1596 "urls": {
1597 "search": {
1598 "base": "https://example.com",
1599 "method": "GET"
1600 }
1601 }
1602 },
1603 "variants": [{
1604 "environment": {
1605 "allRegionsAndLocales": true
1606 }
1607 }],
1608 },
1609 {
1610 "recordType": "engine",
1611 "identifier": "a-engine",
1612 "base": {
1613 "name": "Last Alphabetical",
1614 "classification": "general",
1615 "urls": {
1616 "search": {
1617 "base": "https://example.com",
1618 "method": "GET"
1619 }
1620 }
1621 },
1622 "variants": [{
1623 "environment": {
1624 "allRegionsAndLocales": true,
1625 }
1626 }],
1627 },
1628 {
1629 "recordType": "engine",
1630 "identifier": "default-engine",
1631 "base": {
1632 "name": "default-engine",
1633 "classification": "general",
1634 "urls": {
1635 "search": {
1636 "base": "https://example.com",
1637 "method": "GET"
1638 }
1639 }
1640 },
1641 "variants": [{
1642 "environment": {
1643 "allRegionsAndLocales": true,
1644 }
1645 }],
1646 },
1647 {
1648 "recordType": "engine",
1649 "identifier": "default-private-engine",
1650 "base": {
1651 "name": "default-private-engine",
1652 "classification": "general",
1653 "urls": {
1654 "search": {
1655 "base": "https://example.com",
1656 "method": "GET"
1657 }
1658 }
1659 },
1660 "variants": [{
1661 "environment": {
1662 "allRegionsAndLocales": true,
1663 }
1664 }],
1665 },
1666 {
1667 "recordType": "defaultEngines",
1668 "globalDefault": "default-engine",
1669 "globalDefaultPrivate": "default-private-engine",
1670 },
1671 {
1672 "recordType": "engineOrders",
1673 "orders": [
1674 {
1675 "environment": {
1676 "locales": ["en-CA"],
1677 "regions": ["CA"],
1678 },
1679 "order": ["after-defaults"],
1680 },
1681 ],
1682 },
1683 {
1684 "recordType": "availableLocales",
1685 "locales": ["en-CA", "fr"]
1686 }
1687 ]
1688 })
1689 .to_string(),
1690 );
1691 assert!(
1692 engine_order_config.is_ok(),
1693 "Should have set the configuration successfully. {:?}",
1694 engine_order_config
1695 );
1696 assert!(
1697 config_overrides_result.is_ok(),
1698 "Should have set the configuration overrides successfully. {:?}",
1699 config_overrides_result
1700 );
1701
1702 fn assert_actual_engines_equals_expected(
1703 result: Result<RefinedSearchConfig, SearchApiError>,
1704 expected_engine_orders: Vec<String>,
1705 message: &str,
1706 ) {
1707 assert!(
1708 result.is_ok(),
1709 "Should have filtered the configuration without error. {:?}",
1710 result
1711 );
1712
1713 let refined_config = result.unwrap();
1714 let actual_engine_orders: Vec<String> = refined_config
1715 .engines
1716 .into_iter()
1717 .map(|e| e.identifier)
1718 .collect();
1719
1720 assert_eq!(actual_engine_orders, expected_engine_orders, "{}", message);
1721 }
1722
1723 assert_actual_engines_equals_expected(
1724 Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1725 locale: "en-CA".into(),
1726 region: "CA".into(),
1727 ..Default::default()
1728 }),
1729 vec![
1730 "default-engine".to_string(),
1731 "default-private-engine".to_string(),
1732 "after-defaults".to_string(),
1733 "b-engine".to_string(),
1734 "a-engine".to_string(),
1735 ],
1736 "Should order the default engine first, default private engine second, and the rest of the engines based on order hint then alphabetically by name."
1737 );
1738
1739 let starts_with_wiki_config = Arc::clone(&selector).set_search_config(
1740 json!({
1741 "data": [
1742 {
1743 "recordType": "engine",
1744 "identifier": "wiki-ca",
1745 "base": {
1746 "name": "wiki-ca",
1747 "classification": "general",
1748 "urls": {
1749 "search": {
1750 "base": "https://example.com",
1751 "method": "GET"
1752 }
1753 }
1754 },
1755 "variants": [{
1756 "environment": {
1757 "locales": ["en-CA"],
1758 "regions": ["CA"],
1759 }
1760 }],
1761 },
1762 {
1763 "recordType": "engine",
1764 "identifier": "wiki-uk",
1765 "base": {
1766 "name": "wiki-uk",
1767 "classification": "general",
1768 "urls": {
1769 "search": {
1770 "base": "https://example.com",
1771 "method": "GET"
1772 }
1773 }
1774 },
1775 "variants": [{
1776 "environment": {
1777 "locales": ["en-GB"],
1778 "regions": ["GB"],
1779 }
1780 }],
1781 },
1782 {
1783 "recordType": "engine",
1784 "identifier": "engine-1",
1785 "base": {
1786 "name": "engine-1",
1787 "classification": "general",
1788 "urls": {
1789 "search": {
1790 "base": "https://example.com",
1791 "method": "GET"
1792 }
1793 }
1794 },
1795 "variants": [{
1796 "environment": {
1797 "allRegionsAndLocales": true,
1798 }
1799 }],
1800 },
1801 {
1802 "recordType": "engine",
1803 "identifier": "engine-2",
1804 "base": {
1805 "name": "engine-2",
1806 "classification": "general",
1807 "urls": {
1808 "search": {
1809 "base": "https://example.com",
1810 "method": "GET"
1811 }
1812 }
1813 },
1814 "variants": [{
1815 "environment": {
1816 "allRegionsAndLocales": true,
1817 }
1818 }],
1819 },
1820 {
1821 "recordType": "engineOrders",
1822 "orders": [
1823 {
1824 "environment": {
1825 "locales": ["en-CA"],
1826 "regions": ["CA"],
1827 },
1828 "order": ["wiki*", "engine-1", "engine-2"],
1829 },
1830 {
1831 "environment": {
1832 "locales": ["en-GB"],
1833 "regions": ["GB"],
1834 },
1835 "order": ["wiki*", "engine-1", "engine-2"],
1836 },
1837 ],
1838 },
1839 {
1840 "recordType": "availableLocales",
1841 "locales": ["en-CA", "en-GB", "fr"]
1842 }
1843
1844 ]
1845 })
1846 .to_string(),
1847 );
1848 assert!(
1849 starts_with_wiki_config.is_ok(),
1850 "Should have set the configuration successfully. {:?}",
1851 starts_with_wiki_config
1852 );
1853
1854 assert_actual_engines_equals_expected(
1855 Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1856 locale: "en-CA".into(),
1857 region: "CA".into(),
1858 ..Default::default()
1859 }),
1860 vec![
1861 "wiki-ca".to_string(),
1862 "engine-1".to_string(),
1863 "engine-2".to_string(),
1864 ],
1865 "Should list the wiki-ca engine and other engines in correct orders with the en-CA and CA locale region environment."
1866 );
1867
1868 assert_actual_engines_equals_expected(
1869 Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
1870 locale: "en-GB".into(),
1871 region: "GB".into(),
1872 ..Default::default()
1873 }),
1874 vec![
1875 "wiki-uk".to_string(),
1876 "engine-1".to_string(),
1877 "engine-2".to_string(),
1878 ],
1879 "Should list the wiki-uk engine and other engines in correct orders with the en-GB and GB locale region environment."
1880 );
1881 }
1882
1883 const APPLY_OVERRIDES: bool = true;
1884 const DO_NOT_APPLY_OVERRIDES: bool = false;
1885 const RECORDS_MISSING: bool = false;
1886 const RECORDS_PRESENT: bool = true;
1887
1888 fn setup_remote_settings_test(
1889 should_apply_overrides: bool,
1890 expect_sync_successful: bool,
1891 ) -> Arc<SearchEngineSelector> {
1892 error_support::init_for_tests();
1893 viaduct_reqwest::use_reqwest_backend();
1894
1895 let config = RemoteSettingsConfig2 {
1896 server: Some(RemoteSettingsServer::Custom {
1897 url: mockito::server_url(),
1898 }),
1899 bucket_name: Some(String::from("main")),
1900 app_context: Some(RemoteSettingsContext::default()),
1901 };
1902 let service = Arc::new(RemoteSettingsService::new(String::from(":memory:"), config));
1903
1904 let selector = Arc::new(SearchEngineSelector::new());
1905
1906 Arc::clone(&selector).use_remote_settings_server(&service, should_apply_overrides);
1907 let sync_result = Arc::clone(&service).sync();
1908 assert!(
1909 if expect_sync_successful {
1910 sync_result.is_ok()
1911 } else {
1912 sync_result.is_err()
1913 },
1914 "Should have completed the sync successfully. {:?}",
1915 sync_result
1916 );
1917
1918 selector
1919 }
1920
1921 fn mock_changes_endpoint() -> mockito::Mock {
1922 mock(
1923 "GET",
1924 "/v1/buckets/monitor/collections/changes/changeset?_expected=0",
1925 )
1926 .with_body(response_body_changes())
1927 .with_status(200)
1928 .with_header("content-type", "application/json")
1929 .with_header("etag", "\"1000\"")
1930 .create()
1931 }
1932
1933 fn response_body() -> String {
1934 json!({
1935 "metadata": {
1936 "id": "search-config-v2",
1937 "last_modified": 1000,
1938 "bucket": "main",
1939 "signature": {
1940 "x5u": "fake",
1941 "signature": "fake",
1942 },
1943 },
1944 "timestamp": 1000,
1945 "changes": [
1946 {
1947 "recordType": "engine",
1948 "identifier": "test",
1949 "base": {
1950 "name": "Test",
1951 "classification": "general",
1952 "urls": {
1953 "search": {
1954 "base": "https://example.com",
1955 "method": "GET",
1956 }
1957 }
1958 },
1959 "variants": [{
1960 "environment": {
1961 "allRegionsAndLocales": true
1962 }
1963 }],
1964 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d7",
1965 "schema": 1001,
1966 "last_modified": 1000
1967 },
1968 {
1969 "recordType": "engine",
1970 "identifier": "distro-default",
1971 "base": {
1972 "name": "Distribution Default",
1973 "classification": "general",
1974 "urls": {
1975 "search": {
1976 "base": "https://example.com",
1977 "method": "GET"
1978 }
1979 }
1980 },
1981 "variants": [{
1982 "environment": {
1983 "allRegionsAndLocales": true
1984 }
1985 }],
1986 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d8",
1987 "schema": 1002,
1988 "last_modified": 1000
1989 },
1990 {
1991 "recordType": "engine",
1992 "identifier": "private-default-FR",
1993 "base": {
1994 "name": "Private default FR",
1995 "classification": "general",
1996 "urls": {
1997 "search": {
1998 "base": "https://example.com",
1999 "method": "GET"
2000 }
2001 }
2002 },
2003 "variants": [{
2004 "environment": {
2005 "allRegionsAndLocales": true,
2006 }
2007 }],
2008 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d9",
2009 "schema": 1003,
2010 "last_modified": 1000
2011 },
2012 {
2013 "recordType": "defaultEngines",
2014 "globalDefault": "test",
2015 "specificDefaults": [{
2016 "environment": {
2017 "distributions": ["test-distro"],
2018 },
2019 "default": "distro-default"
2020 }, {
2021 "environment": {
2022 "regions": ["fr"]
2023 },
2024 "defaultPrivate": "private-default-FR"
2025 }],
2026 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0e0",
2027 "schema": 1004,
2028 "last_modified": 1000,
2029 }
2030 ]
2031 })
2032 .to_string()
2033 }
2034
2035 fn response_body_changes() -> String {
2036 json!({
2037 "timestamp": 1000,
2038 "changes": [
2039 {
2040 "collection": "search-config-v2",
2041 "bucket": "main",
2042 "last_modified": 1000,
2043 }
2044 ],
2045 })
2046 .to_string()
2047 }
2048
2049 fn response_body_locales() -> String {
2050 json!({
2051 "metadata": {
2052 "id": "search-config-v2",
2053 "last_modified": 1000,
2054 "bucket": "main",
2055 "signature": {
2056 "x5u": "fake",
2057 "signature": "fake",
2058 },
2059 },
2060 "timestamp": 1000,
2061 "changes": [
2062 {
2063 "recordType": "engine",
2064 "identifier": "engine-de",
2065 "base": {
2066 "name": "German Engine",
2067 "classification": "general",
2068 "urls": {
2069 "search": {
2070 "base": "https://example.com",
2071 "method": "GET",
2072 }
2073 }
2074 },
2075 "variants": [{
2076 "environment": {
2077 "locales": ["de"]
2078 }
2079 }],
2080 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d7",
2081 "schema": 1001,
2082 "last_modified": 1000
2083 },
2084 {
2085 "recordType": "engine",
2086 "identifier": "engine-en-us",
2087 "base": {
2088 "name": "English US Engine",
2089 "classification": "general",
2090 "urls": {
2091 "search": {
2092 "base": "https://example.com",
2093 "method": "GET"
2094 }
2095 }
2096 },
2097 "variants": [{
2098 "environment": {
2099 "locales": ["en-US"]
2100 }
2101 }],
2102 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d8",
2103 "schema": 1002,
2104 "last_modified": 1000
2105 },
2106 {
2107 "recordType": "availableLocales",
2108 "locales": ["de", "en-US"],
2109 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0e0",
2110 "schema": 1004,
2111 "last_modified": 1000,
2112 }
2113 ]
2114 })
2115 .to_string()
2116 }
2117
2118 fn response_body_overrides() -> String {
2119 json!({
2120 "metadata": {
2121 "id": "search-config-overrides-v2",
2122 "last_modified": 1000,
2123 "bucket": "main",
2124 "signature": {
2125 "x5u": "fake",
2126 "signature": "fake",
2127 },
2128 },
2129 "timestamp": 1000,
2130 "changes": [
2131 {
2132 "urls": {
2133 "search": {
2134 "base": "https://example.com/search-overrides",
2135 "method": "GET",
2136 "params": [{
2137 "name": "overrides-name",
2138 "value": "overrides-value",
2139 }],
2140 }
2141 },
2142 "identifier": "test",
2143 "clickUrl": "https://example.com/click-url",
2144 "telemetrySuffix": "overrides-telemetry-suffix",
2145 "partnerCode": "overrides-partner-code",
2146 "id": "c5dcd1da-7126-4abb-846b-ec85b0d4d0d7",
2147 "schema": 1001,
2148 "last_modified": 1000
2149 },
2150 ]
2151 })
2152 .to_string()
2153 }
2154
2155 #[test]
2156 fn test_remote_settings_empty_search_config_records_throws_error() {
2157 let changes_mock = mock_changes_endpoint();
2158 let m = mock(
2159 "GET",
2160 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
2161 )
2162 .with_body(
2163 json!({
2164 "metadata": {
2165 "id": "search-config-v2",
2166 "last_modified": 1000,
2167 "bucket": "main",
2168 "signature": {
2169 "x5u": "fake",
2170 "signature": "fake",
2171 },
2172 },
2173 "timestamp": 1000,
2174 "changes": [
2175 ]})
2176 .to_string(),
2177 )
2178 .with_status(200)
2179 .with_header("content-type", "application/json")
2180 .with_header("etag", "\"1000\"")
2181 .create();
2182
2183 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
2184
2185 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2186 distribution_id: "test-distro".to_string(),
2187 ..Default::default()
2188 });
2189 assert!(
2190 result.is_err(),
2191 "Should throw an error when a configuration has not been specified before filtering"
2192 );
2193 assert!(result
2194 .unwrap_err()
2195 .to_string()
2196 .contains("No search config v2 records received from remote settings"));
2197 changes_mock.expect(1).assert();
2198 m.expect(1).assert();
2199 }
2200
2201 #[test]
2202 fn test_remote_settings_search_config_records_is_none_throws_error() {
2203 let changes_mock = mock_changes_endpoint();
2204 let m1 = mock(
2205 "GET",
2206 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
2207 )
2208 .with_body(response_body())
2209 .with_status(501)
2210 .with_header("content-type", "application/json")
2211 .with_header("etag", "\"1000\"")
2212 .create();
2213
2214 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_MISSING);
2215
2216 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2217 distribution_id: "test-distro".to_string(),
2218 ..Default::default()
2219 });
2220 assert!(
2221 result.is_err(),
2222 "Should throw an error when a configuration has not been specified before filtering"
2223 );
2224 assert!(result
2225 .unwrap_err()
2226 .to_string()
2227 .contains("No search config v2 records received from remote settings"));
2228 changes_mock.expect(1).assert();
2229 m1.expect(1).assert();
2230 }
2231
2232 #[test]
2233 fn test_remote_settings_empty_search_config_overrides_filtered_without_error() {
2234 let changes_mock = mock_changes_endpoint();
2235 let m1 = mock(
2236 "GET",
2237 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
2238 )
2239 .with_body(response_body())
2240 .with_status(200)
2241 .with_header("content-type", "application/json")
2242 .with_header("etag", "\"1000\"")
2243 .create();
2244
2245 let m2 = mock(
2246 "GET",
2247 "/v1/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
2248 )
2249 .with_body(
2250 json!({
2251 "metadata": {
2252 "id": "search-config-overrides-v2",
2253 "last_modified": 1000,
2254 "bucket": "main",
2255 "signature": {
2256 "x5u": "fake",
2257 "signature": "fake",
2258 },
2259 },
2260 "timestamp": 1000,
2261 "changes": [
2262 ]})
2263 .to_string(),
2264 )
2265 .with_status(200)
2266 .with_header("content-type", "application/json")
2267 .with_header("etag", "\"1000\"")
2268 .create();
2269
2270 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_PRESENT);
2271
2272 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2273 distribution_id: "test-distro".to_string(),
2274 ..Default::default()
2275 });
2276 assert!(
2277 result.is_ok(),
2278 "Should have filtered the configuration using an empty search config overrides without causing an error. {:?}",
2279 result
2280 );
2281 changes_mock.expect(1).assert();
2282 m1.expect(1).assert();
2283 m2.expect(1).assert();
2284 }
2285
2286 #[test]
2287 fn test_remote_settings_search_config_overrides_records_is_none_throws_error() {
2288 let changes_mock = mock_changes_endpoint();
2289 let m1 = mock(
2290 "GET",
2291 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
2292 )
2293 .with_body(response_body())
2294 .with_status(200)
2295 .with_header("content-type", "application/json")
2296 .with_header("etag", "\"1000\"")
2297 .create();
2298
2299 let m2 = mock(
2300 "GET",
2301 "/v1/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
2302 )
2303 .with_body(response_body_overrides())
2304 .with_status(501)
2305 .with_header("content-type", "application/json")
2306 .with_header("etag", "\"1000\"")
2307 .create();
2308
2309 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_MISSING);
2310
2311 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2312 distribution_id: "test-distro".to_string(),
2313 ..Default::default()
2314 });
2315 assert!(
2316 result.is_err(),
2317 "Should throw an error when a configuration overrides has not been specified before filtering"
2318 );
2319 assert!(result
2320 .unwrap_err()
2321 .to_string()
2322 .contains("No search config overrides v2 records received from remote settings"));
2323 changes_mock.expect(1).assert();
2324 m1.expect(1).assert();
2325 m2.expect(1).assert();
2326 }
2327
2328 #[test]
2329 fn test_filter_with_remote_settings_overrides() {
2330 let changes_mock = mock_changes_endpoint();
2331 let m1 = mock(
2332 "GET",
2333 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
2334 )
2335 .with_body(response_body())
2336 .with_status(200)
2337 .with_header("content-type", "application/json")
2338 .with_header("etag", "\"1000\"")
2339 .create();
2340
2341 let m2 = mock(
2342 "GET",
2343 "/v1/buckets/main/collections/search-config-overrides-v2/changeset?_expected=0",
2344 )
2345 .with_body(response_body_overrides())
2346 .with_status(200)
2347 .with_header("content-type", "application/json")
2348 .with_header("etag", "\"1000\"")
2349 .create();
2350
2351 let selector = setup_remote_settings_test(APPLY_OVERRIDES, RECORDS_PRESENT);
2352
2353 let test_engine = SearchEngineDefinition {
2354 charset: "UTF-8".to_string(),
2355 classification: SearchEngineClassification::General,
2356 identifier: "test".to_string(),
2357 name: "Test".to_string(),
2358 partner_code: "overrides-partner-code".to_string(),
2359 telemetry_suffix: "overrides-telemetry-suffix".to_string(),
2360 click_url: Some("https://example.com/click-url".to_string()),
2361 urls: SearchEngineUrls {
2362 search: SearchEngineUrl {
2363 base: "https://example.com/search-overrides".to_string(),
2364 method: "GET".to_string(),
2365 params: vec![SearchUrlParam {
2366 name: "overrides-name".to_string(),
2367 value: Some("overrides-value".to_string()),
2368 enterprise_value: None,
2369 experiment_config: None,
2370 }],
2371 search_term_param_name: None,
2372 },
2373 ..Default::default()
2374 },
2375 ..Default::default()
2376 };
2377
2378 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2379 ..Default::default()
2380 });
2381
2382 assert!(
2383 result.is_ok(),
2384 "Should have filtered the configuration without error. {:?}",
2385 result
2386 );
2387 assert_eq!(
2388 result.unwrap().engines[0],
2389 test_engine.clone(),
2390 "Should have applied the overrides to the matching engine"
2391 );
2392 changes_mock.expect(1).assert();
2393 m1.expect(1).assert();
2394 m2.expect(1).assert();
2395 }
2396
2397 #[test]
2398 fn test_filter_with_remote_settings() {
2399 let changes_mock = mock_changes_endpoint();
2400
2401 let m = mock(
2402 "GET",
2403 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
2404 )
2405 .with_body(response_body())
2406 .with_status(200)
2407 .with_header("content-type", "application/json")
2408 .with_header("etag", "\"1000\"")
2409 .create();
2410
2411 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
2412
2413 let test_engine = SearchEngineDefinition {
2414 charset: "UTF-8".to_string(),
2415 classification: SearchEngineClassification::General,
2416 identifier: "test".to_string(),
2417 name: "Test".to_string(),
2418 urls: SearchEngineUrls {
2419 search: SearchEngineUrl {
2420 base: "https://example.com".to_string(),
2421 method: "GET".to_string(),
2422 params: Vec::new(),
2423 search_term_param_name: None,
2424 },
2425 ..Default::default()
2426 },
2427 ..Default::default()
2428 };
2429 let private_default_fr_engine = SearchEngineDefinition {
2430 charset: "UTF-8".to_string(),
2431 classification: SearchEngineClassification::General,
2432 identifier: "private-default-FR".to_string(),
2433 name: "Private default FR".to_string(),
2434 urls: SearchEngineUrls {
2435 search: SearchEngineUrl {
2436 base: "https://example.com".to_string(),
2437 method: "GET".to_string(),
2438 params: Vec::new(),
2439 search_term_param_name: None,
2440 },
2441 ..Default::default()
2442 },
2443 ..Default::default()
2444 };
2445 let distro_default_engine = SearchEngineDefinition {
2446 charset: "UTF-8".to_string(),
2447 classification: SearchEngineClassification::General,
2448 identifier: "distro-default".to_string(),
2449 name: "Distribution Default".to_string(),
2450 urls: SearchEngineUrls {
2451 search: SearchEngineUrl {
2452 base: "https://example.com".to_string(),
2453 method: "GET".to_string(),
2454 params: Vec::new(),
2455 search_term_param_name: None,
2456 },
2457 ..Default::default()
2458 },
2459 ..Default::default()
2460 };
2461
2462 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2463 distribution_id: "test-distro".to_string(),
2464 ..Default::default()
2465 });
2466 assert!(
2467 result.is_ok(),
2468 "Should have filtered the configuration without error. {:?}",
2469 result
2470 );
2471 assert_eq!(
2472 result.unwrap(),
2473 RefinedSearchConfig {
2474 engines: vec![
2475 distro_default_engine.clone(),
2476 private_default_fr_engine.clone(),
2477 test_engine.clone(),
2478 ],
2479 app_default_engine_id: Some("distro-default".to_string()),
2480 app_private_default_engine_id: None
2481 },
2482 "Should have selected the default engine for the matching specific default"
2483 );
2484
2485 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2486 region: "fr".into(),
2487 distribution_id: String::new(),
2488 ..Default::default()
2489 });
2490 assert!(
2491 result.is_ok(),
2492 "Should have filtered the configuration without error. {:?}",
2493 result
2494 );
2495 assert_eq!(
2496 result.unwrap(),
2497 RefinedSearchConfig {
2498 engines: vec![
2499 test_engine.clone(),
2500 private_default_fr_engine.clone(),
2501 distro_default_engine.clone(),
2502 ],
2503 app_default_engine_id: Some("test".to_string()),
2504 app_private_default_engine_id: Some("private-default-FR".to_string())
2505 },
2506 "Should have selected the private default engine for the matching specific default"
2507 );
2508 changes_mock.expect(1).assert();
2509 m.expect(1).assert();
2510 }
2511
2512 #[test]
2513 fn test_filter_with_remote_settings_negotiate_locales() {
2514 let changes_mock = mock_changes_endpoint();
2515 let m = mock(
2516 "GET",
2517 "/v1/buckets/main/collections/search-config-v2/changeset?_expected=0",
2518 )
2519 .with_body(response_body_locales())
2520 .with_status(200)
2521 .with_header("content-type", "application/json")
2522 .with_header("etag", "\"1000\"")
2523 .create();
2524
2525 let selector = setup_remote_settings_test(DO_NOT_APPLY_OVERRIDES, RECORDS_PRESENT);
2526
2527 let de_engine = SearchEngineDefinition {
2528 charset: "UTF-8".to_string(),
2529 classification: SearchEngineClassification::General,
2530 identifier: "engine-de".to_string(),
2531 name: "German Engine".to_string(),
2532 urls: SearchEngineUrls {
2533 search: SearchEngineUrl {
2534 base: "https://example.com".to_string(),
2535 method: "GET".to_string(),
2536 params: Vec::new(),
2537 search_term_param_name: None,
2538 },
2539 ..Default::default()
2540 },
2541 ..Default::default()
2542 };
2543 let en_us_engine = SearchEngineDefinition {
2544 charset: "UTF-8".to_string(),
2545 classification: SearchEngineClassification::General,
2546 identifier: "engine-en-us".to_string(),
2547 name: "English US Engine".to_string(),
2548 urls: SearchEngineUrls {
2549 search: SearchEngineUrl {
2550 base: "https://example.com".to_string(),
2551 method: "GET".to_string(),
2552 params: Vec::new(),
2553 search_term_param_name: None,
2554 },
2555 ..Default::default()
2556 },
2557 ..Default::default()
2558 };
2559
2560 let result_de = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2561 locale: "de-AT".into(),
2562 ..Default::default()
2563 });
2564 assert!(
2565 result_de.is_ok(),
2566 "Should have filtered the configuration without error. {:?}",
2567 result_de
2568 );
2569
2570 assert_eq!(
2571 result_de.unwrap(),
2572 RefinedSearchConfig {
2573 engines: vec![de_engine,],
2574 app_default_engine_id: None,
2575 app_private_default_engine_id: None,
2576 },
2577 "Should have selected the de engine when given de-AT which is not an available locale"
2578 );
2579
2580 let result_en = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2581 locale: "en-AU".to_string(),
2582 ..Default::default()
2583 });
2584 assert_eq!(
2585 result_en.unwrap(),
2586 RefinedSearchConfig {
2587 engines: vec![en_us_engine,],
2588 app_default_engine_id: None,
2589 app_private_default_engine_id: None,
2590 },
2591 "Should have selected the en-us engine when given another english locale we don't support"
2592 );
2593 changes_mock.expect(1).assert();
2594 m.expect(1).assert();
2595 }
2596
2597 #[test]
2598 fn test_configuration_overrides_applied() {
2599 let selector = Arc::new(SearchEngineSelector::new());
2600
2601 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
2602 json!({
2603 "data": [
2604 {
2605 "identifier": "test",
2606 "partnerCode": "overrides-partner-code",
2607 "clickUrl": "https://example.com/click-url",
2608 "telemetrySuffix": "overrides-telemetry-suffix",
2609 "urls": {
2610 "search": {
2611 "base": "https://example.com/search-overrides",
2612 "method": "GET",
2613 "params": [{
2614 "name": "overrides-name",
2615 "value": "overrides-value",
2616 }],
2617 }
2618 },
2619 },
2620 { "identifier": "distro-default",
2622 "partnerCode": "distro-overrides-partner-code",
2623 "clickUrl": "https://example.com/click-url-distro",
2624 "urls": {
2625 "search": {
2626 "base": "https://example.com/search-distro",
2627 },
2628 },
2629 }
2630 ]
2631 })
2632 .to_string(),
2633 );
2634 let config_result = Arc::clone(&selector).set_search_config(
2635 json!({
2636 "data": [
2637 {
2638 "recordType": "engine",
2639 "identifier": "test",
2640 "base": {
2641 "name": "Test",
2642 "classification": "general",
2643 "urls": {
2644 "search": {
2645 "base": "https://example.com",
2646 "method": "GET",
2647 }
2648 }
2649 },
2650 "variants": [{
2651 "environment": {
2652 "allRegionsAndLocales": true
2653 }
2654 }],
2655 },
2656 {
2657 "recordType": "engine",
2658 "identifier": "distro-default",
2659 "base": {
2660 "name": "Distribution Default",
2661 "classification": "general",
2662 "urls": {
2663 "search": {
2664 "base": "https://example.com",
2665 "method": "GET"
2666 }
2667 }
2668 },
2669 "variants": [{
2670 "environment": {
2671 "allRegionsAndLocales": true
2672 },
2673 "telemetrySuffix": "distro-telemetry-suffix",
2674 }],
2675 },
2676 ]
2677 })
2678 .to_string(),
2679 );
2680 assert!(
2681 config_result.is_ok(),
2682 "Should have set the configuration successfully. {:?}",
2683 config_result
2684 );
2685 assert!(
2686 config_overrides_result.is_ok(),
2687 "Should have set the configuration overrides successfully. {:?}",
2688 config_overrides_result
2689 );
2690
2691 let test_engine = SearchEngineDefinition {
2692 charset: "UTF-8".to_string(),
2693 classification: SearchEngineClassification::General,
2694 identifier: "test".to_string(),
2695 name: "Test".to_string(),
2696 partner_code: "overrides-partner-code".to_string(),
2697 telemetry_suffix: "overrides-telemetry-suffix".to_string(),
2698 click_url: Some("https://example.com/click-url".to_string()),
2699 urls: SearchEngineUrls {
2700 search: SearchEngineUrl {
2701 base: "https://example.com/search-overrides".to_string(),
2702 method: "GET".to_string(),
2703 params: vec![SearchUrlParam {
2704 name: "overrides-name".to_string(),
2705 value: Some("overrides-value".to_string()),
2706 enterprise_value: None,
2707 experiment_config: None,
2708 }],
2709 search_term_param_name: None,
2710 },
2711 ..Default::default()
2712 },
2713 ..Default::default()
2714 };
2715 let distro_default_engine = SearchEngineDefinition {
2716 charset: "UTF-8".to_string(),
2717 classification: SearchEngineClassification::General,
2718 identifier: "distro-default".to_string(),
2719 name: "Distribution Default".to_string(),
2720 partner_code: "distro-overrides-partner-code".to_string(),
2721 telemetry_suffix: "distro-telemetry-suffix".to_string(),
2722 click_url: Some("https://example.com/click-url-distro".to_string()),
2723 urls: SearchEngineUrls {
2724 search: SearchEngineUrl {
2725 base: "https://example.com/search-distro".to_string(),
2726 method: "GET".to_string(),
2727 params: Vec::new(),
2728 search_term_param_name: None,
2729 },
2730 ..Default::default()
2731 },
2732 ..Default::default()
2733 };
2734
2735 let result = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2736 ..Default::default()
2737 });
2738 assert!(
2739 result.is_ok(),
2740 "Should have filtered the configuration without error. {:?}",
2741 result
2742 );
2743
2744 assert_eq!(
2745 result.unwrap(),
2746 RefinedSearchConfig {
2747 engines: vec![distro_default_engine.clone(), test_engine.clone(),],
2748 app_default_engine_id: None,
2749 app_private_default_engine_id: None
2750 },
2751 "Should have applied the overrides to the matching engine."
2752 );
2753 }
2754
2755 #[test]
2756 fn test_filter_engine_configuration_negotiate_locales() {
2757 let selector = Arc::new(SearchEngineSelector::new());
2758
2759 let config_overrides_result = Arc::clone(&selector).set_config_overrides(
2760 json!({
2761 "data": [
2762 {
2763 "identifier": "overrides-engine",
2764 "partnerCode": "overrides-partner-code",
2765 "clickUrl": "https://example.com/click-url",
2766 "telemetrySuffix": "overrides-telemetry-suffix",
2767 "urls": {
2768 "search": {
2769 "base": "https://example.com/search-overrides",
2770 "method": "GET",
2771 "params": []
2772 }
2773 }
2774 }
2775 ]
2776 })
2777 .to_string(),
2778 );
2779 let config_result = Arc::clone(&selector).set_search_config(
2780 json!({
2781 "data": [
2782 {
2783 "recordType": "availableLocales",
2784 "locales": ["de", "en-US"]
2785 },
2786 {
2787 "recordType": "engine",
2788 "identifier": "engine-de",
2789 "base": {
2790 "name": "German Engine",
2791 "classification": "general",
2792 "urls": {
2793 "search": {
2794 "base": "https://example.com",
2795 "method": "GET",
2796 }
2797 }
2798 },
2799 "variants": [{
2800 "environment": {
2801 "locales": ["de"]
2802 }
2803 }],
2804 },
2805 {
2806 "recordType": "engine",
2807 "identifier": "engine-en-us",
2808 "base": {
2809 "name": "English US Engine",
2810 "classification": "general",
2811 "urls": {
2812 "search": {
2813 "base": "https://example.com",
2814 "method": "GET"
2815 }
2816 }
2817 },
2818 "variants": [{
2819 "environment": {
2820 "locales": ["en-US"]
2821 }
2822 }],
2823 },
2824 ]
2825 })
2826 .to_string(),
2827 );
2828 assert!(
2829 config_result.is_ok(),
2830 "Should have set the configuration successfully. {:?}",
2831 config_result
2832 );
2833 assert!(
2834 config_overrides_result.is_ok(),
2835 "Should have set the configuration overrides successfully. {:?}",
2836 config_overrides_result
2837 );
2838
2839 let de_engine = SearchEngineDefinition {
2840 charset: "UTF-8".to_string(),
2841 classification: SearchEngineClassification::General,
2842 identifier: "engine-de".to_string(),
2843 name: "German Engine".to_string(),
2844 urls: SearchEngineUrls {
2845 search: SearchEngineUrl {
2846 base: "https://example.com".to_string(),
2847 method: "GET".to_string(),
2848 params: Vec::new(),
2849 search_term_param_name: None,
2850 },
2851 ..Default::default()
2852 },
2853 ..Default::default()
2854 };
2855 let en_us_engine = SearchEngineDefinition {
2856 charset: "UTF-8".to_string(),
2857 classification: SearchEngineClassification::General,
2858 identifier: "engine-en-us".to_string(),
2859 name: "English US Engine".to_string(),
2860 urls: SearchEngineUrls {
2861 search: SearchEngineUrl {
2862 base: "https://example.com".to_string(),
2863 method: "GET".to_string(),
2864 params: Vec::new(),
2865 search_term_param_name: None,
2866 },
2867 ..Default::default()
2868 },
2869 ..Default::default()
2870 };
2871
2872 let result_de = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2873 locale: "de-AT".into(),
2874 ..Default::default()
2875 });
2876 assert!(
2877 result_de.is_ok(),
2878 "Should have filtered the configuration without error. {:?}",
2879 result_de
2880 );
2881
2882 assert_eq!(
2883 result_de.unwrap(),
2884 RefinedSearchConfig {
2885 engines: vec![de_engine,],
2886 app_default_engine_id: None,
2887 app_private_default_engine_id: None,
2888 },
2889 "Should have selected the de engine when given de-AT which is not an available locale"
2890 );
2891
2892 let result_en = Arc::clone(&selector).filter_engine_configuration(SearchUserEnvironment {
2893 locale: "en-AU".to_string(),
2894 ..Default::default()
2895 });
2896 assert_eq!(
2897 result_en.unwrap(),
2898 RefinedSearchConfig {
2899 engines: vec![en_us_engine,],
2900 app_default_engine_id: None,
2901 app_private_default_engine_id: None,
2902 },
2903 "Should have selected the en-us engine when given another english locale we don't support"
2904 );
2905 }
2906}