4c71148ff9
- Add linked_upstream_id to CustomPage model with DB migration
- New POST /api/custom-pages/{pid}/refresh-auth endpoint extracts
credentials from active remote browser and updates linked upstream
- PageViewer toolbar shows key icon button when page has linked upstream
- CustomPages form adds upstream dropdown for remote_browser pages
- Auth capture extracts New-Api-User from localStorage uid/user/self API
- Upstream client sends New-Api-User header in cookie auth mode
- Fix auth capture dialog: transparent background, field persistence,
login URL defaults to base_url/login, focus on click for keyboard input
- Fix upstream test ASCII encoding with non-header characters validation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
130 lines
3.5 KiB
Python
130 lines
3.5 KiB
Python
import asyncio
|
|
|
|
from app.routers.auth_capture import _sanitize_candidate
|
|
from app.services.browser_session_service import BrowserSessionService
|
|
|
|
|
|
class FakeLocator:
|
|
def __init__(self, *, visible=True, count=1):
|
|
self._visible = list(visible) if isinstance(visible, list) else [visible]
|
|
self._count = count
|
|
self.filled = []
|
|
self.clicked = 0
|
|
self.timeouts = []
|
|
|
|
@property
|
|
def first(self):
|
|
return self
|
|
|
|
async def count(self):
|
|
return self._count
|
|
|
|
async def is_visible(self, timeout=0):
|
|
self.timeouts.append(timeout)
|
|
if not self._visible:
|
|
return False
|
|
if len(self._visible) == 1:
|
|
return self._visible[0]
|
|
return self._visible.pop(0)
|
|
|
|
async def fill(self, value, timeout=0):
|
|
self.filled.append((value, timeout))
|
|
|
|
async def click(self, timeout=0):
|
|
self.clicked += 1
|
|
|
|
|
|
class FakePage:
|
|
url = "https://example.test/login"
|
|
|
|
def __init__(self, locators):
|
|
self.locators = locators
|
|
self.queries = []
|
|
|
|
def locator(self, selector):
|
|
self.queries.append(selector)
|
|
return self.locators.get(selector, FakeLocator(visible=False, count=0))
|
|
|
|
|
|
def run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
def test_autofill_retries_until_delayed_fields_are_visible():
|
|
service = BrowserSessionService()
|
|
username = FakeLocator(visible=[False, True])
|
|
password = FakeLocator(visible=True)
|
|
submit = FakeLocator(visible=True)
|
|
page = FakePage({
|
|
"#user": username,
|
|
"#pass": password,
|
|
"#submit": submit,
|
|
})
|
|
|
|
run(service._autofill_login(
|
|
page,
|
|
{
|
|
"enabled": True,
|
|
"username": "alice",
|
|
"password": "secret",
|
|
"username_selector": "#user",
|
|
"password_selector": "#pass",
|
|
"submit_selector": "#submit",
|
|
},
|
|
max_wait_seconds=1,
|
|
poll_interval_seconds=0,
|
|
))
|
|
|
|
assert page.queries[0] == "#user"
|
|
assert "#pass" in page.queries
|
|
assert "input[type='password']" not in page.queries
|
|
assert username.filled == [("alice", 3000)]
|
|
assert password.filled == [("secret", 3000)]
|
|
assert submit.clicked == 1
|
|
|
|
|
|
def test_autofill_returns_without_selectors_when_disabled_or_missing_credentials():
|
|
service = BrowserSessionService()
|
|
|
|
disabled_page = FakePage({"#user": FakeLocator()})
|
|
run(service._autofill_login(
|
|
disabled_page,
|
|
{"enabled": False, "username": "alice", "password": "secret"},
|
|
max_wait_seconds=1,
|
|
poll_interval_seconds=0,
|
|
))
|
|
assert disabled_page.queries == []
|
|
|
|
missing_password_page = FakePage({"#user": FakeLocator()})
|
|
run(service._autofill_login(
|
|
missing_password_page,
|
|
{"enabled": True, "username": "alice", "password": ""},
|
|
max_wait_seconds=1,
|
|
poll_interval_seconds=0,
|
|
))
|
|
assert missing_password_page.queries == []
|
|
|
|
|
|
def test_sanitize_candidate_strips_secret_fields_but_keeps_metadata():
|
|
sanitized = _sanitize_candidate({
|
|
"type": "cookie",
|
|
"source": "cookie:session",
|
|
"value": "Bearer secret-token",
|
|
"preview": "Bearer s…token",
|
|
"label": "session cookie",
|
|
"confidence": 90,
|
|
"cookie_name": "session",
|
|
"cookie_value": "secret-cookie",
|
|
"domain": "example.test",
|
|
})
|
|
|
|
assert sanitized == {
|
|
"type": "cookie",
|
|
"source": "cookie:session",
|
|
"preview": "Bearer s…token",
|
|
"label": "session cookie",
|
|
"confidence": 90,
|
|
"cookie_name": "session",
|
|
"domain": "example.test",
|
|
}
|