From 9817b988fb6c7ad96a134e0cd5c7a043d4c6699d Mon Sep 17 00:00:00 2001 From: Christopher Grote Date: Wed, 14 Oct 2026 16:42:34 +0100 Subject: [PATCH 1/2] FND-3858: add pyatlan_v9 creators for the 5 Salesforce asset types Adds hand-written creator overlays for SalesforceOrganization, SalesforceObject, SalesforceField, SalesforceReport and SalesforceDashboard, and regenerates the 5 classes from atlanhq/models master with them. Each creator emits the exact qualifiedName atlan-salesforce-app emits today (FND-3838 census): Organization {conn}/{source_id} Object {org}/{api_name} Field {object}/{api_name} Report {org}/{source_id} Dashboard {org}/{source_id} Object, Field, Report and Dashboard take the parent qualifiedName, set the denormalised parent qns and the requiredForCreation parent relationship, and derive connection_qualified_name from the parent when it is not given. Blank inputs and too-short parent qns raise. No models change: the containment-derived _QUALIFIED_NAME_PATTERN already matches these shapes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019fLee6iVYNyoTkguJombqF --- .../assets/_overlays/salesforce_dashboard.py | 40 ++++ .../assets/_overlays/salesforce_field.py | 39 ++++ .../assets/_overlays/salesforce_object.py | 40 ++++ .../_overlays/salesforce_organization.py | 32 +++ .../assets/_overlays/salesforce_report.py | 40 ++++ .../model/assets/salesforce_dashboard.py | 40 ++++ pyatlan_v9/model/assets/salesforce_field.py | 39 ++++ pyatlan_v9/model/assets/salesforce_object.py | 40 ++++ .../model/assets/salesforce_organization.py | 32 +++ pyatlan_v9/model/assets/salesforce_report.py | 40 ++++ tests_v9/integration/salesforce_asset_test.py | 198 +++++++++++++++++ tests_v9/unit/model/salesforce_test.py | 204 ++++++++++++++++++ 12 files changed, 784 insertions(+) create mode 100644 pyatlan_v9/model/assets/_overlays/salesforce_dashboard.py create mode 100644 pyatlan_v9/model/assets/_overlays/salesforce_field.py create mode 100644 pyatlan_v9/model/assets/_overlays/salesforce_object.py create mode 100644 pyatlan_v9/model/assets/_overlays/salesforce_organization.py create mode 100644 pyatlan_v9/model/assets/_overlays/salesforce_report.py create mode 100644 tests_v9/integration/salesforce_asset_test.py create mode 100644 tests_v9/unit/model/salesforce_test.py diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_dashboard.py b/pyatlan_v9/model/assets/_overlays/salesforce_dashboard.py new file mode 100644 index 000000000..94c664a2a --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_dashboard.py @@ -0,0 +1,40 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceDashboard": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceDashboard": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceDashboard": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_field.py b/pyatlan_v9/model/assets/_overlays/salesforce_field.py new file mode 100644 index 000000000..c164110b0 --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_field.py @@ -0,0 +1,39 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + object_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceField": + validate_required_fields( + ["name", "api_name", "object_qualified_name"], + [name, api_name, object_qualified_name], + ) + fields = object_qualified_name.split("/") + if len(fields) < 5: + raise ValueError("object_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{object_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + object_qualified_name=object_qualified_name, + organization_qualified_name="/".join(fields[:-1]), + object=RelatedSalesforceObject(qualified_name=object_qualified_name), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceField": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceField": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_object.py b/pyatlan_v9/model/assets/_overlays/salesforce_object.py new file mode 100644 index 000000000..be267da4b --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_object.py @@ -0,0 +1,40 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceObject": + validate_required_fields( + ["name", "api_name", "organization_qualified_name"], + [name, api_name, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceObject": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceObject": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_organization.py b/pyatlan_v9/model/assets/_overlays/salesforce_organization.py new file mode 100644 index 000000000..95b7ec4dd --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_organization.py @@ -0,0 +1,32 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + connection_qualified_name: str, + source_id: str, + ) -> "SalesforceOrganization": + validate_required_fields( + ["name", "connection_qualified_name", "source_id"], + [name, connection_qualified_name, source_id], + ) + fields = connection_qualified_name.split("/") + connector_name = fields[1] if len(fields) > 1 else None + return cls( + name=name, + qualified_name=f"{connection_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=connector_name, + source_id=source_id, + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceOrganization": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceOrganization": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_report.py b/pyatlan_v9/model/assets/_overlays/salesforce_report.py new file mode 100644 index 000000000..b446f6593 --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_report.py @@ -0,0 +1,40 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceReport": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceReport": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceReport": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/salesforce_dashboard.py b/pyatlan_v9/model/assets/salesforce_dashboard.py index ee50223a5..37cfab108 100644 --- a/pyatlan_v9/model/assets/salesforce_dashboard.py +++ b/pyatlan_v9/model/assets/salesforce_dashboard.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -323,6 +324,45 @@ def relate(self) -> "RelatedSalesforceDashboard": return RelatedSalesforceDashboard(guid=self.guid) return RelatedSalesforceDashboard(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceDashboard": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceDashboard": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceDashboard": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_field.py b/pyatlan_v9/model/assets/salesforce_field.py index a9fe84899..40214752f 100644 --- a/pyatlan_v9/model/assets/salesforce_field.py +++ b/pyatlan_v9/model/assets/salesforce_field.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -375,6 +376,44 @@ def relate(self) -> "RelatedSalesforceField": return RelatedSalesforceField(guid=self.guid) return RelatedSalesforceField(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + object_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceField": + validate_required_fields( + ["name", "api_name", "object_qualified_name"], + [name, api_name, object_qualified_name], + ) + fields = object_qualified_name.split("/") + if len(fields) < 5: + raise ValueError("object_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{object_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + object_qualified_name=object_qualified_name, + organization_qualified_name="/".join(fields[:-1]), + object=RelatedSalesforceObject(qualified_name=object_qualified_name), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceField": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceField": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_object.py b/pyatlan_v9/model/assets/salesforce_object.py index 279e88a11..826fe1386 100644 --- a/pyatlan_v9/model/assets/salesforce_object.py +++ b/pyatlan_v9/model/assets/salesforce_object.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -331,6 +332,45 @@ def relate(self) -> "RelatedSalesforceObject": return RelatedSalesforceObject(guid=self.guid) return RelatedSalesforceObject(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceObject": + validate_required_fields( + ["name", "api_name", "organization_qualified_name"], + [name, api_name, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceObject": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceObject": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_organization.py b/pyatlan_v9/model/assets/salesforce_organization.py index 58cb30110..544771bc5 100644 --- a/pyatlan_v9/model/assets/salesforce_organization.py +++ b/pyatlan_v9/model/assets/salesforce_organization.py @@ -24,6 +24,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -307,6 +308,37 @@ def relate(self) -> "RelatedSalesforceOrganization": return RelatedSalesforceOrganization(guid=self.guid) return RelatedSalesforceOrganization(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + connection_qualified_name: str, + source_id: str, + ) -> "SalesforceOrganization": + validate_required_fields( + ["name", "connection_qualified_name", "source_id"], + [name, connection_qualified_name, source_id], + ) + fields = connection_qualified_name.split("/") + connector_name = fields[1] if len(fields) > 1 else None + return cls( + name=name, + qualified_name=f"{connection_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=connector_name, + source_id=source_id, + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceOrganization": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceOrganization": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_report.py b/pyatlan_v9/model/assets/salesforce_report.py index da9d7b494..329845154 100644 --- a/pyatlan_v9/model/assets/salesforce_report.py +++ b/pyatlan_v9/model/assets/salesforce_report.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -323,6 +324,45 @@ def relate(self) -> "RelatedSalesforceReport": return RelatedSalesforceReport(guid=self.guid) return RelatedSalesforceReport(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceReport": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceReport": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceReport": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/tests_v9/integration/salesforce_asset_test.py b/tests_v9/integration/salesforce_asset_test.py new file mode 100644 index 000000000..37eec354a --- /dev/null +++ b/tests_v9/integration/salesforce_asset_test.py @@ -0,0 +1,198 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +from typing import Generator + +import pytest + +from pyatlan_v9.client.atlan import AtlanClient +from pyatlan_v9.model.assets import ( + Connection, + SalesforceDashboard, + SalesforceField, + SalesforceObject, + SalesforceOrganization, + SalesforceReport, +) +from pyatlan_v9.model.enums import AtlanConnectorType +from tests_v9.integration.client import TestId, delete_asset +from tests_v9.integration.connection_test import create_connection + +MODULE_NAME = TestId.make_unique("SALESFORCE") + +ORG_ID = f"00D{MODULE_NAME}" +OBJECT_API_NAME = f"Obj_{MODULE_NAME}__c" +FIELD_API_NAME = f"Fld_{MODULE_NAME}__c" +REPORT_ID = f"00O{MODULE_NAME}" +DASHBOARD_ID = f"01Z{MODULE_NAME}" + + +@pytest.fixture(scope="module") +def connection(client: AtlanClient) -> Generator[Connection, None, None]: + result = create_connection( + client=client, name=MODULE_NAME, connector_type=AtlanConnectorType.SALESFORCE + ) + yield result + delete_asset(client, guid=result.guid, asset_type=Connection) + + +@pytest.fixture(scope="module") +def organization( + client: AtlanClient, connection: Connection +) -> Generator[SalesforceOrganization, None, None]: + to_create = SalesforceOrganization.creator( + name=f"org_{MODULE_NAME}", + connection_qualified_name=connection.qualified_name, + source_id=ORG_ID, + ) + result = client.asset.save(to_create).assets_created( + asset_type=SalesforceOrganization + )[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceOrganization) + + +@pytest.fixture(scope="module") +def sf_object( + client: AtlanClient, organization: SalesforceOrganization +) -> Generator[SalesforceObject, None, None]: + to_create = SalesforceObject.creator( + name=f"object_{MODULE_NAME}", + api_name=OBJECT_API_NAME, + organization_qualified_name=organization.qualified_name, + ) + result = client.asset.save(to_create).assets_created(asset_type=SalesforceObject)[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceObject) + + +@pytest.fixture(scope="module") +def sf_field( + client: AtlanClient, sf_object: SalesforceObject +) -> Generator[SalesforceField, None, None]: + to_create = SalesforceField.creator( + name=f"field_{MODULE_NAME}", + api_name=FIELD_API_NAME, + object_qualified_name=sf_object.qualified_name, + ) + result = client.asset.save(to_create).assets_created(asset_type=SalesforceField)[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceField) + + +@pytest.fixture(scope="module") +def report( + client: AtlanClient, organization: SalesforceOrganization +) -> Generator[SalesforceReport, None, None]: + to_create = SalesforceReport.creator( + name=f"report_{MODULE_NAME}", + source_id=REPORT_ID, + organization_qualified_name=organization.qualified_name, + ) + result = client.asset.save(to_create).assets_created(asset_type=SalesforceReport)[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceReport) + + +@pytest.fixture(scope="module") +def dashboard( + client: AtlanClient, organization: SalesforceOrganization +) -> Generator[SalesforceDashboard, None, None]: + to_create = SalesforceDashboard.creator( + name=f"dashboard_{MODULE_NAME}", + source_id=DASHBOARD_ID, + organization_qualified_name=organization.qualified_name, + ) + result = client.asset.save(to_create).assets_created( + asset_type=SalesforceDashboard + )[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceDashboard) + + +def _retrieve(client, asset, asset_type, connection, expected_qn): + retrieved = client.asset.get_by_guid( + asset.guid, asset_type=asset_type, ignore_relationships=False + ) + assert retrieved.qualified_name == expected_qn + assert retrieved.name == asset.name + assert retrieved.connector_name == AtlanConnectorType.SALESFORCE + assert retrieved.connection_qualified_name == connection.qualified_name + return retrieved + + +def _org_qn(connection: Connection) -> str: + return f"{connection.qualified_name}/{ORG_ID}" + + +def test_organization( + client: AtlanClient, + connection: Connection, + organization: SalesforceOrganization, +): + retrieved = _retrieve( + client, organization, SalesforceOrganization, connection, _org_qn(connection) + ) + assert retrieved.source_id == ORG_ID + + +def test_object( + client: AtlanClient, + connection: Connection, + organization: SalesforceOrganization, + sf_object: SalesforceObject, +): + retrieved = _retrieve( + client, + sf_object, + SalesforceObject, + connection, + f"{_org_qn(connection)}/{OBJECT_API_NAME}", + ) + assert retrieved.api_name == OBJECT_API_NAME + assert retrieved.organization_qualified_name == _org_qn(connection) + assert retrieved.organization.guid == organization.guid + + +def test_field( + client: AtlanClient, + connection: Connection, + sf_object: SalesforceObject, + sf_field: SalesforceField, +): + retrieved = _retrieve( + client, + sf_field, + SalesforceField, + connection, + f"{_org_qn(connection)}/{OBJECT_API_NAME}/{FIELD_API_NAME}", + ) + assert retrieved.api_name == FIELD_API_NAME + assert retrieved.object_qualified_name == sf_object.qualified_name + assert retrieved.organization_qualified_name == _org_qn(connection) + assert retrieved.object.guid == sf_object.guid + + +@pytest.mark.parametrize( + "fixture_name, asset_type, source_id", + [ + ("report", SalesforceReport, REPORT_ID), + ("dashboard", SalesforceDashboard, DASHBOARD_ID), + ], +) +def test_report_and_dashboard( + request, + client: AtlanClient, + connection: Connection, + organization: SalesforceOrganization, + fixture_name, + asset_type, + source_id, +): + asset = request.getfixturevalue(fixture_name) + retrieved = _retrieve( + client, asset, asset_type, connection, f"{_org_qn(connection)}/{source_id}" + ) + assert retrieved.source_id == source_id + assert retrieved.organization_qualified_name == _org_qn(connection) + assert retrieved.organization.guid == organization.guid diff --git a/tests_v9/unit/model/salesforce_test.py b/tests_v9/unit/model/salesforce_test.py new file mode 100644 index 000000000..df3a19f1f --- /dev/null +++ b/tests_v9/unit/model/salesforce_test.py @@ -0,0 +1,204 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for the Salesforce creators in pyatlan_v9. + +The expected qualifiedNames are the exact strings atlan-salesforce-app emits today +(FND-3838 census), so a creator change that would re-key existing Salesforce assets +fails here. +""" + +import pytest + +from pyatlan_v9.model import ( + SalesforceDashboard, + SalesforceField, + SalesforceObject, + SalesforceOrganization, + SalesforceReport, +) + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_ID = "00D5g000004XyZaEAK" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +OBJECT_API_NAME = "Account" +OBJECT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" +FIELD_API_NAME = "Annual_Revenue__c" +FIELD_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account/Annual_Revenue__c" +REPORT_ID = "00O5g000004AbCdEAF" +REPORT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/00O5g000004AbCdEAF" +DASHBOARD_ID = "01Z5g000000AbCdEAF" +DASHBOARD_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/01Z5g000000AbCdEAF" +NAME = "Quarterly Revenue" + + +def _assert_common(sut, expected_qn): + assert sut.qualified_name == expected_qn + assert sut.name == NAME + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + # SalesforceOrganization has no containment parent, so it has no pattern. + pattern = getattr(sut, "_QUALIFIED_NAME_PATTERN", None) + if pattern is not None: + assert pattern.match(sut.qualified_name) + sut.validate() + + +def test_organization_creator_matches_app_qualified_name(): + sut = SalesforceOrganization.creator( + name=NAME, connection_qualified_name=CONNECTION_QN, source_id=ORG_ID + ) + + _assert_common(sut, ORG_QN) + assert sut.source_id == ORG_ID + sut.validate(for_creation=True) + + +def test_object_creator_matches_app_qualified_name(): + sut = SalesforceObject.creator( + name=NAME, api_name=OBJECT_API_NAME, organization_qualified_name=ORG_QN + ) + + _assert_common(sut, OBJECT_QN) + assert sut.api_name == OBJECT_API_NAME + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + sut.validate(for_creation=True) + + +def test_field_creator_matches_app_qualified_name(): + sut = SalesforceField.creator( + name=NAME, api_name=FIELD_API_NAME, object_qualified_name=OBJECT_QN + ) + + _assert_common(sut, FIELD_QN) + assert sut.api_name == FIELD_API_NAME + assert sut.object_qualified_name == OBJECT_QN + assert sut.organization_qualified_name == ORG_QN + assert sut.object.qualified_name == OBJECT_QN + sut.validate(for_creation=True) + + +@pytest.mark.parametrize( + "asset_type, source_id, expected_qn", + [ + (SalesforceReport, REPORT_ID, REPORT_QN), + (SalesforceDashboard, DASHBOARD_ID, DASHBOARD_QN), + ], +) +def test_report_and_dashboard_creator_matches_app_qualified_name( + asset_type, source_id, expected_qn +): + sut = asset_type.creator( + name=NAME, source_id=source_id, organization_qualified_name=ORG_QN + ) + + _assert_common(sut, expected_qn) + assert sut.source_id == source_id + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + sut.validate(for_creation=True) + + +@pytest.mark.parametrize( + "asset_type, kwargs", + [ + ( + SalesforceObject, + {"api_name": OBJECT_API_NAME, "organization_qualified_name": ORG_QN}, + ), + ( + SalesforceField, + {"api_name": FIELD_API_NAME, "object_qualified_name": OBJECT_QN}, + ), + ( + SalesforceReport, + {"source_id": REPORT_ID, "organization_qualified_name": ORG_QN}, + ), + ( + SalesforceDashboard, + {"source_id": DASHBOARD_ID, "organization_qualified_name": ORG_QN}, + ), + ], +) +def test_explicit_connection_qualified_name_is_kept(asset_type, kwargs): + sut = asset_type.creator( + name=NAME, connection_qualified_name=CONNECTION_QN, **kwargs + ) + + assert sut.connection_qualified_name == CONNECTION_QN + + +VALID_KWARGS = { + SalesforceOrganization: { + "name": NAME, + "connection_qualified_name": CONNECTION_QN, + "source_id": ORG_ID, + }, + SalesforceObject: { + "name": NAME, + "api_name": OBJECT_API_NAME, + "organization_qualified_name": ORG_QN, + }, + SalesforceField: { + "name": NAME, + "api_name": FIELD_API_NAME, + "object_qualified_name": OBJECT_QN, + }, + SalesforceReport: { + "name": NAME, + "source_id": REPORT_ID, + "organization_qualified_name": ORG_QN, + }, + SalesforceDashboard: { + "name": NAME, + "source_id": DASHBOARD_ID, + "organization_qualified_name": ORG_QN, + }, +} + +REQUIRED = [ + (asset_type, key) for asset_type, kwargs in VALID_KWARGS.items() for key in kwargs +] + + +@pytest.mark.parametrize("asset_type, key", REQUIRED) +def test_creator_rejects_missing_input(asset_type, key): + kwargs = {**VALID_KWARGS[asset_type], key: None} + + with pytest.raises(ValueError, match=f"{key} is required"): + asset_type.creator(**kwargs) + + +@pytest.mark.parametrize("asset_type, key", REQUIRED) +def test_creator_rejects_blank_input(asset_type, key): + kwargs = {**VALID_KWARGS[asset_type], key: " "} + + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + asset_type.creator(**kwargs) + + +@pytest.mark.parametrize( + "asset_type, key, value", + [ + (SalesforceObject, "organization_qualified_name", CONNECTION_QN), + (SalesforceReport, "organization_qualified_name", CONNECTION_QN), + (SalesforceDashboard, "organization_qualified_name", CONNECTION_QN), + (SalesforceField, "object_qualified_name", ORG_QN), + ], +) +def test_creator_rejects_too_short_parent_qualified_name(asset_type, key, value): + kwargs = {**VALID_KWARGS[asset_type], key: value} + + with pytest.raises(ValueError, match=f"{key} is invalid"): + asset_type.creator(**kwargs) + + +@pytest.mark.parametrize("asset_type", list(VALID_KWARGS)) +def test_create_alias(asset_type): + kwargs = VALID_KWARGS[asset_type] + + assert ( + asset_type.create(**kwargs).qualified_name + == asset_type.creator(**kwargs).qualified_name + ) From 18fe08ec23aa7db0d600a5b272b9fae84c19b9f4 Mon Sep 17 00:00:00 2001 From: Christopher Grote Date: Tue, 20 Oct 2026 18:34:48 +0100 Subject: [PATCH 2/2] FND-3858: regenerate with Salesforce qualifiedNameFormula, per-type unit tests Regenerates the 5 Salesforce classes from the paired models branch (atlanhq/models#2100). Only SalesforceOrganization changes: it has no containment parent, so the formula gives it its first _QUALIFIED_NAME_PATTERN. The other four patterns are unchanged. The models branch was generated without models#2093 (FND-3846 deleteHandler), which is still waiting in sync PR #1081, so this diff carries only Salesforce changes. Splits tests_v9/unit/model/salesforce_test.py into one file per type (salesforce__test.py) and adds updater / trim_to_required cases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019fLee6iVYNyoTkguJombqF --- .../model/assets/salesforce_organization.py | 8 + .../unit/model/salesforce_dashboard_test.py | 100 +++++++++ tests_v9/unit/model/salesforce_field_test.py | 103 +++++++++ tests_v9/unit/model/salesforce_object_test.py | 108 ++++++++++ .../model/salesforce_organization_test.py | 85 ++++++++ tests_v9/unit/model/salesforce_report_test.py | 100 +++++++++ tests_v9/unit/model/salesforce_test.py | 204 ------------------ 7 files changed, 504 insertions(+), 204 deletions(-) create mode 100644 tests_v9/unit/model/salesforce_dashboard_test.py create mode 100644 tests_v9/unit/model/salesforce_field_test.py create mode 100644 tests_v9/unit/model/salesforce_object_test.py create mode 100644 tests_v9/unit/model/salesforce_organization_test.py create mode 100644 tests_v9/unit/model/salesforce_report_test.py delete mode 100644 tests_v9/unit/model/salesforce_test.py diff --git a/pyatlan_v9/model/assets/salesforce_organization.py b/pyatlan_v9/model/assets/salesforce_organization.py index 544771bc5..4358a1c70 100644 --- a/pyatlan_v9/model/assets/salesforce_organization.py +++ b/pyatlan_v9/model/assets/salesforce_organization.py @@ -14,6 +14,7 @@ from __future__ import annotations +import re from typing import Any, ClassVar, List, Union from msgspec import UNSET, UnsetType @@ -250,6 +251,8 @@ def __post_init__(self) -> None: # SDK Methods # ========================================================================= + _QUALIFIED_NAME_PATTERN: ClassVar[re.Pattern] = re.compile(r"^.+/[^/]+$") + def validate(self, for_creation: bool = False) -> None: """ Dry-run validation of this SalesforceOrganization instance. @@ -274,6 +277,11 @@ def validate(self, for_creation: bool = False) -> None: errors.append("name is required") if self.qualified_name is UNSET or self.qualified_name is None: errors.append("qualified_name is required") + elif not self._QUALIFIED_NAME_PATTERN.match(self.qualified_name): + errors.append( + f"qualified_name '{self.qualified_name}' does not match expected " + f"pattern: {self._QUALIFIED_NAME_PATTERN.pattern}" + ) if errors: raise ValueError(f"SalesforceOrganization validation failed: {errors}") diff --git a/tests_v9/unit/model/salesforce_dashboard_test.py b/tests_v9/unit/model/salesforce_dashboard_test.py new file mode 100644 index 000000000..0b0ee1f54 --- /dev/null +++ b/tests_v9/unit/model/salesforce_dashboard_test.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceDashboard.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_dashboard, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceDashboard + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +NAME = "Quarterly Revenue" +VALID = { + "name": NAME, + "source_id": "01Z5g000000AbCdEAF", + "organization_qualified_name": ORG_QN, +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/01Z5g000000AbCdEAF" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceDashboard.creator(**VALID) + + assert ( + sut.qualified_name + == "default/salesforce/1712345678/00D5g000004XyZaEAK/01Z5g000000AbCdEAF" + ) + assert sut.name == NAME + assert sut.source_id == "01Z5g000000AbCdEAF" + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +@pytest.mark.parametrize("value", [CONNECTION_QN, "default"]) +def test_creator_rejects_too_short_organization_qualified_name(value): + with pytest.raises(ValueError, match="organization_qualified_name is invalid"): + SalesforceDashboard.creator(**{**VALID, "organization_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceDashboard.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceDashboard.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceDashboard.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceDashboard.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceDashboard.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceDashboard.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceDashboard.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceDashboard.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceDashboard) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_field_test.py b/tests_v9/unit/model/salesforce_field_test.py new file mode 100644 index 000000000..cb6a30d94 --- /dev/null +++ b/tests_v9/unit/model/salesforce_field_test.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceField.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_field, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceField + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +OBJECT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" +NAME = "Annual Revenue" +VALID = { + "name": NAME, + "api_name": "Annual_Revenue__c", + "object_qualified_name": OBJECT_QN, +} +EXPECTED_QN = ( + "default/salesforce/1712345678/00D5g000004XyZaEAK/Account/Annual_Revenue__c" +) + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceField.creator(**VALID) + + assert sut.qualified_name == ( + "default/salesforce/1712345678/00D5g000004XyZaEAK/Account/Annual_Revenue__c" + ) + assert sut.name == NAME + assert sut.api_name == "Annual_Revenue__c" + assert sut.object_qualified_name == OBJECT_QN + assert sut.organization_qualified_name == ORG_QN + assert sut.object.qualified_name == OBJECT_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +@pytest.mark.parametrize("value", [ORG_QN, "default/salesforce"]) +def test_creator_rejects_too_short_object_qualified_name(value): + with pytest.raises(ValueError, match="object_qualified_name is invalid"): + SalesforceField.creator(**{**VALID, "object_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceField.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceField.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceField.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceField.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceField.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceField.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceField.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceField.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceField) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_object_test.py b/tests_v9/unit/model/salesforce_object_test.py new file mode 100644 index 000000000..a401adbcb --- /dev/null +++ b/tests_v9/unit/model/salesforce_object_test.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceObject.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_object, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceObject + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +NAME = "Account" +VALID = { + "name": NAME, + "api_name": "Account", + "organization_qualified_name": ORG_QN, +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceObject.creator(**VALID) + + assert ( + sut.qualified_name == "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" + ) + assert sut.name == NAME + assert sut.api_name == "Account" + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +def test_creator_uses_api_name_not_label_in_qualified_name(): + sut = SalesforceObject.creator( + name="Custom Thing", api_name="My_Thing__c", organization_qualified_name=ORG_QN + ) + + assert sut.qualified_name == f"{ORG_QN}/My_Thing__c" + assert sut.name == "Custom Thing" + + +@pytest.mark.parametrize("value", [CONNECTION_QN, "default"]) +def test_creator_rejects_too_short_organization_qualified_name(value): + with pytest.raises(ValueError, match="organization_qualified_name is invalid"): + SalesforceObject.creator(**{**VALID, "organization_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceObject.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceObject.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceObject.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceObject.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceObject.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceObject.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceObject.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceObject.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceObject) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_organization_test.py b/tests_v9/unit/model/salesforce_organization_test.py new file mode 100644 index 000000000..17b9a9c7c --- /dev/null +++ b/tests_v9/unit/model/salesforce_organization_test.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceOrganization.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_organization, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceOrganization + +CONNECTION_QN = "default/salesforce/1712345678" +NAME = "Acme Corp" +VALID = { + "name": NAME, + "connection_qualified_name": CONNECTION_QN, + "source_id": "00D5g000004XyZaEAK", +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceOrganization.creator(**VALID) + + assert sut.qualified_name == "default/salesforce/1712345678/00D5g000004XyZaEAK" + assert sut.name == NAME + assert sut.source_id == "00D5g000004XyZaEAK" + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +def test_validate_rejects_qualified_name_without_org_segment(): + sut = SalesforceOrganization.creator(**VALID) + sut.qualified_name = "00D5g000004XyZaEAK" + + with pytest.raises(ValueError, match="does not match expected pattern"): + sut.validate() + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceOrganization.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceOrganization.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceOrganization.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceOrganization.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceOrganization.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceOrganization.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceOrganization) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_report_test.py b/tests_v9/unit/model/salesforce_report_test.py new file mode 100644 index 000000000..06fa2bc4d --- /dev/null +++ b/tests_v9/unit/model/salesforce_report_test.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceReport.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_report, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceReport + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +NAME = "Quarterly Revenue" +VALID = { + "name": NAME, + "source_id": "00O5g000004AbCdEAF", + "organization_qualified_name": ORG_QN, +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/00O5g000004AbCdEAF" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceReport.creator(**VALID) + + assert ( + sut.qualified_name + == "default/salesforce/1712345678/00D5g000004XyZaEAK/00O5g000004AbCdEAF" + ) + assert sut.name == NAME + assert sut.source_id == "00O5g000004AbCdEAF" + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +@pytest.mark.parametrize("value", [CONNECTION_QN, "default"]) +def test_creator_rejects_too_short_organization_qualified_name(value): + with pytest.raises(ValueError, match="organization_qualified_name is invalid"): + SalesforceReport.creator(**{**VALID, "organization_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceReport.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceReport.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceReport.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceReport.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceReport.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceReport.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceReport.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceReport.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceReport) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_test.py b/tests_v9/unit/model/salesforce_test.py deleted file mode 100644 index df3a19f1f..000000000 --- a/tests_v9/unit/model/salesforce_test.py +++ /dev/null @@ -1,204 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright 2026 Atlan Pte. Ltd. - -"""Unit tests for the Salesforce creators in pyatlan_v9. - -The expected qualifiedNames are the exact strings atlan-salesforce-app emits today -(FND-3838 census), so a creator change that would re-key existing Salesforce assets -fails here. -""" - -import pytest - -from pyatlan_v9.model import ( - SalesforceDashboard, - SalesforceField, - SalesforceObject, - SalesforceOrganization, - SalesforceReport, -) - -CONNECTION_QN = "default/salesforce/1712345678" -ORG_ID = "00D5g000004XyZaEAK" -ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" -OBJECT_API_NAME = "Account" -OBJECT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" -FIELD_API_NAME = "Annual_Revenue__c" -FIELD_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account/Annual_Revenue__c" -REPORT_ID = "00O5g000004AbCdEAF" -REPORT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/00O5g000004AbCdEAF" -DASHBOARD_ID = "01Z5g000000AbCdEAF" -DASHBOARD_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/01Z5g000000AbCdEAF" -NAME = "Quarterly Revenue" - - -def _assert_common(sut, expected_qn): - assert sut.qualified_name == expected_qn - assert sut.name == NAME - assert sut.connection_qualified_name == CONNECTION_QN - assert sut.connector_name == "salesforce" - # SalesforceOrganization has no containment parent, so it has no pattern. - pattern = getattr(sut, "_QUALIFIED_NAME_PATTERN", None) - if pattern is not None: - assert pattern.match(sut.qualified_name) - sut.validate() - - -def test_organization_creator_matches_app_qualified_name(): - sut = SalesforceOrganization.creator( - name=NAME, connection_qualified_name=CONNECTION_QN, source_id=ORG_ID - ) - - _assert_common(sut, ORG_QN) - assert sut.source_id == ORG_ID - sut.validate(for_creation=True) - - -def test_object_creator_matches_app_qualified_name(): - sut = SalesforceObject.creator( - name=NAME, api_name=OBJECT_API_NAME, organization_qualified_name=ORG_QN - ) - - _assert_common(sut, OBJECT_QN) - assert sut.api_name == OBJECT_API_NAME - assert sut.organization_qualified_name == ORG_QN - assert sut.organization.qualified_name == ORG_QN - sut.validate(for_creation=True) - - -def test_field_creator_matches_app_qualified_name(): - sut = SalesforceField.creator( - name=NAME, api_name=FIELD_API_NAME, object_qualified_name=OBJECT_QN - ) - - _assert_common(sut, FIELD_QN) - assert sut.api_name == FIELD_API_NAME - assert sut.object_qualified_name == OBJECT_QN - assert sut.organization_qualified_name == ORG_QN - assert sut.object.qualified_name == OBJECT_QN - sut.validate(for_creation=True) - - -@pytest.mark.parametrize( - "asset_type, source_id, expected_qn", - [ - (SalesforceReport, REPORT_ID, REPORT_QN), - (SalesforceDashboard, DASHBOARD_ID, DASHBOARD_QN), - ], -) -def test_report_and_dashboard_creator_matches_app_qualified_name( - asset_type, source_id, expected_qn -): - sut = asset_type.creator( - name=NAME, source_id=source_id, organization_qualified_name=ORG_QN - ) - - _assert_common(sut, expected_qn) - assert sut.source_id == source_id - assert sut.organization_qualified_name == ORG_QN - assert sut.organization.qualified_name == ORG_QN - sut.validate(for_creation=True) - - -@pytest.mark.parametrize( - "asset_type, kwargs", - [ - ( - SalesforceObject, - {"api_name": OBJECT_API_NAME, "organization_qualified_name": ORG_QN}, - ), - ( - SalesforceField, - {"api_name": FIELD_API_NAME, "object_qualified_name": OBJECT_QN}, - ), - ( - SalesforceReport, - {"source_id": REPORT_ID, "organization_qualified_name": ORG_QN}, - ), - ( - SalesforceDashboard, - {"source_id": DASHBOARD_ID, "organization_qualified_name": ORG_QN}, - ), - ], -) -def test_explicit_connection_qualified_name_is_kept(asset_type, kwargs): - sut = asset_type.creator( - name=NAME, connection_qualified_name=CONNECTION_QN, **kwargs - ) - - assert sut.connection_qualified_name == CONNECTION_QN - - -VALID_KWARGS = { - SalesforceOrganization: { - "name": NAME, - "connection_qualified_name": CONNECTION_QN, - "source_id": ORG_ID, - }, - SalesforceObject: { - "name": NAME, - "api_name": OBJECT_API_NAME, - "organization_qualified_name": ORG_QN, - }, - SalesforceField: { - "name": NAME, - "api_name": FIELD_API_NAME, - "object_qualified_name": OBJECT_QN, - }, - SalesforceReport: { - "name": NAME, - "source_id": REPORT_ID, - "organization_qualified_name": ORG_QN, - }, - SalesforceDashboard: { - "name": NAME, - "source_id": DASHBOARD_ID, - "organization_qualified_name": ORG_QN, - }, -} - -REQUIRED = [ - (asset_type, key) for asset_type, kwargs in VALID_KWARGS.items() for key in kwargs -] - - -@pytest.mark.parametrize("asset_type, key", REQUIRED) -def test_creator_rejects_missing_input(asset_type, key): - kwargs = {**VALID_KWARGS[asset_type], key: None} - - with pytest.raises(ValueError, match=f"{key} is required"): - asset_type.creator(**kwargs) - - -@pytest.mark.parametrize("asset_type, key", REQUIRED) -def test_creator_rejects_blank_input(asset_type, key): - kwargs = {**VALID_KWARGS[asset_type], key: " "} - - with pytest.raises(ValueError, match=f"{key} cannot be blank"): - asset_type.creator(**kwargs) - - -@pytest.mark.parametrize( - "asset_type, key, value", - [ - (SalesforceObject, "organization_qualified_name", CONNECTION_QN), - (SalesforceReport, "organization_qualified_name", CONNECTION_QN), - (SalesforceDashboard, "organization_qualified_name", CONNECTION_QN), - (SalesforceField, "object_qualified_name", ORG_QN), - ], -) -def test_creator_rejects_too_short_parent_qualified_name(asset_type, key, value): - kwargs = {**VALID_KWARGS[asset_type], key: value} - - with pytest.raises(ValueError, match=f"{key} is invalid"): - asset_type.creator(**kwargs) - - -@pytest.mark.parametrize("asset_type", list(VALID_KWARGS)) -def test_create_alias(asset_type): - kwargs = VALID_KWARGS[asset_type] - - assert ( - asset_type.create(**kwargs).qualified_name - == asset_type.creator(**kwargs).qualified_name - )