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 super::*;
491    use crate::*;
492    use once_cell::sync::Lazy;
493    use std::{collections::HashMap, vec};
494
495    #[test]
496    fn test_default_search_engine_url() {
497        assert_eq!(
498            SearchEngineUrl::default(),
499            SearchEngineUrl {
500                base: "".to_string(),
501                method: "GET".to_string(),
502                params: Vec::new(),
503                search_term_param_name: None,
504                display_name: None,
505                is_new_until: None,
506                exclude_partner_code_from_telemetry: false,
507                accepted_content_types: None,
508            },
509        );
510    }
511
512    #[test]
513    fn test_default_search_engine_urls() {
514        assert_eq!(
515            SearchEngineUrls::default(),
516            SearchEngineUrls {
517                search: SearchEngineUrl::default(),
518                suggestions: None,
519                trending: None,
520                search_form: None,
521                visual_search: None,
522            },
523        );
524    }
525
526    #[test]
527    fn test_merge_override() {
528        let mut test_engine = SearchEngineDefinition {
529            identifier: "test".to_string(),
530            partner_code: "partner-code".to_string(),
531            telemetry_suffix: "original-telemetry-suffix".to_string(),
532            ..Default::default()
533        };
534
535        let override_record = JSONOverridesRecord {
536            identifier: "test".to_string(),
537            partner_code: "override-partner-code".to_string(),
538            click_url: "https://example.com/click-url".to_string(),
539            telemetry_suffix: None,
540            urls: JSONEngineUrls {
541                search: Some(JSONEngineUrl {
542                    base: Some("https://example.com/override-search".to_string()),
543                    ..Default::default()
544                }),
545                ..Default::default()
546            },
547        };
548
549        test_engine.merge_override(
550            &SearchUserEnvironment {
551                locale: "fi".into(),
552                ..Default::default()
553            },
554            &override_record,
555        );
556
557        assert_eq!(
558            test_engine.partner_code, "override-partner-code",
559            "Should override the partner code"
560        );
561        assert_eq!(
562            test_engine.click_url,
563            Some("https://example.com/click-url".to_string()),
564            "Should override the click url"
565        );
566        assert_eq!(
567            test_engine.urls.search.base, "https://example.com/override-search",
568            "Should override search url"
569        );
570        assert_eq!(
571            test_engine.telemetry_suffix, "original-telemetry-suffix",
572            "Should not override telemetry suffix when telemetry suffix is supplied as None"
573        );
574    }
575
576    #[test]
577    fn test_merge_override_locale_match() {
578        let mut test_engine = SearchEngineDefinition {
579            identifier: "test".to_string(),
580            partner_code: "partner-code".to_string(),
581            telemetry_suffix: "original-telemetry-suffix".to_string(),
582            ..Default::default()
583        };
584
585        let override_record = JSONOverridesRecord {
586            identifier: "test".to_string(),
587            partner_code: "override-partner-code".to_string(),
588            click_url: "https://example.com/click-url".to_string(),
589            telemetry_suffix: None,
590            urls: JSONEngineUrls {
591                search: Some(JSONEngineUrl {
592                    base: Some("https://example.com/override-search".to_string()),
593                    display_name_map: Some(HashMap::from([
594                        // Default display name
595                        ("default".to_string(), "My Display Name".to_string()),
596                        // en-GB locale with unique display name
597                        ("en-GB".to_string(), "en-GB Display Name".to_string()),
598                    ])),
599                    ..Default::default()
600                }),
601                ..Default::default()
602            },
603        };
604
605        test_engine.merge_override(
606            &SearchUserEnvironment {
607                // en-GB locale
608                locale: "en-GB".into(),
609                ..Default::default()
610            },
611            &override_record,
612        );
613
614        assert_eq!(
615            test_engine.urls.search.display_name,
616            Some("en-GB Display Name".to_string()),
617            "Should override display name with en-GB version"
618        );
619    }
620
621    static ENGINES_LIST: Lazy<Vec<SearchEngineDefinition>> = Lazy::new(|| {
622        vec![
623            SearchEngineDefinition {
624                identifier: "engine1".to_string(),
625                name: "Test".to_string(),
626                urls: SearchEngineUrls {
627                    search: SearchEngineUrl {
628                        base: "https://example.com".to_string(),
629                        ..Default::default()
630                    },
631                    ..Default::default()
632                },
633                ..Default::default()
634            },
635            SearchEngineDefinition {
636                identifier: "engine2".to_string(),
637                name: "Test 2".to_string(),
638                urls: SearchEngineUrls {
639                    search: SearchEngineUrl {
640                        base: "https://example.com/2".to_string(),
641                        ..Default::default()
642                    },
643                    ..Default::default()
644                },
645                ..Default::default()
646            },
647            SearchEngineDefinition {
648                identifier: "engine3".to_string(),
649                name: "Test 3".to_string(),
650                urls: SearchEngineUrls {
651                    search: SearchEngineUrl {
652                        base: "https://example.com/3".to_string(),
653                        ..Default::default()
654                    },
655                    ..Default::default()
656                },
657                ..Default::default()
658            },
659            SearchEngineDefinition {
660                identifier: "engine4wildcardmatch".to_string(),
661                name: "Test 4".to_string(),
662                urls: SearchEngineUrls {
663                    search: SearchEngineUrl {
664                        base: "https://example.com/4".to_string(),
665                        ..Default::default()
666                    },
667                    ..Default::default()
668                },
669                ..Default::default()
670            },
671        ]
672    });
673
674    #[test]
675    fn test_determine_default_engines_returns_global_default() {
676        let (default_engine_id, default_engine_private_id) = determine_default_engines(
677            &ENGINES_LIST,
678            Some(JSONDefaultEnginesRecord {
679                global_default: "engine2".to_string(),
680                global_default_private: String::new(),
681                specific_defaults: Vec::new(),
682            }),
683            &SearchUserEnvironment {
684                locale: "fi".into(),
685                ..Default::default()
686            },
687        );
688
689        assert_eq!(
690            default_engine_id.unwrap(),
691            "engine2",
692            "Should have returned the global default engine"
693        );
694        assert!(
695            default_engine_private_id.is_none(),
696            "Should not have returned an id for the private engine"
697        );
698
699        let (default_engine_id, default_engine_private_id) = determine_default_engines(
700            &ENGINES_LIST,
701            Some(JSONDefaultEnginesRecord {
702                global_default: "engine2".to_string(),
703                global_default_private: String::new(),
704                specific_defaults: vec![JSONSpecificDefaultRecord {
705                    default: "engine1".to_string(),
706                    default_private: String::new(),
707                    environment: JSONVariantEnvironment {
708                        locales: vec!["en-GB".to_string()],
709                        ..Default::default()
710                    },
711                }],
712            }),
713            &SearchUserEnvironment {
714                locale: "fi".into(),
715                ..Default::default()
716            },
717        );
718
719        assert_eq!(
720            default_engine_id.unwrap(),
721            "engine2",
722            "Should have returned the global default engine when no specific defaults environments match"
723        );
724        assert!(
725            default_engine_private_id.is_none(),
726            "Should not have returned an id for the private engine"
727        );
728
729        let (default_engine_id, default_engine_private_id) = determine_default_engines(
730            &ENGINES_LIST,
731            Some(JSONDefaultEnginesRecord {
732                global_default: "engine2".to_string(),
733                global_default_private: String::new(),
734                specific_defaults: vec![JSONSpecificDefaultRecord {
735                    default: "engine1".to_string(),
736                    default_private: String::new(),
737                    environment: JSONVariantEnvironment {
738                        locales: vec!["fi".to_string()],
739                        ..Default::default()
740                    },
741                }],
742            }),
743            &SearchUserEnvironment {
744                locale: "fi".into(),
745                ..Default::default()
746            },
747        );
748
749        assert_eq!(
750            default_engine_id.unwrap(),
751            "engine1",
752            "Should have returned the specific default when environments match"
753        );
754        assert!(
755            default_engine_private_id.is_none(),
756            "Should not have returned an id for the private engine"
757        );
758
759        let (default_engine_id, default_engine_private_id) = determine_default_engines(
760            &ENGINES_LIST,
761            Some(JSONDefaultEnginesRecord {
762                global_default: "engine2".to_string(),
763                global_default_private: String::new(),
764                specific_defaults: vec![JSONSpecificDefaultRecord {
765                    default: "engine4*".to_string(),
766                    default_private: String::new(),
767                    environment: JSONVariantEnvironment {
768                        locales: vec!["fi".to_string()],
769                        ..Default::default()
770                    },
771                }],
772            }),
773            &SearchUserEnvironment {
774                locale: "fi".into(),
775                ..Default::default()
776            },
777        );
778
779        assert_eq!(
780            default_engine_id.unwrap(),
781            "engine4wildcardmatch",
782            "Should have returned the specific default when using a wildcard match"
783        );
784        assert!(
785            default_engine_private_id.is_none(),
786            "Should not have returned an id for the private engine"
787        );
788
789        let (default_engine_id, default_engine_private_id) = determine_default_engines(
790            &ENGINES_LIST,
791            Some(JSONDefaultEnginesRecord {
792                global_default: "engine2".to_string(),
793                global_default_private: String::new(),
794                specific_defaults: vec![
795                    JSONSpecificDefaultRecord {
796                        default: "engine4*".to_string(),
797                        default_private: String::new(),
798                        environment: JSONVariantEnvironment {
799                            locales: vec!["fi".to_string()],
800                            ..Default::default()
801                        },
802                    },
803                    JSONSpecificDefaultRecord {
804                        default: "engine3".to_string(),
805                        default_private: String::new(),
806                        environment: JSONVariantEnvironment {
807                            locales: vec!["fi".to_string()],
808                            ..Default::default()
809                        },
810                    },
811                ],
812            }),
813            &SearchUserEnvironment {
814                locale: "fi".into(),
815                ..Default::default()
816            },
817        );
818
819        assert_eq!(
820            default_engine_id.unwrap(),
821            "engine3",
822            "Should have returned the last specific default when multiple environments match"
823        );
824        assert!(
825            default_engine_private_id.is_none(),
826            "Should not have returned an id for the private engine"
827        );
828    }
829
830    #[test]
831    fn test_determine_default_engines_returns_global_default_private() {
832        let (default_engine_id, default_engine_private_id) = determine_default_engines(
833            &ENGINES_LIST,
834            Some(JSONDefaultEnginesRecord {
835                global_default: "engine2".to_string(),
836                global_default_private: "engine3".to_string(),
837                specific_defaults: Vec::new(),
838            }),
839            &SearchUserEnvironment {
840                ..Default::default()
841            },
842        );
843
844        assert_eq!(
845            default_engine_id.unwrap(),
846            "engine2",
847            "Should have returned the global default engine"
848        );
849        assert_eq!(
850            default_engine_private_id.unwrap(),
851            "engine3",
852            "Should have returned the global default engine for private mode"
853        );
854
855        let (default_engine_id, default_engine_private_id) = determine_default_engines(
856            &ENGINES_LIST,
857            Some(JSONDefaultEnginesRecord {
858                global_default: "engine2".to_string(),
859                global_default_private: "engine3".to_string(),
860                specific_defaults: vec![JSONSpecificDefaultRecord {
861                    default: String::new(),
862                    default_private: "engine1".to_string(),
863                    environment: JSONVariantEnvironment {
864                        locales: vec!["en-GB".to_string()],
865                        ..Default::default()
866                    },
867                }],
868            }),
869            &SearchUserEnvironment {
870                locale: "fi".into(),
871                ..Default::default()
872            },
873        );
874
875        assert_eq!(
876            default_engine_id.unwrap(),
877            "engine2",
878            "Should have returned the global default engine when no specific defaults environments match"
879        );
880        assert_eq!(
881            default_engine_private_id.unwrap(),
882            "engine3",
883            "Should have returned the global default engine for private mode when no specific defaults environments match"
884        );
885
886        let (default_engine_id, default_engine_private_id) = determine_default_engines(
887            &ENGINES_LIST,
888            Some(JSONDefaultEnginesRecord {
889                global_default: "engine2".to_string(),
890                global_default_private: "engine3".to_string(),
891                specific_defaults: vec![JSONSpecificDefaultRecord {
892                    default: String::new(),
893                    default_private: "engine1".to_string(),
894                    environment: JSONVariantEnvironment {
895                        locales: vec!["fi".to_string()],
896                        ..Default::default()
897                    },
898                }],
899            }),
900            &SearchUserEnvironment {
901                locale: "fi".into(),
902                ..Default::default()
903            },
904        );
905
906        assert_eq!(
907            default_engine_id.unwrap(),
908            "engine2",
909            "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)."
910        );
911        assert_eq!(
912            default_engine_private_id.unwrap(),
913            "engine1",
914            "Should have returned the specific default engine for private mode when environments match"
915        );
916
917        let (default_engine_id, default_engine_private_id) = determine_default_engines(
918            &ENGINES_LIST,
919            Some(JSONDefaultEnginesRecord {
920                global_default: "engine2".to_string(),
921                global_default_private: String::new(),
922                specific_defaults: vec![JSONSpecificDefaultRecord {
923                    default: String::new(),
924                    default_private: "engine4*".to_string(),
925                    environment: JSONVariantEnvironment {
926                        locales: vec!["fi".to_string()],
927                        ..Default::default()
928                    },
929                }],
930            }),
931            &SearchUserEnvironment {
932                locale: "fi".into(),
933                ..Default::default()
934            },
935        );
936
937        assert_eq!(
938            default_engine_id.unwrap(),
939            "engine2",
940            "Should have returned the global default engine when specific environments match which override the private global default (and not the global default)"
941        );
942        assert_eq!(
943            default_engine_private_id.unwrap(),
944            "engine4wildcardmatch",
945            "Should have returned the specific default for private mode when using a wildcard match"
946        );
947    }
948
949    #[test]
950    fn test_locale_matched_exactly() {
951        let mut user_env = SearchUserEnvironment {
952            locale: "en-CA".into(),
953            ..Default::default()
954        };
955        negotiate_languages(&mut user_env, &["en-CA".to_string(), "fr".to_string()]);
956        assert_eq!(
957            user_env.locale, "en-CA",
958            "Should return user locale unchanged if in available locales"
959        );
960    }
961
962    #[test]
963    fn test_locale_fallback_to_base_locale() {
964        let mut user_env = SearchUserEnvironment {
965            locale: "de-AT".into(),
966            ..Default::default()
967        };
968        negotiate_languages(&mut user_env, &["de".to_string()]);
969        assert_eq!(
970            user_env.locale, "de",
971            "Should fallback to base locale if base is in available locales"
972        );
973    }
974
975    static ENGLISH_LOCALES: &[&str] = &["en-AU", "en-IE", "en-RU", "en-ZA"];
976
977    #[test]
978    fn test_english_locales_fallbacks_to_en_us() {
979        for user_locale in ENGLISH_LOCALES {
980            let mut user_env = SearchUserEnvironment {
981                locale: user_locale.to_string(),
982                ..Default::default()
983            };
984            negotiate_languages(&mut user_env, &["en-US".to_string()]);
985            assert_eq!(
986                user_env.locale, "en-us",
987                "Should remap {} to en-us when en-us is available",
988                user_locale
989            );
990        }
991    }
992
993    #[test]
994    fn test_locale_unmatched() {
995        let mut user_env = SearchUserEnvironment {
996            locale: "fr-CA".into(),
997            ..Default::default()
998        };
999        negotiate_languages(&mut user_env, &["de".to_string(), "en-US".to_string()]);
1000        assert_eq!(
1001            user_env.locale, "fr-CA",
1002            "Should leave locale unchanged if no match or english locale fallback is not found"
1003        );
1004    }
1005}
1006
1007#[cfg(test)]
1008mod from_configuration_details_tests {
1009    use crate::test_helpers::{
1010        ExpectedEngineFromJSONBase, JSON_ENGINE_BASE, JSON_ENGINE_SUBVARIANT, JSON_ENGINE_VARIANT,
1011    };
1012    use crate::*;
1013    use once_cell::sync::Lazy;
1014
1015    #[test]
1016    fn test_fallsback_to_defaults() {
1017        // This test doesn't use `..Default::default()` as we want to
1018        // be explicit about `JSONEngineBase` and handling `None`
1019        // options/default values.
1020        let result = SearchEngineDefinition::from_configuration_details(
1021            &SearchUserEnvironment {
1022                locale: "fi".into(),
1023                ..Default::default()
1024            },
1025            "test",
1026            JSONEngineBase {
1027                aliases: None,
1028                charset: None,
1029                classification: SearchEngineClassification::General,
1030                name: "Test".to_string(),
1031                partner_code: None,
1032                urls: JSONEngineUrls {
1033                    search: Some(JSONEngineUrl {
1034                        base: Some("https://example.com".to_string()),
1035                        ..Default::default()
1036                    }),
1037                    suggestions: None,
1038                    trending: None,
1039                    search_form: None,
1040                    visual_search: None,
1041                },
1042            },
1043            &JSONEngineVariant {
1044                environment: JSONVariantEnvironment {
1045                    all_regions_and_locales: true,
1046                    ..Default::default()
1047                },
1048                is_new_until: None,
1049                optional: false,
1050                partner_code: None,
1051                telemetry_suffix: None,
1052                urls: None,
1053                sub_variants: vec![],
1054            },
1055            &None,
1056        );
1057
1058        assert_eq!(
1059            result,
1060            SearchEngineDefinition {
1061                aliases: Vec::new(),
1062                charset: "UTF-8".to_string(),
1063                classification: SearchEngineClassification::General,
1064                identifier: "test".to_string(),
1065                is_new_until: None,
1066                partner_code: String::new(),
1067                name: "Test".to_string(),
1068                optional: false,
1069                order_hint: None,
1070                telemetry_suffix: String::new(),
1071                urls: SearchEngineUrls {
1072                    search: SearchEngineUrl {
1073                        base: "https://example.com".to_string(),
1074                        ..Default::default()
1075                    },
1076                    suggestions: None,
1077                    trending: None,
1078                    search_form: None,
1079                    visual_search: None,
1080                },
1081                click_url: None
1082            }
1083        )
1084    }
1085
1086    #[test]
1087    fn test_uses_base_values_only() {
1088        let result = SearchEngineDefinition::from_configuration_details(
1089            &SearchUserEnvironment {
1090                locale: "fi".into(),
1091                ..Default::default()
1092            },
1093            "test",
1094            Lazy::force(&JSON_ENGINE_BASE).clone(),
1095            &JSONEngineVariant {
1096                environment: JSONVariantEnvironment {
1097                    all_regions_and_locales: true,
1098                    ..Default::default()
1099                },
1100                is_new_until: None,
1101                optional: false,
1102                partner_code: None,
1103                telemetry_suffix: None,
1104                urls: None,
1105                sub_variants: vec![],
1106            },
1107            &None,
1108        );
1109        assert_eq!(
1110            result,
1111            ExpectedEngineFromJSONBase::new("test", "Test").build()
1112        );
1113    }
1114
1115    #[test]
1116    fn test_uses_locale_specific_visual_display_name() {
1117        let result = SearchEngineDefinition::from_configuration_details(
1118            &SearchUserEnvironment {
1119                locale: "en-GB".into(),
1120                ..Default::default()
1121            },
1122            "test",
1123            Lazy::force(&JSON_ENGINE_BASE).clone(),
1124            &JSONEngineVariant {
1125                environment: JSONVariantEnvironment {
1126                    all_regions_and_locales: true,
1127                    ..Default::default()
1128                },
1129                is_new_until: None,
1130                optional: false,
1131                partner_code: None,
1132                telemetry_suffix: None,
1133                urls: None,
1134                sub_variants: vec![],
1135            },
1136            &None,
1137        );
1138
1139        assert_eq!(
1140            result,
1141            ExpectedEngineFromJSONBase::new("test", "Test")
1142                .visual_search_display_name("Visual Search en-GB")
1143                .build()
1144        );
1145    }
1146
1147    #[test]
1148    fn test_merges_variants() {
1149        let result = SearchEngineDefinition::from_configuration_details(
1150            &SearchUserEnvironment {
1151                locale: "fi".into(),
1152                ..Default::default()
1153            },
1154            "test",
1155            Lazy::force(&JSON_ENGINE_BASE).clone(),
1156            &JSON_ENGINE_VARIANT,
1157            &None,
1158        );
1159
1160        assert_eq!(
1161            result,
1162            ExpectedEngineFromJSONBase::new("test", "Test")
1163                .variant_is_new_until("2063-04-05")
1164                .variant_optional(true)
1165                .variant_partner_code("trek")
1166                .variant_telemetry_suffix("star")
1167                .variant_search_url(
1168                    "https://example.com/variant",
1169                    "GET",
1170                    "variant",
1171                    "test variant",
1172                    "ship",
1173                )
1174                .variant_suggestions_url(
1175                    "https://example.com/suggestions-variant",
1176                    "GET",
1177                    "suggest-variant",
1178                    "sugg test variant",
1179                    "variant",
1180                )
1181                .variant_trending_url(
1182                    "https://example.com/trending-variant",
1183                    "GET",
1184                    "trend-variant",
1185                    "trend test variant",
1186                    "trend",
1187                    true,
1188                )
1189                .variant_search_form_url(
1190                    "https://example.com/search_form",
1191                    "GET",
1192                    "search-form-name",
1193                    "search-form-value",
1194                )
1195                .variant_visual_search_url(
1196                    "https://example.com/visual-search-variant",
1197                    "visual-search-variant-name",
1198                    "visual-search-variant-value",
1199                    "url_variant",
1200                    "Visual Search Variant",
1201                    "2096-02-02",
1202                )
1203                .build()
1204        );
1205    }
1206
1207    #[test]
1208    fn test_merges_variant_and_uses_locale_specific_visual_search_display_name() {
1209        let result = SearchEngineDefinition::from_configuration_details(
1210            &SearchUserEnvironment {
1211                locale: "en-GB".into(),
1212                ..Default::default()
1213            },
1214            "test",
1215            Lazy::force(&JSON_ENGINE_BASE).clone(),
1216            &JSON_ENGINE_VARIANT,
1217            &None,
1218        );
1219
1220        assert_eq!(
1221            result,
1222            ExpectedEngineFromJSONBase::new("test", "Test")
1223                .variant_is_new_until("2063-04-05")
1224                .variant_optional(true)
1225                .variant_partner_code("trek")
1226                .variant_telemetry_suffix("star")
1227                .variant_search_url(
1228                    "https://example.com/variant",
1229                    "GET",
1230                    "variant",
1231                    "test variant",
1232                    "ship",
1233                )
1234                .variant_suggestions_url(
1235                    "https://example.com/suggestions-variant",
1236                    "GET",
1237                    "suggest-variant",
1238                    "sugg test variant",
1239                    "variant",
1240                )
1241                .variant_trending_url(
1242                    "https://example.com/trending-variant",
1243                    "GET",
1244                    "trend-variant",
1245                    "trend test variant",
1246                    "trend",
1247                    true,
1248                )
1249                .variant_search_form_url(
1250                    "https://example.com/search_form",
1251                    "GET",
1252                    "search-form-name",
1253                    "search-form-value",
1254                )
1255                .variant_visual_search_url(
1256                    "https://example.com/visual-search-variant",
1257                    "visual-search-variant-name",
1258                    "visual-search-variant-value",
1259                    "url_variant",
1260                    // locale-specific display name is the key difference here
1261                    "Visual Search Variant en-GB",
1262                    "2096-02-02",
1263                )
1264                .build()
1265        );
1266    }
1267
1268    #[test]
1269    fn test_merges_sub_variants() {
1270        let result = SearchEngineDefinition::from_configuration_details(
1271            &SearchUserEnvironment {
1272                locale: "fi".into(),
1273                ..Default::default()
1274            },
1275            "test",
1276            Lazy::force(&JSON_ENGINE_BASE).clone(),
1277            &JSON_ENGINE_VARIANT,
1278            &Some(JSON_ENGINE_SUBVARIANT.clone()),
1279        );
1280
1281        assert_eq!(
1282            result,
1283            ExpectedEngineFromJSONBase::new("test", "Test")
1284                .variant_is_new_until("2063-04-05")
1285                .variant_optional(true)
1286                .subvariant_partner_code("trek2")
1287                .subvariant_telemetry_suffix("star2")
1288                .subvariant_search_url(
1289                    "https://example.com/subvariant",
1290                    "GET",
1291                    "subvariant",
1292                    "test subvariant",
1293                    "shuttle",
1294                )
1295                .subvariant_suggestions_url(
1296                    "https://example.com/suggestions-subvariant",
1297                    "GET",
1298                    "suggest-subvariant",
1299                    "sugg test subvariant",
1300                    "subvariant",
1301                    true,
1302                )
1303                .subvariant_trending_url(
1304                    "https://example.com/trending-subvariant",
1305                    "GET",
1306                    "trend-subvariant",
1307                    "trend test subvariant",
1308                    "subtrend",
1309                )
1310                .subvariant_search_form_url(
1311                    "https://example.com/search-form-subvariant",
1312                    "GET",
1313                    "search-form-subvariant",
1314                    "search form subvariant",
1315                )
1316                .subvariant_visual_search_url(
1317                    "https://example.com/visual-search-subvariant",
1318                    "visual-search-subvariant-name",
1319                    "visual-search-subvariant-value",
1320                    "url_subvariant",
1321                    "Visual Search Subvariant",
1322                    "2097-03-03",
1323                )
1324                .build()
1325        );
1326    }
1327
1328    #[test]
1329    fn test_merges_subvariant_and_uses_locale_specific_visual_search_display_name() {
1330        let result = SearchEngineDefinition::from_configuration_details(
1331            &SearchUserEnvironment {
1332                locale: "en-GB".into(),
1333                ..Default::default()
1334            },
1335            "test",
1336            Lazy::force(&JSON_ENGINE_BASE).clone(),
1337            &JSON_ENGINE_VARIANT,
1338            &Some(JSON_ENGINE_SUBVARIANT.clone()),
1339        );
1340
1341        assert_eq!(
1342            result,
1343            ExpectedEngineFromJSONBase::new("test", "Test")
1344                .variant_is_new_until("2063-04-05")
1345                .variant_optional(true)
1346                .subvariant_partner_code("trek2")
1347                .subvariant_telemetry_suffix("star2")
1348                .subvariant_search_url(
1349                    "https://example.com/subvariant",
1350                    "GET",
1351                    "subvariant",
1352                    "test subvariant",
1353                    "shuttle",
1354                )
1355                .subvariant_suggestions_url(
1356                    "https://example.com/suggestions-subvariant",
1357                    "GET",
1358                    "suggest-subvariant",
1359                    "sugg test subvariant",
1360                    "subvariant",
1361                    true,
1362                )
1363                .subvariant_trending_url(
1364                    "https://example.com/trending-subvariant",
1365                    "GET",
1366                    "trend-subvariant",
1367                    "trend test subvariant",
1368                    "subtrend",
1369                )
1370                .subvariant_search_form_url(
1371                    "https://example.com/search-form-subvariant",
1372                    "GET",
1373                    "search-form-subvariant",
1374                    "search form subvariant",
1375                )
1376                .subvariant_visual_search_url(
1377                    "https://example.com/visual-search-subvariant",
1378                    "visual-search-subvariant-name",
1379                    "visual-search-subvariant-value",
1380                    "url_subvariant",
1381                    // locale-specific display name is the key difference here
1382                    "Visual Search Subvariant en-GB",
1383                    "2097-03-03",
1384                )
1385                .build()
1386        );
1387    }
1388}