Integration tests (350 passing): - test_evidence_summary.py: HTML/PDF generation, XSS safety, anchor rendering - test_tor.py: Tor module unit tests (mocked, no Tor needed) - test_c2pa_importer.py: Import result dataclass, trust evaluation, graceful degradation - test_file_attestation.py: All file types (PNG, PDF, CSV, empty, large), determinism - test_paths.py: Registry correctness, env var override, all paths under BASE_DIR - test_killswitch_coverage.py: Tor keys, trusted keys, carrier history destruction Playwright e2e infrastructure: - tests/e2e/ with conftest (live server, auth fixtures), helpers (test file generators) - test_auth.py: Setup flow, login/logout, protected routes - test_attest.py: Image/PDF/CSV attestation, verify, attestation log - test_dropbox.py: Token creation, source upload, branding check - test_keys.py: Identity display, trust store - test_fieldkit.py: Status dashboard, killswitch page - test_navigation.py: All nav links, responsive layout Run: pytest (unit/integration) or pytest -m e2e tests/e2e/ (browser) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
7.0 KiB
Python
187 lines
7.0 KiB
Python
"""
|
|
e2e tests for the source drop box feature.
|
|
|
|
The drop box is split into two distinct surfaces:
|
|
- Admin surface (/dropbox/admin) — authenticated, token management
|
|
- Source surface (/dropbox/upload/<token>) — unauthenticated, CSRF-exempt
|
|
|
|
Tests that exercise the source surface navigate in a fresh browser context
|
|
(or just navigate directly to the upload URL) to confirm there is no
|
|
session/authentication requirement on that path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import time
|
|
|
|
import pytest
|
|
from playwright.sync_api import Page, expect
|
|
|
|
from tests.e2e.helpers import create_test_image, create_test_txt
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _create_token(page: Page, live_server: str, label: str = "e2e source", hours: int = 24) -> str:
|
|
"""Create a drop box token via the admin UI and return the full upload URL."""
|
|
page.goto(f"{live_server}/dropbox/admin")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
label_input = page.locator("input[name='label']")
|
|
if label_input.count():
|
|
label_input.fill(label)
|
|
|
|
page.fill("input[name='hours']", str(hours))
|
|
page.fill("input[name='max_files']", "5")
|
|
page.click("button[type='submit']")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Flash message contains the upload URL
|
|
flash = page.locator("[class*='alert']").first.inner_text()
|
|
match = re.search(r"http://\S+", flash)
|
|
if not match:
|
|
raise RuntimeError(f"No upload URL found in flash message: {flash!r}")
|
|
|
|
return match.group(0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Admin panel tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_dropbox_admin_page(live_server: str, authenticated_page: Page) -> None:
|
|
"""The /dropbox/admin page loads and shows the token creation form."""
|
|
page = authenticated_page
|
|
page.goto(f"{live_server}/dropbox/admin")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
expect(page.locator("input[name='label']")).to_be_visible()
|
|
expect(page.locator("input[name='hours']")).to_be_visible()
|
|
expect(page.locator("input[name='max_files']")).to_be_visible()
|
|
expect(page.locator("button[type='submit']")).to_be_visible()
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_create_upload_token(live_server: str, authenticated_page: Page) -> None:
|
|
"""Creating a token shows a success flash and the token appears in the active list."""
|
|
page = authenticated_page
|
|
upload_url = _create_token(page, live_server, label="my-e2e-source")
|
|
|
|
# The upload URL must contain the expected prefix
|
|
assert "/dropbox/upload/" in upload_url, f"Unexpected upload URL: {upload_url}"
|
|
|
|
# The token should now appear in the active token table
|
|
# (token[:12] is shown in the table as per the template)
|
|
token_slug = upload_url.split("/dropbox/upload/")[1].split("?")[0]
|
|
table = page.locator("table")
|
|
if table.count() > 0:
|
|
expect(table).to_contain_text(token_slug[:12])
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_source_upload_page_accessible_without_auth(
|
|
live_server: str, authenticated_page: Page, page: Page
|
|
) -> None:
|
|
"""The source upload page is accessible without any authentication.
|
|
|
|
We get the URL via the admin (authenticated), then open it in a *separate*
|
|
fresh page that has no session cookie.
|
|
"""
|
|
upload_url = _create_token(authenticated_page, live_server, label="anon-test")
|
|
|
|
# Navigate to the upload URL in the unauthenticated page
|
|
page.goto(upload_url)
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Must not redirect to login
|
|
assert "/login" not in page.url, (
|
|
f"Source upload page redirected to login: {page.url}"
|
|
)
|
|
|
|
# Upload form must be present
|
|
expect(page.locator("input[type='file'], input[name='files']")).to_be_visible()
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_source_upload_file(live_server: str, authenticated_page: Page, page: Page) -> None:
|
|
"""A source can upload a file via the drop box and receives a receipt code."""
|
|
upload_url = _create_token(authenticated_page, live_server, label="upload-test")
|
|
|
|
# Submit a file as the anonymous source (unauthenticated page)
|
|
page.goto(upload_url)
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
txt_bytes = create_test_txt()
|
|
page.set_input_files(
|
|
"input[type='file'], input[name='files']",
|
|
files=[{"name": "tip.txt", "mimeType": "text/plain", "buffer": txt_bytes}],
|
|
)
|
|
|
|
page.click("button[type='submit']")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# The response page should show a receipt code
|
|
body_text = page.locator("body").inner_text()
|
|
assert any(word in body_text.lower() for word in ("receipt", "success", "received", "upload")), (
|
|
f"No success/receipt indication found after upload. Body: {body_text[:300]}"
|
|
)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_invalid_token_rejected(live_server: str, page: Page) -> None:
|
|
"""A request with an invalid/missing token returns 404, not a login redirect."""
|
|
page.goto(f"{live_server}/dropbox/upload/totally-invalid-token-xyz")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Should be a 404 / plain-text "expired or invalid" message, NOT a redirect to login
|
|
assert "/login" not in page.url, (
|
|
f"Invalid token redirected to login instead of showing 404: {page.url}"
|
|
)
|
|
body = page.locator("body").inner_text()
|
|
assert any(word in body.lower() for word in ("expired", "invalid", "not found")), (
|
|
f"Expected 'expired or invalid' message, got: {body[:200]}"
|
|
)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_revoke_token(live_server: str, authenticated_page: Page, page: Page) -> None:
|
|
"""An admin can revoke a token; after revocation the upload URL returns 404."""
|
|
admin_page = authenticated_page
|
|
upload_url = _create_token(admin_page, live_server, label="revoke-test")
|
|
token = upload_url.split("/dropbox/upload/")[1].split("?")[0]
|
|
|
|
# Verify the token works before revocation
|
|
page.goto(upload_url)
|
|
page.wait_for_load_state("networkidle")
|
|
assert "/login" not in page.url
|
|
|
|
# Revoke via admin UI
|
|
admin_page.goto(f"{live_server}/dropbox/admin")
|
|
admin_page.wait_for_load_state("networkidle")
|
|
|
|
# Find the revoke button for this token and click it
|
|
revoke_form = admin_page.locator(f"form input[name='token'][value='{token}']").locator("..")
|
|
if revoke_form.count() == 0:
|
|
# Try by partial token match in the table row
|
|
revoke_form = admin_page.locator("form").filter(
|
|
has=admin_page.locator(f"input[value='{token}']")
|
|
)
|
|
|
|
if revoke_form.count() > 0:
|
|
revoke_form.locator("button[type='submit']").click()
|
|
admin_page.wait_for_load_state("networkidle")
|
|
|
|
# Now the upload URL should return 404
|
|
page.goto(upload_url)
|
|
page.wait_for_load_state("networkidle")
|
|
body = page.locator("body").inner_text()
|
|
assert any(word in body.lower() for word in ("expired", "invalid", "not found")), (
|
|
f"Expected 404 after revocation, got: {body[:200]}"
|
|
)
|