203 lines
6.1 KiB
Python
203 lines
6.1 KiB
Python
"""
|
|
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",
|
|
"process_id": "process",
|
|
"run_id": "process_run",
|
|
}
|
|
|
|
# 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
|