This commit introduces a new end-to-end test script to validate the recent model refactoring changes. The test suite performs two primary checks for a list of target objects: - **ID Vision Compliance:** Verifies that all primary and foreign key fields are returned as string IDs, ensuring adherence to the V3 ID Vision standard. - **Excluded Fields Stripping:** Attempts a PATCH operation with fields explicitly listed in and verifies that these fields are not updated in the database, confirming the mechanism functions as intended. This test is essential for ensuring the stability and correctness of the API's interaction with the refactored models.
170 lines
9.0 KiB
Python
170 lines
9.0 KiB
Python
import requests
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
# Add project root to sys.path for local imports
|
|
sys.path.append(os.getcwd())
|
|
|
|
# --- Configuration ---
|
|
BASE_URL = "https://dev-api.oneskyit.com/v3/crud"
|
|
API_KEY = "PMM4n50teUCaOMMTN8qOJA"
|
|
|
|
# Test Targets: (Object Type, Valid ID Random for an existing record, a field expected to be excluded from DB writes)
|
|
# These IDs are placeholders or from existing test data.
|
|
# Replace "temp_id_..." with actual valid IDs from your dev environment or dynamically create them.
|
|
# The excluded_field_to_test should be a field that exists in the model but NOT in the base database table for that object,
|
|
# or a nested object that should be ignored during PUT/PATCH.
|
|
TARGETS = [
|
|
("event", "NQ6v6X1u2c5", "event_cfg"), # event_cfg is a nested object
|
|
("event_session", "F0PZd1bNcuD", "account_id"), # account_id is excluded from event_session table writes
|
|
("event_location", "GZvFjgIIZQg", "account_id"), # account_id is excluded from event_location table writes
|
|
("event_track", "some_valid_track_id", "account_id"), # account_id is excluded from event_track table writes
|
|
("event_presentation", "some_valid_presentation_id", "account_id"), # account_id is excluded from event_presentation table writes
|
|
("event_file", "a2pPIT_W28o", "account_id"), # account_id is excluded from event_file table writes
|
|
("event_presenter", "some_valid_presenter_id", "account_id"), # account_id is excluded from event_presenter table writes
|
|
("event_abstract", "some_valid_abstract_id", "account_id"), # account_id is excluded from event_abstract table writes
|
|
("event_badge", "JPUG-87-80-88", "account_id"), # account_id is excluded from event_badge table writes
|
|
("event_person", "STI-PyWO6ODzLV8", "extended_json"), # Use a non-FK excluded field for person for better test coverage
|
|
("event_exhibit_tracking", "KVypw_xntSY", "account_id"), # account_id is excluded from event_exhibit_tracking table writes
|
|
("event_exhibit", "xK_9yEj1bQY", "event_exhibit_tracking_list"), # event_exhibit_tracking_list is a nested object
|
|
("event_device", "GZvFjgIIZQg", "account_id"), # account_id is excluded from event_device table writes
|
|
("event_cfg", "some_valid_event_cfg_id", "account_id"), # account_id is excluded from event_cfg table writes
|
|
("event_registration", "some_valid_registration_id", "cfg"), # cfg is a nested object
|
|
("event_registration_cfg", "some_valid_registration_cfg_id", "account_id"), # account_id is in table, not excluded (need to pick another field)
|
|
("event_person_profile", "some_valid_person_profile_id", "account_id"), # account_id is excluded from event_person_profile table writes
|
|
("person", "STI-PyWO6ODzLV8", "membership_person_id"), # membership_person_id is excluded from person table writes
|
|
("address", "gUpFV3CX5UI", "country_name"), # country_name is a convenience field
|
|
("contact", "dzGCDpaoYJA", "address"), # address is a nested object
|
|
("organization", "some_valid_organization_id", "contact"), # contact is a nested object
|
|
]
|
|
|
|
def get_headers(ignore_extra_fields=False):
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"X-Aether-API-Key": API_KEY,
|
|
"x-no-account-id": "bypass"
|
|
}
|
|
if ignore_extra_fields:
|
|
headers["x-ae-ignore-extra-fields"] = "true"
|
|
return headers
|
|
|
|
def print_result(label, success, message=""):
|
|
status = "✅ PASS" if success else "❌ FAIL"
|
|
print(f"{status} | {label} {': ' + message if message else ''}")
|
|
return success
|
|
|
|
def verify_id_vision_compliance(obj_type, record_id):
|
|
"""
|
|
Verifies that the object returns ONLY string IDs for all ID fields (Vision Standard).
|
|
"""
|
|
label = f"ID Vision Compliance for {obj_type} (ID: {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 = []
|
|
|
|
# Check all fields ending in _id (except external_id)
|
|
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"Field '{key}' is {type(val).__name__} ({val})")
|
|
elif isinstance(val, str) and len(val) < 11 and val.isdigit():
|
|
failures.append(f"Field '{key}' looks like a stringified integer: '{val}'") # Possible integer leakage
|
|
|
|
if not failures:
|
|
return print_result(label, True, "All ID fields are strings.")
|
|
else:
|
|
return print_result(label, False, f"Integer leakage detected: {', '.join(failures)}")
|
|
else:
|
|
return print_result(label, False, f"GET failed with status {response.status_code}: {response.text[:100]}")
|
|
|
|
except Exception as e:
|
|
return print_result(label, False, f"Exception during GET: {e}")
|
|
|
|
def verify_excluded_fields_stripped(obj_type, record_id, excluded_field_name, excluded_field_value):
|
|
"""
|
|
Verifies that a PATCH operation with an excluded field (from fields_to_exclude_from_db)
|
|
does not cause an error, implying the backend successfully stripped it.
|
|
It also verifies that the field's value is not updated in the database.
|
|
"""
|
|
label = f"Excluded field '{excluded_field_name}' stripping on PATCH for {obj_type} (ID: {record_id})"
|
|
url = f"{BASE_URL}/{obj_type}/{record_id}"
|
|
|
|
# 1. Get current state to verify no change
|
|
try:
|
|
initial_response = requests.get(url, headers=get_headers())
|
|
if initial_response.status_code != 200:
|
|
return print_result(label, False, f"Initial GET failed with status {initial_response.status_code} for {obj_type}")
|
|
initial_data = initial_response.json().get('data', {})
|
|
initial_field_value = initial_data.get(excluded_field_name)
|
|
except Exception as e:
|
|
return print_result(label, False, f"Exception during initial GET for {obj_type}: {e}")
|
|
|
|
# 2. Attempt PATCH with excluded field
|
|
# Use 'description' as a generic update field, as most tables should have it.
|
|
# If a table doesn't have 'description', this test will fail, and we'll need a more specific valid field.
|
|
payload = {
|
|
"description": f"Test description updated by {int(time.time())}",
|
|
excluded_field_name: excluded_field_value # The field that should be excluded
|
|
}
|
|
|
|
try:
|
|
patch_response = requests.patch(url, headers=get_headers(ignore_extra_fields=True), data=json.dumps(payload))
|
|
|
|
if patch_response.status_code == 200:
|
|
# 3. Verify the field was not actually updated in the DB
|
|
final_response = requests.get(url, headers=get_headers())
|
|
if final_response.status_code != 200:
|
|
return print_result(label, False, f"Final GET after PATCH failed with status {final_response.status_code}")
|
|
final_data = final_response.json().get('data', {})
|
|
final_field_value = final_data.get(excluded_field_name)
|
|
|
|
if initial_field_value == final_field_value:
|
|
return print_result(label, True, f"PATCH succeeded, and excluded field '{excluded_field_name}' was not updated.")
|
|
else:
|
|
return print_result(label, False, f"PATCH updated excluded field '{excluded_field_name}'. Initial: '{initial_field_value}', Final: '{final_field_value}'")
|
|
else:
|
|
return print_result(label, False, f"PATCH failed with status {patch_response.status_code}: {patch_response.text[:200]}")
|
|
|
|
except Exception as e:
|
|
return print_result(label, False, f"Exception during PATCH: {e}")
|
|
|
|
# --- Main Execution ---
|
|
if __name__ == "__main__":
|
|
print("🚀 Starting Aether V3 Model Refactor Verification Suite")
|
|
|
|
overall_results = []
|
|
|
|
# NOTE: Fill in valid existing IDs for the TARGETS list before running.
|
|
# Otherwise, tests expecting valid IDs will fail.
|
|
|
|
for obj_type, record_id, excluded_field in TARGETS:
|
|
print(f"\n--- Testing Object Type: {obj_type} (ID: {record_id}) ---")
|
|
|
|
# Basic check to ensure a valid record_id is being used (not a placeholder)
|
|
if "some_valid_" in record_id:
|
|
print_result(f"Setup for {obj_type}", False, f"Skipping tests for {obj_type} due to placeholder ID '{record_id}'. Please provide a valid existing ID.")
|
|
overall_results.append(False) # Mark as failed because test wasn't run meaningfully
|
|
continue
|
|
|
|
# 1. Verify ID Vision Compliance
|
|
overall_results.append(verify_id_vision_compliance(obj_type, record_id))
|
|
|
|
# 2. Verify excluded fields on PATCH
|
|
if excluded_field:
|
|
overall_results.append(verify_excluded_fields_stripped(obj_type, record_id, excluded_field, "THIS_VALUE_SHOULD_BE_IGNORED"))
|
|
|
|
print("-" * 40)
|
|
|
|
if all(overall_results):
|
|
print("\n🏆 ALL MODEL REFACTOR VERIFICATION TESTS PASSED!")
|
|
sys.exit(0)
|
|
else:
|
|
print("\n🚨 SOME MODEL REFACTOR VERIFICATION TESTS FAILED!")
|
|
sys.exit(1)
|