bimp-persona 0.0.1

Browser persona configuration and resolved fingerprint values for Bimp.
Documentation
use crate::*;

#[test]
fn chrome_preset_exposes_expected_identity() {
    let persona = BrowserPersona::from_preset(BrowserPersonaPreset::ChromeStable);

    assert_eq!(persona.browser_name, "Chrome");
    assert!(persona.network.user_agent.contains("Chrome/136"));
    assert_eq!(persona.graphics.webgl_vendor, "Intel Inc.");
    assert_eq!(persona.graphics.webgl_masked_vendor, "WebKit");
    assert_eq!(persona.graphics.webgl_masked_renderer, "WebKit WebGL");
    assert!(!persona.automation.webdriver);
}

#[test]
fn stable_seed_is_deterministic() {
    assert_eq!(
        PersonaSeed::from_stable_input("/tmp/profile").as_str(),
        PersonaSeed::from_stable_input("/tmp/profile").as_str()
    );
    assert_ne!(
        PersonaSeed::from_stable_input("/tmp/profile-a").as_str(),
        PersonaSeed::from_stable_input("/tmp/profile-b").as_str()
    );
}

#[test]
fn persona_config_resolves_seed_and_viewport() {
    let persona = PersonaConfig {
        startup_url: Some(" https://example.com/start ".to_string()),
        seed: Some(PersonaSeed::new("session-a")),
        viewport: Viewport {
            width: 1280,
            height: 720,
            device_scale_factor: 2,
        },
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.seed.as_str(), "session-a");
    assert_eq!(
        persona.startup_url.as_deref(),
        Some("https://example.com/start")
    );
    assert_eq!(persona.viewport.width, 1280);
    assert_eq!(persona.screen.avail_height, 720);
    assert_eq!(persona.screen.device_scale_factor, 2);
}

#[test]
fn persona_config_applies_screen_depth_overrides() {
    let persona = PersonaConfig {
        viewport: Viewport {
            width: 1728,
            height: 1117,
            device_scale_factor: 2,
        },
        screen: Some(ScreenConfig {
            avail_height: Some(1079),
            avail_left: Some(0),
            avail_top: Some(25),
            left: Some(0),
            top: Some(0),
            color_depth: Some(30),
            pixel_depth: Some(30),
            is_extended: Some(false),
            orientation_type: Some("landscape-primary".to_string()),
            orientation_angle: Some(0),
            ..Default::default()
        }),
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.viewport.width, 1728);
    assert_eq!(persona.viewport.device_scale_factor, 2);
    assert_eq!(persona.screen.width, 1728);
    assert_eq!(persona.screen.height, 1117);
    assert_eq!(persona.screen.avail_height, 1079);
    assert_eq!(persona.screen.avail_left, 0);
    assert_eq!(persona.screen.avail_top, 25);
    assert_eq!(persona.screen.left, 0);
    assert_eq!(persona.screen.top, 0);
    assert_eq!(persona.screen.color_depth, 30);
    assert_eq!(persona.screen.pixel_depth, 30);
    assert!(!persona.screen.is_extended);
    assert_eq!(persona.screen.orientation_type, "landscape-primary");
    assert_eq!(persona.screen.orientation_angle, 0);
}

#[test]
fn persona_config_applies_window_overrides() {
    let persona = PersonaConfig {
        viewport: Viewport {
            width: 1710,
            height: 896,
            device_scale_factor: 2,
        },
        window: Some(WindowConfig {
            outer_width: Some(1710),
            outer_height: Some(1017),
            screen_x: Some(0),
            screen_y: Some(23),
        }),
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.viewport.width, 1710);
    assert_eq!(persona.viewport.height, 896);
    assert_eq!(persona.window.outer_width, 1710);
    assert_eq!(persona.window.outer_height, 1017);
    assert_eq!(persona.window.screen_x, 0);
    assert_eq!(persona.window.screen_y, 23);
}

#[test]
fn persona_config_applies_css_prefix_overrides() {
    let persona = PersonaConfig {
        css: Some(CssConfig {
            moz_prefix_enabled: Some(true),
            webkit_prefix_enabled: Some(false),
        }),
        ..Default::default()
    }
    .resolve();

    assert!(persona.css.moz_prefix_enabled);
    assert!(!persona.css.webkit_prefix_enabled);
}

#[test]
fn persona_config_applies_graphics_overrides() {
    let persona = PersonaConfig {
        graphics: Some(GraphicsConfig {
            webgl_vendor: Some("Google Inc. (Intel Inc.)".to_string()),
            webgl_renderer: Some(
                "ANGLE (Intel Inc., Intel(R) UHD Graphics 630, OpenGL 4.1)".to_string(),
            ),
            webgl_masked_vendor: Some("WebKit".to_string()),
            webgl_masked_renderer: Some("WebKit WebGL".to_string()),
        }),
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.graphics.webgl_vendor, "Google Inc. (Intel Inc.)");
    assert_eq!(
        persona.graphics.webgl_renderer,
        "ANGLE (Intel Inc., Intel(R) UHD Graphics 630, OpenGL 4.1)"
    );
    assert_eq!(persona.graphics.webgl_masked_vendor, "WebKit");
    assert_eq!(persona.graphics.webgl_masked_renderer, "WebKit WebGL");
}

#[test]
fn persona_config_applies_navigator_overrides() {
    let persona = PersonaConfig {
        navigator: Some(NavigatorConfig {
            platform: Some("Win32".to_string()),
            languages: Some(vec!["en-GB".to_string(), "en".to_string()]),
            hardware_concurrency: Some(12),
            device_memory_gb: Some(16),
            max_touch_points: Some(1),
            do_not_track: Some("1".to_string()),
            expose_global_privacy_control: Some(true),
            global_privacy_control: Some(true),
            permissions_enabled: Some(false),
            bluetooth_enabled: Some(false),
            bluetooth_available: Some(false),
            media_devices_enabled: Some(false),
            webgpu_enabled: Some(true),
            offscreen_canvas_enabled: Some(false),
            service_worker_enabled: Some(false),
            ua_platform_version: Some("10.0.0".to_string()),
            ua_architecture: Some("arm".to_string()),
            ua_bitness: Some("64".to_string()),
            ua_model: Some("Mac15,3".to_string()),
            pdf_viewer_enabled: Some(false),
            ..Default::default()
        }),
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.js.platform, "Win32");
    assert_eq!(persona.js.language, "en-GB");
    assert_eq!(persona.js.languages, ["en-GB", "en"]);
    assert_eq!(persona.network.accept_language, "en-GB,en;q=0.9");
    assert_eq!(persona.js.hardware_concurrency, 12);
    assert_eq!(persona.js.device_memory_gb, 16);
    assert_eq!(persona.js.max_touch_points, 1);
    assert_eq!(persona.js.do_not_track, "1");
    assert!(persona.js.expose_global_privacy_control);
    assert!(persona.js.global_privacy_control);
    assert!(!persona.js.permissions_enabled);
    assert!(!persona.js.bluetooth_enabled);
    assert!(!persona.js.bluetooth_available);
    assert!(!persona.js.media_devices_enabled);
    assert!(persona.js.webgpu_enabled);
    assert!(!persona.js.offscreen_canvas_enabled);
    assert!(!persona.js.service_worker_enabled);
    assert_eq!(persona.js.ua_platform_version, "10.0.0");
    assert_eq!(persona.js.ua_architecture, "arm");
    assert_eq!(persona.js.ua_bitness, "64");
    assert_eq!(persona.js.ua_model, "Mac15,3");
    assert!(!persona.js.pdf_viewer_enabled);
}

#[test]
fn persona_config_applies_media_device_overrides() {
    let persona = PersonaConfig {
        media: Some(MediaConfig {
            speech_voices: Some(vec!["Samantha".to_string(), "Alex".to_string()]),
            audio_inputs: Some(2),
            video_inputs: Some(1),
            audio_outputs: Some(3),
        }),
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.media.audio_inputs, 2);
    assert_eq!(persona.media.video_inputs, 1);
    assert_eq!(persona.media.audio_outputs, 3);
    assert_eq!(persona.media.speech_voices, ["Samantha", "Alex"]);
}

#[test]
fn persona_config_applies_font_overrides() {
    let persona = PersonaConfig {
        fonts: Some(FontConfig {
            families: Some(vec![
                "Helvetica Neue".to_string(),
                "  ".to_string(),
                "Galvji".to_string(),
            ]),
            seed: Some(PersonaSeed::new("font-test-seed")),
        }),
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.fonts.families, ["Helvetica Neue", "Galvji"]);
    assert_eq!(persona.fonts.seed.as_str(), "font-test-seed");
}

#[test]
fn persona_config_applies_canvas_overrides() {
    let persona = PersonaConfig {
        canvas: Some(CanvasConfig {
            noise_enabled: Some(false),
            noise_amplitude: Some(2),
            blink_low_entropy_probe: Some(false),
            seed: Some(PersonaSeed::new("canvas-test-seed")),
        }),
        ..Default::default()
    }
    .resolve();

    assert!(!persona.canvas.noise_enabled);
    assert_eq!(persona.canvas.noise_amplitude, 2);
    assert!(!persona.canvas.blink_low_entropy_probe);
    assert_eq!(persona.canvas.seed.as_str(), "canvas-test-seed");
}

#[test]
fn persona_config_applies_domrect_overrides() {
    let persona = PersonaConfig {
        domrect: Some(DomRectConfig {
            enabled: Some(false),
            quantization_steps_per_px: Some(128),
            fill_empty_client_rects: Some(false),
            seed: Some(PersonaSeed::new("domrect-test-seed")),
        }),
        ..Default::default()
    }
    .resolve();

    assert!(!persona.domrect.enabled);
    assert_eq!(persona.domrect.quantization_steps_per_px, 128);
    assert!(!persona.domrect.fill_empty_client_rects);
    assert_eq!(persona.domrect.seed.as_str(), "domrect-test-seed");
}

#[test]
fn persona_config_applies_engine_overrides() {
    let persona = PersonaConfig {
        engine: Some(EngineConfig {
            enabled: Some(false),
            to_fixed_range_error_message: Some("digits are invalid".to_string()),
            array_constructor_source: Some("function Array() { [native code] }".to_string()),
        }),
        ..Default::default()
    }
    .resolve();

    assert!(!persona.engine.enabled);
    assert_eq!(
        persona.engine.to_fixed_range_error_message,
        "digits are invalid"
    );
    assert_eq!(
        persona.engine.array_constructor_source,
        "function Array() { [native code] }"
    );
}

#[test]
fn resolved_persona_round_trips_through_json() {
    let persona = PersonaConfig {
        seed: Some(PersonaSeed::new("round-trip")),
        ..Default::default()
    }
    .resolve();

    let encoded = serde_json::to_string(&persona).unwrap();
    let decoded: ResolvedPersona = serde_json::from_str(&encoded).unwrap();

    assert_eq!(decoded, persona);
}

#[test]
fn persona_config_applies_locale_timezone_and_accept_language_together() {
    let persona: ResolvedPersona = serde_json::from_str::<PersonaConfig>(
        r#"{
                "locale": "fr-FR",
                "languages": ["fr-FR", "fr"],
                "accept_language": "fr-FR,fr;q=0.9",
                "timezone": "Europe/Paris"
            }"#,
    )
    .unwrap()
    .resolve();

    assert_eq!(persona.js.language, "fr-FR");
    assert_eq!(persona.js.languages, ["fr-FR", "fr"]);
    assert_eq!(persona.geo.locale, "fr-FR");
    assert_eq!(persona.network.accept_language, "fr-FR,fr;q=0.9");
    assert_eq!(persona.js.timezone, "Europe/Paris");
    assert_eq!(persona.geo.timezone, "Europe/Paris");
}

#[test]
fn persona_config_derives_languages_from_accept_language() {
    let persona = PersonaConfig {
        accept_language: Some("de-DE,de;q=0.9".to_string()),
        ..Default::default()
    }
    .resolve();

    assert_eq!(persona.js.language, "de-DE");
    assert_eq!(persona.js.languages, ["de-DE", "de"]);
    assert_eq!(persona.geo.locale, "de-DE");
    assert_eq!(persona.network.accept_language, "de-DE,de;q=0.9");
}