Project

General

Profile

Download (9.87 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / test / unit / test_patterns_query_manager.py @ 7218849a

1
# SPDX-License-Identifier: CC0-1.0
2

    
3
"""
4
Haketilo unit tests - building pattern tree and putting it in a content script
5
"""
6

    
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the CC0 1.0 Universal License as published by
13
# the Creative Commons Corporation.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# CC0 1.0 Universal License for more details.
19

    
20
import pytest
21
import re
22
import json
23
from selenium.webdriver.support.ui import WebDriverWait
24

    
25
from ..script_loader import load_script
26

    
27
def simple_sample_mapping(patterns, fruit):
28
    if type(patterns) is not list:
29
        patterns = [patterns]
30
    payloads = dict([(p, {'identifier': f'{fruit}-{p}'}) for p in patterns])
31
    return {
32
        'source_copyright': [],
33
        'type': 'mapping',
34
        'identifier': f'inject-{fruit}',
35
        'payloads': payloads
36
    }
37

    
38
content_script_tree_re = re.compile(r'this.haketilo_pattern_tree = (.*);')
39
def extract_tree_data(content_script_text):
40
    return json.loads(content_script_tree_re.search(content_script_text)[1])
41

    
42
content_script_mapping_re = re.compile(r'this.haketilo_mappings = (.*);')
43
def extract_mappings_data(content_script_text):
44
    return json.loads(content_script_mapping_re.search(content_script_text)[1])
45

    
46
# Fields that are not relevant for testing are omitted from these mapping
47
# definitions.
48
sample_mappings = [simple_sample_mapping(pats, fruit) for pats, fruit in [
49
    (['https://gotmyowndoma.in/index.html',
50
      'http://gotmyowndoma.in/index.html'], 'banana'),
51
    (['https://***.gotmyowndoma.in/index.html',
52
      'https://**.gotmyowndoma.in/index.html',
53
      'https://*.gotmyowndoma.in/index.html',
54
      'https://gotmyowndoma.in/index.html'], 'orange'),
55
    ('https://gotmyowndoma.in/index.html/***', 'grape'),
56
    ('http://gotmyowndoma.in/index.html/***', 'melon'),
57
    ('https://gotmyowndoma.in/index.html', 'peach'),
58
    ('https://gotmyowndoma.in/*', 'pear'),
59
    ('https://gotmyowndoma.in/**', 'raspberry'),
60
    ('https://gotmyowndoma.in/***', 'strawberry'),
61
    ('https://***.gotmyowndoma.in/index.html', 'apple'),
62
    ('https://***.gotmyowndoma.in/*', 'avocado'),
63
    ('https://***.gotmyowndoma.in/**', 'papaya'),
64
    ('https://***.gotmyowndoma.in/***', 'kiwi')
65
]]
66

    
67
sample_blocking = [f'http{s}://{dw}gotmyown%sdoma.in{i}{pw}'
68
                   for dw in ('', '***.', '**.', '*.')
69
                   for i in ('/index.html', '')
70
                   for pw in ('', '/', '/*')
71
                   for s in ('', 's')]
72
sample_blocking = [{'pattern': pattern % (i if i > 1 else ''),
73
                    'allow': bool(i & 1)}
74
                   for i, pattern in enumerate(sample_blocking)]
75

    
76
# Even though patterns_query_manager.js is normally meant to run from background
77
# page, some tests can be as well performed running it from a normal page.
78
@pytest.mark.get_page('https://gotmyowndoma.in')
79
def test_pqm_tree_building(driver, execute_in_page):
80
    """
81
    patterns_query_manager.js tracks Haketilo's internal database and builds a
82
    constantly-updated pattern tree based on its contents. Mock the database and
83
    verify tree building works properly.
84
    """
85
    execute_in_page(load_script('background/patterns_query_manager.js'))
86
    # Mock IndexedDB and build patterns tree.
87
    execute_in_page(
88
        '''
89
        const [initial_mappings, initial_blocking] = arguments.slice(0, 2);
90
        let mappingchange, blockingchange;
91

    
92
        haketilodb.track.mapping = function (cb) {
93
            mappingchange = cb;
94

    
95
            return [{}, initial_mappings];
96
        }
97
        haketilodb.track.blocking = function (cb) {
98
            blockingchange = cb;
99

    
100
            return [{}, initial_blocking];
101
        }
102

    
103
        let last_script;
104
        let unregister_called = 0;
105
        async function register_mock(injection)
106
        {
107
            await new Promise(resolve => setTimeout(resolve, 1));
108
            last_script = injection.js[0].code;
109
            return {unregister: () => unregister_called++};
110
        }
111
        browser = {contentScripts: {register: register_mock}};
112

    
113
        returnval(start());
114
        ''',
115
        sample_mappings[0:2], sample_blocking[0:2])
116

    
117
    found, tree, content_script, deregistrations = execute_in_page(
118
        '''
119
        returnval([pqt.search(tree, arguments[0]).next().value,
120
                   tree, last_script, unregister_called]);
121
        ''',
122
        'https://gotmyowndoma.in/index.html')
123
    best_pattern = 'https://gotmyowndoma.in/index.html'
124
    assert found == \
125
        dict([('~allow', 1),
126
              *[(f'inject-{fruit}', {'identifier': f'{fruit}-{best_pattern}'})
127
                for fruit in ('banana', 'orange')]])
128
    assert tree == extract_tree_data(content_script)
129
    assert deregistrations == 0
130

    
131
    def condition_all_added(driver):
132
        last_script = execute_in_page('returnval(last_script);')
133
        nums = [i for i in range(len(sample_blocking)) if i > 1]
134
        return (all([('gotmyown%sdoma' % i) in last_script for i in nums]) and
135
                all([m['identifier'] in last_script for m in sample_mappings]))
136

    
137
    execute_in_page(
138
        '''
139
        for (const mapping of arguments[0])
140
            mappingchange({key: mapping.identifier, new_val: mapping});
141
        for (const blocking of arguments[1])
142
            blockingchange({key: blocking.pattern, new_val: blocking});
143
        ''',
144
        sample_mappings[2:], sample_blocking[2:])
145
    WebDriverWait(driver, 10).until(condition_all_added)
146

    
147
    odd_mappings = \
148
        [m['identifier'] for i, m in enumerate(sample_mappings) if i & 1]
149
    odd_blocking = \
150
        [b['pattern'] for i, b in enumerate(sample_blocking) if i & 1]
151
    even_mappings = \
152
        [m['identifier'] for i, m in enumerate(sample_mappings) if 1 - i & 1]
153
    even_blocking = \
154
        [b['pattern'] for i, b in enumerate(sample_blocking) if 1 - i & 1]
155

    
156
    def condition_odd_removed(driver):
157
        last_script = execute_in_page('returnval(last_script);')
158
        nums = [i for i in range(len(sample_blocking)) if i > 1 and 1 - i & 1]
159
        return (all([id not in last_script for id in odd_mappings]) and
160
                all([id in last_script for id in even_mappings]) and
161
                all([p not in last_script for p in odd_blocking[1:]]) and
162
                all([('gotmyown%sdoma' % i) in last_script for i in nums]))
163

    
164
    def condition_all_removed(driver):
165
        content_script = execute_in_page('returnval(last_script);')
166
        return extract_tree_data(content_script) == {}
167

    
168
    execute_in_page(
169
        '''
170
        arguments[0].forEach(identifier => mappingchange({key: identifier}));
171
        arguments[1].forEach(pattern => blockingchange({key: pattern}));
172
        ''',
173
        odd_mappings, odd_blocking)
174

    
175
    WebDriverWait(driver, 10).until(condition_odd_removed)
176

    
177
    execute_in_page(
178
        '''
179
        arguments[0].forEach(identifier => mappingchange({key: identifier}));
180
        arguments[1].forEach(pattern => blockingchange({key: pattern}));
181
        ''',
182
        even_mappings, even_blocking)
183

    
184
    WebDriverWait(driver, 10).until(condition_all_removed)
185

    
186
content_js = '''
187
let already_run = false;
188
this.haketilo_content_script_main = function() {
189
    if (already_run)
190
        return;
191
    already_run = true;
192
    document.documentElement.innerHTML = "<body><div id='tree-json'>";
193
    document.getElementById("tree-json").innerText =
194
        JSON.stringify(this.haketilo_pattern_tree);
195
}
196
if (this.haketilo_pattern_tree !== undefined)
197
    this.haketilo_content_script_main();
198
'''
199

    
200
def background_js():
201
    pqm_js = load_script('background/patterns_query_manager.js',
202
                         "#IMPORT background/broadcast_broker.js")
203
    return pqm_js + '; broadcast_broker.start(); start();'
204

    
205
@pytest.mark.ext_data({
206
    'content_script':    content_js,
207
    'background_script': background_js
208
})
209
@pytest.mark.usefixtures('webextension')
210
def test_pqm_script_injection(driver, execute_in_page):
211
    # Let's open a normal page in a second window. Window 0 will be used to make
212
    # changed to IndexedDB and window 1 to test the working of content scripts.
213
    driver.execute_script('window.open("about:blank", "_blank");')
214
    windows = [*driver.window_handles]
215
    assert len(windows) == 2
216

    
217
    def run_content_script():
218
        driver.switch_to.window(windows[1])
219
        driver.get('https://gotmyowndoma.in/index.html')
220
        windows[1] = driver.window_handles[1]
221
        return driver.execute_script(
222
            '''
223
            return (document.getElementById("tree-json") || {}).innerText;
224
            ''')
225

    
226
    for attempt in range(10):
227
        json_txt = run_content_script()
228
        if json.loads(json_txt) == {}:
229
            break;
230
        assert attempt != 9
231

    
232
    driver.switch_to.window(windows[0])
233
    execute_in_page(load_script('common/indexeddb.js'))
234

    
235
    sample_data = {
236
        'mappings': dict([(sm['identifier'], {'1.0': sm})
237
                          for sm in sample_mappings]),
238
        'resources': {},
239
        'files': {}
240
    }
241
    execute_in_page('returnval(save_items(arguments[0]));', sample_data)
242

    
243
    for attempt in range(10):
244
        tree_json = run_content_script()
245
        json.loads(tree_json)
246
        if all([m['identifier'] in tree_json for m in sample_mappings]):
247
            break
248
        assert attempt != 9
249

    
250
    driver.switch_to.window(windows[0])
251
    execute_in_page(
252
        '''{
253
        const identifiers = arguments[0];
254
        async function remove_items()
255
        {
256
            const ctx = await start_items_transaction(["mapping"], {});
257
            for (const id of identifiers)
258
                await remove_mapping(id, ctx);
259
            await finalize_transaction(ctx);
260
        }
261
        returnval(remove_items());
262
        }''',
263
        [sm['identifier'] for sm in sample_mappings])
264

    
265
    for attempt in range(10):
266
        if json.loads(run_content_script()) == {}:
267
            break
268
        assert attempt != 9
(13-13/22)