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:
@@ -46,7 +46,7 @@ async def get_child_obj_li(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
List Child Objects (One-to-Many).
|
List Child Objects (One-to-Many).
|
||||||
|
|
||||||
Retrieves a list of child objects associated with a specific parent.
|
Retrieves a list of child objects associated with a specific parent.
|
||||||
1. Verifies parent existence and user access to the parent.
|
1. Verifies parent existence and user access to the parent.
|
||||||
2. Filters children where `{parent_obj_type}_id` matches the parent's ID.
|
2. Filters children where `{parent_obj_type}_id` matches the parent's ID.
|
||||||
@@ -62,7 +62,7 @@ async def get_child_obj_li(
|
|||||||
and_like_dict_obj = None
|
and_like_dict_obj = None
|
||||||
or_like_dict_obj = None
|
or_like_dict_obj = None
|
||||||
and_in_dict_li_obj = None
|
and_in_dict_li_obj = None
|
||||||
|
|
||||||
jp_obj = safe_json_loads(urllib.parse.unquote(jp)) if jp else None
|
jp_obj = safe_json_loads(urllib.parse.unquote(jp)) if jp else None
|
||||||
if jp_obj:
|
if jp_obj:
|
||||||
if jp_obj.get('qry'): qry_dict_li = jp_obj['qry']
|
if jp_obj.get('qry'): qry_dict_li = jp_obj['qry']
|
||||||
@@ -74,7 +74,7 @@ async def get_child_obj_li(
|
|||||||
|
|
||||||
order_by_li = safe_json_loads(order_by_li)
|
order_by_li = safe_json_loads(order_by_li)
|
||||||
obj_name = child_obj_type
|
obj_name = child_obj_type
|
||||||
|
|
||||||
if obj_name not in obj_type_kv_li or parent_obj_type not in obj_type_kv_li:
|
if obj_name not in obj_type_kv_li or parent_obj_type not in obj_type_kv_li:
|
||||||
return mk_resp(data=False, status_code=400, response=response, status_message=f"Invalid object type(s).")
|
return mk_resp(data=False, status_code=400, response=response, status_message=f"Invalid object type(s).")
|
||||||
|
|
||||||
@@ -125,10 +125,10 @@ async def get_child_obj_li(
|
|||||||
if sql_result is False:
|
if sql_result is False:
|
||||||
# Standardized rich error bubbling
|
# Standardized rich error bubbling
|
||||||
db_err = format_db_error(get_last_sql_error())
|
db_err = format_db_error(get_last_sql_error())
|
||||||
|
|
||||||
# If it's a schema error (like Unknown Column), it's a 400 Bad Request
|
# If it's a schema error (like Unknown Column), it's a 400 Bad Request
|
||||||
status_code = 400 if db_err.category == "database_schema" else 500
|
status_code = 400 if db_err.category == "database_schema" else 500
|
||||||
|
|
||||||
return mk_resp(data=False, status_code=status_code, response=response, status_message="Listing failed due to database error.", details=db_err.dict())
|
return mk_resp(data=False, status_code=status_code, response=response, status_message="Listing failed due to database error.", details=db_err.dict())
|
||||||
|
|
||||||
if sql_result:
|
if sql_result:
|
||||||
@@ -155,7 +155,7 @@ async def search_child_obj_li(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Search Child Objects (POST).
|
Search Child Objects (POST).
|
||||||
|
|
||||||
Advanced search endpoint for nested objects.
|
Advanced search endpoint for nested objects.
|
||||||
"""
|
"""
|
||||||
from app.db_sql import redis_lookup_id_random, sql_select
|
from app.db_sql import redis_lookup_id_random, sql_select
|
||||||
@@ -239,7 +239,7 @@ async def post_child_obj(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create Child Object.
|
Create Child Object.
|
||||||
|
|
||||||
1. Verifies Parent existence and access.
|
1. Verifies Parent existence and access.
|
||||||
2. Automatically links the new child to the parent (`{parent_obj_type}_id` = parent_id).
|
2. Automatically links the new child to the parent (`{parent_obj_type}_id` = parent_id).
|
||||||
3. Performs standard creation logic (validation, injection, sanitization).
|
3. Performs standard creation logic (validation, injection, sanitization).
|
||||||
@@ -295,6 +295,10 @@ async def post_child_obj(
|
|||||||
|
|
||||||
data_to_insert = validated_obj.dict(exclude_unset=True)
|
data_to_insert = validated_obj.dict(exclude_unset=True)
|
||||||
|
|
||||||
|
# Re-inject parent FK after model serialization. Some model root_validators strip
|
||||||
|
# integer IDs (a Vision ID anti-leakage guard) which would drop the FK from the dict.
|
||||||
|
data_to_insert[f'{parent_obj_type}_id'] = resolved_parent_id
|
||||||
|
|
||||||
if sql_insert_result := sql_insert(data=data_to_insert, table_name=table_name_insert):
|
if sql_insert_result := sql_insert(data=data_to_insert, table_name=table_name_insert):
|
||||||
new_obj_id = sql_insert_result
|
new_obj_id = sql_insert_result
|
||||||
new_obj_id_random = get_id_random(record_id=new_obj_id, table_name=child_obj_type)
|
new_obj_id_random = get_id_random(record_id=new_obj_id, table_name=child_obj_type)
|
||||||
@@ -324,7 +328,7 @@ async def get_child_obj(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retrieve Child Object.
|
Retrieve Child Object.
|
||||||
|
|
||||||
Verifies that the child belongs to the specified parent.
|
Verifies that the child belongs to the specified parent.
|
||||||
"""
|
"""
|
||||||
from app.db_sql import redis_lookup_id_random, sql_select
|
from app.db_sql import redis_lookup_id_random, sql_select
|
||||||
@@ -373,7 +377,7 @@ async def patch_child_obj(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update Child Object.
|
Update Child Object.
|
||||||
|
|
||||||
Verifies that the child belongs to the specified parent before updating.
|
Verifies that the child belongs to the specified parent before updating.
|
||||||
"""
|
"""
|
||||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
|
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
|
||||||
@@ -435,7 +439,7 @@ async def get_child_obj(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retrieve Child Object.
|
Retrieve Child Object.
|
||||||
|
|
||||||
Verifies that the child belongs to the specified parent.
|
Verifies that the child belongs to the specified parent.
|
||||||
"""
|
"""
|
||||||
from app.db_sql import redis_lookup_id_random, sql_select
|
from app.db_sql import redis_lookup_id_random, sql_select
|
||||||
@@ -484,7 +488,7 @@ async def patch_child_obj(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update Child Object.
|
Update Child Object.
|
||||||
|
|
||||||
Verifies that the child belongs to the specified parent before updating.
|
Verifies that the child belongs to the specified parent before updating.
|
||||||
"""
|
"""
|
||||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
|
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
|
||||||
@@ -545,7 +549,7 @@ async def delete_child_obj(
|
|||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Delete Child Object.
|
Delete Child Object.
|
||||||
|
|
||||||
Verifies that the child belongs to the specified parent before deleting.
|
Verifies that the child belongs to the specified parent before deleting.
|
||||||
"""
|
"""
|
||||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete
|
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ These consolidated scripts are the primary verification tool for the V3 API.
|
|||||||
| `test_e2e_v3_event_vision_parity.py`| **Vision ID**: Verifies string-ID enforcement across event models. |
|
| `test_e2e_v3_event_vision_parity.py`| **Vision ID**: Verifies string-ID enforcement across event models. |
|
||||||
| `test_e2e_v3_cms_vision_parity.py`| **Vision ID**: Verifies string-ID enforcement across CMS (post/comment) models. |
|
| `test_e2e_v3_cms_vision_parity.py`| **Vision ID**: Verifies string-ID enforcement across CMS (post/comment) models. |
|
||||||
| `test_e2e_v3_core_vision_parity.py`| **Vision ID**: Verifies string-ID and polymorphic resolution across core models (Account, Person, Address, Contact, DataStore). |
|
| `test_e2e_v3_core_vision_parity.py`| **Vision ID**: Verifies string-ID and polymorphic resolution across core models (Account, Person, Address, Contact, DataStore). |
|
||||||
| `test_e2e_v3_demo_parity.py` | **Demo Parity**: Comprehensive check for Badge, Exhibit, Tracking, and nested Journal Entries. |
|
| `test_e2e_v3_demo_parity.py` | **Demo Parity + Nested Create Regression**: Vision ID check for Badge, Exhibit, Tracking; nested create lifecycle (POST+DELETE) for `journal/journal_entry` and `event/event_session`; alias resolution. **Run after any model or nested-router change.** |
|
||||||
| `test_e2e_v3_action_event_file.py` | **Event Actions**: Specialized atomic upload and linking for event files. |
|
| `test_e2e_v3_action_event_file.py` | **Event Actions**: Specialized atomic upload and linking for event files. |
|
||||||
| `test_e2e_v3_action_zoom.py` | **Zoom Integration**: Verifies OAuth and ticket sync logic for Zoom Events. |
|
| `test_e2e_v3_action_zoom.py` | **Zoom Integration**: Verifies OAuth and ticket sync logic for Zoom Events. |
|
||||||
| `test_e2e_v3_accounts.py` | CRUD verification for the core Account object. |
|
| `test_e2e_v3_accounts.py` | CRUD verification for the core Account object. |
|
||||||
@@ -41,6 +41,21 @@ These consolidated scripts are the primary verification tool for the V3 API.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🚦 When to Run Tests
|
||||||
|
|
||||||
|
Tests exist to be used — run the relevant suite whenever you touch backend code, not just when something breaks.
|
||||||
|
|
||||||
|
| Change type | Required suites |
|
||||||
|
| :--- | :--- |
|
||||||
|
| Model `root_validator` / ID Vision changes | `test_e2e_v3_demo_parity.py`, `test_e2e_v3_event_vision_parity.py`, `test_e2e_v3_core_vision_parity.py` |
|
||||||
|
| Nested router (`api_crud_v3_nested.py`) changes | `test_e2e_v3_demo_parity.py` |
|
||||||
|
| Search / filter changes | `test_e2e_v3_search_engine.py` |
|
||||||
|
| Auth / account context changes | `test_e2e_v3_security_audit.py`, `test_e2e_v3_auth_security.py` |
|
||||||
|
| File upload / download changes | `test_e2e_v3_actions_file_lifecycle.py` |
|
||||||
|
| Any backend change before frontend hand-off | All of the above |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🧹 Maintenance Policy
|
## 🧹 Maintenance Policy
|
||||||
|
|
||||||
1. **Standardization**: All E2E tests should use the standard Agent API Key (`PMM4n50teUCaOMMTN8qOJA`) and provide clean `[✅ PASS]` or `[❌ FAIL]` output.
|
1. **Standardization**: All E2E tests should use the standard Agent API Key (`PMM4n50teUCaOMMTN8qOJA`) and provide clean `[✅ PASS]` or `[❌ FAIL]` output.
|
||||||
@@ -67,4 +82,56 @@ To maintain a "nice" and readable test suite, follow these patterns in all new P
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Path Requirements
|
### Path Requirements
|
||||||
Always run test scripts from the **project root** directory. Most scripts include `sys.path.append(os.getcwd())` to ensure local imports work correctly.
|
Always run test scripts from the **project root** directory. Most scripts include `sys.path.append(os.getcwd())` to ensure local imports work correctly.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development / Testing / Demo environment information
|
||||||
|
* Use snake_case (or Snake_Case or Snake_case or test_NASA_example or test_API_key)
|
||||||
|
* Aether test/demo base URL: 'http://demo.localhost:5173'
|
||||||
|
* Aether development API: 'https://dev-api.oneskyit.com'
|
||||||
|
|
||||||
|
These are IDs for records that we can use for testing. Please do not delete them. They are also used for demo purposes with clients.
|
||||||
|
|
||||||
|
### Core Modules
|
||||||
|
* Aether test/demo Account: '_XY7DXtc9MY' (1) "One Sky IT Demo"
|
||||||
|
* Aether test/demo Site: '92vkYC4fVEl' (12) "One Sky IT Demo"
|
||||||
|
* Aether test/demo Site Domain: '_6jcTbnJk-o' (12) "demo.localhost:5173"
|
||||||
|
* Aether test/demo Site Domain: 'heXRgHOs4ns' (30) "sk-demo.oneskyit.com"
|
||||||
|
* Aether test/demo Site Domain: 'DASm8fP92yw' (69) "dev-demo.oneskyit.com"
|
||||||
|
* Aether test/demo Site Domain: '2i_0Za6yRPo' (2) "demo.oneskyit.com"
|
||||||
|
* Aether test/demo Person: 'QWODAPCNLQU' (49) "Osiris Idem"
|
||||||
|
* Aether test/demo Person: 'HMQRNPIXQMK' (48) "Cleo Idem"
|
||||||
|
|
||||||
|
### Events Modules
|
||||||
|
* Aether test/demo Event: 'pjrcghqwert' (1) "Demo One Sky IT Conference"
|
||||||
|
* Aether test/demo Event Session: 'DOW3h7v6H42' (703) "How To Do Things"
|
||||||
|
* Aether test/demo Event Session (Digital Posters): "K8cxUIEWyQk" "The Beginning of Digital Posters!"
|
||||||
|
* Aether test/demo Event Session (Digital Posters): "1Un1xI1Rgk8" "Poster Session 99: All about posters!"
|
||||||
|
* Aether test/demo Event Presentation: '7U2eXSjR6H4' (1670) "Build a House"
|
||||||
|
* Aether test/demo Event Presenter: 'gT-hxnifb-0' (2202) "Bob The Builder"
|
||||||
|
* Aether test/demo Event File: 'OOsHXtng5mr' (2985) "1 Quick Test for macOS.mp4"
|
||||||
|
* Aether test/demo Event Badge: 'UIJT-73-63-61' (37163) "Scott Idem"
|
||||||
|
* Aether test/demo Event Person: 'ffkKxiHpOEC' (16603) "Scott Idem"
|
||||||
|
* Aether test/demo Event Badge Template: 'jgfixEpYp1B' (18) "Dev Demo 202x"
|
||||||
|
* Aether test/demo Event Badge Template: 'rzmUgsk7mkq' (19) "Dev Demo 202x Workshops"
|
||||||
|
* Aether test/demo Event Location: 'VXXY-98-46-14' (26) "Ballroom 1"
|
||||||
|
* Aether test/demo Event Location: 'FGRN-67-92-45' (298) "Ballroom AB"
|
||||||
|
* Aether test/demo Event Location: 'PQKB-15-39-81' (78) "Poster Display Station A"
|
||||||
|
|
||||||
|
### Journals Module
|
||||||
|
* Aether test/demo Journal: 'BVYE-94-46-29' (42) "Testing Things"
|
||||||
|
* Aether test/demo Journal Entry: 'xRx-Y4-h3-fU' (233) "Another Journal Entry in the Test Journal"
|
||||||
|
|
||||||
|
### Archives Module (IDAA Archives)
|
||||||
|
* Aether test/demo Archive: 'nAA2bHLv8RK' (1) "One Sky Test Archive"
|
||||||
|
* Aether test/demo Archive Content: 'UjKzrk-GKu5' (1) "Hosted File Test"
|
||||||
|
|
||||||
|
### Posts Module (IDAA Bulletin Board)
|
||||||
|
* Aether test/demo Post:
|
||||||
|
* Aether test/demo Post:
|
||||||
|
|
||||||
|
### Events Module (IDAA Recovery Meetings)
|
||||||
|
* Aether test/demo Event: '1Pkd025vvxU' (36) "IDAA Recovery Meeting Test"
|
||||||
|
* Aether test/demo Event: 'gIZgAjISkf8' (43) "IDAA Recovery Meeting Test"
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
BASE_URL = "https://dev-api.oneskyit.com/v3/crud"
|
BASE_URL = "https://dev-api.oneskyit.com/v3/crud"
|
||||||
API_KEY = "PMM4n50teUCaOMMTN8qOJA" # Agent API Key
|
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)
|
# Test Targets: (Object Type, Valid ID Random)
|
||||||
# Note: These IDs are extracted from real active records.
|
# 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} ---")
|
print(f"--- Testing {obj_type}: {record_id} ---")
|
||||||
url = f"{BASE_URL}/{obj_type}/{record_id}"
|
url = f"{BASE_URL}/{obj_type}/{record_id}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=get_headers())
|
response = requests.get(url, headers=get_headers())
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json().get('data', {})
|
data = response.json().get('data', {})
|
||||||
failures = []
|
failures = []
|
||||||
|
|
||||||
# 1. Check Vision Standard (All *_id fields must be strings)
|
# 1. Check Vision Standard (All *_id fields must be strings)
|
||||||
for key, val in data.items():
|
for key, val in data.items():
|
||||||
if key == "id" or (key.endswith("_id") and not key.endswith("external_id")):
|
if key == "id" or (key.endswith("_id") and not key.endswith("external_id")):
|
||||||
if val is not None and not isinstance(val, str):
|
if val is not None and not isinstance(val, str):
|
||||||
failures.append(f"{key} is {type(val).__name__} ({val})")
|
failures.append(f"{key} is {type(val).__name__} ({val})")
|
||||||
|
|
||||||
# 2. Specific check for account_id in tracking
|
# 2. Specific check for account_id in tracking
|
||||||
if obj_type == "event_exhibit_tracking":
|
if obj_type == "event_exhibit_tracking":
|
||||||
if "account_id" not in data or data["account_id"] is None:
|
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:
|
else:
|
||||||
print(f" ❌ [ERROR] Status {response.status_code}: {response.text[:200]}")
|
print(f" ❌ [ERROR] Status {response.status_code}: {response.text[:200]}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" 💥 [EXCEPTION] {e}")
|
print(f" 💥 [EXCEPTION] {e}")
|
||||||
return False
|
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():
|
def test_nested_alias_resolution():
|
||||||
"""
|
"""
|
||||||
Verifies that the 'entry' alias and nested resolution works for journals.
|
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"
|
parent_id = "OGQK-02-04-94"
|
||||||
child_id = "xWX-NX-e6-EN"
|
child_id = "xWX-NX-e6-EN"
|
||||||
url = f"{BASE_URL}/journal/{parent_id}/entry/{child_id}"
|
url = f"{BASE_URL}/journal/{parent_id}/entry/{child_id}"
|
||||||
|
|
||||||
resp = requests.get(url, headers=get_headers())
|
resp = requests.get(url, headers=get_headers())
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
print(f" ✅ [PASS] Nested alias resolution successful.")
|
print(f" ✅ [PASS] Nested alias resolution successful.")
|
||||||
@@ -88,8 +146,9 @@ def test_nested_alias_resolution():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
suite_start = time.time()
|
||||||
print("🚀 Starting Aether V3 Demo Parity Suite\n")
|
print("🚀 Starting Aether V3 Demo Parity Suite\n")
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for obj_type, record_id in TARGETS:
|
for obj_type, record_id in TARGETS:
|
||||||
results.append(verify_demo_parity(obj_type, record_id))
|
results.append(verify_demo_parity(obj_type, record_id))
|
||||||
@@ -97,7 +156,24 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
results.append(test_nested_alias_resolution())
|
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):
|
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:
|
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