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