From 9d825eaaa0715ee5244a09bc3d1968aa1664d048 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Wed, 26 Jan 2022 22:13:01 +0100 Subject: add new root content script --- test/unit/test_content.py | 119 +++++++++++++++++++++++++++++++ test/unit/test_patterns_query_manager.py | 44 ++++++++---- test/unit/test_popup.py | 9 ++- test/unit/test_webrequest.py | 6 +- 4 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 test/unit/test_content.py (limited to 'test') diff --git a/test/unit/test_content.py b/test/unit/test_content.py new file mode 100644 index 0000000..c8e0987 --- /dev/null +++ b/test/unit/test_content.py @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: CC0-1.0 + +""" +Haketilo unit tests - main content script +""" + +# This file is part of Haketilo +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the CC0 1.0 Universal License as published by +# the Creative Commons Corporation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# CC0 1.0 Universal License for more details. + +import pytest +import json +from selenium.webdriver.support.ui import WebDriverWait + +from ..script_loader import load_script + +# From: +# https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts/register +# it is unclear whether the dynamically-registered content script is guaranteed +# to be always executed after statically-registered ones. We want to test both +# cases, so we'll make the mocked dynamic content script execute before +# content.js on http:// pages and after it on https:// pages. +dynamic_script = \ + '''; + this.haketilo_secret = "abracadabra"; + this.haketilo_pattern_tree = {}; + this.haketilo_defualt_allow = false; + + if (this.haketilo_content_script_main) + this.haketilo_content_script_main(); + ''' + +content_script = \ + ''' + /* Mock dynamic content script - case 'before'. */ + if (/#dynamic_before$/.test(document.URL)) { + %s; + } + + /* Place amalgamated content.js here. */ + %s; + + /* Rest of mocks */ + const data_to_verify = {}; + function data_set(prop, val) { + data_to_verify[prop] = val; + window.wrappedJSObject.data_to_verify = JSON.stringify(data_to_verify); + } + + repo_query_cacher.start = () => data_set("cacher_started", true); + + enforce_blocking = policy => data_set("enforcing", policy); + + browser.runtime.onMessage.addListener = async function (listener_cb) { + await new Promise(cb => setTimeout(cb, 0)); + + /* Mock a good request. */ + const set_good = val => data_set("good_request_result", val); + listener_cb(["page_info"], {}, val => set_good(val)); + + /* Mock a bad request. */ + const set_bad = val => data_set("bad_request_result", val); + listener_cb(["???"], {}, val => set_bad(val)); + } + + /* main() call - normally present in content.js, inside '#IF !UNIT_TEST'. */ + main(); + + /* Mock dynamic content script - case 'after'. */ + if (/#dynamic_after$/.test(document.URL)) { + %s; + } + + data_set("script_run_without_errors", true); + ''' % (dynamic_script, load_script('content/content.js'), dynamic_script) + +@pytest.mark.ext_data({'content_script': content_script}) +@pytest.mark.usefixtures('webextension') +@pytest.mark.parametrize('target', ['dynamic_before', 'dynamic_after']) +def test_content_unprivileged_page(driver, execute_in_page, target): + """ + Test functioning of content.js on an page using unprivileged schema (e.g. + 'https://' and not 'about:'). + """ + driver.get(f'https://gotmyowndoma.in/index.html#{target}') + data = json.loads(driver.execute_script('return window.data_to_verify;')) + + assert 'gotmyowndoma.in' in data['good_request_result']['url'] + assert 'bad_request_result' not in data + + assert data['cacher_started'] == True + + assert data['enforcing']['allow'] == False + assert 'mapping' not in data['enforcing'] + assert 'error' not in data['enforcing'] + + assert data['script_run_without_errors'] == True + +@pytest.mark.ext_data({'content_script': content_script}) +@pytest.mark.usefixtures('webextension') +@pytest.mark.parametrize('target', ['dynamic_before', 'dynamic_after']) +def test_content_privileged_page(driver, execute_in_page, target): + """ + Test functioning of content.js on an page considered privileged (e.g. a + directory listing at 'file:///'). + """ + driver.get(f'file:///#{target}') + data = json.loads(driver.execute_script('return window.data_to_verify;')) + + assert data == {'script_run_without_errors': True} diff --git a/test/unit/test_patterns_query_manager.py b/test/unit/test_patterns_query_manager.py index 4e6d1bf..c6ebb81 100644 --- a/test/unit/test_patterns_query_manager.py +++ b/test/unit/test_patterns_query_manager.py @@ -18,7 +18,6 @@ Haketilo unit tests - building pattern tree and putting it in a content script # CC0 1.0 Universal License for more details. import pytest -import re import json from selenium.webdriver.support.ui import WebDriverWait @@ -35,13 +34,19 @@ def simple_sample_mapping(patterns, fruit): 'payloads': payloads } -content_script_tree_re = re.compile(r'this.haketilo_pattern_tree = (.*);') -def extract_tree_data(content_script_text): - return json.loads(content_script_tree_re.search(content_script_text)[1]) - -content_script_mapping_re = re.compile(r'this.haketilo_mappings = (.*);') -def extract_mappings_data(content_script_text): - return json.loads(content_script_mapping_re.search(content_script_text)[1]) +def get_content_script_values(driver, content_script): + """ + Allow easy extraction of 'this.something = ...' values from generated + content script and verify the content script is syntactically correct. + """ + return driver.execute_script( + ''' + function value_holder() { + %s; + return this; + } + return value_holder.call({}); + ''' % content_script) # Fields that are not relevant for testing are omitted from these mapping # definitions. @@ -87,7 +92,7 @@ def test_pqm_tree_building(driver, execute_in_page): execute_in_page( ''' const [initial_mappings, initial_blocking] = arguments.slice(0, 2); - let mappingchange, blockingchange; + let mappingchange, blockingchange, settingchange; haketilodb.track.mapping = function (cb) { mappingchange = cb; @@ -99,6 +104,11 @@ def test_pqm_tree_building(driver, execute_in_page): return [{}, initial_blocking]; } + haketilodb.track.settings = function (cb) { + settingchange = cb; + + return [{}, [{name: "default_allow", value: true}]]; + } let last_script; let unregister_called = 0; @@ -110,7 +120,7 @@ def test_pqm_tree_building(driver, execute_in_page): } browser = {contentScripts: {register: register_mock}}; - returnval(start()); + returnval(start("abracadabra")); ''', sample_mappings[0:2], sample_blocking[0:2]) @@ -125,17 +135,24 @@ def test_pqm_tree_building(driver, execute_in_page): dict([('~allow', 1), *[(f'inject-{fruit}', {'identifier': f'{fruit}-{best_pattern}'}) for fruit in ('banana', 'orange')]]) - assert tree == extract_tree_data(content_script) + cs_values = get_content_script_values(driver, content_script) + assert cs_values['haketilo_secret'] == 'abracadabra' + assert cs_values['haketilo_pattern_tree'] == tree + assert cs_values['haketilo_default_allow'] == True assert deregistrations == 0 def condition_all_added(driver): last_script = execute_in_page('returnval(last_script);') + cs_values = get_content_script_values(driver, last_script) nums = [i for i in range(len(sample_blocking)) if i > 1] - return (all([('gotmyown%sdoma' % i) in last_script for i in nums]) and + return (cs_values['haketilo_default_allow'] == False and + all([('gotmyown%sdoma' % i) in last_script for i in nums]) and all([m['identifier'] in last_script for m in sample_mappings])) execute_in_page( ''' + const new_setting_val = {name: "default_allow", value: false}; + settingchange({key: "default_allow", new_val: new_setting_val}); for (const mapping of arguments[0]) mappingchange({key: mapping.identifier, new_val: mapping}); for (const blocking of arguments[1]) @@ -163,7 +180,8 @@ def test_pqm_tree_building(driver, execute_in_page): def condition_all_removed(driver): content_script = execute_in_page('returnval(last_script);') - return extract_tree_data(content_script) == {} + cs_values = get_content_script_values(driver, content_script) + return cs_values['haketilo_pattern_tree'] == {} execute_in_page( ''' diff --git a/test/unit/test_popup.py b/test/unit/test_popup.py index 5319d72..bc53e6c 100644 --- a/test/unit/test_popup.py +++ b/test/unit/test_popup.py @@ -62,6 +62,10 @@ mocked_page_infos = { **unprivileged_page_info, 'mapping': 'm1', 'payload': {'identifier': 'res1'} + }, + 'error': { + **unprivileged_page_info, + 'error': True } } @@ -143,7 +147,7 @@ def test_popup_display(driver, execute_in_page, page_info_key): assert by_id['page_url'].text == mocked_page_infos[page_info_key]['url'] assert not by_id['repo_query_container'].is_displayed() - if 'blocked' in page_info_key or page_info_key == 'mapping': + if 'blocked' in page_info_key or page_info_key in ('mapping', 'error'): assert by_id['scripts_blocked'].text.lower() == 'yes' elif 'allowed' in page_info_key: assert by_id['scripts_blocked'].text.lower() == 'no' @@ -167,6 +171,9 @@ def test_popup_display(driver, execute_in_page, page_info_key): elif 'default' in page_info_key: 'by default_policy)' in mapping_text + if page_info_key == 'error': + assert mapping_text == 'None (error occured when determining policy)' + @pytest.mark.ext_data(popup_ext_data) @pytest.mark.usefixtures('webextension') def test_popup_repo_query(driver, execute_in_page): diff --git a/test/unit/test_webrequest.py b/test/unit/test_webrequest.py index 598f43b..fb24b3d 100644 --- a/test/unit/test_webrequest.py +++ b/test/unit/test_webrequest.py @@ -30,6 +30,8 @@ def webrequest_js(): '''; // Mock pattern tree. tree = pqt.make(); + // Mock default allow. + default_allow = {name: "default_allow", value: true}; // Rule to block scripts. pqt.register(tree, "https://site.with.scripts.block.ed/***", @@ -40,10 +42,6 @@ def webrequest_js(): pqt.register(tree, "https://site.with.paylo.ad/***", "somemapping", {identifier: "someresource"}); - // Mock IndexedDB. - haketilodb.track.settings = - () => [{}, [{name: "default_allow", value: true}]]; - // Mock stream_filter. stream_filter.apply = (details, headers, policy) => headers; -- cgit v1.2.3