File size: 5,559 Bytes
795f737 | 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 | """Real Chromium integration tests; goals/actions are test fixtures, not a policy."""
import json
from pathlib import Path
import tempfile
import unittest
from playwright.sync_api import sync_playwright
from baim.actions import Action, Kind, Decision
from baim.authority import Authority
from baim.browser import Browser
from baim.memory import Recorder
from baim.runtime import Runtime
class BrowserTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.pw = sync_playwright().start()
cls.chromium = cls.pw.chromium.launch(headless=True)
@classmethod
def tearDownClass(cls):
cls.chromium.close()
cls.pw.stop()
def setUp(self):
self.context = self.chromium.new_context()
self.browser = Browser(self.context)
self.authority = Authority()
self.authority.replace('Fill the labelled field and activate the button')
self.browser.page.set_content('''<label>Destination<input></label>
<button onclick="document.querySelector('output').textContent='Accepted'">Proceed</button>
<output>Pending</output><input type=password aria-label="Secret">
<div id=host></div><iframe srcdoc="<button>Inside</button>"></iframe>
<script>document.querySelector('#host').attachShadow({mode:'open'}).innerHTML='<button>Shadow</button>'</script>''')
self.browser.observe()
self.runtime = Runtime(self.browser, self.authority, lambda *_: True,
completion=lambda b: 'acknowledgment_visible' if b.page.locator('output').inner_text() == 'Accepted' else None)
def tearDown(self):
self.context.close()
def ref(self, name):
return next(e.ref for e in self.browser.state.elements if e.name == name)
def execute(self, kind, *args):
return self.runtime.execute(Decision(self.authority.ticket(self.browser.state), Action(kind, args)))
def test_multistep_execution_and_completion(self):
self.assertEqual(self.execute(Kind.FINISH).code, 'COMPLETION_NOT_PROVEN')
self.assertEqual(self.execute(Kind.TYPE, self.ref('Destination'), 'Tokyo').status, 'ok')
self.browser.observe()
self.assertEqual(self.execute(Kind.CLICK, self.ref('Proceed')).status, 'ok')
self.browser.observe()
self.assertEqual(self.execute(Kind.FINISH).evidence, 'acknowledgment_visible')
self.assertEqual(self.browser.page.locator('label input').input_value(), 'Tokyo')
def test_frame_and_shadow_nodes(self):
self.assertEqual(self.execute(Kind.CLICK, self.ref('Inside')).status, 'ok')
self.browser.observe()
self.assertEqual(self.execute(Kind.CLICK, self.ref('Shadow')).status, 'ok')
def test_path_optimization_preserves_semantics(self):
optimized = self.browser.state
self.browser.path_mode = 'naive'
naive = self.browser.observe()
self.assertEqual(optimized.elements, naive.elements)
self.assertEqual(optimized.state_hash, naive.state_hash)
def test_changed_and_detached_targets(self):
ref = self.ref('Proceed')
self.browser.page.locator('button').first.evaluate("el => el.textContent = 'Changed'")
self.assertEqual(self.execute(Kind.CLICK, ref).code, 'CHANGED_TARGET')
self.browser.page.locator('button').first.evaluate('el => el.remove()')
self.assertEqual(self.execute(Kind.CLICK, ref).code, 'DETACHED_TARGET')
def test_old_task_cannot_execute_or_finish(self):
decision = Decision(self.authority.ticket(self.browser.state), Action(Kind.CLICK, (self.ref('Proceed'),)))
self.authority.replace('New task')
self.assertEqual(self.runtime.execute(decision).code, 'STALE_AUTHORITY_OR_OBSERVATION')
self.assertEqual(self.execute(Kind.FINISH).code, 'NO_COMPLETION_VERIFIER')
self.assertEqual(self.browser.page.locator('output').inner_text(), 'Pending')
def test_loops_sensitive_targets_and_url_schemes(self):
secret = next(e.ref for e in self.browser.state.elements if e.sensitive)
self.assertEqual(self.execute(Kind.TYPE, secret, 'dont-log-me').code, 'SENSITIVE_TARGET_REQUIRES_HOST_FLOW')
self.assertEqual(self.execute(Kind.NAVIGATE, 'javascript:alert(1)').code, 'UNSAFE_URL')
for _ in range(3):
self.assertEqual(self.execute(Kind.WAIT, 0).status, 'ok')
self.assertEqual(self.execute(Kind.WAIT, 0).code, 'REPEATED_ACTION')
def test_recording_contains_no_raw_typed_values(self):
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / 'events.sqlite'
recorder = Recorder(path)
self.runtime.recorder = recorder
try:
self.assertEqual(self.execute(Kind.TYPE, self.ref('Destination'), 'sensitive-value-123').status, 'ok')
self.assertEqual(recorder.db.execute('SELECT count(*) FROM events').fetchone()[0], 1)
finally:
recorder.close()
self.assertNotIn(b'sensitive-value-123', path.read_bytes())
def test_document_navigation_invalidates_all_actions(self):
self.browser.page.goto('data:text/html,<p>New document</p>')
self.assertEqual(self.execute(Kind.WAIT, 0).code, 'CHANGED_DOCUMENT')
def test_host_policy_denies_page_instruction(self):
self.runtime.permission = lambda *_: False
self.assertEqual(self.execute(Kind.CLICK, self.ref('Proceed')).code, 'HOST_POLICY_DENIED')
self.assertEqual(self.browser.page.locator('output').inner_text(), 'Pending')
|