1 |
01e977f9
|
Wojtek Kosior
|
# 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 |
07a883fe
|
Wojtek Kosior
|
# Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org>
|
10 |
01e977f9
|
Wojtek Kosior
|
#
|
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 json
|
22 |
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
23 |
|
|
|
24 |
|
|
from ..script_loader import load_script
|
25 |
|
|
|
26 |
|
|
def simple_sample_mapping(patterns, fruit):
|
27 |
702eefd2
|
Wojtek Kosior
|
if type(patterns) is not list:
|
28 |
|
|
patterns = [patterns]
|
29 |
|
|
payloads = dict([(p, {'identifier': f'{fruit}-{p}'}) for p in patterns])
|
30 |
01e977f9
|
Wojtek Kosior
|
return {
|
31 |
|
|
'source_copyright': [],
|
32 |
|
|
'type': 'mapping',
|
33 |
|
|
'identifier': f'inject-{fruit}',
|
34 |
|
|
'payloads': payloads
|
35 |
|
|
}
|
36 |
|
|
|
37 |
9d825eaa
|
Wojtek Kosior
|
def get_content_script_values(driver, content_script):
|
38 |
|
|
"""
|
39 |
|
|
Allow easy extraction of 'this.something = ...' values from generated
|
40 |
|
|
content script and verify the content script is syntactically correct.
|
41 |
|
|
"""
|
42 |
|
|
return driver.execute_script(
|
43 |
|
|
'''
|
44 |
|
|
function value_holder() {
|
45 |
|
|
%s;
|
46 |
|
|
return this;
|
47 |
|
|
}
|
48 |
|
|
return value_holder.call({});
|
49 |
|
|
''' % content_script)
|
50 |
01e977f9
|
Wojtek Kosior
|
|
51 |
|
|
# Fields that are not relevant for testing are omitted from these mapping
|
52 |
|
|
# definitions.
|
53 |
|
|
sample_mappings = [simple_sample_mapping(pats, fruit) for pats, fruit in [
|
54 |
|
|
(['https://gotmyowndoma.in/index.html',
|
55 |
|
|
'http://gotmyowndoma.in/index.html'], 'banana'),
|
56 |
|
|
(['https://***.gotmyowndoma.in/index.html',
|
57 |
|
|
'https://**.gotmyowndoma.in/index.html',
|
58 |
|
|
'https://*.gotmyowndoma.in/index.html',
|
59 |
|
|
'https://gotmyowndoma.in/index.html'], 'orange'),
|
60 |
|
|
('https://gotmyowndoma.in/index.html/***', 'grape'),
|
61 |
|
|
('http://gotmyowndoma.in/index.html/***', 'melon'),
|
62 |
|
|
('https://gotmyowndoma.in/index.html', 'peach'),
|
63 |
|
|
('https://gotmyowndoma.in/*', 'pear'),
|
64 |
|
|
('https://gotmyowndoma.in/**', 'raspberry'),
|
65 |
|
|
('https://gotmyowndoma.in/***', 'strawberry'),
|
66 |
|
|
('https://***.gotmyowndoma.in/index.html', 'apple'),
|
67 |
|
|
('https://***.gotmyowndoma.in/*', 'avocado'),
|
68 |
|
|
('https://***.gotmyowndoma.in/**', 'papaya'),
|
69 |
|
|
('https://***.gotmyowndoma.in/***', 'kiwi')
|
70 |
|
|
]]
|
71 |
|
|
|
72 |
07a883fe
|
Wojtek Kosior
|
sample_blocking = [f'http{s}://{dw}gotmyown%sdoma.in{i}{pw}'
|
73 |
|
|
for dw in ('', '***.', '**.', '*.')
|
74 |
|
|
for i in ('/index.html', '')
|
75 |
|
|
for pw in ('', '/', '/*')
|
76 |
|
|
for s in ('', 's')]
|
77 |
|
|
sample_blocking = [{'pattern': pattern % (i if i > 1 else ''),
|
78 |
|
|
'allow': bool(i & 1)}
|
79 |
|
|
for i, pattern in enumerate(sample_blocking)]
|
80 |
|
|
|
81 |
01e977f9
|
Wojtek Kosior
|
# Even though patterns_query_manager.js is normally meant to run from background
|
82 |
07a883fe
|
Wojtek Kosior
|
# page, some tests can be as well performed running it from a normal page.
|
83 |
01e977f9
|
Wojtek Kosior
|
@pytest.mark.get_page('https://gotmyowndoma.in')
|
84 |
|
|
def test_pqm_tree_building(driver, execute_in_page):
|
85 |
|
|
"""
|
86 |
|
|
patterns_query_manager.js tracks Haketilo's internal database and builds a
|
87 |
|
|
constantly-updated pattern tree based on its contents. Mock the database and
|
88 |
|
|
verify tree building works properly.
|
89 |
|
|
"""
|
90 |
|
|
execute_in_page(load_script('background/patterns_query_manager.js'))
|
91 |
|
|
# Mock IndexedDB and build patterns tree.
|
92 |
|
|
execute_in_page(
|
93 |
|
|
'''
|
94 |
07a883fe
|
Wojtek Kosior
|
const [initial_mappings, initial_blocking] = arguments.slice(0, 2);
|
95 |
9d825eaa
|
Wojtek Kosior
|
let mappingchange, blockingchange, settingchange;
|
96 |
07a883fe
|
Wojtek Kosior
|
|
97 |
7218849a
|
Wojtek Kosior
|
haketilodb.track.mapping = function (cb) {
|
98 |
01e977f9
|
Wojtek Kosior
|
mappingchange = cb;
|
99 |
|
|
|
100 |
|
|
return [{}, initial_mappings];
|
101 |
|
|
}
|
102 |
07a883fe
|
Wojtek Kosior
|
haketilodb.track.blocking = function (cb) {
|
103 |
|
|
blockingchange = cb;
|
104 |
|
|
|
105 |
|
|
return [{}, initial_blocking];
|
106 |
|
|
}
|
107 |
9d825eaa
|
Wojtek Kosior
|
haketilodb.track.settings = function (cb) {
|
108 |
|
|
settingchange = cb;
|
109 |
|
|
|
110 |
|
|
return [{}, [{name: "default_allow", value: true}]];
|
111 |
|
|
}
|
112 |
01e977f9
|
Wojtek Kosior
|
|
113 |
|
|
let last_script;
|
114 |
|
|
let unregister_called = 0;
|
115 |
|
|
async function register_mock(injection)
|
116 |
|
|
{
|
117 |
|
|
await new Promise(resolve => setTimeout(resolve, 1));
|
118 |
|
|
last_script = injection.js[0].code;
|
119 |
|
|
return {unregister: () => unregister_called++};
|
120 |
|
|
}
|
121 |
|
|
browser = {contentScripts: {register: register_mock}};
|
122 |
|
|
|
123 |
9d825eaa
|
Wojtek Kosior
|
returnval(start("abracadabra"));
|
124 |
01e977f9
|
Wojtek Kosior
|
''',
|
125 |
07a883fe
|
Wojtek Kosior
|
sample_mappings[0:2], sample_blocking[0:2])
|
126 |
01e977f9
|
Wojtek Kosior
|
|
127 |
|
|
found, tree, content_script, deregistrations = execute_in_page(
|
128 |
|
|
'''
|
129 |
|
|
returnval([pqt.search(tree, arguments[0]).next().value,
|
130 |
|
|
tree, last_script, unregister_called]);
|
131 |
|
|
''',
|
132 |
|
|
'https://gotmyowndoma.in/index.html')
|
133 |
702eefd2
|
Wojtek Kosior
|
best_pattern = 'https://gotmyowndoma.in/index.html'
|
134 |
|
|
assert found == \
|
135 |
07a883fe
|
Wojtek Kosior
|
dict([('~allow', 1),
|
136 |
|
|
*[(f'inject-{fruit}', {'identifier': f'{fruit}-{best_pattern}'})
|
137 |
|
|
for fruit in ('banana', 'orange')]])
|
138 |
9d825eaa
|
Wojtek Kosior
|
cs_values = get_content_script_values(driver, content_script)
|
139 |
|
|
assert cs_values['haketilo_secret'] == 'abracadabra'
|
140 |
|
|
assert cs_values['haketilo_pattern_tree'] == tree
|
141 |
|
|
assert cs_values['haketilo_default_allow'] == True
|
142 |
01e977f9
|
Wojtek Kosior
|
assert deregistrations == 0
|
143 |
|
|
|
144 |
07a883fe
|
Wojtek Kosior
|
def condition_all_added(driver):
|
145 |
01e977f9
|
Wojtek Kosior
|
last_script = execute_in_page('returnval(last_script);')
|
146 |
9d825eaa
|
Wojtek Kosior
|
cs_values = get_content_script_values(driver, last_script)
|
147 |
07a883fe
|
Wojtek Kosior
|
nums = [i for i in range(len(sample_blocking)) if i > 1]
|
148 |
9d825eaa
|
Wojtek Kosior
|
return (cs_values['haketilo_default_allow'] == False and
|
149 |
|
|
all([('gotmyown%sdoma' % i) in last_script for i in nums]) and
|
150 |
07a883fe
|
Wojtek Kosior
|
all([m['identifier'] in last_script for m in sample_mappings]))
|
151 |
01e977f9
|
Wojtek Kosior
|
|
152 |
|
|
execute_in_page(
|
153 |
4c6a2323
|
Wojtek Kosior
|
'''{
|
154 |
9d825eaa
|
Wojtek Kosior
|
const new_setting_val = {name: "default_allow", value: false};
|
155 |
|
|
settingchange({key: "default_allow", new_val: new_setting_val});
|
156 |
702eefd2
|
Wojtek Kosior
|
for (const mapping of arguments[0])
|
157 |
|
|
mappingchange({key: mapping.identifier, new_val: mapping});
|
158 |
07a883fe
|
Wojtek Kosior
|
for (const blocking of arguments[1])
|
159 |
|
|
blockingchange({key: blocking.pattern, new_val: blocking});
|
160 |
4c6a2323
|
Wojtek Kosior
|
}''',
|
161 |
07a883fe
|
Wojtek Kosior
|
sample_mappings[2:], sample_blocking[2:])
|
162 |
|
|
WebDriverWait(driver, 10).until(condition_all_added)
|
163 |
|
|
|
164 |
|
|
odd_mappings = \
|
165 |
|
|
[m['identifier'] for i, m in enumerate(sample_mappings) if i & 1]
|
166 |
|
|
odd_blocking = \
|
167 |
|
|
[b['pattern'] for i, b in enumerate(sample_blocking) if i & 1]
|
168 |
|
|
even_mappings = \
|
169 |
|
|
[m['identifier'] for i, m in enumerate(sample_mappings) if 1 - i & 1]
|
170 |
|
|
even_blocking = \
|
171 |
|
|
[b['pattern'] for i, b in enumerate(sample_blocking) if 1 - i & 1]
|
172 |
01e977f9
|
Wojtek Kosior
|
|
173 |
|
|
def condition_odd_removed(driver):
|
174 |
|
|
last_script = execute_in_page('returnval(last_script);')
|
175 |
07a883fe
|
Wojtek Kosior
|
nums = [i for i in range(len(sample_blocking)) if i > 1 and 1 - i & 1]
|
176 |
|
|
return (all([id not in last_script for id in odd_mappings]) and
|
177 |
|
|
all([id in last_script for id in even_mappings]) and
|
178 |
|
|
all([p not in last_script for p in odd_blocking[1:]]) and
|
179 |
|
|
all([('gotmyown%sdoma' % i) in last_script for i in nums]))
|
180 |
01e977f9
|
Wojtek Kosior
|
|
181 |
|
|
def condition_all_removed(driver):
|
182 |
|
|
content_script = execute_in_page('returnval(last_script);')
|
183 |
9d825eaa
|
Wojtek Kosior
|
cs_values = get_content_script_values(driver, content_script)
|
184 |
|
|
return cs_values['haketilo_pattern_tree'] == {}
|
185 |
01e977f9
|
Wojtek Kosior
|
|
186 |
|
|
execute_in_page(
|
187 |
|
|
'''
|
188 |
702eefd2
|
Wojtek Kosior
|
arguments[0].forEach(identifier => mappingchange({key: identifier}));
|
189 |
07a883fe
|
Wojtek Kosior
|
arguments[1].forEach(pattern => blockingchange({key: pattern}));
|
190 |
01e977f9
|
Wojtek Kosior
|
''',
|
191 |
07a883fe
|
Wojtek Kosior
|
odd_mappings, odd_blocking)
|
192 |
01e977f9
|
Wojtek Kosior
|
|
193 |
|
|
WebDriverWait(driver, 10).until(condition_odd_removed)
|
194 |
|
|
|
195 |
|
|
execute_in_page(
|
196 |
|
|
'''
|
197 |
702eefd2
|
Wojtek Kosior
|
arguments[0].forEach(identifier => mappingchange({key: identifier}));
|
198 |
07a883fe
|
Wojtek Kosior
|
arguments[1].forEach(pattern => blockingchange({key: pattern}));
|
199 |
01e977f9
|
Wojtek Kosior
|
''',
|
200 |
07a883fe
|
Wojtek Kosior
|
even_mappings, even_blocking)
|
201 |
01e977f9
|
Wojtek Kosior
|
|
202 |
|
|
WebDriverWait(driver, 10).until(condition_all_removed)
|
203 |
|
|
|
204 |
4c6a2323
|
Wojtek Kosior
|
def condition_default_allowed_again(driver):
|
205 |
|
|
content_script = execute_in_page('returnval(last_script);')
|
206 |
|
|
cs_values = get_content_script_values(driver, content_script)
|
207 |
|
|
return cs_values['haketilo_default_allow'] == True
|
208 |
|
|
|
209 |
|
|
execute_in_page(
|
210 |
|
|
'''{
|
211 |
|
|
const new_setting_val = {name: "default_allow", value: true};
|
212 |
|
|
settingchange({key: "default_allow", new_val: new_setting_val});
|
213 |
|
|
}''')
|
214 |
|
|
|
215 |
|
|
WebDriverWait(driver, 10).until(condition_default_allowed_again)
|
216 |
|
|
|
217 |
01e977f9
|
Wojtek Kosior
|
content_js = '''
|
218 |
|
|
let already_run = false;
|
219 |
|
|
this.haketilo_content_script_main = function() {
|
220 |
|
|
if (already_run)
|
221 |
|
|
return;
|
222 |
|
|
already_run = true;
|
223 |
|
|
document.documentElement.innerHTML = "<body><div id='tree-json'>";
|
224 |
|
|
document.getElementById("tree-json").innerText =
|
225 |
|
|
JSON.stringify(this.haketilo_pattern_tree);
|
226 |
|
|
}
|
227 |
|
|
if (this.haketilo_pattern_tree !== undefined)
|
228 |
|
|
this.haketilo_content_script_main();
|
229 |
|
|
'''
|
230 |
|
|
|
231 |
|
|
def background_js():
|
232 |
|
|
pqm_js = load_script('background/patterns_query_manager.js',
|
233 |
|
|
"#IMPORT background/broadcast_broker.js")
|
234 |
|
|
return pqm_js + '; broadcast_broker.start(); start();'
|
235 |
|
|
|
236 |
|
|
@pytest.mark.ext_data({
|
237 |
|
|
'content_script': content_js,
|
238 |
|
|
'background_script': background_js
|
239 |
|
|
})
|
240 |
|
|
@pytest.mark.usefixtures('webextension')
|
241 |
|
|
def test_pqm_script_injection(driver, execute_in_page):
|
242 |
|
|
# Let's open a normal page in a second window. Window 0 will be used to make
|
243 |
|
|
# changed to IndexedDB and window 1 to test the working of content scripts.
|
244 |
|
|
driver.execute_script('window.open("about:blank", "_blank");')
|
245 |
4c6a2323
|
Wojtek Kosior
|
WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
|
246 |
01e977f9
|
Wojtek Kosior
|
windows = [*driver.window_handles]
|
247 |
|
|
|
248 |
|
|
def run_content_script():
|
249 |
|
|
driver.switch_to.window(windows[1])
|
250 |
|
|
driver.get('https://gotmyowndoma.in/index.html')
|
251 |
|
|
windows[1] = driver.window_handles[1]
|
252 |
|
|
return driver.execute_script(
|
253 |
|
|
'''
|
254 |
|
|
return (document.getElementById("tree-json") || {}).innerText;
|
255 |
|
|
''')
|
256 |
|
|
|
257 |
|
|
for attempt in range(10):
|
258 |
|
|
json_txt = run_content_script()
|
259 |
|
|
if json.loads(json_txt) == {}:
|
260 |
|
|
break;
|
261 |
|
|
assert attempt != 9
|
262 |
|
|
|
263 |
|
|
driver.switch_to.window(windows[0])
|
264 |
|
|
execute_in_page(load_script('common/indexeddb.js'))
|
265 |
|
|
|
266 |
|
|
sample_data = {
|
267 |
|
|
'mappings': dict([(sm['identifier'], {'1.0': sm})
|
268 |
|
|
for sm in sample_mappings]),
|
269 |
|
|
'resources': {},
|
270 |
|
|
'files': {}
|
271 |
|
|
}
|
272 |
|
|
execute_in_page('returnval(save_items(arguments[0]));', sample_data)
|
273 |
|
|
|
274 |
|
|
for attempt in range(10):
|
275 |
|
|
tree_json = run_content_script()
|
276 |
|
|
json.loads(tree_json)
|
277 |
|
|
if all([m['identifier'] in tree_json for m in sample_mappings]):
|
278 |
|
|
break
|
279 |
|
|
assert attempt != 9
|
280 |
|
|
|
281 |
|
|
driver.switch_to.window(windows[0])
|
282 |
|
|
execute_in_page(
|
283 |
|
|
'''{
|
284 |
|
|
const identifiers = arguments[0];
|
285 |
|
|
async function remove_items()
|
286 |
|
|
{
|
287 |
7218849a
|
Wojtek Kosior
|
const ctx = await start_items_transaction(["mapping"], {});
|
288 |
01e977f9
|
Wojtek Kosior
|
for (const id of identifiers)
|
289 |
|
|
await remove_mapping(id, ctx);
|
290 |
702eefd2
|
Wojtek Kosior
|
await finalize_transaction(ctx);
|
291 |
01e977f9
|
Wojtek Kosior
|
}
|
292 |
|
|
returnval(remove_items());
|
293 |
|
|
}''',
|
294 |
|
|
[sm['identifier'] for sm in sample_mappings])
|
295 |
|
|
|
296 |
|
|
for attempt in range(10):
|
297 |
|
|
if json.loads(run_content_script()) == {}:
|
298 |
|
|
break
|
299 |
|
|
assert attempt != 9
|