fix(nested-crud): re-inject parent FK after model serialization to prevent 1364 errors
Root cause: child model root_validators (Vision ID anti-leakage guard) strip
integer IDs before they can be serialized into the INSERT dict, causing MariaDB
to reject the INSERT with 'Field does not have a default value' (1364).
Fix: re-inject resolved_parent_id into data_to_insert after validated_obj.dict()
in post_child_obj(). This is safe — the integer was already verified against the
DB before model validation.
Affected (were all broken since ~2026-01-27):
- journal/{id}/journal_entry/
- event/{id}/event_session/
- event/{id}/event_person/
- event/{id}/event_registration/
- event/{id}/event_presenter/
- event/{id}/event_presentation/
- event/{id}/event_location/
- event/{id}/event_track/
- event/{id}/event_device/
- event/{id}/event_abstract/
- event/{id}/event_badge/ (different symptom: NULL FK)
Tests: add nested create lifecycle regression tests to test_e2e_v3_demo_parity.py
- POST + Vision check + DELETE for journal/journal_entry and event/event_session
- All 9 checks passing (7s)
Docs: update tests/README.md with accurate demo_parity description and
a 'When to Run Tests' matrix to prevent future gaps in coverage.
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
# --- Configuration ---
|
||||
BASE_URL = "https://dev-api.oneskyit.com/v3/crud"
|
||||
API_KEY = "PMM4n50teUCaOMMTN8qOJA" # Agent API Key
|
||||
# ACCOUNT_ID = "_XY7DXtc9MY"
|
||||
|
||||
# Stable parent IDs used for nested create regression tests.
|
||||
# journal account: nqOzejLCDXM | event account: GpLf_bnywCs
|
||||
JOURNAL_PARENT_ID = "OGQK-02-04-94"
|
||||
EVENT_PARENT_ID = "vfzVJF0LH1O"
|
||||
|
||||
# Test Targets: (Object Type, Valid ID Random)
|
||||
# Note: These IDs are extracted from real active records.
|
||||
@@ -30,20 +35,20 @@ def verify_demo_parity(obj_type, record_id):
|
||||
"""
|
||||
print(f"--- Testing {obj_type}: {record_id} ---")
|
||||
url = f"{BASE_URL}/{obj_type}/{record_id}"
|
||||
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=get_headers())
|
||||
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json().get('data', {})
|
||||
failures = []
|
||||
|
||||
|
||||
# 1. Check Vision Standard (All *_id fields must be strings)
|
||||
for key, val in data.items():
|
||||
if key == "id" or (key.endswith("_id") and not key.endswith("external_id")):
|
||||
if val is not None and not isinstance(val, str):
|
||||
failures.append(f"{key} is {type(val).__name__} ({val})")
|
||||
|
||||
|
||||
# 2. Specific check for account_id in tracking
|
||||
if obj_type == "event_exhibit_tracking":
|
||||
if "account_id" not in data or data["account_id"] is None:
|
||||
@@ -64,11 +69,64 @@ def verify_demo_parity(obj_type, record_id):
|
||||
else:
|
||||
print(f" ❌ [ERROR] Status {response.status_code}: {response.text[:200]}")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f" 💥 [EXCEPTION] {e}")
|
||||
return False
|
||||
|
||||
def test_nested_create_lifecycle(parent_type, parent_id, child_type, payload):
|
||||
"""
|
||||
Regression test for nested POST create (parent FK injection).
|
||||
|
||||
Bug: root_validators on child models stripped integer parent FKs before
|
||||
INSERT, causing MariaDB 1364 errors. Fixed in api_crud_v3_nested.py by
|
||||
re-injecting resolved_parent_id into data_to_insert after serialization.
|
||||
|
||||
Verifies:
|
||||
1. POST /{parent_type}/{parent_id}/{child_type}/ returns 200
|
||||
2. Response data has a string 'id' (Vision Standard)
|
||||
3. Cleanup: DELETE the created record
|
||||
"""
|
||||
label = f"Nested Create ({parent_type}/{child_type})"
|
||||
print(f"\n--- Regression: {label} ---")
|
||||
url = f"{BASE_URL}/{parent_type}/{parent_id}/{child_type}/"
|
||||
headers = get_headers()
|
||||
|
||||
# --- CREATE ---
|
||||
resp = requests.post(url, headers=headers, json=payload)
|
||||
if resp.status_code != 200:
|
||||
print(f" ❌ [FAIL] POST returned {resp.status_code}: {resp.text[:300]}")
|
||||
return False
|
||||
|
||||
data = resp.json().get('data', {})
|
||||
new_id = data.get('id') or data.get('obj_id_random')
|
||||
|
||||
if not new_id or not isinstance(new_id, str):
|
||||
print(f" ❌ [FAIL] No string 'id' in response. Got: {data}")
|
||||
return False
|
||||
|
||||
print(f" ✅ [PASS] Created {child_type} with id: {new_id}")
|
||||
|
||||
# --- VISION COMPLIANCE: parent FK must not appear as integer ---
|
||||
for key, val in data.items():
|
||||
if (key == 'id' or key.endswith('_id')) and not key.endswith('external_id'):
|
||||
if val is not None and not isinstance(val, str):
|
||||
print(f" ❌ [FAIL] Vision violation: {key} is {type(val).__name__} ({val})")
|
||||
return False
|
||||
|
||||
print(f" ✅ [PASS] Vision Standard: all ID fields are strings.")
|
||||
|
||||
# --- CLEANUP ---
|
||||
delete_url = f"{BASE_URL}/{parent_type}/{parent_id}/{child_type}/{new_id}"
|
||||
del_resp = requests.delete(delete_url, headers=headers)
|
||||
if del_resp.status_code == 200:
|
||||
print(f" ✅ [PASS] Cleanup: deleted {new_id}")
|
||||
else:
|
||||
print(f" ⚠️ [WARN] Cleanup failed ({del_resp.status_code}) — manual cleanup may be needed for {new_id}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_nested_alias_resolution():
|
||||
"""
|
||||
Verifies that the 'entry' alias and nested resolution works for journals.
|
||||
@@ -78,7 +136,7 @@ def test_nested_alias_resolution():
|
||||
parent_id = "OGQK-02-04-94"
|
||||
child_id = "xWX-NX-e6-EN"
|
||||
url = f"{BASE_URL}/journal/{parent_id}/entry/{child_id}"
|
||||
|
||||
|
||||
resp = requests.get(url, headers=get_headers())
|
||||
if resp.status_code == 200:
|
||||
print(f" ✅ [PASS] Nested alias resolution successful.")
|
||||
@@ -88,8 +146,9 @@ def test_nested_alias_resolution():
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
suite_start = time.time()
|
||||
print("🚀 Starting Aether V3 Demo Parity Suite\n")
|
||||
|
||||
|
||||
results = []
|
||||
for obj_type, record_id in TARGETS:
|
||||
results.append(verify_demo_parity(obj_type, record_id))
|
||||
@@ -97,7 +156,24 @@ if __name__ == "__main__":
|
||||
|
||||
results.append(test_nested_alias_resolution())
|
||||
|
||||
# --- Nested Create Regression Tests ---
|
||||
# These guard against the Jan 2026 bug where child model root_validators
|
||||
# stripped the parent FK integer before INSERT, causing MariaDB 1364 errors.
|
||||
results.append(test_nested_create_lifecycle(
|
||||
parent_type='journal',
|
||||
parent_id=JOURNAL_PARENT_ID,
|
||||
child_type='journal_entry',
|
||||
payload={'name': '[e2e-test] nested create regression', 'enable': False},
|
||||
))
|
||||
results.append(test_nested_create_lifecycle(
|
||||
parent_type='event',
|
||||
parent_id=EVENT_PARENT_ID,
|
||||
child_type='event_session',
|
||||
payload={'name': '[e2e-test] nested create regression', 'enable': False},
|
||||
))
|
||||
|
||||
elapsed = time.time() - suite_start
|
||||
if all(results):
|
||||
print("\n🏆 DEMO SUITE SUCCESS: All critical endpoints are verified stable.")
|
||||
print(f"\n🏆 DEMO SUITE SUCCESS: All critical endpoints are verified stable. ({elapsed:.2f}s)")
|
||||
else:
|
||||
print("\n🚨 DEMO SUITE FAILURE: Some critical checks failed.")
|
||||
print(f"\n🚨 DEMO SUITE FAILURE: Some critical checks failed. ({elapsed:.2f}s)")
|
||||
|
||||
Reference in New Issue
Block a user