search/
selector.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! This module defines the main `SearchEngineSelector`.
6
7use 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/// SearchEngineSelector parses the JSON configuration for
28/// search engines and returns the applicable engines depending
29/// on their region + locale.
30#[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    /// Sets the RemoteSettingsService to use. The selector will create the
41    /// relevant remote settings client(s) from the service.
42    ///
43    /// # Params:
44    ///   - `service`: The remote settings service instance for the application.
45    ///   - `options`: The remote settings options to be passed to the client(s).
46    ///   - `apply_engine_overrides`: Whether or not to apply overrides from
47    ///     `search-config-v2-overrides` to the selected engines. Should be false unless the
48    ///     application supports the click URL feature.
49    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    /// Sets the search configuration from the given string. If the configuration
64    /// string is unchanged since the last update, the cached configuration is
65    /// reused to avoid unnecessary reprocessing. This helps optimize performance,
66    /// particularly during test runs where the same configuration may be used
67    /// repeatedly.
68    #[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    /// Clears the search configuration from memory if it is known that it is
87    /// not required for a time, e.g. if the configuration will only be re-filtered
88    /// after an app/environment update.
89    pub fn clear_search_config(self: Arc<Self>) {}
90
91    /// Filters the search configuration with the user's given environment,
92    /// and returns the set of engines and parameters that should be presented
93    /// to the user.
94    #[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            // Remote settings ships dumps of the collections, so it is highly
102            // unlikely that we'll ever hit the case where we have no records.
103            // However, just in case of an issue that does causes us to receive
104            // no records, we will raise an error so that the application can
105            // handle or record it appropriately.
106            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                        // TODO: Bug 1947241 - Find a way to avoid having to serialise the records
125                        // back to strings and then deserialise them into the records that we want.
126                        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                    // No classification specified to test fallback.
445                    "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                { // Test partial override with some missing fields
2621                  "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}