diff --git a/tests/e2e/test_v3_model_refactor_verification.py b/tests/e2e/test_v3_model_refactor_verification.py new file mode 100644 index 0000000..c109205 --- /dev/null +++ b/tests/e2e/test_v3_model_refactor_verification.py @@ -0,0 +1,169 @@ +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)