88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from src.views import login
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
LOGIN_JS = ROOT / "frontend" / "login.js"
|
|
|
|
|
|
def test_expired_session_reason_explains_preserved_private_work():
|
|
harness = f"""
|
|
const createLoginController = require({json.dumps(str(LOGIN_JS))});
|
|
const status = {{ textContent: '' }};
|
|
const controller = createLoginController({{
|
|
form: {{ reset: () => {{}} }}, status, button: {{ disabled: false }},
|
|
fetchImpl: async () => new Response('{{}}', {{ status: 200 }}),
|
|
location: {{ replace: () => {{}} }},
|
|
}});
|
|
controller.showReason('session-expired');
|
|
const expired = status.textContent;
|
|
controller.showReason('https://evil.example/redirect');
|
|
process.stdout.write(JSON.stringify({{ expired, ignored: status.textContent }}));
|
|
"""
|
|
result = subprocess.run(
|
|
["node", "-e", harness], text=True, capture_output=True, check=True
|
|
)
|
|
state = json.loads(result.stdout)
|
|
|
|
assert state["expired"] == (
|
|
"Your session expired. Private drafts remain on this device. "
|
|
"Sign in to continue."
|
|
)
|
|
assert state["ignored"] == state["expired"]
|
|
|
|
|
|
def test_rate_limited_login_disables_submit_and_counts_down():
|
|
harness = f"""
|
|
const createLoginController = require({json.dumps(str(LOGIN_JS))});
|
|
const state = {{ reset: 0, interval: null }};
|
|
const status = {{ textContent: '' }};
|
|
const button = {{ disabled: false }};
|
|
const form = {{ reset: () => state.reset += 1 }};
|
|
const controller = createLoginController({{
|
|
form, status, button,
|
|
fetchImpl: async () => new Response(JSON.stringify({{ detail: 'Too many sign-in attempts' }}), {{ status: 429, headers: {{ 'Retry-After': '2' }} }}),
|
|
location: {{ replace: () => {{}} }},
|
|
setIntervalImpl: callback => {{ state.interval = callback; return 1; }},
|
|
clearIntervalImpl: () => {{}},
|
|
}});
|
|
(async () => {{
|
|
await controller.submit('never-store-this-token');
|
|
state.initial = {{ disabled: button.disabled, status: status.textContent, reset: state.reset }};
|
|
state.interval();
|
|
state.afterTick = {{ disabled: button.disabled, status: status.textContent }};
|
|
state.interval();
|
|
state.finished = {{ disabled: button.disabled, status: status.textContent }};
|
|
process.stdout.write(JSON.stringify(state));
|
|
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
|
"""
|
|
result = subprocess.run(
|
|
["node", "-e", harness], text=True, capture_output=True, check=True
|
|
)
|
|
state = json.loads(result.stdout)
|
|
|
|
assert state["initial"] == {
|
|
"disabled": True,
|
|
"status": "Too many attempts. Try again in 2 seconds.",
|
|
"reset": 1,
|
|
}
|
|
assert state["afterTick"]["disabled"] is True
|
|
assert state["finished"] == {
|
|
"disabled": False,
|
|
"status": "You can try signing in again.",
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_login_page_loads_rate_limit_controller():
|
|
html = await login()
|
|
|
|
assert '<script src="static/login.js"></script>' in html
|
|
assert "main{box-sizing:border-box" in html
|
|
assert '<p id="status" role="status" aria-live="polite"></p>' in html
|