fix: resolve secondary FKs in nested POST (event_badge_template_id)
In the nested POST handler (api_crud_v3_nested.py), sanitize_payload was running before model instantiation. For secondary FK fields like event_badge_template_id, sanitize_payload resolved the random string → integer, then the model's root_validator stripped the integer back to None (Vision ID anti-leakage guard). Only the parent FK survived because it was explicitly re-injected after serialization. Fix: moved sanitize_payload to run on data_to_insert after serialization, matching the flat V3 POST pattern (api_crud_v3.py). Also moved account_id injection to after sanitize_payload, fixing a latent bug where account_id was silently written as NULL on non-bypass auth. Adds regression test to test_e2e_v3_demo_parity.py that creates an event_badge via nested POST with event_badge_template_id and verifies the field is non-None in the response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,10 @@ API_KEY = "PMM4n50teUCaOMMTN8qOJA" # Agent API Key
|
||||
# journal account: nqOzejLCDXM | event account: GpLf_bnywCs
|
||||
JOURNAL_PARENT_ID = "OGQK-02-04-94"
|
||||
EVENT_PARENT_ID = "vfzVJF0LH1O"
|
||||
# event_person: ffkKxiHpOEC (16603) "Scott Idem" under Demo event
|
||||
EVENT_PERSON_PARENT_ID = "ffkKxiHpOEC"
|
||||
# event_badge_template: jgfixEpYp1B (18) "Dev Demo 202x"
|
||||
EVENT_BADGE_TEMPLATE_ID = "jgfixEpYp1B"
|
||||
|
||||
# Test Targets: (Object Type, Valid ID Random)
|
||||
# Note: These IDs are extracted from real active records.
|
||||
@@ -127,6 +131,75 @@ def test_nested_create_lifecycle(parent_type, parent_id, child_type, payload):
|
||||
return True
|
||||
|
||||
|
||||
def test_nested_create_secondary_fk(parent_type, parent_id, child_type, payload, required_fk_fields):
|
||||
"""
|
||||
Regression test for secondary FK resolution in nested POST create.
|
||||
|
||||
Bug: sanitize_payload ran BEFORE model instantiation in the nested POST handler.
|
||||
For FKs other than the parent FK (e.g. event_badge_template_id on event_badge),
|
||||
sanitize_payload resolved the string → integer, then the model's root_validator
|
||||
stripped the integer back to None (Vision ID anti-leakage guard). The parent FK
|
||||
survived only because it was explicitly re-injected; secondary FKs were silently lost.
|
||||
|
||||
Fix (api_crud_v3_nested.py): moved sanitize_payload to run on data_to_insert AFTER
|
||||
model serialization, matching the flat V3 POST pattern.
|
||||
|
||||
Verifies:
|
||||
1. POST returns 200.
|
||||
2. Each field in required_fk_fields is present AND non-None in the response.
|
||||
3. All *_id fields are strings (Vision Standard).
|
||||
4. Cleanup: DELETE the created record.
|
||||
"""
|
||||
label = f"Nested Secondary FK ({parent_type}/{child_type})"
|
||||
print(f"\n--- Regression: {label} ---")
|
||||
url = f"{BASE_URL}/{parent_type}/{parent_id}/{child_type}/"
|
||||
headers = get_headers()
|
||||
|
||||
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}")
|
||||
|
||||
# Check required secondary FK fields are present and non-None
|
||||
for field in required_fk_fields:
|
||||
val = data.get(field)
|
||||
if val is None:
|
||||
print(f" ❌ [FAIL] Secondary FK '{field}' is None — was not saved to DB.")
|
||||
# Still attempt cleanup
|
||||
requests.delete(f"{BASE_URL}/{parent_type}/{parent_id}/{child_type}/{new_id}", headers=headers)
|
||||
return False
|
||||
if not isinstance(val, str):
|
||||
print(f" ❌ [FAIL] Secondary FK '{field}' is {type(val).__name__} ({val}) — must be string (Vision Standard).")
|
||||
requests.delete(f"{BASE_URL}/{parent_type}/{parent_id}/{child_type}/{new_id}", headers=headers)
|
||||
return False
|
||||
print(f" ✅ [PASS] Secondary FK '{field}' = {val}")
|
||||
|
||||
# Vision compliance: 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):
|
||||
print(f" ❌ [FAIL] Vision violation: {key} is {type(val).__name__} ({val})")
|
||||
requests.delete(f"{BASE_URL}/{parent_type}/{parent_id}/{child_type}/{new_id}", headers=headers)
|
||||
return False
|
||||
print(f" ✅ [PASS] Vision Standard: all ID fields are strings.")
|
||||
|
||||
# Cleanup
|
||||
del_resp = requests.delete(f"{BASE_URL}/{parent_type}/{parent_id}/{child_type}/{new_id}", 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.
|
||||
@@ -171,6 +244,21 @@ if __name__ == "__main__":
|
||||
child_type='event_session',
|
||||
payload={'name': '[e2e-test] nested create regression', 'enable': False},
|
||||
))
|
||||
# Secondary FK regression: event_badge_template_id must survive nested POST
|
||||
# (was silently dropped as NULL before the sanitize_payload order fix)
|
||||
results.append(test_nested_create_secondary_fk(
|
||||
parent_type='event_person',
|
||||
parent_id=EVENT_PERSON_PARENT_ID,
|
||||
child_type='event_badge',
|
||||
payload={
|
||||
'event_badge_template_id': EVENT_BADGE_TEMPLATE_ID,
|
||||
'given_name': '[e2e-test]',
|
||||
'family_name': 'secondary-fk-regression',
|
||||
'enable': False,
|
||||
'hide': True,
|
||||
},
|
||||
required_fk_fields=['event_badge_template_id'],
|
||||
))
|
||||
|
||||
elapsed = time.time() - suite_start
|
||||
if all(results):
|
||||
|
||||
Reference in New Issue
Block a user