""" e2e tests for the attestation and verification pages. Each test that needs to attest a file first ensures an identity key exists by navigating to /keys and generating one. That step is idempotent — the server silently ignores a second generate request if a key already exists. Terminology used in comments ----------------------------- - attest form:
at /attest - file input name: "image" (the field name in the HTML, even for non-images) - optional text inputs: "caption", "location_name" - verify form: same structure at /verify - file input name: "image" """ from __future__ import annotations import io from pathlib import Path import pytest from playwright.sync_api import Page, expect from tests.e2e.helpers import create_test_csv, create_test_image, create_test_pdf, create_test_txt # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _ensure_identity(page: Page, live_server: str) -> None: """Generate an Ed25519 identity if one does not already exist.""" page.goto(f"{live_server}/keys/") page.wait_for_load_state("networkidle") # If the "Generate Identity" button is visible the key is missing; click it. gen_button = page.locator("form[action*='generate_identity'] button") if gen_button.count() > 0: gen_button.click() page.wait_for_load_state("networkidle") def _attest_bytes( page: Page, live_server: str, file_bytes: bytes, filename: str, caption: str = "", ) -> None: """Upload *file_bytes* as *filename* via the /attest form.""" page.goto(f"{live_server}/attest") page.wait_for_load_state("networkidle") page.set_input_files( "input[name='image']", files=[{"name": filename, "mimeType": _mime(filename), "buffer": file_bytes}], ) if caption: page.fill("input[name='caption']", caption) page.click("button[type='submit']") page.wait_for_load_state("networkidle") def _mime(filename: str) -> str: ext = filename.rsplit(".", 1)[-1].lower() return { "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "pdf": "application/pdf", "csv": "text/csv", "txt": "text/plain", }.get(ext, "application/octet-stream") # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- @pytest.mark.e2e def test_attest_page_loads(live_server: str, authenticated_page: Page) -> None: """The /attest page renders the file upload form.""" page = authenticated_page page.goto(f"{live_server}/attest") page.wait_for_load_state("networkidle") expect(page.locator("input[name='image']")).to_be_visible() expect(page.locator("button[type='submit']")).to_be_visible() @pytest.mark.e2e def test_attest_image_file(live_server: str, authenticated_page: Page) -> None: """Attesting a PNG image shows the success result page with all hash fields.""" page = authenticated_page _ensure_identity(page, live_server) img_bytes = create_test_image(64, 64) _attest_bytes(page, live_server, img_bytes, "test_capture.png", caption="e2e test image") # Result page has the success alert expect(page.locator(".alert-success")).to_contain_text("Attestation created successfully") # Record ID and SHA-256 must be present expect(page.locator("body")).to_contain_text("Record ID") expect(page.locator("body")).to_contain_text("SHA-256") # Caption was saved expect(page.locator("body")).to_contain_text("e2e test image") @pytest.mark.e2e def test_attest_pdf_file(live_server: str, authenticated_page: Page) -> None: """Attesting a PDF succeeds and the result page notes SHA-256-only (no perceptual hashes).""" page = authenticated_page _ensure_identity(page, live_server) pdf_bytes = create_test_pdf() _attest_bytes(page, live_server, pdf_bytes, "evidence.pdf") expect(page.locator(".alert-success")).to_contain_text("Attestation created successfully") # Non-image attestation note must appear expect(page.locator(".alert-info")).to_contain_text("cryptographic hash") # Perceptual hash fields must NOT appear for PDFs expect(page.locator("body")).not_to_contain_text("pHash") @pytest.mark.e2e def test_attest_csv_file(live_server: str, authenticated_page: Page) -> None: """Attesting a CSV file succeeds.""" page = authenticated_page _ensure_identity(page, live_server) csv_bytes = create_test_csv() _attest_bytes(page, live_server, csv_bytes, "sensor_data.csv") expect(page.locator(".alert-success")).to_contain_text("Attestation created successfully") @pytest.mark.e2e def test_attest_requires_identity(live_server: str, authenticated_page: Page) -> None: """The submit button is disabled when no identity key is configured. NOTE: This test only checks the rendered HTML state. We do not actually delete the identity key — that would break subsequent tests in the session. Instead we verify the template logic: the template disables the button and shows a warning when has_identity is False. We observe the button state based on whether an identity was just generated. """ page = authenticated_page page.goto(f"{live_server}/attest") page.wait_for_load_state("networkidle") # If identity is absent, a warning alert should be visible and the button disabled. # If identity is present, the button is enabled. submit = page.locator("button[type='submit']") warning = page.locator(".alert-warning") if warning.count() > 0: # No identity — button must be disabled expect(submit).to_be_disabled() else: # Identity present — button must be enabled expect(submit).to_be_enabled() @pytest.mark.e2e def test_verify_attested_file(live_server: str, authenticated_page: Page) -> None: """Attest a file then immediately verify it — verification must succeed.""" page = authenticated_page _ensure_identity(page, live_server) img_bytes = create_test_image(80, 80) filename = "verify_me.png" # Attest _attest_bytes(page, live_server, img_bytes, filename) expect(page.locator(".alert-success")).to_contain_text("Attestation created successfully") # Verify the same bytes page.goto(f"{live_server}/verify") page.wait_for_load_state("networkidle") page.set_input_files( "input[name='image']", files=[{"name": filename, "mimeType": "image/png", "buffer": img_bytes}], ) page.click("button[type='submit']") page.wait_for_load_state("networkidle") # Verification result must show a match expect(page.locator(".alert-success")).to_contain_text("matching attestation") @pytest.mark.e2e def test_verify_tampered_file(live_server: str, authenticated_page: Page) -> None: """A file modified after attestation must not verify (no matching attestation).""" page = authenticated_page _ensure_identity(page, live_server) original_bytes = create_test_image(90, 90) # Attest the original _attest_bytes(page, live_server, original_bytes, "tampered.png") expect(page.locator(".alert-success")).to_contain_text("Attestation created successfully") # Tamper: flip a single byte near the end of the image data tampered = bytearray(original_bytes) tampered[-50] ^= 0xFF tampered_bytes = bytes(tampered) # Verify the tampered version — must not match page.goto(f"{live_server}/verify") page.wait_for_load_state("networkidle") page.set_input_files( "input[name='image']", files=[{"name": "tampered.png", "mimeType": "image/png", "buffer": tampered_bytes}], ) page.click("button[type='submit']") page.wait_for_load_state("networkidle") # Warning = no match found (the alert-warning class is used for "not found") # OR the alert-success is absent success_alert = page.locator(".alert-success") warning_alert = page.locator(".alert-warning") assert warning_alert.count() > 0 or success_alert.count() == 0, ( "Tampered file incorrectly verified as matching" ) @pytest.mark.e2e def test_attestation_log(live_server: str, authenticated_page: Page) -> None: """After attesting multiple files the /attest/log page lists them all.""" page = authenticated_page _ensure_identity(page, live_server) # Attest three distinct files for i in range(3): img = create_test_image(32 + i * 8, 32 + i * 8) _attest_bytes(page, live_server, img, f"log_test_{i}.png", caption=f"log entry {i}") expect(page.locator(".alert-success")).to_contain_text("Attestation created successfully") # Check the log page page.goto(f"{live_server}/attest/log") page.wait_for_load_state("networkidle") # The log should contain at least 3 rows (may have more from other tests) rows = page.locator("table tbody tr") assert rows.count() >= 3, ( f"Expected at least 3 rows in attestation log, got {rows.count()}" ) @pytest.mark.e2e def test_batch_attest(live_server: str, authenticated_page: Page) -> None: """The /attest/batch JSON endpoint accepts multiple files and returns results.""" import json page = authenticated_page _ensure_identity(page, live_server) # Use the fetch API to POST two files to the batch endpoint img1 = create_test_image(48, 48) img2 = create_test_image(56, 56) # Encode images as base64 for transfer to the browser context import base64 img1_b64 = base64.b64encode(img1).decode() img2_b64 = base64.b64encode(img2).decode() result = page.evaluate( """async ([img1_b64, img2_b64]) => { const b64 = (s) => Uint8Array.from(atob(s), c => c.charCodeAt(0)); const form = new FormData(); form.append('images', new File([b64(img1_b64)], 'batch1.png', {type: 'image/png'})); form.append('images', new File([b64(img2_b64)], 'batch2.png', {type: 'image/png'})); const resp = await fetch('/attest/batch', {method: 'POST', body: form}); return await resp.json(); }""", [img1_b64, img2_b64], ) assert result.get("total") == 2, f"Unexpected batch result: {result}" assert result.get("errors") == 0, f"Batch had errors: {result}" assert result.get("attested", 0) + result.get("skipped", 0) == 2