aboutsummaryrefslogtreecommitdiff
path: root/test/haketilo_test/unit/test_content.py
blob: 98ea93019410ac2f1c63d5cbed02db0f6e94802f (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# SPDX-License-Identifier: CC0-1.0

"""
Haketilo unit tests - main content script
"""

# This file is part of Haketilo
#
# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
#
# 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_default_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 */

    function mock_decide_policy() {
        nonce = "12345";
        return {
            allow: false,
            mapping: "what-is-programmers-favorite-drinking-place",
            payload: {identifier: "foo-bar"},
            nonce,
            csp: "prefetch-src 'none'; script-src-attr 'none'; script-src 'nonce-12345'; script-src-elem 'nonce-12345';"
        };
    }

    async function mock_payload_error([type, res_id]) {
        if (type === "indexeddb_files")
            return {error: {haketilo_error_type: "missing", id: res_id}};
    }

    async function mock_payload_ok([type, res_id]) {
        if (type === "indexeddb_files")
            return {files: [1, 2].map(n => `window.hak_injected_${n} = ${n};`)};
    }

    if (/payload_error/.test(document.URL)) {
        browser.runtime.sendMessage = mock_payload_error;
        decide_policy = mock_decide_policy;
    } else if (/payload_ok/.test(document.URL)) {
        browser.runtime.sendMessage = mock_payload_ok;
        decide_policy = mock_decide_policy;
    }
    /* Otherwise, script blocking policy without payload to inject is used. */

    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);
    haketilo_apis.start     = () => data_set("apis_started", true);

    enforce_blocking = policy => data_set("enforcing", policy);

    browser.runtime.onMessage.addListener = async function (listener_cb) {
        await new Promise(cb => setTimeout(cb, 10));

        /* Mock a good request. */
        const set_good = val => data_set("good_request_result", val);
        data_set("good_request_returned",
                 !!listener_cb(["page_info"], {}, val => set_good(val)));

        /* Mock a bad request. */
        const set_bad = val => data_set("bad_request_result", val);
        data_set("bad_request_returned",
                 !!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('target1', ['dynamic_before', 'dynamic_after'])
@pytest.mark.parametrize('target2', [
    'scripts_blocked',
    'payload_error',
    'payload_ok'
])
def test_content_unprivileged_page(driver, execute_in_page, target1, target2):
    """
    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#{target1}-{target2}')

    def get_data(driver):
        data = driver.execute_script('return window.data_to_verify;')
        return data if 'good_request_result' in data else False

    data = json.loads(WebDriverWait(driver, 10).until(get_data))

    assert 'gotmyowndoma.in' in data['good_request_result']['url']
    assert 'bad_request_result' not in data

    assert data['good_request_returned'] == True
    assert data['bad_request_returned'] == False

    assert data['cacher_started'] == True
    assert data.get('apis_started', False) == (target2 == 'payload_ok')

    for obj in (data['good_request_result'], data['enforcing']):
        assert obj['allow'] == False

    assert 'error' not in data['enforcing']

    if target2.startswith('payload'):
        for obj in (data['good_request_result'], data['enforcing']):
            assert obj['payload']['identifier'] == 'foo-bar'
            assert 'mapping' in obj
    else:
        assert 'payload' not in data['enforcing']
        assert 'mapping' not in data['enforcing']

    assert data['script_run_without_errors'] == True

    def vars_made_by_payload(driver):
        vars_values = driver.execute_script(
            '''
            return [
                ...[1, 2].map(n => window[`hak_injected_${n}`]),
                window.haketilo_version
            ];
            ''')
        if vars_values != [None, None, None]:
            return vars_values

    if target2 == 'payload_error':
        assert data['good_request_result']['error'] == {
            'haketilo_error_type': 'missing',
            'id': 'foo-bar'
        }
    elif target2 == 'payload_ok':
        vars_values = WebDriverWait(driver, 10).until(vars_made_by_payload)
        assert vars_values[:2] == [1, 2]
        assert type(vars_values[2]) == str

@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}