Skip to content

Commit 9eb4010

Browse files
Move back privacy test pages to reduce duplication (#329)
* Move back privacy test pages to reduce duplication * Fix up ts and add CODEOWNERS
1 parent 6d145db commit 9eb4010

File tree

13 files changed

+508
-224
lines changed

13 files changed

+508
-224
lines changed

CODEOWNERS

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ src/locales/click-to-load/ @kzar @ladamski
1111
inject/android.js @jonathanKingston @joshliebe
1212
inject/chrome-mv3.js @kzar @sammacbeth
1313
inject/chrome.js @jonathanKingston @sammacbeth
14-
inject/windows.js @jonathanKingston @q71114 @szanto90balazs
14+
inject/windows.js @jonathanKingston @q71114 @szanto90balazs
15+
16+
# Test owners
17+
integration-tests/test-pages/ @kdzwinel @jonathanKingston

integration-test/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
jsLoader: 'import',
44
spec_files: [
55
'**/*.js',
6+
'!test-pages/**/*.js',
67
'!pages/**/*.js',
78
'!extension/**/*.js'
89
],

integration-test/helpers/harness.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,29 @@ export async function setup (ops = {}) {
6464
* @returns {http.Server}
6565
*/
6666
function setupServer (port) {
67+
return _startupServerInternal('../pages', port)
68+
}
69+
70+
/**
71+
* @param {number|string} [port]
72+
* @returns {http.Server}
73+
*/
74+
function setupIntegrationPagesServer (port) {
75+
return _startupServerInternal('../test-pages', port)
76+
}
77+
78+
/**
79+
* @param {string} pathName
80+
* @param {number|string} [port]
81+
* @returns {http.Server}
82+
*/
83+
function _startupServerInternal (pathName, port) {
6784
const server = http.createServer(function (req, res) {
6885
const url = new URL(req.url, `http://${req.headers.host}`)
6986
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
7087
const importUrl = new URL(import.meta.url)
7188
const dirname = importUrl.pathname.replace(/\/[^/]*$/, '')
72-
const pathname = path.join(dirname, '../pages', url.pathname)
89+
const pathname = path.join(dirname, pathName, url.pathname)
7390

7491
fs.readFile(pathname, (err, data) => {
7592
if (err) {
@@ -118,10 +135,11 @@ export async function setup (ops = {}) {
118135

119136
// wait until contentScopeFeatures.init(args) has completed
120137
await page.waitForFunction(() => {
138+
window.dispatchEvent(new Event('content-scope-init-complete'))
121139
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
122140
return window.__content_scope_status === 'initialized'
123141
})
124142
}
125143

126-
return { browser, teardown, setupServer, gotoAndWait }
144+
return { browser, teardown, setupServer, setupIntegrationPagesServer, gotoAndWait }
127145
}

integration-test/test-pages.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Tests for runtime checks
3+
*/
4+
import { processConfig } from '../src/utils.js'
5+
import { setup } from './helpers/harness.js'
6+
import * as fs from 'fs'
7+
8+
describe('Test integration pages', () => {
9+
let browser
10+
let server
11+
let teardown
12+
let setupIntegrationPagesServer
13+
let gotoAndWait
14+
beforeAll(async () => {
15+
({ browser, setupIntegrationPagesServer, teardown, gotoAndWait } = await setup({ withExtension: true }))
16+
server = setupIntegrationPagesServer()
17+
})
18+
afterAll(async () => {
19+
await server?.close()
20+
await teardown()
21+
})
22+
23+
it('Script that should not execute', async () => {
24+
const pages = {
25+
'runtime-checks/pages/basic-run.html': 'runtime-checks/config/basic-run.json',
26+
'runtime-checks/pages/filter-props.html': 'runtime-checks/config/filter-props.json'
27+
}
28+
for (const pageName in pages) {
29+
const configName = pages[pageName]
30+
31+
const port = server.address().port
32+
const page = await browser.newPage()
33+
const res = fs.readFileSync(process.cwd() + '/integration-test/test-pages/' + configName)
34+
// @ts-expect-error - JSON.parse returns any
35+
const config = JSON.parse(res)
36+
// Pollyfill for globalThis methods needed in processConfig
37+
globalThis.document = {
38+
referrer: 'http://localhost:8080',
39+
location: {
40+
href: 'http://localhost:8080',
41+
// @ts-expect-error - ancestorOrigins is not defined in the type definition
42+
ancestorOrigins: {
43+
length: 0
44+
}
45+
}
46+
}
47+
globalThis.location = {
48+
href: 'http://localhost:8080',
49+
// @ts-expect-error - ancestorOrigins is not defined in the type definition
50+
ancestorOrigins: {
51+
length: 0
52+
}
53+
}
54+
55+
const processedConfig = processConfig(config, /* userList */ [], /* preferences */ {}/*, platformSpecificFeatures = [] */)
56+
57+
await gotoAndWait(page, `http://localhost:${port}/${pageName}?automation=true`, processedConfig)
58+
// Check page results
59+
const pageResults = await page.evaluate(
60+
async () => {
61+
let res
62+
const promise = new Promise(resolve => {
63+
res = resolve
64+
})
65+
// @ts-expect-error - results is not defined in the type definition
66+
if (window.results) {
67+
// @ts-expect-error - results is not defined in the type definition
68+
res(window.results)
69+
} else {
70+
window.addEventListener('results-ready', (e) => {
71+
// @ts-expect-error - e.detail is not defined in the type definition
72+
res(e.detail)
73+
})
74+
}
75+
return promise
76+
}
77+
)
78+
for (const key in pageResults) {
79+
for (const result of pageResults[key]) {
80+
expect(result.result).withContext(key + ':\n ' + result.name).toEqual(result.expected)
81+
}
82+
}
83+
}
84+
})
85+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"features": {
3+
"runtimeChecks": {
4+
"state": "enabled",
5+
"exceptions": [],
6+
"settings": {
7+
"taintCheck": "enabled",
8+
"matchAllDomains": "enabled",
9+
"matchAllStackDomains": "enabled",
10+
"overloadInstanceOf": "enabled",
11+
"elementRemovalTimeout": 1000,
12+
"domains": [
13+
],
14+
"stackDomains": [
15+
]
16+
}
17+
}
18+
}
19+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"features": {
3+
"runtimeChecks": {
4+
"state": "enabled",
5+
"exceptions": [],
6+
"settings": {
7+
"taintCheck": "enabled",
8+
"matchAllDomains": "enabled",
9+
"matchAllStackDomains": "enabled",
10+
"overloadInstanceOf": "enabled",
11+
"domains": [
12+
],
13+
"stackDomains": [
14+
],
15+
"tagModifiers": {
16+
"script": {
17+
"filters": {
18+
"property": ["madeUpProp1", "madeUpProp3"],
19+
"attribute": ["madeupattr1", "madeupattr3"]
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>Runtime checks</title>
7+
</head>
8+
<body>
9+
<p><a href="../../index.html">[Home]</a></p>
10+
11+
<p>Runtime element interrogation (runtimeChecks) allows our clients the ability to validate, inspect and modify elements as they get injected into a web page by website scripts.</p>
12+
<ul>
13+
<li><a href="./pages/basic-run.html">Basic Run</a> - <a href="./config/basic-run.json">Config</a></li>
14+
<li><a href="./pages/filter-props.html">Filter props</a> - <a href="./config/filter-props.json">Config</a></li>
15+
</ul>
16+
17+
</body>
18+
</html>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>Runtime checks</title>
7+
<link rel="stylesheet" href="../../shared/style.css">
8+
</head>
9+
<body>
10+
<script src="../../shared/utils.js"></script>
11+
<p><a href="../index.html">[Runtime checks]</a></p>
12+
13+
<p>This page verifies that runtime checking is enabled given the corresponding <a href="../config/basic-run.json">config</a></p>
14+
15+
<script>
16+
// eslint-disable-next-line no-undef
17+
test('Script that should not execute', async () => {
18+
window.scripty1Ran = false;
19+
const scriptElement = document.createElement('script');
20+
scriptElement.innerText = 'window.scripty1Ran = true';
21+
scriptElement.id = 'scripty';
22+
scriptElement.setAttribute('type', 'application/evilscript');
23+
document.body.appendChild(scriptElement);
24+
const hadInspectorNode = scriptElement === document.querySelector('ddg-runtime-checks:last-of-type');
25+
// Continue to modify the script element after it has been added to the DOM
26+
scriptElement.integrity = 'sha256-123';
27+
scriptElement.madeUpProp = 'val';
28+
const instanceofResult = scriptElement instanceof HTMLScriptElement;
29+
const scripty = document.querySelector('#scripty');
30+
31+
return [
32+
{ name: 'hadInspectorNode', result: hadInspectorNode, expected: true },
33+
{ name: 'expect script to match', result: scripty, expected: scriptElement },
34+
{ name: 'instanceof matches HTMLScriptElement', result: instanceofResult, expected: true },
35+
{ name: 'scripty.integrity', result: scripty.integrity, expected: 'sha256-123' },
36+
{ name: 'scripty.madeUpProp', result: scripty.madeUpProp, expected: 'val' },
37+
{ name: 'scripty.type', result: scripty.type, expected: 'application/evilscript' },
38+
{ name: 'scripty.id', result: scripty.id, expected: 'scripty' },
39+
{ name: 'script ran', result: window.scripty1Ran, expected: false }
40+
];
41+
});
42+
43+
// eslint-disable-next-line no-undef
44+
test('Script that should execute', async () => {
45+
window.scripty2Ran = false;
46+
const scriptElement = document.createElement('script');
47+
scriptElement.innerText = 'window.scripty2Ran = true';
48+
scriptElement.id = 'scripty2';
49+
scriptElement.setAttribute('type', 'application/javascript');
50+
document.body.appendChild(scriptElement);
51+
const hadInspectorNode = scriptElement === document.querySelector('ddg-runtime-checks:last-of-type');
52+
// Continue to modify the script element after it has been added to the DOM
53+
scriptElement.madeUpProp = 'val';
54+
const instanceofResult = scriptElement instanceof HTMLScriptElement;
55+
const scripty = document.querySelector('#scripty2');
56+
57+
return [
58+
{ name: 'hadInspectorNode', result: hadInspectorNode, expected: true },
59+
{ name: 'expect script to match', result: scripty, expected: scriptElement },
60+
{ name: 'instanceof matches HTMLScriptElement', result: instanceofResult, expected: true },
61+
{ name: 'scripty.madeUpProp', result: scripty.madeUpProp, expected: 'val' },
62+
{ name: 'scripty.type', result: scripty.type, expected: 'application/javascript' },
63+
{ name: 'scripty.id', result: scripty.id, expected: 'scripty2' },
64+
{ name: 'script ran', result: window.scripty2Ran, expected: true }
65+
];
66+
});
67+
68+
// eslint-disable-next-line no-undef
69+
test('Invalid external script should trigger error listeners', async () => {
70+
const scriptElement = document.createElement('script');
71+
scriptElement.id = 'scripty3';
72+
scriptElement.src = 'invalid://url';
73+
scriptElement.setAttribute('type', 'application/javascript');
74+
75+
let listenerCount = 0;
76+
let resolver = null;
77+
const promise = new Promise(resolve => {
78+
resolver = resolve;
79+
});
80+
scriptElement.onerror = () => {
81+
listenerCount++;
82+
resolver();
83+
};
84+
85+
let resolver2 = null;
86+
const promise2 = new Promise(resolve => {
87+
resolver2 = resolve;
88+
});
89+
scriptElement.addEventListener('error', () => {
90+
listenerCount++;
91+
resolver2();
92+
});
93+
94+
document.body.appendChild(scriptElement);
95+
await Promise.all([promise, promise2]);
96+
97+
const hadInspectorNode = scriptElement === document.querySelector('ddg-runtime-checks:last-of-type');
98+
// Continue to modify the script element after it has been added to the DOM
99+
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
100+
scriptElement.madeUpProp = 'val';
101+
const instanceofResult = scriptElement instanceof HTMLScriptElement;
102+
const scripty = document.querySelector('#scripty3');
103+
104+
return [
105+
{ name: 'listenerCount', result: listenerCount, expected: 2 },
106+
{ name: 'hadInspectorNode', result: hadInspectorNode, expected: true },
107+
{ name: 'instanceof matches HTMLScriptElement', result: instanceofResult, expected: true },
108+
{ name: 'scripty.madeUpProp', result: scripty.madeUpProp, expected: 'val' },
109+
{ name: 'scripty.type', result: scripty.type, expected: 'application/javascript' },
110+
{ name: 'scripty.id', result: scripty.id, expected: 'scripty3' },
111+
{ name: 'scripty.src', result: scripty.src, expected: 'invalid://url' }
112+
];
113+
});
114+
115+
// eslint-disable-next-line no-undef
116+
renderResults();
117+
</script>
118+
</body>
119+
</html>

0 commit comments

Comments
 (0)