""" Form Data Factory ================= Generates valid POST form data for any route, using: 1. Introspected Form field names/types from the route 2. Seed data UUIDs for FK references (domain_id, project_id, etc.) 3. Heuristic value generation based on field name patterns This eliminates hardcoded form data in tests. When a route's form fields change, tests automatically adapt. """ from __future__ import annotations from datetime import date, datetime, timezone from typing import Any from tests.introspect import FormField # --------------------------------------------------------------------------- # Field name -> value resolution rules # --------------------------------------------------------------------------- # FK fields map to seed fixture keys FK_FIELD_MAP = { "domain_id": "domain", "area_id": "area", "project_id": "project", "task_id": "task", "folder_id": "weblink_folder", "parent_id": None, # Usually optional, skip "meeting_id": None, "contact_id": "contact", "release_id": None, "note_id": "note", "list_id": "list", } # Field name pattern -> static test value NAME_PATTERNS: list[tuple[str, Any]] = [ # Exact matches first ("title", "Test Title Auto"), ("name", "Test Name Auto"), ("first_name", "TestFirst"), ("last_name", "TestLast"), ("description", "Auto-generated test description"), ("body", "Auto-generated test body content"), ("raw_text", "Auto capture line 1\nAuto capture line 2"), ("url", "https://test.example.com"), ("email", "autotest@example.com"), ("phone", "555-0199"), ("company", "Test Corp"), ("role", "Tester"), ("color", "#AA55CC"), ("icon", "star"), ("status", "active"), ("priority", "3"), ("content_format", "rich"), ("meeting_date", None), # Resolved dynamically ("start_date", None), ("end_date", None), ("start_time", None), ("end_time", None), ("due_date", None), ("start_at", None), ("end_at", None), ("focus_date", None), ("sort_order", "0"), ("estimated_minutes", "30"), ("energy_required", "medium"), ("context", ""), ("recurrence", ""), ("version_label", "v1.0"), ("agenda", "Test agenda"), ("notes_body", "Test notes"), ("location", "Test Location"), ("tags", ""), ("notes", "Test notes field"), ] def _resolve_date_field(field_name: str) -> str: """Generate appropriate date/time string based on field name.""" now = datetime.now(timezone.utc) if "time" in field_name and "date" not in field_name: return now.strftime("%H:%M") if "date" in field_name: return date.today().isoformat() if field_name in ("start_at", "end_at"): return now.isoformat() return date.today().isoformat() def build_form_data( form_fields: list[FormField], seed_data: dict[str, dict] | None = None, ) -> dict[str, str]: """ Build a valid form data dict for a POST request. Args: form_fields: List of FormField from route introspection. seed_data: Dict mapping entity type to seed fixture dict. e.g. {"domain": {"id": "abc-123", ...}, "project": {"id": "def-456", ...}} Returns: Dict of field_name -> string value, ready for httpx POST data. """ seed_data = seed_data or {} data: dict[str, str] = {} for field in form_fields: if field.is_file: continue # Skip file uploads in form data value = _resolve_field_value(field, seed_data) if value is not None: data[field.name] = str(value) return data def _resolve_field_value( field: FormField, seed_data: dict[str, dict], ) -> Any | None: """Resolve a single field's test value.""" name = field.name # 1. FK fields -> look up seed data UUID if name in FK_FIELD_MAP: entity_type = FK_FIELD_MAP[name] if entity_type is None: # Optional FK with no mapping, return None (skip) return "" if not field.required else None if entity_type in seed_data: val = seed_data[entity_type] # Support both flat UUID strings and dict with "id" key if isinstance(val, dict) and "id" in val: return val["id"] elif isinstance(val, str): return val # Required FK but no seed data available return None if not field.required else "" # 2. Date/time fields if any(kw in name for kw in ("date", "time", "_at")): return _resolve_date_field(name) # 3. Pattern matching on field name for pattern_name, pattern_value in NAME_PATTERNS: if name == pattern_name: if pattern_value is None: return _resolve_date_field(name) return pattern_value # 4. Partial name matching for common patterns if "title" in name: return "Test Title Auto" if "name" in name: return "Test Name Auto" if "description" in name or "desc" in name: return "Auto test description" if "url" in name: return "https://test.example.com" if "email" in name: return "auto@test.com" # 5. Type-based fallback if "int" in field.annotation.lower(): return "0" if "bool" in field.annotation.lower(): return "false" # 6. If field has a default, use it if field.default is not None: return str(field.default) # 7. Last resort for required string fields if field.required: return f"test_{name}" return "" def build_edit_data( form_fields: list[FormField], seed_data: dict[str, dict] | None = None, ) -> dict[str, str]: """ Build form data for an edit/update request. Same as build_form_data but prefixes string values with "Updated " so tests can verify the edit took effect. """ data = build_form_data(form_fields, seed_data) for key, value in data.items(): # Only modify display-name fields, not IDs/dates/status if key in ("title", "name", "first_name", "last_name", "description", "body"): data[key] = f"Updated {value}" return data