""" Dynamic CRUD Tests ================== Auto-discovers all POST routes and generates valid form data from introspected Form() field signatures. No hardcoded form payloads. When you add a new entity router with standard CRUD, these tests automatically cover create/edit/delete on next run. Tests: - All POST /create routes accept valid form data and redirect 303 - All POST /{id}/edit routes accept valid form data and redirect 303 - All POST /{id}/delete routes redirect 303 - All POST action routes don't crash (303 or other non-500) """ from __future__ import annotations import re import uuid import pytest from httpx import AsyncClient from tests.registry import ALL_ROUTES, resolve_path from tests.introspect import RouteKind from tests.form_factory import build_form_data, build_edit_data # --------------------------------------------------------------------------- # Collect POST routes by kind # --------------------------------------------------------------------------- _CREATE_ROUTES = [r for r in ALL_ROUTES if r.kind == RouteKind.CREATE and not r.has_file_upload] _EDIT_ROUTES = [r for r in ALL_ROUTES if r.kind == RouteKind.EDIT and not r.has_file_upload] _DELETE_ROUTES = [r for r in ALL_ROUTES if r.kind == RouteKind.DELETE] _ACTION_ROUTES = [r for r in ALL_ROUTES if r.kind in (RouteKind.ACTION, RouteKind.TOGGLE)] # --------------------------------------------------------------------------- # Create: POST /entity/create with auto-generated form data -> 303 # --------------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( "route", _CREATE_ROUTES, ids=[f"CREATE {r.path}" for r in _CREATE_ROUTES], ) async def test_create_redirects(client: AsyncClient, all_seeds: dict, route): """POST to create routes with valid form data should redirect 303.""" form_data = build_form_data(route.form_fields, all_seeds) if not form_data: pytest.skip(f"No form fields discovered for {route.path}") r = await client.post(route.path, data=form_data, follow_redirects=False) assert r.status_code in (303, 302, 307), ( f"POST {route.path} returned {r.status_code} with data {form_data}" ) # --------------------------------------------------------------------------- # Edit: POST /entity/{id}/edit with auto-generated form data -> 303 # --------------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( "route", _EDIT_ROUTES, ids=[f"EDIT {r.path}" for r in _EDIT_ROUTES], ) async def test_edit_redirects(client: AsyncClient, all_seeds: dict, route): """POST to edit routes with valid form data should redirect 303.""" resolved = resolve_path(route.path, all_seeds) if resolved is None: pytest.skip(f"No seed data mapping for {route.path}") form_data = build_edit_data(route.form_fields, all_seeds) if not form_data: pytest.skip(f"No form fields discovered for {route.path}") r = await client.post(resolved, data=form_data, follow_redirects=False) assert r.status_code in (303, 302, 307), ( f"POST {resolved} returned {r.status_code} with data {form_data}" ) # --------------------------------------------------------------------------- # Delete: POST /entity/{id}/delete -> 303 # --------------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( "route", _DELETE_ROUTES, ids=[f"DELETE {r.path}" for r in _DELETE_ROUTES], ) async def test_delete_redirects(client: AsyncClient, all_seeds: dict, route): """POST to delete routes should redirect 303.""" resolved = resolve_path(route.path, all_seeds) if resolved is None: pytest.skip(f"No seed data mapping for {route.path}") r = await client.post(resolved, follow_redirects=False) assert r.status_code in (303, 302, 307, 404), ( f"POST {resolved} returned {r.status_code}" ) # --------------------------------------------------------------------------- # Action routes: POST /entity/{id}/toggle, etc. -> non-500 # --------------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( "route", _ACTION_ROUTES, ids=[f"ACTION {r.path}" for r in _ACTION_ROUTES], ) async def test_action_does_not_crash(client: AsyncClient, all_seeds: dict, route): """POST action routes should not return 500.""" resolved = resolve_path(route.path, all_seeds) if resolved is None: # Try building form data for actions that need it (e.g. /focus/add) form_data = build_form_data(route.form_fields, all_seeds) if route.form_fields else {} r = await client.post(route.path, data=form_data, follow_redirects=False) else: form_data = build_form_data(route.form_fields, all_seeds) if route.form_fields else {} r = await client.post(resolved, data=form_data, follow_redirects=False) assert r.status_code != 500, ( f"POST {resolved or route.path} returned 500 (server error)" ) # --------------------------------------------------------------------------- # Verify create actually persists: create then check list page # --------------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( "route", [r for r in _CREATE_ROUTES if r.prefix in ("/domains", "/contacts", "/meetings")], ids=[f"PERSIST {r.path}" for r in _CREATE_ROUTES if r.prefix in ("/domains", "/contacts", "/meetings")], ) async def test_create_persists_in_list(client: AsyncClient, all_seeds: dict, route): """Items created via POST should appear on the list page.""" form_data = build_form_data(route.form_fields, all_seeds) if not form_data: pytest.skip(f"No form fields for {route.path}") # Use a unique name to search for marker = f"AutoTest_{uuid.uuid4().hex[:8]}" for key in ("name", "title", "first_name"): if key in form_data: form_data[key] = marker break else: pytest.skip(f"No name/title field found for {route.path}") await client.post(route.path, data=form_data, follow_redirects=False) # Check the list page list_path = route.prefix + "/" r = await client.get(list_path) assert marker in r.text, ( f"Created item '{marker}' not found on {list_path}" )