relay_event_normalization/normalize/
contexts.rs

1//! Computation and normalization of contexts from event data.
2
3use std::collections::HashMap;
4
5use once_cell::sync::Lazy;
6use regex::Regex;
7use relay_event_schema::protocol::{
8    BrowserContext, Context, Cookies, OsContext, ResponseContext, RuntimeContext,
9};
10use relay_protocol::{Annotated, Empty, Value};
11
12/// Environment.OSVersion (GetVersionEx) or RuntimeInformation.OSDescription on Windows
13static OS_WINDOWS_REGEX1: Lazy<Regex> = Lazy::new(|| {
14    Regex::new(r"^(Microsoft )?Windows (NT )?(?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$")
15        .unwrap()
16});
17static OS_WINDOWS_REGEX2: Lazy<Regex> = Lazy::new(|| {
18    Regex::new(r"^Windows\s+\d+\s+\((?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$").unwrap()
19});
20
21static OS_ANDROID_REGEX: Lazy<Regex> = Lazy::new(|| {
22    Regex::new(r"^Android (OS )?(?P<version>\d+(\.\d+){0,2}) / API-(?P<api>(\d+))").unwrap()
23});
24
25/// Format sent by Unreal Engine on macOS
26static OS_MACOS_REGEX: Lazy<Regex> = Lazy::new(|| {
27    Regex::new(r"^Mac OS X (?P<version>\d+\.\d+\.\d+)( \((?P<build>[a-fA-F0-9]+)\))?$").unwrap()
28});
29
30/// Format sent by Unity on iOS
31static OS_IOS_REGEX: Lazy<Regex> =
32    Lazy::new(|| Regex::new(r"^iOS (?P<version>\d+\.\d+\.\d+)").unwrap());
33
34/// Format sent by Unity on iPadOS
35static OS_IPADOS_REGEX: Lazy<Regex> =
36    Lazy::new(|| Regex::new(r"^iPadOS (?P<version>\d+\.\d+\.\d+)").unwrap());
37
38/// Specific regex to parse Linux distros
39static OS_LINUX_DISTRO_UNAME_REGEX: Lazy<Regex> = Lazy::new(|| {
40    Regex::new(r"^Linux (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?) (?P<name>[a-zA-Z]+) (?P<version>\d+(\.\d+){0,2})").unwrap()
41});
42
43/// Environment.OSVersion or RuntimeInformation.OSDescription (uname) on Mono and CoreCLR on
44/// macOS, iOS, Linux, etc.
45static OS_UNAME_REGEX: Lazy<Regex> = Lazy::new(|| {
46    Regex::new(r"^(?P<name>[a-zA-Z]+) (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?)").unwrap()
47});
48
49/// Mono 5.4, .NET Core 2.0
50static RUNTIME_DOTNET_REGEX: Lazy<Regex> =
51    Lazy::new(|| Regex::new(r"^(?P<name>.*) (?P<version>\d+\.\d+(\.\d+){0,2}).*$").unwrap());
52
53/// A hashmap that translates from the android model to the more human-friendly product-names.
54/// E.g. NE2211 -> OnePlus 10 Pro 5G
55static ANDROID_MODEL_NAMES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
56    let mut map = HashMap::new();
57    // Note that windows paths with backslashes '\' won't work on unix systems.
58    let android_str = include_str!("android_models.csv");
59
60    let mut lines = android_str.lines();
61
62    let header = lines.next().expect("CSV file should have a header");
63
64    let header_fields: Vec<&str> = header.split(',').collect();
65    let model_index = header_fields.iter().position(|&s| s.trim() == "Model");
66    let product_name_index = header_fields
67        .iter()
68        .position(|&s| s.trim() == "Marketing Name");
69
70    let (model_index, product_name_index) = match (model_index, product_name_index) {
71        (Some(model_index), Some(product_name_index)) => (model_index, product_name_index),
72        (_, _) => {
73            relay_log::error!(
74                "failed to find model and/or marketing name headers for android-model map",
75            );
76
77            return HashMap::new();
78        }
79    };
80
81    for line in lines {
82        let fields: Vec<&str> = line.split(',').collect();
83        if fields.len() > std::cmp::max(model_index, product_name_index) {
84            map.insert(
85                fields[model_index].trim(),
86                fields[product_name_index].trim(),
87            );
88        }
89    }
90    map
91});
92
93fn normalize_runtime_context(runtime: &mut RuntimeContext) {
94    if runtime.name.value().is_empty() && runtime.version.value().is_empty() {
95        if let Some(raw_description) = runtime.raw_description.as_str() {
96            if let Some(captures) = RUNTIME_DOTNET_REGEX.captures(raw_description) {
97                runtime.name = captures.name("name").map(|m| m.as_str().to_string()).into();
98                runtime.version = captures
99                    .name("version")
100                    .map(|m| m.as_str().to_string())
101                    .into();
102            }
103        }
104    }
105
106    // RuntimeInformation.FrameworkDescription doesn't return a very useful value.
107    // Example: ".NET Framework 4.7.3056.0"
108    // Use release keys from registry sent as #build
109    if let Some(name) = runtime.name.as_str() {
110        if let Some(build) = runtime.build.as_str() {
111            if name.starts_with(".NET Framework") {
112                let version = match build {
113                    "378389" => Some("4.5".to_string()),
114                    "378675" => Some("4.5.1".to_string()),
115                    "378758" => Some("4.5.1".to_string()),
116                    "379893" => Some("4.5.2".to_string()),
117                    "393295" => Some("4.6".to_string()),
118                    "393297" => Some("4.6".to_string()),
119                    "394254" => Some("4.6.1".to_string()),
120                    "394271" => Some("4.6.1".to_string()),
121                    "394802" => Some("4.6.2".to_string()),
122                    "394806" => Some("4.6.2".to_string()),
123                    "460798" => Some("4.7".to_string()),
124                    "460805" => Some("4.7".to_string()),
125                    "461308" => Some("4.7.1".to_string()),
126                    "461310" => Some("4.7.1".to_string()),
127                    "461808" => Some("4.7.2".to_string()),
128                    "461814" => Some("4.7.2".to_string()),
129                    "528040" => Some("4.8".to_string()),
130                    "528049" => Some("4.8".to_string()),
131                    "528209" => Some("4.8".to_string()),
132                    "528372" => Some("4.8".to_string()),
133                    "528449" => Some("4.8".to_string()),
134                    _ => None,
135                };
136
137                if let Some(version) = version {
138                    runtime.version = version.into();
139                }
140            }
141        }
142    }
143
144    // Calculation of the computed context for the runtime.
145    // The equivalent calculation is done in `sentry` in `src/sentry/interfaces/contexts.py`.
146    if runtime.runtime.value().is_none() {
147        if let (Some(name), Some(version)) = (runtime.name.value(), runtime.version.value()) {
148            runtime.runtime = Annotated::from(format!("{} {}", name, version));
149        }
150    }
151}
152
153/// Parses the Windows build number from the description and maps it to a marketing name.
154/// Source: <https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions>.
155/// Note: We cannot distinguish between Windows Server and PC versions, so we map to the PC versions
156/// here.
157fn get_windows_version(description: &str) -> Option<(&str, &str)> {
158    let captures = OS_WINDOWS_REGEX1
159        .captures(description)
160        .or_else(|| OS_WINDOWS_REGEX2.captures(description))?;
161
162    let full_version = captures.name("version")?.as_str();
163    let build_number_str = captures.name("build_number")?.as_str();
164    let build_number = build_number_str.parse::<u64>().ok()?;
165
166    let version_name = match build_number {
167        // Not considering versions below Windows XP
168        2600..=3790 => "XP",
169        6002 => "Vista",
170        7601 => "7",
171        9200 => "8",
172        9600 => "8.1",
173        10240..=19044 => "10",
174        22000..=22999 => "11",
175        // Fall back to raw version:
176        _ => full_version,
177    };
178
179    Some((version_name, build_number_str))
180}
181
182#[allow(dead_code)]
183/// Returns the API version of an Android description.
184///
185/// TODO use this to add a tag `android.api` to the message (not yet 100% decided)
186pub fn get_android_api_version(description: &str) -> Option<&str> {
187    if let Some(captures) = OS_ANDROID_REGEX.captures(description) {
188        captures.name("api").map(|m| m.as_str())
189    } else {
190        None
191    }
192}
193
194fn normalize_os_context(os: &mut OsContext) {
195    if os.name.value().is_some() || os.version.value().is_some() {
196        compute_os_context(os);
197        return;
198    }
199
200    if let Some(raw_description) = os.raw_description.as_str() {
201        if let Some((version, build_number)) = get_windows_version(raw_description) {
202            os.name = "Windows".to_string().into();
203            os.version = version.to_string().into();
204            if os.build.is_empty() {
205                // Keep raw version as build
206                os.build.set_value(Some(build_number.to_string().into()));
207            }
208        } else if let Some(captures) = OS_MACOS_REGEX.captures(raw_description) {
209            os.name = "macOS".to_string().into();
210            os.version = captures
211                .name("version")
212                .map(|m| m.as_str().to_string())
213                .into();
214            os.build = captures
215                .name("build")
216                .map(|m| m.as_str().to_string().into())
217                .into();
218        } else if let Some(captures) = OS_IOS_REGEX.captures(raw_description) {
219            os.name = "iOS".to_string().into();
220            os.version = captures
221                .name("version")
222                .map(|m| m.as_str().to_string())
223                .into();
224            os.build = captures
225                .name("build")
226                .map(|m| m.as_str().to_string().into())
227                .into();
228        } else if let Some(captures) = OS_IPADOS_REGEX.captures(raw_description) {
229            os.name = "iPadOS".to_string().into();
230            os.version = captures
231                .name("version")
232                .map(|m| m.as_str().to_string())
233                .into();
234            os.build = captures
235                .name("build")
236                .map(|m| m.as_str().to_string().into())
237                .into();
238        } else if let Some(captures) = OS_LINUX_DISTRO_UNAME_REGEX.captures(raw_description) {
239            os.name = captures.name("name").map(|m| m.as_str().to_string()).into();
240            os.version = captures
241                .name("version")
242                .map(|m| m.as_str().to_string())
243                .into();
244            os.kernel_version = captures
245                .name("kernel_version")
246                .map(|m| m.as_str().to_string())
247                .into();
248        } else if let Some(captures) = OS_UNAME_REGEX.captures(raw_description) {
249            os.name = captures.name("name").map(|m| m.as_str().to_string()).into();
250            os.kernel_version = captures
251                .name("kernel_version")
252                .map(|m| m.as_str().to_string())
253                .into();
254        } else if let Some(captures) = OS_ANDROID_REGEX.captures(raw_description) {
255            os.name = "Android".to_string().into();
256            os.version = captures
257                .name("version")
258                .map(|m| m.as_str().to_string())
259                .into();
260        }
261    }
262
263    compute_os_context(os);
264}
265
266fn compute_os_context(os: &mut OsContext) {
267    // Calculation of the computed context for the os.
268    // The equivalent calculation is done in `sentry` in `src/sentry/interfaces/contexts.py`.
269    if os.os.value().is_none() {
270        if let (Some(name), Some(version)) = (os.name.value(), os.version.value()) {
271            os.os = Annotated::from(format!("{} {}", name, version));
272        }
273    }
274}
275
276fn normalize_browser_context(browser: &mut BrowserContext) {
277    // Calculation of the computed context for the browser.
278    // The equivalent calculation is done in `sentry` in `src/sentry/interfaces/contexts.py`.
279    if browser.browser.value().is_none() {
280        if let (Some(name), Some(version)) = (browser.name.value(), browser.version.value()) {
281            browser.browser = Annotated::from(format!("{} {}", name, version));
282        }
283    }
284}
285
286fn parse_raw_response_data(response: &ResponseContext) -> Option<(&'static str, Value)> {
287    let raw = response.data.as_str()?;
288
289    serde_json::from_str(raw)
290        .ok()
291        .map(|value| ("application/json", value))
292}
293
294fn normalize_response_data(response: &mut ResponseContext) {
295    // Always derive the `inferred_content_type` from the response body, even if there is a
296    // `Content-Type` header present. This value can technically be ingested (due to the schema) but
297    // should always be overwritten in normalization. Only if inference fails, fall back to the
298    // content type header.
299    if let Some((content_type, parsed_data)) = parse_raw_response_data(response) {
300        // Retain meta data on the body (e.g. trimming annotations) but remove anything on the
301        // inferred content type.
302        response.data.set_value(Some(parsed_data));
303        response.inferred_content_type = Annotated::from(content_type.to_string());
304    } else {
305        response.inferred_content_type = response
306            .headers
307            .value()
308            .and_then(|headers| headers.get_header("Content-Type"))
309            .map(|value| value.split(';').next().unwrap_or(value).to_string())
310            .into();
311    }
312}
313
314fn normalize_response(response: &mut ResponseContext) {
315    normalize_response_data(response);
316
317    let headers = match response.headers.value_mut() {
318        Some(headers) => headers,
319        None => return,
320    };
321
322    if response.cookies.value().is_some() {
323        headers.remove("Set-Cookie");
324        return;
325    }
326
327    let cookie_header = match headers.get_header("Set-Cookie") {
328        Some(header) => header,
329        None => return,
330    };
331
332    if let Ok(new_cookies) = Cookies::parse(cookie_header) {
333        response.cookies = Annotated::from(new_cookies);
334        headers.remove("Set-Cookie");
335    }
336}
337
338/// Normalizes the given context.
339pub fn normalize_context(context: &mut Context) {
340    match context {
341        Context::Runtime(runtime) => normalize_runtime_context(runtime),
342        Context::Os(os) => normalize_os_context(os),
343        Context::Browser(browser) => normalize_browser_context(browser),
344        Context::Response(response) => normalize_response(response),
345        Context::Device(device) => {
346            if let Some(product_name) = device
347                .as_ref()
348                .model
349                .value()
350                .and_then(|model| ANDROID_MODEL_NAMES.get(model.as_str()))
351            {
352                device.name.set_value(Some(product_name.to_string()))
353            }
354        }
355        _ => {}
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use relay_event_schema::protocol::{Headers, LenientString, PairList};
362    use relay_protocol::Object;
363    use similar_asserts::assert_eq;
364
365    use super::*;
366
367    #[test]
368    fn test_get_product_name() {
369        assert_eq!(
370            ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
371            &"OnePlus 10 Pro 5G"
372        );
373
374        assert_eq!(
375            ANDROID_MODEL_NAMES.get("MP04").unwrap(),
376            &"A13 Pro Max 5G EEA"
377        );
378
379        assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
380
381        assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
382    }
383
384    #[test]
385    fn test_dotnet_framework_48_without_build_id() {
386        let mut runtime = RuntimeContext {
387            raw_description: ".NET Framework 4.8.4250.0".to_string().into(),
388            ..RuntimeContext::default()
389        };
390
391        normalize_runtime_context(&mut runtime);
392        assert_eq!(Some(".NET Framework"), runtime.name.as_str());
393        assert_eq!(Some("4.8.4250.0"), runtime.version.as_str());
394    }
395
396    #[test]
397    fn test_dotnet_framework_472() {
398        let mut runtime = RuntimeContext {
399            raw_description: ".NET Framework 4.7.3056.0".to_string().into(),
400            build: LenientString("461814".to_string()).into(),
401            ..RuntimeContext::default()
402        };
403
404        normalize_runtime_context(&mut runtime);
405        assert_eq!(Some(".NET Framework"), runtime.name.as_str());
406        assert_eq!(Some("4.7.2"), runtime.version.as_str());
407    }
408
409    #[test]
410    fn test_dotnet_framework_future_version() {
411        let mut runtime = RuntimeContext {
412            raw_description: ".NET Framework 200.0".to_string().into(),
413            build: LenientString("999999".to_string()).into(),
414            ..RuntimeContext::default()
415        };
416
417        // Unmapped build number doesn't override version
418        normalize_runtime_context(&mut runtime);
419        assert_eq!(Some(".NET Framework"), runtime.name.as_str());
420        assert_eq!(Some("200.0"), runtime.version.as_str());
421    }
422
423    #[test]
424    fn test_dotnet_native() {
425        let mut runtime = RuntimeContext {
426            raw_description: ".NET Native 2.0".to_string().into(),
427            ..RuntimeContext::default()
428        };
429
430        normalize_runtime_context(&mut runtime);
431        assert_eq!(Some(".NET Native"), runtime.name.as_str());
432        assert_eq!(Some("2.0"), runtime.version.as_str());
433    }
434
435    #[test]
436    fn test_dotnet_core() {
437        let mut runtime = RuntimeContext {
438            raw_description: ".NET Core 2.0".to_string().into(),
439            ..RuntimeContext::default()
440        };
441
442        normalize_runtime_context(&mut runtime);
443        assert_eq!(Some(".NET Core"), runtime.name.as_str());
444        assert_eq!(Some("2.0"), runtime.version.as_str());
445    }
446
447    #[test]
448    fn test_windows_7_or_server_2008() {
449        // Environment.OSVersion on Windows 7 (CoreCLR 1.0+, .NET Framework 1.1+, Mono 1+)
450        let mut os = OsContext {
451            raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
452                .to_string()
453                .into(),
454            ..OsContext::default()
455        };
456
457        normalize_os_context(&mut os);
458        assert_eq!(Some("Windows"), os.name.as_str());
459        assert_eq!(Some("7"), os.version.as_str());
460    }
461
462    #[test]
463    fn test_windows_8_or_server_2012_or_later() {
464        // Environment.OSVersion on Windows 10 (CoreCLR 1.0+, .NET Framework 1.1+, Mono 1+)
465        // *or later, due to GetVersionEx deprecated on Windows 8.1
466        // It's a potentially really misleading API on newer platforms
467        // Only used if RuntimeInformation.OSDescription is not available (old runtimes)
468        let mut os = OsContext {
469            raw_description: "Microsoft Windows NT 6.2.9200.0".to_string().into(),
470            ..OsContext::default()
471        };
472
473        normalize_os_context(&mut os);
474        assert_eq!(Some("Windows"), os.name.as_str());
475        assert_eq!(Some("8"), os.version.as_str());
476    }
477
478    #[test]
479    fn test_windows_10() {
480        // RuntimeInformation.OSDescription on Windows 10 (CoreCLR 2.0+, .NET
481        // Framework 4.7.1+, Mono 5.4+)
482        let mut os = OsContext {
483            raw_description: "Microsoft Windows 10.0.16299".to_string().into(),
484            ..OsContext::default()
485        };
486
487        normalize_os_context(&mut os);
488        assert_eq!(Some("Windows"), os.name.as_str());
489        assert_eq!(Some("10"), os.version.as_str());
490    }
491
492    #[test]
493    fn test_windows_11() {
494        // https://github.com/getsentry/relay/issues/1201
495        let mut os = OsContext {
496            raw_description: "Microsoft Windows 10.0.22000".to_string().into(),
497            ..OsContext::default()
498        };
499
500        normalize_os_context(&mut os);
501        assert_eq!(Some("Windows"), os.name.as_str());
502        assert_eq!(Some("11"), os.version.as_str());
503    }
504
505    #[test]
506    fn test_windows_11_future1() {
507        // This is fictional as of today, but let's be explicit about the behavior we expect.
508        let mut os = OsContext {
509            raw_description: "Microsoft Windows 10.0.22001".to_string().into(),
510            ..OsContext::default()
511        };
512
513        normalize_os_context(&mut os);
514        assert_eq!(Some("Windows"), os.name.as_str());
515        assert_eq!(Some("11"), os.version.as_str());
516    }
517
518    #[test]
519    fn test_windows_11_future2() {
520        // This is fictional, but let's be explicit about the behavior we expect.
521        let mut os = OsContext {
522            raw_description: "Microsoft Windows 10.1.23456".to_string().into(),
523            ..OsContext::default()
524        };
525
526        normalize_os_context(&mut os);
527        assert_eq!(Some("Windows"), os.name.as_str());
528        assert_eq!(Some("10.1.23456"), os.version.as_str());
529    }
530
531    #[test]
532    fn test_macos_os_version() {
533        // Environment.OSVersion on macOS (CoreCLR 1.0+, Mono 1+)
534        let mut os = OsContext {
535            raw_description: "Unix 17.5.0.0".to_string().into(),
536            ..OsContext::default()
537        };
538
539        normalize_os_context(&mut os);
540        assert_eq!(Some("Unix"), os.name.as_str());
541        assert_eq!(Some("17.5.0"), os.kernel_version.as_str());
542    }
543
544    #[test]
545    fn test_macos_runtime() {
546        // RuntimeInformation.OSDescription on macOS (CoreCLR 2.0+, Mono 5.4+)
547        let mut os = OsContext {
548        raw_description: "Darwin 17.5.0 Darwin Kernel Version 17.5.0: Mon Mar  5 22:24:32 PST 2018; root:xnu-4570.51.1~1/RELEASE_X86_64".to_string().into(),
549        ..OsContext::default()
550    };
551
552        normalize_os_context(&mut os);
553        assert_eq!(Some("Darwin"), os.name.as_str());
554        assert_eq!(Some("17.5.0"), os.kernel_version.as_str());
555    }
556
557    #[test]
558    fn test_centos_os_version() {
559        // Environment.OSVersion on CentOS 7 (CoreCLR 1.0+, Mono 1+)
560        let mut os = OsContext {
561            raw_description: "Unix 3.10.0.693".to_string().into(),
562            ..OsContext::default()
563        };
564
565        normalize_os_context(&mut os);
566        assert_eq!(Some("Unix"), os.name.as_str());
567        assert_eq!(Some("3.10.0.693"), os.kernel_version.as_str());
568    }
569
570    #[test]
571    fn test_centos_runtime_info() {
572        // RuntimeInformation.OSDescription on CentOS 7 (CoreCLR 2.0+, Mono 5.4+)
573        let mut os = OsContext {
574            raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
575                .to_string()
576                .into(),
577            ..OsContext::default()
578        };
579
580        normalize_os_context(&mut os);
581        assert_eq!(Some("Linux"), os.name.as_str());
582        assert_eq!(Some("3.10.0"), os.kernel_version.as_str());
583    }
584
585    #[test]
586    fn test_wsl_ubuntu() {
587        // RuntimeInformation.OSDescription on Windows Subsystem for Linux (Ubuntu)
588        // (CoreCLR 2.0+, Mono 5.4+)
589        let mut os = OsContext {
590            raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
591                .to_string()
592                .into(),
593            ..OsContext::default()
594        };
595
596        normalize_os_context(&mut os);
597        assert_eq!(Some("Linux"), os.name.as_str());
598        assert_eq!(Some("4.4.0"), os.kernel_version.as_str());
599    }
600
601    #[test]
602    fn test_macos_with_build() {
603        let mut os = OsContext {
604            raw_description: "Mac OS X 10.14.2 (18C54)".to_string().into(),
605            ..OsContext::default()
606        };
607
608        normalize_os_context(&mut os);
609        assert_eq!(Some("macOS"), os.name.as_str());
610        assert_eq!(Some("10.14.2"), os.version.as_str());
611        assert_eq!(Some("18C54"), os.build.as_str());
612    }
613
614    #[test]
615    fn test_macos_without_build() {
616        let mut os = OsContext {
617            raw_description: "Mac OS X 10.14.2".to_string().into(),
618            ..OsContext::default()
619        };
620
621        normalize_os_context(&mut os);
622        assert_eq!(Some("macOS"), os.name.as_str());
623        assert_eq!(Some("10.14.2"), os.version.as_str());
624        assert_eq!(None, os.build.value());
625    }
626
627    #[test]
628    fn test_name_not_overwritten() {
629        let mut os = OsContext {
630            name: "Properly defined name".to_string().into(),
631            raw_description: "Linux 4.4.0".to_string().into(),
632            ..OsContext::default()
633        };
634
635        normalize_os_context(&mut os);
636        assert_eq!(Some("Properly defined name"), os.name.as_str());
637    }
638
639    #[test]
640    fn test_version_not_overwritten() {
641        let mut os = OsContext {
642            version: "Properly defined version".to_string().into(),
643            raw_description: "Linux 4.4.0".to_string().into(),
644            ..OsContext::default()
645        };
646
647        normalize_os_context(&mut os);
648        assert_eq!(Some("Properly defined version"), os.version.as_str());
649    }
650
651    #[test]
652    fn test_no_name() {
653        let mut os = OsContext::default();
654
655        normalize_os_context(&mut os);
656        assert_eq!(None, os.name.value());
657        assert_eq!(None, os.version.value());
658        assert_eq!(None, os.kernel_version.value());
659        assert_eq!(None, os.raw_description.value());
660    }
661
662    #[test]
663    fn test_unity_mac_os() {
664        let mut os = OsContext {
665            raw_description: "Mac OS X 10.16.0".to_string().into(),
666            ..OsContext::default()
667        };
668        normalize_os_context(&mut os);
669        assert_eq!(Some("macOS"), os.name.as_str());
670        assert_eq!(Some("10.16.0"), os.version.as_str());
671        assert_eq!(None, os.build.value());
672    }
673
674    #[test]
675    fn test_unity_ios() {
676        let mut os = OsContext {
677            raw_description: "iOS 17.5.1".to_string().into(),
678            ..OsContext::default()
679        };
680
681        normalize_os_context(&mut os);
682        assert_eq!(Some("iOS"), os.name.as_str());
683        assert_eq!(Some("17.5.1"), os.version.as_str());
684        assert_eq!(None, os.build.value());
685    }
686
687    #[test]
688    fn test_unity_ipados() {
689        let mut os = OsContext {
690            raw_description: "iPadOS 17.5.1".to_string().into(),
691            ..OsContext::default()
692        };
693
694        normalize_os_context(&mut os);
695        assert_eq!(Some("iPadOS"), os.name.as_str());
696        assert_eq!(Some("17.5.1"), os.version.as_str());
697        assert_eq!(None, os.build.value());
698    }
699
700    //OS_WINDOWS_REGEX = r#"^(Microsoft )?Windows (NT )?(?P<version>\d+\.\d+\.\d+).*$"#;
701    #[test]
702    fn test_unity_windows_os() {
703        let mut os = OsContext {
704            raw_description: "Windows 10  (10.0.19042) 64bit".to_string().into(),
705            ..OsContext::default()
706        };
707        normalize_os_context(&mut os);
708        assert_eq!(Some("Windows"), os.name.as_str());
709        assert_eq!(Some("10"), os.version.as_str());
710        assert_eq!(Some(&LenientString("19042".to_string())), os.build.value());
711    }
712
713    #[test]
714    fn test_unity_android_os() {
715        let mut os = OsContext {
716            raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
717                .to_string()
718                .into(),
719            ..OsContext::default()
720        };
721        normalize_os_context(&mut os);
722        assert_eq!(Some("Android"), os.name.as_str());
723        assert_eq!(Some("11"), os.version.as_str());
724        assert_eq!(None, os.build.value());
725    }
726
727    #[test]
728    fn test_unity_android_api_version() {
729        let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
730        assert_eq!(Some("30"), get_android_api_version(description));
731    }
732
733    #[test]
734    fn test_linux_5_11() {
735        let mut os = OsContext {
736            raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_string().into(),
737            ..OsContext::default()
738        };
739        normalize_os_context(&mut os);
740        assert_eq!(Some("Ubuntu"), os.name.as_str());
741        assert_eq!(Some("20.04"), os.version.as_str());
742        assert_eq!(Some("5.11"), os.kernel_version.as_str());
743        assert_eq!(None, os.build.value());
744    }
745
746    #[test]
747    fn test_android_4_4_2() {
748        let mut os = OsContext {
749            raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
750                .to_string()
751                .into(),
752            ..OsContext::default()
753        };
754        normalize_os_context(&mut os);
755        assert_eq!(Some("Android"), os.name.as_str());
756        assert_eq!(Some("4.4.2"), os.version.as_str());
757        assert_eq!(None, os.build.value());
758    }
759
760    #[test]
761    fn test_infer_json() {
762        let mut response = ResponseContext {
763            data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_string())),
764            ..ResponseContext::default()
765        };
766
767        let mut expected_value = Object::new();
768        expected_value.insert(
769            "foo".to_string(),
770            Annotated::from(Value::String("bar".into())),
771        );
772
773        normalize_response(&mut response);
774        assert_eq!(
775            response.inferred_content_type.as_str(),
776            Some("application/json")
777        );
778        assert_eq!(response.data.value(), Some(&Value::Object(expected_value)));
779    }
780
781    #[test]
782    fn test_broken_json_with_fallback() {
783        let mut response = ResponseContext {
784            data: Annotated::from(Value::String(r#"{"foo":"b"#.to_string())),
785            headers: Annotated::from(Headers(PairList(vec![Annotated::new((
786                Annotated::new("Content-Type".to_string().into()),
787                Annotated::new("text/plain; encoding=utf-8".to_string().into()),
788            ))]))),
789            ..ResponseContext::default()
790        };
791
792        normalize_response(&mut response);
793        assert_eq!(response.inferred_content_type.as_str(), Some("text/plain"));
794        assert_eq!(response.data.as_str(), Some(r#"{"foo":"b"#));
795    }
796
797    #[test]
798    fn test_broken_json_without_fallback() {
799        let mut response = ResponseContext {
800            data: Annotated::from(Value::String(r#"{"foo":"b"#.to_string())),
801            ..ResponseContext::default()
802        };
803
804        normalize_response(&mut response);
805        assert_eq!(response.inferred_content_type.value(), None);
806        assert_eq!(response.data.as_str(), Some(r#"{"foo":"b"#));
807    }
808
809    #[test]
810    fn test_os_computed_context() {
811        let mut os = OsContext {
812            name: "Windows".to_string().into(),
813            version: "10".to_string().into(),
814            ..OsContext::default()
815        };
816
817        normalize_os_context(&mut os);
818        assert_eq!(Some("Windows 10"), os.os.as_str());
819    }
820
821    #[test]
822    fn test_os_computed_context_missing_version() {
823        let mut os = OsContext {
824            name: "Windows".to_string().into(),
825            ..OsContext::default()
826        };
827
828        normalize_os_context(&mut os);
829        assert_eq!(None, os.os.value());
830    }
831
832    #[test]
833    fn test_runtime_computed_context() {
834        let mut runtime = RuntimeContext {
835            name: "Python".to_string().into(),
836            version: "3.9.0".to_string().into(),
837            ..RuntimeContext::default()
838        };
839
840        normalize_runtime_context(&mut runtime);
841        assert_eq!(Some("Python 3.9.0"), runtime.runtime.as_str());
842    }
843
844    #[test]
845    fn test_runtime_computed_context_missing_version() {
846        let mut runtime = RuntimeContext {
847            name: "Python".to_string().into(),
848            ..RuntimeContext::default()
849        };
850
851        normalize_runtime_context(&mut runtime);
852        assert_eq!(None, runtime.runtime.value());
853    }
854
855    #[test]
856    fn test_browser_computed_context() {
857        let mut browser = BrowserContext {
858            name: "Firefox".to_string().into(),
859            version: "89.0".to_string().into(),
860            ..BrowserContext::default()
861        };
862
863        normalize_browser_context(&mut browser);
864        assert_eq!(Some("Firefox 89.0"), browser.browser.as_str());
865    }
866
867    #[test]
868    fn test_browser_computed_context_missing_version() {
869        let mut browser = BrowserContext {
870            name: "Firefox".to_string().into(),
871            ..BrowserContext::default()
872        };
873
874        normalize_browser_context(&mut browser);
875        assert_eq!(None, browser.browser.value());
876    }
877}