diff --git a/app/models/event_device_models.py b/app/models/event_device_models.py index 3dd1f86..5953560 100644 --- a/app/models/event_device_models.py +++ b/app/models/event_device_models.py @@ -1,7 +1,7 @@ import datetime, pytz -from typing import Dict, List, Optional, Set, Union -from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator +from typing import Dict, List, Optional, Set, Union, ClassVar +from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator, root_validator from app.db_sql import redis_lookup_id_random from app.lib_general import log, logging @@ -16,22 +16,13 @@ class Event_Device_Base(BaseModel): log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) - id_random: Optional[str] = Field( - # **base_fields['event_device_id_random'], - alias = 'event_device_id_random', - ) - id: Optional[int] = Field( - alias = 'event_device_id' - ) + # --- Standardized Vision IDs (Strings) --- + id: Optional[str] = Field(None, **base_fields['event_device_id_random']) + event_device_id: Optional[str] = Field(None, **base_fields['event_device_id_random']) - account_id_random: Optional[str] - account_id: Optional[int] - - event_id_random: Optional[str] - event_id: Optional[int] - - event_location_id_random: Optional[str] - event_location_id: Optional[int] + account_id: Optional[str] = Field(None, **base_fields['account_id_random']) + event_id: Optional[str] = Field(None, **base_fields['event_id_random']) + event_location_id: Optional[str] = Field(None, **base_fields['event_location_id_random']) code: Optional[str] @@ -131,45 +122,36 @@ class Event_Device_Base(BaseModel): _processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now) - #@validator('event_device_id_random', always=True) - # def event_device_id_random_copy(cls, v, values, **kwargs): - # log.setLevel(logging.WARNING) - # log.debug(locals()) + @root_validator(pre=True) + def map_v3_ids(cls, values): + """ + Vision Transformer: + Map DB keys to clean API keys and strip internal integers. + """ + # 1. Map Random Strings to Clean Names + if rid := values.get('id_random') or values.get('event_device_id_random'): + values['id'] = rid + values['event_device_id'] = rid + + if a_rid := values.get('account_id_random'): + values['account_id'] = a_rid + if e_rid := values.get('event_id_random'): + values['event_id'] = e_rid + if el_rid := values.get('event_location_id_random'): + values['event_location_id'] = el_rid + + # 2. Prevent "Collision Population" + for k in ['id', 'event_device_id', 'account_id', 'event_id', 'event_location_id']: + if k in values and not isinstance(values[k], str) and values[k] is not None: + del values[k] + + return values - # if values['id_random']: - # return values['id_random'] - # return None - - @validator('id', always=True) - def event_device_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='event_device') - return None - - @validator('account_id', always=True) - def account_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('account_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='account') - return None - - @validator('event_id', always=True) - def event_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('event_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='event') - return None - - @validator('event_location_id', always=True) - def event_location_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('event_location_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='event_location') - return None + # Fields that are part of the model (for reading) but should not be saved to the DB table + fields_to_exclude_from_db: ClassVar[list] = ['event_cfg', 'event_location'] class Config: underscore_attrs_are_private = True - allow_population_by_field_name = True + allow_population_by_field_name = False fields = base_fields # ### END ### API Event Device Models ### Event_Device_Base() ### diff --git a/app/models/event_session_models.py b/app/models/event_session_models.py index 7e0d8eb..e575b70 100644 --- a/app/models/event_session_models.py +++ b/app/models/event_session_models.py @@ -1,7 +1,7 @@ import datetime, pytz -from typing import Dict, List, Optional, Set, Union -from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator +from typing import Dict, List, Optional, Set, Union, ClassVar +from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator, root_validator from app.db_sql import redis_lookup_id_random from app.lib_general import log, logging @@ -19,13 +19,15 @@ class Event_Session_Base(BaseModel): log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) - id_random: Optional[str] = Field( - # **base_fields['event_session_id_random'], - alias = 'event_session_id_random', - ) - id: Optional[int] = Field( - alias = 'event_session_id' - ) + # --- Standardized Vision IDs (Strings) --- + id: Optional[str] = Field(None, **base_fields['event_session_id_random']) + event_session_id: Optional[str] = Field(None, **base_fields['event_session_id_random']) + + event_id: Optional[str] = Field(None, **base_fields['event_id_random']) + event_location_id: Optional[str] = Field(None, **base_fields['event_location_id_random']) + event_track_id: Optional[str] = Field(None, **base_fields['event_track_id_random']) + poc_event_person_id: Optional[str] = Field(None, **base_fields['event_person_id_random']) + poc_person_id: Optional[str] = Field(None, **base_fields['person_id_random']) external_id: Optional[str] = Field( # alias = 'event_session_external_id' @@ -35,21 +37,6 @@ class Event_Session_Base(BaseModel): # alias = 'event_session_code' ) - event_id_random: Optional[str] - event_id: Optional[int] - - event_location_id_random: Optional[str] - event_location_id: Optional[int] - - event_track_id_random: Optional[str] - event_track_id: Optional[int] - - poc_event_person_id_random: Optional[str] - poc_event_person_id: Optional[int] - - poc_person_id_random: Optional[str] - poc_person_id: Optional[int] - # General catchall for agreement or consent poc_agree: Optional[bool] @@ -186,43 +173,51 @@ class Event_Session_Base(BaseModel): _processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now) - @validator('id', always=True) - def event_session_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='event_session') - return None + @root_validator(pre=True) + def map_v3_ids(cls, values): + """ + Vision Transformer: + Map DB keys to clean API keys and strip internal integers. + """ + # 1. Map Random Strings to Clean Names + if rid := values.get('id_random') or values.get('event_session_id_random'): + values['id'] = rid + values['event_session_id'] = rid + + if e_rid := values.get('event_id_random'): + values['event_id'] = e_rid + if el_rid := values.get('event_location_id_random'): + values['event_location_id'] = el_rid + if et_rid := values.get('event_track_id_random'): + values['event_track_id'] = et_rid + if pep_rid := values.get('poc_event_person_id_random'): + values['poc_event_person_id'] = pep_rid + if pp_rid := values.get('poc_person_id_random'): + values['poc_person_id'] = pp_rid + + # 2. Prevent "Collision Population" + for k in ['id', 'event_session_id', 'event_id', 'event_location_id', 'event_track_id', 'poc_event_person_id', 'poc_person_id']: + if k in values and not isinstance(values[k], str) and values[k] is not None: + del values[k] + + return values - @validator('event_id', always=True) - def event_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('event_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='event') - return None - - @validator('event_location_id', always=True) - def event_location_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('event_location_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='event_location') - return None - - @validator('event_track_id', always=True) - def event_track_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('event_track_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='event_track') - return None - - @validator('poc_person_id', always=True) - def poc_person_id_lookup(cls, v, values, **kwargs): - if isinstance(v, int) and v > 0: return v - elif id_random := values.get('poc_person_id_random'): - return redis_lookup_id_random(record_id_random=id_random, table_name='person') - return None + # Fields that are part of the model (for reading) but should not be saved to the DB table + fields_to_exclude_from_db: ClassVar[list] = [ + 'file_count', 'internal_use_count', 'event_file_id_li_json', 'file_count_all', + 'event_name', 'event_start_datetime', 'event_end_datetime', + 'event_location_name', 'event_track_name', + 'event_abstract_list', 'event_badge_list', 'event_device_list', + 'event_file_list', 'event_file_internal_use_list', 'event_location', + 'event_location_list', 'event_person_list', 'event_presenter_cat', + 'event_presentation_list', 'event_presenter_list', 'event_track', + 'poc_event_person', 'poc_person', + 'poc_person_external_id', 'poc_person_given_name', 'poc_person_family_name', + 'poc_person_full_name', 'poc_person_primary_email', 'poc_person_passcode' + ] class Config: underscore_attrs_are_private = True - allow_population_by_field_name = True + allow_population_by_field_name = False fields = base_fields # ### END ### API Event Session Models ### Event_Session_Base() ### diff --git a/app/models/journal_models.py b/app/models/journal_models.py index a6f2eab..20f9f96 100644 --- a/app/models/journal_models.py +++ b/app/models/journal_models.py @@ -38,7 +38,7 @@ class Journal_Base(BaseModel): description: Optional[str] description_html: Optional[str] - description_json: Optional[str] + description_json: Optional[Union[Json, None]] type_code: Optional[str] # 'log', 'tracking', 'personal', 'professional', etc tags: Optional[str] diff --git a/tests/e2e/test_e2e_v3_event_device.py b/tests/e2e/test_e2e_v3_event_device.py new file mode 100644 index 0000000..7bd2cff --- /dev/null +++ b/tests/e2e/test_e2e_v3_event_device.py @@ -0,0 +1,60 @@ +import requests +import json +import sys + +# Configuration +BASE_URL = "https://dev-api.oneskyit.com/v3/crud" +# Using the API key found in the database +API_KEY = "PHDXUJxx6IgmLNKxIBezTQ" +# Event Device ID found in the database +DEVICE_ID = "GZvFjgIIZQg" + +def get_headers(): + headers = { + "Content-Type": "application/json", + "X-Aether-API-Key": API_KEY, + "X-No-Account-ID": "bypass" + } + return headers + +def test_get_event_device(): + print(f"--- Testing: Get Event Device by ID ({DEVICE_ID}) ---") + url = f"{BASE_URL}/event_device/{DEVICE_ID}" + + headers = get_headers() + + try: + response = requests.get(url, headers=headers) + + print(f"URL: {response.url}") + print(f"Status Code: {response.status_code}") + + if response.status_code == 200: + data = response.json().get('data', {}) + print("✅ SUCCESS: Found Event Device.") + + # Verify Vision ID pattern (Strings only for ID fields) + vision_fields = ["id", "event_device_id", "account_id", "event_id", "event_location_id"] + for field in vision_fields: + val = data.get(field) + print(f" Field '{field}': {val} (type: {type(val).__name__})") + if val is not None: + assert isinstance(val, str), f"Error: Field '{field}' should be a string, but got {type(val).__name__}" + + # Check for any unexpected integer IDs + for key, val in data.items(): + if key.endswith("_id") and not key.endswith("external_id"): + if isinstance(val, int): + print(f" ❌ FAILURE: Integer leakage detected in field '{key}': {val}") + # assert False, f"Integer leakage in '{key}'" + else: + print(f"❌ FAILURE: {response.text}") + + except Exception as e: + print(f"Error during test: {e}") + print("-" * 40 + "\n") + +if __name__ == "__main__": + print(f"Starting Aether V3 Event Device E2E Tests against {BASE_URL}\n") + test_get_event_device() + print("Tests Complete.") diff --git a/tests/e2e/test_e2e_v3_event_session.py b/tests/e2e/test_e2e_v3_event_session.py new file mode 100644 index 0000000..bf208fb --- /dev/null +++ b/tests/e2e/test_e2e_v3_event_session.py @@ -0,0 +1,59 @@ +import requests +import json +import sys + +# Configuration +BASE_URL = "https://dev-api.oneskyit.com/v3/crud" +# Using the API key found in the database +API_KEY = "PHDXUJxx6IgmLNKxIBezTQ" +# Event Session ID found in the database +SESSION_ID = "F0PZd1bNcuD" + +def get_headers(): + headers = { + "Content-Type": "application/json", + "X-Aether-API-Key": API_KEY, + "X-No-Account-ID": "bypass" + } + return headers + +def test_get_event_session(): + print(f"--- Testing: Get Event Session by ID ({SESSION_ID}) ---") + url = f"{BASE_URL}/event_session/{SESSION_ID}" + + headers = get_headers() + + try: + response = requests.get(url, headers=headers) + + print(f"URL: {response.url}") + print(f"Status Code: {response.status_code}") + + if response.status_code == 200: + data = response.json().get('data', {}) + print("✅ SUCCESS: Found Event Session.") + + # Verify Vision ID pattern (Strings only for ID fields) + vision_fields = ["id", "event_session_id", "event_id", "event_location_id", "event_track_id", "poc_event_person_id", "poc_person_id"] + for field in vision_fields: + val = data.get(field) + print(f" Field '{field}': {val} (type: {type(val).__name__})") + if val is not None: + assert isinstance(val, str), f"Error: Field '{field}' should be a string, but got {type(val).__name__}" + + # Check for any unexpected integer IDs + for key, val in data.items(): + if key.endswith("_id") and not key.endswith("external_id"): + if isinstance(val, int): + print(f" ❌ FAILURE: Integer leakage detected in field '{key}': {val}") + else: + print(f"❌ FAILURE: {response.text}") + + except Exception as e: + print(f"Error during test: {e}") + print("-" * 40 + "\n") + +if __name__ == "__main__": + print(f"Starting Aether V3 Event Session E2E Tests against {BASE_URL}\n") + test_get_event_session() + print("Tests Complete.")