search/
filter.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 functions for managing the filtering of the configuration.
6
7use crate::configuration_overrides_types::JSONOverridesRecord;
8use crate::environment_matching::matches_user_environment;
9use crate::{
10    error::Error, JSONDefaultEnginesRecord, JSONEngineBase, JSONEngineMethod, JSONEngineRecord,
11    JSONEngineUrl, JSONEngineUrls, JSONEngineVariant, JSONSearchConfigurationRecords,
12    RefinedSearchConfig, SearchEngineDefinition, SearchEngineUrl, SearchEngineUrls,
13    SearchUserEnvironment,
14};
15use crate::{sort_helpers, JSONAvailableLocalesRecord, JSONEngineOrdersRecord};
16use remote_settings::RemoteSettingsRecord;
17use std::collections::HashSet;
18
19impl Default for SearchEngineUrl {
20    fn default() -> Self {
21        Self {
22            base: Default::default(),
23            method: JSONEngineMethod::default().as_str().to_string(),
24            params: Default::default(),
25            search_term_param_name: Default::default(),
26            display_name: Default::default(),
27            is_new_until: Default::default(),
28            exclude_partner_code_from_telemetry: Default::default(),
29            accepted_content_types: Default::default(),
30        }
31    }
32}
33
34impl SearchEngineUrl {
35    fn merge(&mut self, user_environment: &SearchUserEnvironment, preferred: &JSONEngineUrl) {
36        if let Some(base) = &preferred.base {
37            self.base = base.clone();
38        }
39        if let Some(method) = &preferred.method {
40            self.method = method.as_str().to_string();
41        }
42        if let Some(params) = &preferred.params {
43            self.params = params.clone();
44        }
45        if let Some(search_term_param_name) = &preferred.search_term_param_name {
46            self.search_term_param_name = Some(search_term_param_name.clone());
47        }
48        if let Some(display_name_map) = &preferred.display_name_map {
49            self.display_name = display_name_map
50                .get(&user_environment.locale)
51                .or_else(|| display_name_map.get("default"))
52                .cloned();
53        }
54        if let Some(is_new_until) = &preferred.is_new_until {
55            self.is_new_until = Some(is_new_until.clone());
56        }
57        if let Some(accepted_content_types) = &preferred.accepted_content_types {
58            self.accepted_content_types = Some(accepted_content_types.clone());
59        }
60        self.exclude_partner_code_from_telemetry = preferred.exclude_partner_code_from_telemetry;
61    }
62}
63
64impl SearchEngineUrls {
65    fn merge(&mut self, user_environment: &SearchUserEnvironment, preferred: &JSONEngineUrls) {
66        if let Some(search_url) = &preferred.search {
67            self.search.merge(user_environment, search_url);
68        }
69        if let Some(suggestions) = &preferred.suggestions {
70            self.suggestions
71                .get_or_insert_with(Default::default)
72                .merge(user_environment, suggestions);
73        }
74        if let Some(trending) = &preferred.trending {
75            self.trending
76                .get_or_insert_with(Default::default)
77                .merge(user_environment, trending);
78        }
79        if let Some(search_form) = &preferred.search_form {
80            self.search_form
81                .get_or_insert_with(Default::default)
82                .merge(user_environment, search_form);
83        }
84        if let Some(visual_search) = &preferred.visual_search {
85            self.visual_search
86                .get_or_insert_with(Default::default)
87                .merge(user_environment, visual_search);
88        }
89    }
90}
91
92impl SearchEngineDefinition {
93    fn merge_variant(
94        &mut self,
95        user_environment: &SearchUserEnvironment,
96        variant: &JSONEngineVariant,
97    ) {
98        if !self.optional {
99            self.optional = variant.optional;
100        }
101        if let Some(partner_code) = &variant.partner_code {
102            self.partner_code = partner_code.clone();
103        }
104        if let Some(telemetry_suffix) = &variant.telemetry_suffix {
105            self.telemetry_suffix = telemetry_suffix.clone();
106        }
107        if let Some(urls) = &variant.urls {
108            self.urls.merge(user_environment, urls);
109        }
110        if let Some(is_new_until) = &variant.is_new_until {
111            self.is_new_until = Some(is_new_until.clone());
112        }
113    }
114
115    fn merge_override(
116        &mut self,
117        user_environment: &SearchUserEnvironment,
118        override_record: &JSONOverridesRecord,
119    ) {
120        self.partner_code = override_record.partner_code.clone();
121        self.urls.merge(user_environment, &override_record.urls);
122        self.click_url = Some(override_record.click_url.clone());
123
124        if let Some(telemetry_suffix) = &override_record.telemetry_suffix {
125            self.telemetry_suffix = telemetry_suffix.clone();
126        }
127    }
128
129    pub(crate) fn from_configuration_details(
130        user_environment: &SearchUserEnvironment,
131        identifier: &str,
132        base: JSONEngineBase,
133        variant: &JSONEngineVariant,
134        sub_variant: &Option<JSONEngineVariant>,
135    ) -> SearchEngineDefinition {
136        let mut engine_definition = SearchEngineDefinition {
137            aliases: base.aliases.unwrap_or_default(),
138            charset: base.charset.unwrap_or_else(|| "UTF-8".to_string()),
139            classification: base.classification,
140            identifier: identifier.to_string(),
141            name: base.name,
142            optional: variant.optional,
143            order_hint: None,
144            partner_code: base.partner_code.unwrap_or_default(),
145            telemetry_suffix: String::new(),
146            urls: SearchEngineUrls::default(),
147            click_url: None,
148            is_new_until: None,
149        };
150
151        engine_definition.urls.merge(user_environment, &base.urls);
152        engine_definition.merge_variant(user_environment, variant);
153        if let Some(sub_variant) = sub_variant {
154            engine_definition.merge_variant(user_environment, sub_variant);
155        }
156
157        engine_definition
158    }
159}
160
161pub(crate) struct FilterRecordsResult {
162    engines: Vec<SearchEngineDefinition>,
163    default_engines_record: Option<JSONDefaultEnginesRecord>,
164    engine_orders_record: Option<JSONEngineOrdersRecord>,
165}
166
167pub(crate) trait Filter {
168    fn filter_records(
169        &self,
170        user_environment: &mut SearchUserEnvironment,
171        overrides: Option<Vec<JSONOverridesRecord>>,
172    ) -> Result<FilterRecordsResult, Error>;
173}
174
175fn apply_overrides(
176    user_environment: &SearchUserEnvironment,
177    engines: &mut [SearchEngineDefinition],
178    overrides: &[JSONOverridesRecord],
179) {
180    for override_record in overrides {
181        for engine in engines.iter_mut() {
182            if engine.identifier == override_record.identifier {
183                engine.merge_override(user_environment, override_record);
184            }
185        }
186    }
187}
188
189fn negotiate_languages(user_environment: &mut SearchUserEnvironment, available_locales: &[String]) {
190    let user_locale = user_environment.locale.to_lowercase();
191
192    let available_locales_set: HashSet<String> = available_locales
193        .iter()
194        .map(|locale| locale.to_lowercase())
195        .collect();
196
197    if available_locales_set.contains(&user_locale) {
198        return;
199    }
200    if user_locale.starts_with("en-") {
201        user_environment.locale = "en-us".to_string();
202        return;
203    }
204    if let Some(index) = user_locale.find('-') {
205        let base_locale = &user_locale[..index];
206        if available_locales_set.contains(base_locale) {
207            user_environment.locale = base_locale.to_string();
208        }
209    }
210}
211
212impl Filter for Vec<RemoteSettingsRecord> {
213    fn filter_records(
214        &self,
215        user_environment: &mut SearchUserEnvironment,
216        overrides: Option<Vec<JSONOverridesRecord>>,
217    ) -> Result<FilterRecordsResult, Error> {
218        let mut available_locales = Vec::new();
219        for record in self {
220            if let Some(val) = record.fields.get("recordType") {
221                if *val == "availableLocales" {
222                    let stringified = serde_json::to_string(&record.fields)?;
223                    let locales_record: Option<JSONAvailableLocalesRecord> =
224                        serde_json::from_str(&stringified)?;
225                    available_locales = locales_record.unwrap().locales;
226                }
227            }
228        }
229        negotiate_languages(user_environment, &available_locales);
230
231        let mut engines = Vec::new();
232        let mut default_engines_record = None;
233        let mut engine_orders_record = None;
234
235        for record in self {
236            // TODO: Bug 1947241 - Find a way to avoid having to serialise the records
237            // back to strings and then deserialise them into the records that we want.
238            let stringified = serde_json::to_string(&record.fields)?;
239            match record.fields.get("recordType") {
240                Some(val) if *val == "engine" => {
241                    let engine_config: Option<JSONEngineRecord> =
242                        serde_json::from_str(&stringified)?;
243                    if let Some(engine_config) = engine_config {
244                        let result =
245                            maybe_extract_engine_config(user_environment, Box::new(engine_config));
246                        engines.extend(result);
247                    }
248                }
249                Some(val) if *val == "defaultEngines" => {
250                    default_engines_record = serde_json::from_str(&stringified)?;
251                }
252                Some(val) if *val == "engineOrders" => {
253                    engine_orders_record = serde_json::from_str(&stringified)?;
254                }
255                Some(val) if *val == "availableLocales" => {
256                    // Handled above
257                }
258                // These cases are acceptable - we expect the potential for new
259                // record types/options so that we can be flexible.
260                Some(_val) => {}
261                None => {}
262            }
263        }
264
265        if let Some(overrides_data) = &overrides {
266            apply_overrides(user_environment, &mut engines, overrides_data);
267        }
268
269        Ok(FilterRecordsResult {
270            engines,
271            default_engines_record,
272            engine_orders_record,
273        })
274    }
275}
276
277impl Filter for Vec<JSONSearchConfigurationRecords> {
278    fn filter_records(
279        &self,
280        user_environment: &mut SearchUserEnvironment,
281        overrides: Option<Vec<JSONOverridesRecord>>,
282    ) -> Result<FilterRecordsResult, Error> {
283        let mut available_locales = Vec::new();
284        for record in self {
285            if let JSONSearchConfigurationRecords::AvailableLocales(locales_record) = record {
286                available_locales = locales_record.locales.clone();
287            }
288        }
289        negotiate_languages(user_environment, &available_locales);
290
291        let mut engines = Vec::new();
292        let mut default_engines_record = None;
293        let mut engine_orders_record = None;
294
295        for record in self {
296            match record {
297                JSONSearchConfigurationRecords::Engine(engine) => {
298                    let result = maybe_extract_engine_config(user_environment, engine.clone());
299                    engines.extend(result);
300                }
301                JSONSearchConfigurationRecords::DefaultEngines(default_engines) => {
302                    default_engines_record = Some(default_engines);
303                }
304                JSONSearchConfigurationRecords::EngineOrders(engine_orders) => {
305                    engine_orders_record = Some(engine_orders)
306                }
307                JSONSearchConfigurationRecords::AvailableLocales(_) => {
308                    // Handled above
309                }
310                JSONSearchConfigurationRecords::Unknown => {
311                    // Prevents panics if a new record type is added in future.
312                }
313            }
314        }
315
316        if let Some(overrides_data) = &overrides {
317            apply_overrides(user_environment, &mut engines, overrides_data);
318        }
319
320        Ok(FilterRecordsResult {
321            engines,
322            default_engines_record: default_engines_record.cloned(),
323            engine_orders_record: engine_orders_record.cloned(),
324        })
325    }
326}
327
328pub(crate) fn filter_engine_configuration_impl(
329    user_environment: SearchUserEnvironment,
330    configuration: &impl Filter,
331    overrides: Option<Vec<JSONOverridesRecord>>,
332) -> Result<RefinedSearchConfig, Error> {
333    let mut user_environment = user_environment.clone();
334    user_environment.locale = user_environment.locale.to_lowercase();
335    user_environment.region = user_environment.region.to_lowercase();
336    user_environment.version = user_environment.version.to_lowercase();
337
338    let filtered_result = configuration.filter_records(&mut user_environment, overrides);
339
340    filtered_result.map(|result| {
341        let (default_engine_id, default_private_engine_id) = determine_default_engines(
342            &result.engines,
343            result.default_engines_record,
344            &user_environment,
345        );
346
347        let mut engines = result.engines.clone();
348
349        if let Some(orders_record) = result.engine_orders_record {
350            for order_data in &orders_record.orders {
351                if matches_user_environment(&order_data.environment, &user_environment) {
352                    sort_helpers::set_engine_order(&mut engines, &order_data.order);
353                }
354            }
355        }
356
357        engines.sort_by(|a, b| {
358            sort_helpers::sort(
359                default_engine_id.as_ref(),
360                default_private_engine_id.as_ref(),
361                a,
362                b,
363            )
364        });
365
366        RefinedSearchConfig {
367            engines,
368            app_default_engine_id: default_engine_id,
369            app_private_default_engine_id: default_private_engine_id,
370        }
371    })
372}
373
374fn maybe_extract_engine_config(
375    user_environment: &SearchUserEnvironment,
376    record: Box<JSONEngineRecord>,
377) -> Option<SearchEngineDefinition> {
378    let JSONEngineRecord {
379        identifier,
380        variants,
381        base,
382    } = *record;
383    let matching_variant = variants
384        .into_iter()
385        .rev()
386        .find(|r| matches_user_environment(&r.environment, user_environment));
387
388    let mut matching_sub_variant = None;
389    if let Some(variant) = &matching_variant {
390        matching_sub_variant = variant
391            .sub_variants
392            .iter()
393            .rev()
394            .find(|r| matches_user_environment(&r.environment, user_environment))
395            .cloned();
396    }
397
398    matching_variant.map(|variant| {
399        SearchEngineDefinition::from_configuration_details(
400            user_environment,
401            &identifier,
402            base,
403            &variant,
404            &matching_sub_variant,
405        )
406    })
407}
408
409fn determine_default_engines(
410    engines: &[SearchEngineDefinition],
411    default_engines_record: Option<JSONDefaultEnginesRecord>,
412    user_environment: &SearchUserEnvironment,
413) -> (Option<String>, Option<String>) {
414    match default_engines_record {
415        None => (None, None),
416        Some(record) => {
417            let mut default_engine_id = None;
418            let mut default_engine_private_id = None;
419
420            let specific_default = record
421                .specific_defaults
422                .into_iter()
423                .rev()
424                .find(|r| matches_user_environment(&r.environment, user_environment));
425
426            if let Some(specific_default) = specific_default {
427                // Check the engine is present in the list of engines before
428                // we return it as default.
429                if let Some(engine_id) =
430                    find_engine_id_with_match(engines, specific_default.default)
431                {
432                    default_engine_id.replace(engine_id);
433                }
434                if let Some(private_engine_id) =
435                    find_engine_id_with_match(engines, specific_default.default_private)
436                {
437                    default_engine_private_id.replace(private_engine_id);
438                }
439            }
440
441            (
442                // If we haven't found a default engine in a specific default,
443                // then fall back to the global default engine - but only if that
444                // exists in the engine list.
445                //
446                // For the normal mode engine (`default_engine_id`), this would
447                // effectively be considered an error. However, we can't do anything
448                // sensible here, so we will return `None` to the application, and
449                // that can handle it.
450                default_engine_id.or_else(|| find_engine_id(engines, record.global_default)),
451                default_engine_private_id
452                    .or_else(|| find_engine_id(engines, record.global_default_private)),
453            )
454        }
455    }
456}
457
458fn find_engine_id(engines: &[SearchEngineDefinition], engine_id: String) -> Option<String> {
459    if engine_id.is_empty() {
460        return None;
461    }
462    match engines.iter().any(|e| e.identifier == engine_id) {
463        true => Some(engine_id.clone()),
464        false => None,
465    }
466}
467
468fn find_engine_id_with_match(
469    engines: &[SearchEngineDefinition],
470    engine_id_match: String,
471) -> Option<String> {
472    if engine_id_match.is_empty() {
473        return None;
474    }
475    if let Some(match_no_star) = engine_id_match.strip_suffix('*') {
476        return engines
477            .iter()
478            .find(|e| e.identifier.starts_with(match_no_star))
479            .map(|e| e.identifier.clone());
480    }
481
482    engines
483        .iter()
484        .find(|e| e.identifier == engine_id_match)
485        .map(|e| e.identifier.clone())
486}
487
488#[cfg(test)]
489mod tests {
490    use std::{collections::HashMap, vec};
491
492    use super::*;
493    use crate::*;
494    use once_cell::sync::Lazy;
495
496    #[test]
497    fn test_default_search_engine_url() {
498        assert_eq!(
499            SearchEngineUrl::default(),
500            SearchEngineUrl {
501                base: "".to_string(),
502                method: "GET".to_string(),
503                params: Vec::new(),
504                search_term_param_name: None,
505                display_name: None,
506                is_new_until: None,
507                exclude_partner_code_from_telemetry: false,
508                accepted_content_types: None,
509            },
510        );
511    }
512
513    #[test]
514    fn test_default_search_engine_urls() {
515        assert_eq!(
516            SearchEngineUrls::default(),
517            SearchEngineUrls {
518                search: SearchEngineUrl::default(),
519                suggestions: None,
520                trending: None,
521                search_form: None,
522                visual_search: None,
523            },
524        );
525    }
526
527    #[test]
528    fn test_merge_override() {
529        let mut test_engine = SearchEngineDefinition {
530            identifier: "test".to_string(),
531            partner_code: "partner-code".to_string(),
532            telemetry_suffix: "original-telemetry-suffix".to_string(),
533            ..Default::default()
534        };
535
536        let override_record = JSONOverridesRecord {
537            identifier: "test".to_string(),
538            partner_code: "override-partner-code".to_string(),
539            click_url: "https://example.com/click-url".to_string(),
540            telemetry_suffix: None,
541            urls: JSONEngineUrls {
542                search: Some(JSONEngineUrl {
543                    base: Some("https://example.com/override-search".to_string()),
544                    ..Default::default()
545                }),
546                ..Default::default()
547            },
548        };
549
550        test_engine.merge_override(
551            &SearchUserEnvironment {
552                locale: "fi".into(),
553                ..Default::default()
554            },
555            &override_record,
556        );
557
558        assert_eq!(
559            test_engine.partner_code, "override-partner-code",
560            "Should override the partner code"
561        );
562        assert_eq!(
563            test_engine.click_url,
564            Some("https://example.com/click-url".to_string()),
565            "Should override the click url"
566        );
567        assert_eq!(
568            test_engine.urls.search.base, "https://example.com/override-search",
569            "Should override search url"
570        );
571        assert_eq!(
572            test_engine.telemetry_suffix, "original-telemetry-suffix",
573            "Should not override telemetry suffix when telemetry suffix is supplied as None"
574        );
575    }
576
577    #[test]
578    fn test_merge_override_locale_match() {
579        let mut test_engine = SearchEngineDefinition {
580            identifier: "test".to_string(),
581            partner_code: "partner-code".to_string(),
582            telemetry_suffix: "original-telemetry-suffix".to_string(),
583            ..Default::default()
584        };
585
586        let override_record = JSONOverridesRecord {
587            identifier: "test".to_string(),
588            partner_code: "override-partner-code".to_string(),
589            click_url: "https://example.com/click-url".to_string(),
590            telemetry_suffix: None,
591            urls: JSONEngineUrls {
592                search: Some(JSONEngineUrl {
593                    base: Some("https://example.com/override-search".to_string()),
594                    display_name_map: Some(HashMap::from([
595                        // Default display name
596                        ("default".to_string(), "My Display Name".to_string()),
597                        // en-GB locale with unique display name
598                        ("en-GB".to_string(), "en-GB Display Name".to_string()),
599                    ])),
600                    ..Default::default()
601                }),
602                ..Default::default()
603            },
604        };
605
606        test_engine.merge_override(
607            &SearchUserEnvironment {
608                // en-GB locale
609                locale: "en-GB".into(),
610                ..Default::default()
611            },
612            &override_record,
613        );
614
615        assert_eq!(
616            test_engine.urls.search.display_name,
617            Some("en-GB Display Name".to_string()),
618            "Should override display name with en-GB version"
619        );
620    }
621
622    #[test]
623    fn test_from_configuration_details_fallsback_to_defaults() {
624        // This test doesn't use `..Default::default()` as we want to
625        // be explicit about `JSONEngineBase` and handling `None`
626        // options/default values.
627        let result = SearchEngineDefinition::from_configuration_details(
628            &SearchUserEnvironment {
629                locale: "fi".into(),
630                ..Default::default()
631            },
632            "test",
633            JSONEngineBase {
634                aliases: None,
635                charset: None,
636                classification: SearchEngineClassification::General,
637                name: "Test".to_string(),
638                partner_code: None,
639                urls: JSONEngineUrls {
640                    search: Some(JSONEngineUrl {
641                        base: Some("https://example.com".to_string()),
642                        ..Default::default()
643                    }),
644                    suggestions: None,
645                    trending: None,
646                    search_form: None,
647                    visual_search: None,
648                },
649            },
650            &JSONEngineVariant {
651                environment: JSONVariantEnvironment {
652                    all_regions_and_locales: true,
653                    ..Default::default()
654                },
655                is_new_until: None,
656                optional: false,
657                partner_code: None,
658                telemetry_suffix: None,
659                urls: None,
660                sub_variants: vec![],
661            },
662            &None,
663        );
664
665        assert_eq!(
666            result,
667            SearchEngineDefinition {
668                aliases: Vec::new(),
669                charset: "UTF-8".to_string(),
670                classification: SearchEngineClassification::General,
671                identifier: "test".to_string(),
672                is_new_until: None,
673                partner_code: String::new(),
674                name: "Test".to_string(),
675                optional: false,
676                order_hint: None,
677                telemetry_suffix: String::new(),
678                urls: SearchEngineUrls {
679                    search: SearchEngineUrl {
680                        base: "https://example.com".to_string(),
681                        ..Default::default()
682                    },
683                    suggestions: None,
684                    trending: None,
685                    search_form: None,
686                    visual_search: None,
687                },
688                click_url: None
689            }
690        )
691    }
692
693    static ENGINE_BASE: Lazy<JSONEngineBase> = Lazy::new(|| JSONEngineBase {
694        aliases: Some(vec!["foo".to_string(), "bar".to_string()]),
695        charset: Some("ISO-8859-15".to_string()),
696        classification: SearchEngineClassification::Unknown,
697        name: "Test".to_string(),
698        partner_code: Some("firefox".to_string()),
699        urls: JSONEngineUrls {
700            search: Some(JSONEngineUrl {
701                base: Some("https://example.com".to_string()),
702                method: Some(crate::JSONEngineMethod::Post),
703                params: Some(vec![
704                    SearchUrlParam {
705                        name: "param".to_string(),
706                        value: Some("test param".to_string()),
707                        enterprise_value: None,
708                        experiment_config: None,
709                    },
710                    SearchUrlParam {
711                        name: "enterprise-name".to_string(),
712                        value: None,
713                        enterprise_value: Some("enterprise-value".to_string()),
714                        experiment_config: None,
715                    },
716                ]),
717                search_term_param_name: Some("baz".to_string()),
718                ..Default::default()
719            }),
720            suggestions: Some(JSONEngineUrl {
721                base: Some("https://example.com/suggestions".to_string()),
722                method: Some(crate::JSONEngineMethod::Get),
723                params: Some(vec![SearchUrlParam {
724                    name: "suggest-name".to_string(),
725                    value: None,
726                    enterprise_value: None,
727                    experiment_config: Some("suggest-experiment-value".to_string()),
728                }]),
729                search_term_param_name: Some("suggest".to_string()),
730                ..Default::default()
731            }),
732            trending: Some(JSONEngineUrl {
733                base: Some("https://example.com/trending".to_string()),
734                method: Some(crate::JSONEngineMethod::Get),
735                params: Some(vec![SearchUrlParam {
736                    name: "trend-name".to_string(),
737                    value: Some("trend-value".to_string()),
738                    enterprise_value: None,
739                    experiment_config: None,
740                }]),
741                ..Default::default()
742            }),
743            search_form: Some(JSONEngineUrl {
744                base: Some("https://example.com/search_form".to_string()),
745                method: Some(crate::JSONEngineMethod::Get),
746                params: Some(vec![SearchUrlParam {
747                    name: "search-form-name".to_string(),
748                    value: Some("search-form-value".to_string()),
749                    enterprise_value: None,
750                    experiment_config: None,
751                }]),
752                ..Default::default()
753            }),
754            visual_search: Some(JSONEngineUrl {
755                base: Some("https://example.com/visual_search".to_string()),
756                method: Some(crate::JSONEngineMethod::Get),
757                params: Some(vec![SearchUrlParam {
758                    name: "visual-search-name".to_string(),
759                    value: Some("visual-search-value".to_string()),
760                    enterprise_value: None,
761                    experiment_config: None,
762                }]),
763                search_term_param_name: Some("url".to_string()),
764                display_name_map: Some(HashMap::from([
765                    // Default display name
766                    ("default".to_string(), "Visual Search".to_string()),
767                    // en-GB locale with unique display name
768                    ("en-GB".to_string(), "Visual Search en-GB".to_string()),
769                ])),
770                is_new_until: Some("2095-01-01".to_string()),
771                exclude_partner_code_from_telemetry: true,
772                accepted_content_types: Some(vec![
773                    "image/gif".to_string(),
774                    "image/jpeg".to_string(),
775                ]),
776            }),
777        },
778    });
779
780    #[test]
781    fn test_from_configuration_details_uses_values() {
782        let result = SearchEngineDefinition::from_configuration_details(
783            &SearchUserEnvironment {
784                locale: "fi".into(),
785                ..Default::default()
786            },
787            "test",
788            Lazy::force(&ENGINE_BASE).clone(),
789            &JSONEngineVariant {
790                environment: JSONVariantEnvironment {
791                    all_regions_and_locales: true,
792                    ..Default::default()
793                },
794                is_new_until: None,
795                optional: false,
796                partner_code: None,
797                telemetry_suffix: None,
798                urls: None,
799                sub_variants: vec![],
800            },
801            &None,
802        );
803
804        assert_eq!(
805            result,
806            SearchEngineDefinition {
807                aliases: vec!["foo".to_string(), "bar".to_string()],
808                charset: "ISO-8859-15".to_string(),
809                classification: SearchEngineClassification::Unknown,
810                identifier: "test".to_string(),
811                is_new_until: None,
812                partner_code: "firefox".to_string(),
813                name: "Test".to_string(),
814                optional: false,
815                order_hint: None,
816                telemetry_suffix: String::new(),
817                urls: SearchEngineUrls {
818                    search: SearchEngineUrl {
819                        base: "https://example.com".to_string(),
820                        method: "POST".to_string(),
821                        params: vec![
822                            SearchUrlParam {
823                                name: "param".to_string(),
824                                value: Some("test param".to_string()),
825                                enterprise_value: None,
826                                experiment_config: None,
827                            },
828                            SearchUrlParam {
829                                name: "enterprise-name".to_string(),
830                                value: None,
831                                enterprise_value: Some("enterprise-value".to_string()),
832                                experiment_config: None,
833                            },
834                        ],
835                        search_term_param_name: Some("baz".to_string()),
836                        ..Default::default()
837                    },
838                    suggestions: Some(SearchEngineUrl {
839                        base: "https://example.com/suggestions".to_string(),
840                        method: "GET".to_string(),
841                        params: vec![SearchUrlParam {
842                            name: "suggest-name".to_string(),
843                            value: None,
844                            enterprise_value: None,
845                            experiment_config: Some("suggest-experiment-value".to_string()),
846                        }],
847                        search_term_param_name: Some("suggest".to_string()),
848                        ..Default::default()
849                    }),
850                    trending: Some(SearchEngineUrl {
851                        base: "https://example.com/trending".to_string(),
852                        method: "GET".to_string(),
853                        params: vec![SearchUrlParam {
854                            name: "trend-name".to_string(),
855                            value: Some("trend-value".to_string()),
856                            enterprise_value: None,
857                            experiment_config: None,
858                        }],
859                        ..Default::default()
860                    }),
861                    search_form: Some(SearchEngineUrl {
862                        base: "https://example.com/search_form".to_string(),
863                        method: "GET".to_string(),
864                        params: vec![SearchUrlParam {
865                            name: "search-form-name".to_string(),
866                            value: Some("search-form-value".to_string()),
867                            enterprise_value: None,
868                            experiment_config: None,
869                        }],
870                        ..Default::default()
871                    }),
872                    visual_search: Some(SearchEngineUrl {
873                        base: "https://example.com/visual_search".to_string(),
874                        method: "GET".to_string(),
875                        params: vec![SearchUrlParam {
876                            name: "visual-search-name".to_string(),
877                            value: Some("visual-search-value".to_string()),
878                            enterprise_value: None,
879                            experiment_config: None,
880                        }],
881                        search_term_param_name: Some("url".to_string()),
882                        display_name: Some("Visual Search".to_string()),
883                        is_new_until: Some("2095-01-01".to_string()),
884                        exclude_partner_code_from_telemetry: true,
885                        accepted_content_types: Some(vec![
886                            "image/gif".to_string(),
887                            "image/jpeg".to_string(),
888                        ]),
889                    }),
890                },
891                click_url: None
892            }
893        )
894    }
895
896    #[test]
897    fn test_from_configuration_details_uses_values_locale_match() {
898        let result = SearchEngineDefinition::from_configuration_details(
899            &SearchUserEnvironment {
900                // en-GB locale
901                locale: "en-GB".into(),
902                ..Default::default()
903            },
904            "test",
905            Lazy::force(&ENGINE_BASE).clone(),
906            &JSONEngineVariant {
907                environment: JSONVariantEnvironment {
908                    all_regions_and_locales: true,
909                    ..Default::default()
910                },
911                is_new_until: None,
912                optional: false,
913                partner_code: None,
914                telemetry_suffix: None,
915                urls: None,
916                sub_variants: vec![],
917            },
918            &None,
919        );
920
921        assert_eq!(
922            result,
923            SearchEngineDefinition {
924                aliases: vec!["foo".to_string(), "bar".to_string()],
925                charset: "ISO-8859-15".to_string(),
926                classification: SearchEngineClassification::Unknown,
927                identifier: "test".to_string(),
928                is_new_until: None,
929                partner_code: "firefox".to_string(),
930                name: "Test".to_string(),
931                optional: false,
932                order_hint: None,
933                telemetry_suffix: String::new(),
934                urls: SearchEngineUrls {
935                    search: SearchEngineUrl {
936                        base: "https://example.com".to_string(),
937                        method: "POST".to_string(),
938                        params: vec![
939                            SearchUrlParam {
940                                name: "param".to_string(),
941                                value: Some("test param".to_string()),
942                                enterprise_value: None,
943                                experiment_config: None,
944                            },
945                            SearchUrlParam {
946                                name: "enterprise-name".to_string(),
947                                value: None,
948                                enterprise_value: Some("enterprise-value".to_string()),
949                                experiment_config: None,
950                            },
951                        ],
952                        search_term_param_name: Some("baz".to_string()),
953                        ..Default::default()
954                    },
955                    suggestions: Some(SearchEngineUrl {
956                        base: "https://example.com/suggestions".to_string(),
957                        method: "GET".to_string(),
958                        params: vec![SearchUrlParam {
959                            name: "suggest-name".to_string(),
960                            value: None,
961                            enterprise_value: None,
962                            experiment_config: Some("suggest-experiment-value".to_string()),
963                        }],
964                        search_term_param_name: Some("suggest".to_string()),
965                        ..Default::default()
966                    }),
967                    trending: Some(SearchEngineUrl {
968                        base: "https://example.com/trending".to_string(),
969                        method: "GET".to_string(),
970                        params: vec![SearchUrlParam {
971                            name: "trend-name".to_string(),
972                            value: Some("trend-value".to_string()),
973                            enterprise_value: None,
974                            experiment_config: None,
975                        }],
976                        ..Default::default()
977                    }),
978                    search_form: Some(SearchEngineUrl {
979                        base: "https://example.com/search_form".to_string(),
980                        method: "GET".to_string(),
981                        params: vec![SearchUrlParam {
982                            name: "search-form-name".to_string(),
983                            value: Some("search-form-value".to_string()),
984                            enterprise_value: None,
985                            experiment_config: None,
986                        }],
987                        ..Default::default()
988                    }),
989                    visual_search: Some(SearchEngineUrl {
990                        base: "https://example.com/visual_search".to_string(),
991                        method: "GET".to_string(),
992                        params: vec![SearchUrlParam {
993                            name: "visual-search-name".to_string(),
994                            value: Some("visual-search-value".to_string()),
995                            enterprise_value: None,
996                            experiment_config: None,
997                        }],
998                        search_term_param_name: Some("url".to_string()),
999                        // Should be the en-GB display name since the "en-GB"
1000                        // locale is present in `display_name_map`.
1001                        display_name: Some("Visual Search en-GB".to_string()),
1002                        is_new_until: Some("2095-01-01".to_string()),
1003                        exclude_partner_code_from_telemetry: true,
1004                        accepted_content_types: Some(vec![
1005                            "image/gif".to_string(),
1006                            "image/jpeg".to_string(),
1007                        ]),
1008                    }),
1009                },
1010                click_url: None
1011            }
1012        )
1013    }
1014
1015    static ENGINE_VARIANT: Lazy<JSONEngineVariant> = Lazy::new(|| JSONEngineVariant {
1016        environment: JSONVariantEnvironment {
1017            all_regions_and_locales: true,
1018            ..Default::default()
1019        },
1020        is_new_until: Some("2063-04-05".to_string()),
1021        optional: true,
1022        partner_code: Some("trek".to_string()),
1023        telemetry_suffix: Some("star".to_string()),
1024        urls: Some(JSONEngineUrls {
1025            search: Some(JSONEngineUrl {
1026                base: Some("https://example.com/variant".to_string()),
1027                method: Some(JSONEngineMethod::Get),
1028                params: Some(vec![SearchUrlParam {
1029                    name: "variant".to_string(),
1030                    value: Some("test variant".to_string()),
1031                    enterprise_value: None,
1032                    experiment_config: None,
1033                }]),
1034                search_term_param_name: Some("ship".to_string()),
1035                ..Default::default()
1036            }),
1037            suggestions: Some(JSONEngineUrl {
1038                base: Some("https://example.com/suggestions-variant".to_string()),
1039                method: Some(JSONEngineMethod::Get),
1040                params: Some(vec![SearchUrlParam {
1041                    name: "suggest-variant".to_string(),
1042                    value: Some("sugg test variant".to_string()),
1043                    enterprise_value: None,
1044                    experiment_config: None,
1045                }]),
1046                search_term_param_name: Some("variant".to_string()),
1047                ..Default::default()
1048            }),
1049            trending: Some(JSONEngineUrl {
1050                base: Some("https://example.com/trending-variant".to_string()),
1051                method: Some(JSONEngineMethod::Get),
1052                params: Some(vec![SearchUrlParam {
1053                    name: "trend-variant".to_string(),
1054                    value: Some("trend test variant".to_string()),
1055                    enterprise_value: None,
1056                    experiment_config: None,
1057                }]),
1058                search_term_param_name: Some("trend".to_string()),
1059                exclude_partner_code_from_telemetry: true,
1060                ..Default::default()
1061            }),
1062            search_form: Some(JSONEngineUrl {
1063                base: Some("https://example.com/search_form".to_string()),
1064                method: Some(crate::JSONEngineMethod::Get),
1065                params: Some(vec![SearchUrlParam {
1066                    name: "search-form-name".to_string(),
1067                    value: Some("search-form-value".to_string()),
1068                    enterprise_value: None,
1069                    experiment_config: None,
1070                }]),
1071                ..Default::default()
1072            }),
1073            visual_search: Some(JSONEngineUrl {
1074                base: Some("https://example.com/visual-search-variant".to_string()),
1075                method: Some(JSONEngineMethod::Get),
1076                params: Some(vec![SearchUrlParam {
1077                    name: "visual-search-variant-name".to_string(),
1078                    value: Some("visual-search-variant-value".to_string()),
1079                    enterprise_value: None,
1080                    experiment_config: None,
1081                }]),
1082                search_term_param_name: Some("url_variant".to_string()),
1083                display_name_map: Some(HashMap::from([
1084                    ("default".to_string(), "Visual Search Variant".to_string()),
1085                    // en-GB locale with unique display name
1086                    (
1087                        "en-GB".to_string(),
1088                        "Visual Search Variant en-GB".to_string(),
1089                    ),
1090                ])),
1091                is_new_until: Some("2096-02-02".to_string()),
1092                accepted_content_types: Some(vec![
1093                    "image/png".to_string(),
1094                    "image/jpeg".to_string(),
1095                ]),
1096                ..Default::default()
1097            }),
1098        }),
1099        sub_variants: vec![],
1100    });
1101
1102    #[test]
1103    fn test_from_configuration_details_merges_variants() {
1104        let result = SearchEngineDefinition::from_configuration_details(
1105            &SearchUserEnvironment {
1106                locale: "fi".into(),
1107                ..Default::default()
1108            },
1109            "test",
1110            Lazy::force(&ENGINE_BASE).clone(),
1111            &ENGINE_VARIANT,
1112            &None,
1113        );
1114
1115        assert_eq!(
1116            result,
1117            SearchEngineDefinition {
1118                aliases: vec!["foo".to_string(), "bar".to_string()],
1119                charset: "ISO-8859-15".to_string(),
1120                classification: SearchEngineClassification::Unknown,
1121                identifier: "test".to_string(),
1122                is_new_until: Some("2063-04-05".to_string()),
1123                partner_code: "trek".to_string(),
1124                name: "Test".to_string(),
1125                optional: true,
1126                order_hint: None,
1127                telemetry_suffix: "star".to_string(),
1128                urls: SearchEngineUrls {
1129                    search: SearchEngineUrl {
1130                        base: "https://example.com/variant".to_string(),
1131                        method: "GET".to_string(),
1132                        params: vec![SearchUrlParam {
1133                            name: "variant".to_string(),
1134                            value: Some("test variant".to_string()),
1135                            enterprise_value: None,
1136                            experiment_config: None,
1137                        }],
1138                        search_term_param_name: Some("ship".to_string()),
1139                        ..Default::default()
1140                    },
1141                    suggestions: Some(SearchEngineUrl {
1142                        base: "https://example.com/suggestions-variant".to_string(),
1143                        method: "GET".to_string(),
1144                        params: vec![SearchUrlParam {
1145                            name: "suggest-variant".to_string(),
1146                            value: Some("sugg test variant".to_string()),
1147                            enterprise_value: None,
1148                            experiment_config: None,
1149                        }],
1150                        search_term_param_name: Some("variant".to_string()),
1151                        ..Default::default()
1152                    }),
1153                    trending: Some(SearchEngineUrl {
1154                        base: "https://example.com/trending-variant".to_string(),
1155                        method: "GET".to_string(),
1156                        params: vec![SearchUrlParam {
1157                            name: "trend-variant".to_string(),
1158                            value: Some("trend test variant".to_string()),
1159                            enterprise_value: None,
1160                            experiment_config: None,
1161                        }],
1162                        search_term_param_name: Some("trend".to_string()),
1163                        exclude_partner_code_from_telemetry: true,
1164                        ..Default::default()
1165                    }),
1166                    search_form: Some(SearchEngineUrl {
1167                        base: "https://example.com/search_form".to_string(),
1168                        method: "GET".to_string(),
1169                        params: vec![SearchUrlParam {
1170                            name: "search-form-name".to_string(),
1171                            value: Some("search-form-value".to_string()),
1172                            enterprise_value: None,
1173                            experiment_config: None,
1174                        }],
1175                        ..Default::default()
1176                    }),
1177                    visual_search: Some(SearchEngineUrl {
1178                        base: "https://example.com/visual-search-variant".to_string(),
1179                        method: "GET".to_string(),
1180                        params: vec![SearchUrlParam {
1181                            name: "visual-search-variant-name".to_string(),
1182                            value: Some("visual-search-variant-value".to_string()),
1183                            enterprise_value: None,
1184                            experiment_config: None,
1185                        }],
1186                        search_term_param_name: Some("url_variant".to_string()),
1187                        // Should be the "default" display name since the "fi"
1188                        // locale isn't present in `display_name_map`.
1189                        display_name: Some("Visual Search Variant".to_string()),
1190                        is_new_until: Some("2096-02-02".to_string()),
1191                        exclude_partner_code_from_telemetry: false,
1192                        accepted_content_types: Some(vec![
1193                            "image/png".to_string(),
1194                            "image/jpeg".to_string(),
1195                        ]),
1196                    }),
1197                },
1198                click_url: None
1199            }
1200        )
1201    }
1202
1203    #[test]
1204    fn test_from_configuration_details_merges_variants_locale_match() {
1205        let result = SearchEngineDefinition::from_configuration_details(
1206            &SearchUserEnvironment {
1207                // en-GB locale
1208                locale: "en-GB".into(),
1209                ..Default::default()
1210            },
1211            "test",
1212            Lazy::force(&ENGINE_BASE).clone(),
1213            &ENGINE_VARIANT,
1214            &None,
1215        );
1216
1217        assert_eq!(
1218            result,
1219            SearchEngineDefinition {
1220                aliases: vec!["foo".to_string(), "bar".to_string()],
1221                charset: "ISO-8859-15".to_string(),
1222                classification: SearchEngineClassification::Unknown,
1223                identifier: "test".to_string(),
1224                is_new_until: Some("2063-04-05".to_string()),
1225                partner_code: "trek".to_string(),
1226                name: "Test".to_string(),
1227                optional: true,
1228                order_hint: None,
1229                telemetry_suffix: "star".to_string(),
1230                urls: SearchEngineUrls {
1231                    search: SearchEngineUrl {
1232                        base: "https://example.com/variant".to_string(),
1233                        method: "GET".to_string(),
1234                        params: vec![SearchUrlParam {
1235                            name: "variant".to_string(),
1236                            value: Some("test variant".to_string()),
1237                            enterprise_value: None,
1238                            experiment_config: None,
1239                        }],
1240                        search_term_param_name: Some("ship".to_string()),
1241                        ..Default::default()
1242                    },
1243                    suggestions: Some(SearchEngineUrl {
1244                        base: "https://example.com/suggestions-variant".to_string(),
1245                        method: "GET".to_string(),
1246                        params: vec![SearchUrlParam {
1247                            name: "suggest-variant".to_string(),
1248                            value: Some("sugg test variant".to_string()),
1249                            enterprise_value: None,
1250                            experiment_config: None,
1251                        }],
1252                        search_term_param_name: Some("variant".to_string()),
1253                        ..Default::default()
1254                    }),
1255                    trending: Some(SearchEngineUrl {
1256                        base: "https://example.com/trending-variant".to_string(),
1257                        method: "GET".to_string(),
1258                        params: vec![SearchUrlParam {
1259                            name: "trend-variant".to_string(),
1260                            value: Some("trend test variant".to_string()),
1261                            enterprise_value: None,
1262                            experiment_config: None,
1263                        }],
1264                        search_term_param_name: Some("trend".to_string()),
1265                        exclude_partner_code_from_telemetry: true,
1266                        ..Default::default()
1267                    }),
1268                    search_form: Some(SearchEngineUrl {
1269                        base: "https://example.com/search_form".to_string(),
1270                        method: "GET".to_string(),
1271                        params: vec![SearchUrlParam {
1272                            name: "search-form-name".to_string(),
1273                            value: Some("search-form-value".to_string()),
1274                            enterprise_value: None,
1275                            experiment_config: None,
1276                        }],
1277                        ..Default::default()
1278                    }),
1279                    visual_search: Some(SearchEngineUrl {
1280                        base: "https://example.com/visual-search-variant".to_string(),
1281                        method: "GET".to_string(),
1282                        params: vec![SearchUrlParam {
1283                            name: "visual-search-variant-name".to_string(),
1284                            value: Some("visual-search-variant-value".to_string()),
1285                            enterprise_value: None,
1286                            experiment_config: None,
1287                        }],
1288                        search_term_param_name: Some("url_variant".to_string()),
1289                        // Should be the en-GB display name since the "en-GB"
1290                        // locale is present in `display_name_map`.
1291                        display_name: Some("Visual Search Variant en-GB".to_string()),
1292                        is_new_until: Some("2096-02-02".to_string()),
1293                        exclude_partner_code_from_telemetry: false,
1294                        accepted_content_types: Some(vec![
1295                            "image/png".to_string(),
1296                            "image/jpeg".to_string(),
1297                        ]),
1298                    }),
1299                },
1300                click_url: None
1301            }
1302        )
1303    }
1304
1305    static ENGINE_SUBVARIANT: Lazy<JSONEngineVariant> = Lazy::new(|| JSONEngineVariant {
1306        environment: JSONVariantEnvironment {
1307            all_regions_and_locales: true,
1308            ..Default::default()
1309        },
1310        is_new_until: Some("2063-04-05".to_string()),
1311        optional: true,
1312        partner_code: Some("trek2".to_string()),
1313        telemetry_suffix: Some("star2".to_string()),
1314        urls: Some(JSONEngineUrls {
1315            search: Some(JSONEngineUrl {
1316                base: Some("https://example.com/subvariant".to_string()),
1317                method: Some(JSONEngineMethod::Get),
1318                params: Some(vec![SearchUrlParam {
1319                    name: "subvariant".to_string(),
1320                    value: Some("test subvariant".to_string()),
1321                    enterprise_value: None,
1322                    experiment_config: None,
1323                }]),
1324                search_term_param_name: Some("shuttle".to_string()),
1325                ..Default::default()
1326            }),
1327            suggestions: Some(JSONEngineUrl {
1328                base: Some("https://example.com/suggestions-subvariant".to_string()),
1329                method: Some(JSONEngineMethod::Get),
1330                params: Some(vec![SearchUrlParam {
1331                    name: "suggest-subvariant".to_string(),
1332                    value: Some("sugg test subvariant".to_string()),
1333                    enterprise_value: None,
1334                    experiment_config: None,
1335                }]),
1336                search_term_param_name: Some("subvariant".to_string()),
1337                exclude_partner_code_from_telemetry: true,
1338                ..Default::default()
1339            }),
1340            trending: Some(JSONEngineUrl {
1341                base: Some("https://example.com/trending-subvariant".to_string()),
1342                method: Some(JSONEngineMethod::Get),
1343                params: Some(vec![SearchUrlParam {
1344                    name: "trend-subvariant".to_string(),
1345                    value: Some("trend test subvariant".to_string()),
1346                    enterprise_value: None,
1347                    experiment_config: None,
1348                }]),
1349                search_term_param_name: Some("subtrend".to_string()),
1350                ..Default::default()
1351            }),
1352            search_form: Some(JSONEngineUrl {
1353                base: Some("https://example.com/search-form-subvariant".to_string()),
1354                method: Some(crate::JSONEngineMethod::Get),
1355                params: Some(vec![SearchUrlParam {
1356                    name: "search-form-subvariant".to_string(),
1357                    value: Some("search form subvariant".to_string()),
1358                    enterprise_value: None,
1359                    experiment_config: None,
1360                }]),
1361                ..Default::default()
1362            }),
1363            visual_search: Some(JSONEngineUrl {
1364                base: Some("https://example.com/visual-search-subvariant".to_string()),
1365                method: Some(JSONEngineMethod::Get),
1366                params: Some(vec![SearchUrlParam {
1367                    name: "visual-search-subvariant-name".to_string(),
1368                    value: Some("visual-search-subvariant-value".to_string()),
1369                    enterprise_value: None,
1370                    experiment_config: None,
1371                }]),
1372                search_term_param_name: Some("url_subvariant".to_string()),
1373                display_name_map: Some(HashMap::from([
1374                    (
1375                        "default".to_string(),
1376                        "Visual Search Subvariant".to_string(),
1377                    ),
1378                    // en-GB locale with unique display name
1379                    (
1380                        "en-GB".to_string(),
1381                        "Visual Search Subvariant en-GB".to_string(),
1382                    ),
1383                ])),
1384                is_new_until: Some("2097-03-03".to_string()),
1385                accepted_content_types: Some(vec![
1386                    "image/jpeg".to_string(),
1387                    "image/webp".to_string(),
1388                ]),
1389                ..Default::default()
1390            }),
1391        }),
1392        sub_variants: vec![],
1393    });
1394
1395    #[test]
1396    fn test_from_configuration_details_merges_sub_variants() {
1397        let result = SearchEngineDefinition::from_configuration_details(
1398            &SearchUserEnvironment {
1399                locale: "fi".into(),
1400                ..Default::default()
1401            },
1402            "test",
1403            Lazy::force(&ENGINE_BASE).clone(),
1404            &ENGINE_VARIANT,
1405            &Some(ENGINE_SUBVARIANT.clone()),
1406        );
1407
1408        assert_eq!(
1409            result,
1410            SearchEngineDefinition {
1411                aliases: vec!["foo".to_string(), "bar".to_string()],
1412                charset: "ISO-8859-15".to_string(),
1413                classification: SearchEngineClassification::Unknown,
1414                identifier: "test".to_string(),
1415                is_new_until: Some("2063-04-05".to_string()),
1416                partner_code: "trek2".to_string(),
1417                name: "Test".to_string(),
1418                optional: true,
1419                order_hint: None,
1420                telemetry_suffix: "star2".to_string(),
1421                urls: SearchEngineUrls {
1422                    search: SearchEngineUrl {
1423                        base: "https://example.com/subvariant".to_string(),
1424                        method: "GET".to_string(),
1425                        params: vec![SearchUrlParam {
1426                            name: "subvariant".to_string(),
1427                            value: Some("test subvariant".to_string()),
1428                            enterprise_value: None,
1429                            experiment_config: None,
1430                        }],
1431                        search_term_param_name: Some("shuttle".to_string()),
1432                        ..Default::default()
1433                    },
1434                    suggestions: Some(SearchEngineUrl {
1435                        base: "https://example.com/suggestions-subvariant".to_string(),
1436                        method: "GET".to_string(),
1437                        params: vec![SearchUrlParam {
1438                            name: "suggest-subvariant".to_string(),
1439                            value: Some("sugg test subvariant".to_string()),
1440                            enterprise_value: None,
1441                            experiment_config: None,
1442                        }],
1443                        search_term_param_name: Some("subvariant".to_string()),
1444                        exclude_partner_code_from_telemetry: true,
1445                        ..Default::default()
1446                    }),
1447                    trending: Some(SearchEngineUrl {
1448                        base: "https://example.com/trending-subvariant".to_string(),
1449                        method: "GET".to_string(),
1450                        params: vec![SearchUrlParam {
1451                            name: "trend-subvariant".to_string(),
1452                            value: Some("trend test subvariant".to_string()),
1453                            enterprise_value: None,
1454                            experiment_config: None,
1455                        }],
1456                        search_term_param_name: Some("subtrend".to_string()),
1457                        ..Default::default()
1458                    }),
1459                    search_form: Some(SearchEngineUrl {
1460                        base: "https://example.com/search-form-subvariant".to_string(),
1461                        method: "GET".to_string(),
1462                        params: vec![SearchUrlParam {
1463                            name: "search-form-subvariant".to_string(),
1464                            value: Some("search form subvariant".to_string()),
1465                            enterprise_value: None,
1466                            experiment_config: None,
1467                        }],
1468                        ..Default::default()
1469                    }),
1470                    visual_search: Some(SearchEngineUrl {
1471                        base: "https://example.com/visual-search-subvariant".to_string(),
1472                        method: "GET".to_string(),
1473                        params: vec![SearchUrlParam {
1474                            name: "visual-search-subvariant-name".to_string(),
1475                            value: Some("visual-search-subvariant-value".to_string()),
1476                            enterprise_value: None,
1477                            experiment_config: None,
1478                        }],
1479                        search_term_param_name: Some("url_subvariant".to_string()),
1480                        // Should be the "default" display name since the "fi"
1481                        // locale isn't present in `display_name_map`.
1482                        display_name: Some("Visual Search Subvariant".to_string()),
1483                        is_new_until: Some("2097-03-03".to_string()),
1484                        exclude_partner_code_from_telemetry: false,
1485                        accepted_content_types: Some(vec![
1486                            "image/jpeg".to_string(),
1487                            "image/webp".to_string(),
1488                        ]),
1489                    }),
1490                },
1491                click_url: None
1492            }
1493        )
1494    }
1495
1496    #[test]
1497    fn test_from_configuration_details_merges_sub_variants_locale_match() {
1498        let result = SearchEngineDefinition::from_configuration_details(
1499            &SearchUserEnvironment {
1500                // en-GB locale
1501                locale: "en-GB".into(),
1502                ..Default::default()
1503            },
1504            "test",
1505            Lazy::force(&ENGINE_BASE).clone(),
1506            &ENGINE_VARIANT,
1507            &Some(ENGINE_SUBVARIANT.clone()),
1508        );
1509
1510        assert_eq!(
1511            result,
1512            SearchEngineDefinition {
1513                aliases: vec!["foo".to_string(), "bar".to_string()],
1514                charset: "ISO-8859-15".to_string(),
1515                classification: SearchEngineClassification::Unknown,
1516                identifier: "test".to_string(),
1517                is_new_until: Some("2063-04-05".to_string()),
1518                partner_code: "trek2".to_string(),
1519                name: "Test".to_string(),
1520                optional: true,
1521                order_hint: None,
1522                telemetry_suffix: "star2".to_string(),
1523                urls: SearchEngineUrls {
1524                    search: SearchEngineUrl {
1525                        base: "https://example.com/subvariant".to_string(),
1526                        method: "GET".to_string(),
1527                        params: vec![SearchUrlParam {
1528                            name: "subvariant".to_string(),
1529                            value: Some("test subvariant".to_string()),
1530                            enterprise_value: None,
1531                            experiment_config: None,
1532                        }],
1533                        search_term_param_name: Some("shuttle".to_string()),
1534                        ..Default::default()
1535                    },
1536                    suggestions: Some(SearchEngineUrl {
1537                        base: "https://example.com/suggestions-subvariant".to_string(),
1538                        method: "GET".to_string(),
1539                        params: vec![SearchUrlParam {
1540                            name: "suggest-subvariant".to_string(),
1541                            value: Some("sugg test subvariant".to_string()),
1542                            enterprise_value: None,
1543                            experiment_config: None,
1544                        }],
1545                        search_term_param_name: Some("subvariant".to_string()),
1546                        exclude_partner_code_from_telemetry: true,
1547                        ..Default::default()
1548                    }),
1549                    trending: Some(SearchEngineUrl {
1550                        base: "https://example.com/trending-subvariant".to_string(),
1551                        method: "GET".to_string(),
1552                        params: vec![SearchUrlParam {
1553                            name: "trend-subvariant".to_string(),
1554                            value: Some("trend test subvariant".to_string()),
1555                            enterprise_value: None,
1556                            experiment_config: None,
1557                        }],
1558                        search_term_param_name: Some("subtrend".to_string()),
1559                        ..Default::default()
1560                    }),
1561                    search_form: Some(SearchEngineUrl {
1562                        base: "https://example.com/search-form-subvariant".to_string(),
1563                        method: "GET".to_string(),
1564                        params: vec![SearchUrlParam {
1565                            name: "search-form-subvariant".to_string(),
1566                            value: Some("search form subvariant".to_string()),
1567                            enterprise_value: None,
1568                            experiment_config: None,
1569                        }],
1570                        ..Default::default()
1571                    }),
1572                    visual_search: Some(SearchEngineUrl {
1573                        base: "https://example.com/visual-search-subvariant".to_string(),
1574                        method: "GET".to_string(),
1575                        params: vec![SearchUrlParam {
1576                            name: "visual-search-subvariant-name".to_string(),
1577                            value: Some("visual-search-subvariant-value".to_string()),
1578                            enterprise_value: None,
1579                            experiment_config: None,
1580                        }],
1581                        search_term_param_name: Some("url_subvariant".to_string()),
1582                        // Should be the en-GB display name since the "en-GB"
1583                        // locale is present in `display_name_map`.
1584                        display_name: Some("Visual Search Subvariant en-GB".to_string()),
1585                        is_new_until: Some("2097-03-03".to_string()),
1586                        exclude_partner_code_from_telemetry: false,
1587                        accepted_content_types: Some(vec![
1588                            "image/jpeg".to_string(),
1589                            "image/webp".to_string(),
1590                        ]),
1591                    }),
1592                },
1593                click_url: None
1594            }
1595        )
1596    }
1597
1598    static ENGINES_LIST: Lazy<Vec<SearchEngineDefinition>> = Lazy::new(|| {
1599        vec![
1600            SearchEngineDefinition {
1601                identifier: "engine1".to_string(),
1602                name: "Test".to_string(),
1603                urls: SearchEngineUrls {
1604                    search: SearchEngineUrl {
1605                        base: "https://example.com".to_string(),
1606                        ..Default::default()
1607                    },
1608                    ..Default::default()
1609                },
1610                ..Default::default()
1611            },
1612            SearchEngineDefinition {
1613                identifier: "engine2".to_string(),
1614                name: "Test 2".to_string(),
1615                urls: SearchEngineUrls {
1616                    search: SearchEngineUrl {
1617                        base: "https://example.com/2".to_string(),
1618                        ..Default::default()
1619                    },
1620                    ..Default::default()
1621                },
1622                ..Default::default()
1623            },
1624            SearchEngineDefinition {
1625                identifier: "engine3".to_string(),
1626                name: "Test 3".to_string(),
1627                urls: SearchEngineUrls {
1628                    search: SearchEngineUrl {
1629                        base: "https://example.com/3".to_string(),
1630                        ..Default::default()
1631                    },
1632                    ..Default::default()
1633                },
1634                ..Default::default()
1635            },
1636            SearchEngineDefinition {
1637                identifier: "engine4wildcardmatch".to_string(),
1638                name: "Test 4".to_string(),
1639                urls: SearchEngineUrls {
1640                    search: SearchEngineUrl {
1641                        base: "https://example.com/4".to_string(),
1642                        ..Default::default()
1643                    },
1644                    ..Default::default()
1645                },
1646                ..Default::default()
1647            },
1648        ]
1649    });
1650
1651    #[test]
1652    fn test_determine_default_engines_returns_global_default() {
1653        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1654            &ENGINES_LIST,
1655            Some(JSONDefaultEnginesRecord {
1656                global_default: "engine2".to_string(),
1657                global_default_private: String::new(),
1658                specific_defaults: Vec::new(),
1659            }),
1660            &SearchUserEnvironment {
1661                locale: "fi".into(),
1662                ..Default::default()
1663            },
1664        );
1665
1666        assert_eq!(
1667            default_engine_id.unwrap(),
1668            "engine2",
1669            "Should have returned the global default engine"
1670        );
1671        assert!(
1672            default_engine_private_id.is_none(),
1673            "Should not have returned an id for the private engine"
1674        );
1675
1676        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1677            &ENGINES_LIST,
1678            Some(JSONDefaultEnginesRecord {
1679                global_default: "engine2".to_string(),
1680                global_default_private: String::new(),
1681                specific_defaults: vec![JSONSpecificDefaultRecord {
1682                    default: "engine1".to_string(),
1683                    default_private: String::new(),
1684                    environment: JSONVariantEnvironment {
1685                        locales: vec!["en-GB".to_string()],
1686                        ..Default::default()
1687                    },
1688                }],
1689            }),
1690            &SearchUserEnvironment {
1691                locale: "fi".into(),
1692                ..Default::default()
1693            },
1694        );
1695
1696        assert_eq!(
1697            default_engine_id.unwrap(),
1698            "engine2",
1699            "Should have returned the global default engine when no specific defaults environments match"
1700        );
1701        assert!(
1702            default_engine_private_id.is_none(),
1703            "Should not have returned an id for the private engine"
1704        );
1705
1706        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1707            &ENGINES_LIST,
1708            Some(JSONDefaultEnginesRecord {
1709                global_default: "engine2".to_string(),
1710                global_default_private: String::new(),
1711                specific_defaults: vec![JSONSpecificDefaultRecord {
1712                    default: "engine1".to_string(),
1713                    default_private: String::new(),
1714                    environment: JSONVariantEnvironment {
1715                        locales: vec!["fi".to_string()],
1716                        ..Default::default()
1717                    },
1718                }],
1719            }),
1720            &SearchUserEnvironment {
1721                locale: "fi".into(),
1722                ..Default::default()
1723            },
1724        );
1725
1726        assert_eq!(
1727            default_engine_id.unwrap(),
1728            "engine1",
1729            "Should have returned the specific default when environments match"
1730        );
1731        assert!(
1732            default_engine_private_id.is_none(),
1733            "Should not have returned an id for the private engine"
1734        );
1735
1736        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1737            &ENGINES_LIST,
1738            Some(JSONDefaultEnginesRecord {
1739                global_default: "engine2".to_string(),
1740                global_default_private: String::new(),
1741                specific_defaults: vec![JSONSpecificDefaultRecord {
1742                    default: "engine4*".to_string(),
1743                    default_private: String::new(),
1744                    environment: JSONVariantEnvironment {
1745                        locales: vec!["fi".to_string()],
1746                        ..Default::default()
1747                    },
1748                }],
1749            }),
1750            &SearchUserEnvironment {
1751                locale: "fi".into(),
1752                ..Default::default()
1753            },
1754        );
1755
1756        assert_eq!(
1757            default_engine_id.unwrap(),
1758            "engine4wildcardmatch",
1759            "Should have returned the specific default when using a wildcard match"
1760        );
1761        assert!(
1762            default_engine_private_id.is_none(),
1763            "Should not have returned an id for the private engine"
1764        );
1765
1766        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1767            &ENGINES_LIST,
1768            Some(JSONDefaultEnginesRecord {
1769                global_default: "engine2".to_string(),
1770                global_default_private: String::new(),
1771                specific_defaults: vec![
1772                    JSONSpecificDefaultRecord {
1773                        default: "engine4*".to_string(),
1774                        default_private: String::new(),
1775                        environment: JSONVariantEnvironment {
1776                            locales: vec!["fi".to_string()],
1777                            ..Default::default()
1778                        },
1779                    },
1780                    JSONSpecificDefaultRecord {
1781                        default: "engine3".to_string(),
1782                        default_private: String::new(),
1783                        environment: JSONVariantEnvironment {
1784                            locales: vec!["fi".to_string()],
1785                            ..Default::default()
1786                        },
1787                    },
1788                ],
1789            }),
1790            &SearchUserEnvironment {
1791                locale: "fi".into(),
1792                ..Default::default()
1793            },
1794        );
1795
1796        assert_eq!(
1797            default_engine_id.unwrap(),
1798            "engine3",
1799            "Should have returned the last specific default when multiple environments match"
1800        );
1801        assert!(
1802            default_engine_private_id.is_none(),
1803            "Should not have returned an id for the private engine"
1804        );
1805    }
1806
1807    #[test]
1808    fn test_determine_default_engines_returns_global_default_private() {
1809        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1810            &ENGINES_LIST,
1811            Some(JSONDefaultEnginesRecord {
1812                global_default: "engine2".to_string(),
1813                global_default_private: "engine3".to_string(),
1814                specific_defaults: Vec::new(),
1815            }),
1816            &SearchUserEnvironment {
1817                ..Default::default()
1818            },
1819        );
1820
1821        assert_eq!(
1822            default_engine_id.unwrap(),
1823            "engine2",
1824            "Should have returned the global default engine"
1825        );
1826        assert_eq!(
1827            default_engine_private_id.unwrap(),
1828            "engine3",
1829            "Should have returned the global default engine for private mode"
1830        );
1831
1832        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1833            &ENGINES_LIST,
1834            Some(JSONDefaultEnginesRecord {
1835                global_default: "engine2".to_string(),
1836                global_default_private: "engine3".to_string(),
1837                specific_defaults: vec![JSONSpecificDefaultRecord {
1838                    default: String::new(),
1839                    default_private: "engine1".to_string(),
1840                    environment: JSONVariantEnvironment {
1841                        locales: vec!["en-GB".to_string()],
1842                        ..Default::default()
1843                    },
1844                }],
1845            }),
1846            &SearchUserEnvironment {
1847                locale: "fi".into(),
1848                ..Default::default()
1849            },
1850        );
1851
1852        assert_eq!(
1853            default_engine_id.unwrap(),
1854            "engine2",
1855            "Should have returned the global default engine when no specific defaults environments match"
1856        );
1857        assert_eq!(
1858            default_engine_private_id.unwrap(),
1859            "engine3",
1860            "Should have returned the global default engine for private mode when no specific defaults environments match"
1861        );
1862
1863        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1864            &ENGINES_LIST,
1865            Some(JSONDefaultEnginesRecord {
1866                global_default: "engine2".to_string(),
1867                global_default_private: "engine3".to_string(),
1868                specific_defaults: vec![JSONSpecificDefaultRecord {
1869                    default: String::new(),
1870                    default_private: "engine1".to_string(),
1871                    environment: JSONVariantEnvironment {
1872                        locales: vec!["fi".to_string()],
1873                        ..Default::default()
1874                    },
1875                }],
1876            }),
1877            &SearchUserEnvironment {
1878                locale: "fi".into(),
1879                ..Default::default()
1880            },
1881        );
1882
1883        assert_eq!(
1884            default_engine_id.unwrap(),
1885            "engine2",
1886            "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)."
1887        );
1888        assert_eq!(
1889            default_engine_private_id.unwrap(),
1890            "engine1",
1891            "Should have returned the specific default engine for private mode when environments match"
1892        );
1893
1894        let (default_engine_id, default_engine_private_id) = determine_default_engines(
1895            &ENGINES_LIST,
1896            Some(JSONDefaultEnginesRecord {
1897                global_default: "engine2".to_string(),
1898                global_default_private: String::new(),
1899                specific_defaults: vec![JSONSpecificDefaultRecord {
1900                    default: String::new(),
1901                    default_private: "engine4*".to_string(),
1902                    environment: JSONVariantEnvironment {
1903                        locales: vec!["fi".to_string()],
1904                        ..Default::default()
1905                    },
1906                }],
1907            }),
1908            &SearchUserEnvironment {
1909                locale: "fi".into(),
1910                ..Default::default()
1911            },
1912        );
1913
1914        assert_eq!(
1915            default_engine_id.unwrap(),
1916            "engine2",
1917            "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)"
1918        );
1919        assert_eq!(
1920            default_engine_private_id.unwrap(),
1921            "engine4wildcardmatch",
1922            "Should have returned the specific default for private mode when using a wildcard match"
1923        );
1924    }
1925
1926    #[test]
1927    fn test_locale_matched_exactly() {
1928        let mut user_env = SearchUserEnvironment {
1929            locale: "en-CA".into(),
1930            ..Default::default()
1931        };
1932        negotiate_languages(&mut user_env, &["en-CA".to_string(), "fr".to_string()]);
1933        assert_eq!(
1934            user_env.locale, "en-CA",
1935            "Should return user locale unchanged if in available locales"
1936        );
1937    }
1938
1939    #[test]
1940    fn test_locale_fallback_to_base_locale() {
1941        let mut user_env = SearchUserEnvironment {
1942            locale: "de-AT".into(),
1943            ..Default::default()
1944        };
1945        negotiate_languages(&mut user_env, &["de".to_string()]);
1946        assert_eq!(
1947            user_env.locale, "de",
1948            "Should fallback to base locale if base is in available locales"
1949        );
1950    }
1951
1952    static ENGLISH_LOCALES: &[&str] = &["en-AU", "en-IE", "en-RU", "en-ZA"];
1953
1954    #[test]
1955    fn test_english_locales_fallbacks_to_en_us() {
1956        for user_locale in ENGLISH_LOCALES {
1957            let mut user_env = SearchUserEnvironment {
1958                locale: user_locale.to_string(),
1959                ..Default::default()
1960            };
1961            negotiate_languages(&mut user_env, &["en-US".to_string()]);
1962            assert_eq!(
1963                user_env.locale, "en-us",
1964                "Should remap {} to en-us when en-us is available",
1965                user_locale
1966            );
1967        }
1968    }
1969
1970    #[test]
1971    fn test_locale_unmatched() {
1972        let mut user_env = SearchUserEnvironment {
1973            locale: "fr-CA".into(),
1974            ..Default::default()
1975        };
1976        negotiate_languages(&mut user_env, &["de".to_string(), "en-US".to_string()]);
1977        assert_eq!(
1978            user_env.locale, "fr-CA",
1979            "Should leave locale unchanged if no match or english locale fallback is not found"
1980        );
1981    }
1982}