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