diff --git a/src/snowflake/cli/_plugins/nativeapp/commands.py b/src/snowflake/cli/_plugins/nativeapp/commands.py index b60ea157fa..55ca0019a0 100644 --- a/src/snowflake/cli/_plugins/nativeapp/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/commands.py @@ -31,6 +31,7 @@ from snowflake.cli._plugins.nativeapp.entities.application_package import ( ApplicationPackageEntityModel, ) +from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade from snowflake.cli._plugins.nativeapp.v2_conversions.compat import ( find_entity, force_project_definition_v2, @@ -198,7 +199,7 @@ def app_open( ) app_id = options["app_entity_id"] app = ws.get_entity(app_id) - if app.get_existing_app_info(): + if get_snowflake_facade().get_existing_app_info(app.name, app.role): typer.launch(app.get_snowsight_url()) return MessageResult(f"Snowflake Native App opened in browser.") else: diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 040e60e82f..1f794c2db9 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -26,11 +26,8 @@ ) from snowflake.cli._plugins.nativeapp.constants import ( ALLOWED_SPECIAL_COMMENTS, - AUTHORIZE_TELEMETRY_COL, COMMENT_COL, - NAME_COL, OWNER_COL, - SPECIAL_COMMENT, ) from snowflake.cli._plugins.nativeapp.entities.application_package import ( ApplicationPackageEntity, @@ -53,10 +50,14 @@ SameAccountInstallMethod, ) from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( + UpgradeApplicationRestrictionError, +) from snowflake.cli._plugins.nativeapp.utils import needs_confirmation from snowflake.cli._plugins.workspace.context import ActionContext from snowflake.cli.api.cli_global_context import get_cli_context, span from snowflake.cli.api.console.abc import AbstractConsole +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.entities.common import ( EntityBase, attach_spans_to_entity_actions, @@ -71,13 +72,7 @@ from snowflake.cli.api.errno import ( APPLICATION_NO_LONGER_AVAILABLE, APPLICATION_OWNS_EXTERNAL_OBJECTS, - APPLICATION_REQUIRES_TELEMETRY_SHARING, - CANNOT_DISABLE_MANDATORY_TELEMETRY, - CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, DOES_NOT_EXIST_OR_NOT_AUTHORIZED, - NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, - ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ) from snowflake.cli.api.metrics import CLICounterField from snowflake.cli.api.project.schemas.entities.common import ( @@ -98,15 +93,6 @@ log = logging.getLogger(__name__) -# Reasons why an `alter application ... upgrade` might fail -UPGRADE_RESTRICTION_CODES = { - CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, - ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, - NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, - APPLICATION_NO_LONGER_AVAILABLE, -} - ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str}) @@ -191,11 +177,11 @@ def __init__( def _contains_mandatory_events(self, events_definitions: List[Dict[str, str]]): return any(event["sharing"] == "MANDATORY" for event in events_definitions) - def should_authorize_event_sharing_during_create( + def should_authorize_event_sharing( self, ) -> Optional[bool]: """ - Determines whether event sharing should be authorized during the creation of the application object. + Determines whether event sharing should be authorized. Outputs: - None: Event sharing should not be updated or explicitly set. @@ -208,35 +194,6 @@ def should_authorize_event_sharing_during_create( return self._share_mandatory_events - def should_authorize_event_sharing_after_upgrade( - self, - upgraded_app_properties: Dict[str, str], - ) -> Optional[bool]: - """ - Determines whether event sharing should be authorized after upgrading the application object. - - :param upgraded_app_properties: The properties of the application after upgrading. - - Outputs: - - None: Event sharing should not be updated or explicitly set. - - True: Event sharing should be authorized. - - False: Event sharing should be disabled. - """ - - if not self._event_sharing_enabled: - return None - - current_app_authorization = ( - upgraded_app_properties.get(AUTHORIZE_TELEMETRY_COL, "false").lower() - == "true" - ) - - # Skip the update if the current value is the same as the one we want to set - if current_app_authorization == self._share_mandatory_events: - return None - - return self._share_mandatory_events - def event_sharing_warning(self, message: str): """ Logs a warning message about event sharing, and emits an event sharing warning metric. @@ -360,6 +317,18 @@ def post_deploy_hooks(self) -> list[PostDeployHook] | None: model = self._entity_model return model.meta and model.meta.post_deploy + @property + def console(self) -> AbstractConsole: + return self._workspace_ctx.console + + @property + def debug(self) -> bool | None: + return self._entity_model.debug + + @property + def telemetry(self) -> EventSharingTelemetry | None: + return self._entity_model.telemetry + def action_deploy( self, action_ctx: ActionContext, @@ -456,14 +425,14 @@ def action_drop( """ Attempts to drop the application object if all validations and user prompts allow so. """ - console = self._workspace_ctx.console - needs_confirm = True # 1. If existing application is not found, exit gracefully - show_obj_row = self.get_existing_app_info() + show_obj_row = get_snowflake_facade().get_existing_app_info( + self.name, self.role + ) if show_obj_row is None: - console.warning( + self.console.warning( f"Role {self.role} does not own any application object with the name {self.name}, or the application object does not exist." ) return @@ -490,7 +459,7 @@ def action_drop( ) ) if not should_drop_object: - console.message(f"Did not drop application object {self.name}.") + self.console.message(f"Did not drop application object {self.name}.") # The user desires to keep the app, therefore we can't proceed since it would # leave behind an orphan app when we get to dropping the package raise typer.Abort() @@ -529,22 +498,22 @@ def action_drop( if has_objects_to_drop: if cascade is True: # If the user explicitly passed the --cascade flag - console.message(cascade_true_message) - with console.indented(): + self.console.message(cascade_true_message) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) + self.console.message(_application_object_to_str(obj)) elif cascade is False: # If the user explicitly passed the --no-cascade flag - console.message(cascade_false_message) - with console.indented(): + self.console.message(cascade_false_message) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) + self.console.message(_application_object_to_str(obj)) elif interactive: # If the user didn't pass any cascade flag and the session is interactive - console.message(message_prefix) - with console.indented(): + self.console.message(message_prefix) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) + self.console.message(_application_object_to_str(obj)) user_response = typer.prompt( interactive_prompt, show_default=False, @@ -558,11 +527,11 @@ def action_drop( raise typer.Abort() else: # Else abort since we don't know what to do and can't ask the user - console.message(message_prefix) - with console.indented(): + self.console.message(message_prefix) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) - console.message(non_interactive_abort) + self.console.message(_application_object_to_str(obj)) + self.console.message(non_interactive_abort) raise typer.Abort() elif cascade is None: # If there's nothing to drop, set cascade to an explicit False value @@ -570,7 +539,7 @@ def action_drop( # 4. All validations have passed, drop object drop_generic_object( - console=console, + console=self.console, object_type="application", object_name=self.name, role=self.role, @@ -634,182 +603,138 @@ def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]: ).fetchall() return [{"name": row[1], "type": row[2]} for row in results] - @span("update_app_object") - def create_or_upgrade_app( + def _upgrade_app( self, - package: ApplicationPackageEntity, stage_fqn: str, install_method: SameAccountInstallMethod, + event_sharing: EventSharingHandler, policy: PolicyBase, interactive: bool, - ): - model = self._entity_model - console = self._workspace_ctx.console - debug_mode = model.debug + ) -> list[tuple[str]] | None: + self.console.step(f"Upgrading existing application object {self.name}.") - stage_fqn = stage_fqn or package.stage_fqn - stage_schema = extract_schema(stage_fqn) - - sql_executor = get_sql_executor() - with sql_executor.use_role(self.role): - event_sharing = EventSharingHandler( - telemetry_definition=model.telemetry, - deploy_root=package.deploy_root, + try: + return get_snowflake_facade().upgrade_application( + name=self.name, install_method=install_method, - console=console, + stage_fqn=stage_fqn, + debug_mode=self.debug, + should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), + role=self.role, + warehouse=self.warehouse, ) + except UpgradeApplicationRestrictionError as err: + self.console.warning(err.message) + self.drop_application_before_upgrade(policy=policy, interactive=interactive) + return None - # 1. Need to use a warehouse to create an application object - with sql_executor.use_warehouse(self.warehouse): + def _create_app( + self, + stage_fqn: str, + install_method: SameAccountInstallMethod, + event_sharing: EventSharingHandler, + package: ApplicationPackageEntity, + ) -> list[tuple[str]]: + self.console.step(f"Creating new application object {self.name} in account.") + + if package.role != self.role: + get_snowflake_facade().grant_privileges_to_role( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier=package.name, + role_to_grant=self.role, + role_to_use=package.role, + ) - # 2. Check for an existing application by the same name - show_app_row = self.get_existing_app_info() + stage_schema = extract_schema(stage_fqn) + get_snowflake_facade().grant_privileges_to_role( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier=f"{package.name}.{stage_schema}", + role_to_grant=self.role, + role_to_use=package.role, + ) - # 3. If existing application is found, perform a few validations and upgrade the application object. - if show_app_row: - install_method.ensure_app_usable( - app_name=self.name, - app_role=self.role, - show_app_row=show_app_row, - ) + get_snowflake_facade().grant_privileges_to_role( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier=stage_fqn, + role_to_grant=self.role, + role_to_use=package.role, + ) - # If all the above checks are in order, proceed to upgrade - try: - console.step( - f"Upgrading existing application object {self.name}." - ) - using_clause = install_method.using_clause(stage_fqn) - upgrade_cursor = sql_executor.execute_query( - f"alter application {self.name} upgrade {using_clause}", - ) - print_messages(console, upgrade_cursor) + return get_snowflake_facade().create_application( + name=self.name, + package_name=package.name, + install_method=install_method, + stage_fqn=stage_fqn, + debug_mode=self.debug, + should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), + role=self.role, + warehouse=self.warehouse, + ) - events_definitions = ( - get_snowflake_facade().get_event_definitions( - self.name, self.role - ) - ) + @span("update_app_object") + def create_or_upgrade_app( + self, + package: ApplicationPackageEntity, + stage_fqn: str, + install_method: SameAccountInstallMethod, + policy: PolicyBase, + interactive: bool, + ): + event_sharing = EventSharingHandler( + telemetry_definition=self.telemetry, + deploy_root=package.deploy_root, + install_method=install_method, + console=self.console, + ) - app_properties = get_snowflake_facade().get_app_properties( - self.name, self.role - ) - new_authorize_event_sharing_value = ( - event_sharing.should_authorize_event_sharing_after_upgrade( - app_properties, - ) - ) - if new_authorize_event_sharing_value is not None: - log.info( - "Setting telemetry sharing authorization to %s", - new_authorize_event_sharing_value, - ) - sql_executor.execute_query( - f"alter application {self.name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(new_authorize_event_sharing_value).upper()}" - ) - events_to_share = event_sharing.events_to_share( - events_definitions - ) - if events_to_share is not None: - get_snowflake_facade().share_telemetry_events( - self.name, events_to_share - ) - - if install_method.is_dev_mode: - # if debug_mode is present (controlled), ensure it is up-to-date - if debug_mode is not None: - sql_executor.execute_query( - f"alter application {self.name} set debug_mode = {debug_mode}" - ) - - # hooks always executed after a create or upgrade - self.execute_post_deploy_hooks() - return - - except ProgrammingError as err: - if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: - event_sharing.event_sharing_error( - "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file.", - err, - ) - elif err.errno in UPGRADE_RESTRICTION_CODES: - console.warning(err.msg) - self.drop_application_before_upgrade( - policy=policy, interactive=interactive - ) - else: - generic_sql_error_handler(err=err) - - # 4. With no (more) existing application objects, create an application object using the release directives - console.step(f"Creating new application object {self.name} in account.") - - if self.role != package.role: - with sql_executor.use_role(package.role): - sql_executor.execute_query( - f"grant install, develop on application package {package.name} to role {self.role}" - ) - sql_executor.execute_query( - f"grant usage on schema {package.name}.{stage_schema} to role {self.role}" - ) - sql_executor.execute_query( - f"grant read on stage {stage_fqn} to role {self.role}" - ) + # 1. Check for an existing application by the same name + show_app_row = get_snowflake_facade().get_existing_app_info( + self.name, self.role + ) - try: - # by default, applications are created in debug mode when possible; - # this can be overridden in the project definition - debug_mode_clause = "" - if install_method.is_dev_mode: - initial_debug_mode = ( - debug_mode if debug_mode is not None else True - ) - debug_mode_clause = f"debug_mode = {initial_debug_mode}" + stage_fqn = stage_fqn or package.stage_fqn - authorize_telemetry_clause = "" - new_authorize_event_sharing_value = ( - event_sharing.should_authorize_event_sharing_during_create() - ) - if new_authorize_event_sharing_value is not None: - log.info( - "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s", - new_authorize_event_sharing_value, - ) - authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(new_authorize_event_sharing_value).upper()}" - - using_clause = install_method.using_clause(stage_fqn) - create_cursor = sql_executor.execute_query( - dedent( - f"""\ - create application {self.name} - from application package {package.name} {using_clause} {debug_mode_clause}{authorize_telemetry_clause} - comment = {SPECIAL_COMMENT} - """ - ), - ) - print_messages(console, create_cursor) - events_definitions = get_snowflake_facade().get_event_definitions( - self.name, self.role - ) + # 2. If existing application is found, try to upgrade the application object. + create_or_upgrade_result = None + if show_app_row: + create_or_upgrade_result = self._upgrade_app( + stage_fqn=stage_fqn, + install_method=install_method, + event_sharing=event_sharing, + policy=policy, + interactive=interactive, + ) - events_to_share = event_sharing.events_to_share(events_definitions) - if events_to_share is not None: - get_snowflake_facade().share_telemetry_events( - self.name, events_to_share - ) + # 3. If no existing application found, or we performed a drop before the upgrade, we proceed to create + if create_or_upgrade_result is None: + create_or_upgrade_result = self._create_app( + stage_fqn=stage_fqn, + install_method=install_method, + event_sharing=event_sharing, + package=package, + ) - # hooks always executed after a create or upgrade - self.execute_post_deploy_hooks() + print_messages(self.console, create_or_upgrade_result) - except ProgrammingError as err: - if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: - event_sharing.event_sharing_error( - "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file.", - err, - ) - generic_sql_error_handler(err) + events_definitions = get_snowflake_facade().get_event_definitions( + self.name, self.role + ) + + events_to_share = event_sharing.events_to_share(events_definitions) + if events_to_share is not None: + get_snowflake_facade().share_telemetry_events( + self.name, events_to_share, self.role + ) + + # hooks always executed after a create or upgrade + self.execute_post_deploy_hooks() def execute_post_deploy_hooks(self): execute_post_deploy_hooks( - console=self._workspace_ctx.console, + console=self.console, project_root=self.project_root, post_deploy_hooks=self.post_deploy_hooks, deployed_object_type="application", @@ -833,69 +758,59 @@ def use_application_warehouse(self): ) ) - def get_existing_app_info(self) -> Optional[dict]: - """ - Check for an existing application object by the same name as in project definition, in account. - It executes a 'show applications like' query and returns the result as single row, if one exists. - """ - sql_executor = get_sql_executor() - with sql_executor.use_role(self.role): - return sql_executor.show_specific_object( - "applications", self.name, name_col=NAME_COL - ) - def drop_application_before_upgrade( self, policy: PolicyBase, interactive: bool, cascade: bool = False, ): - console = self._workspace_ctx.console - - if cascade: - try: - if application_objects := self.get_objects_owned_by_application(): - application_objects_str = _application_objects_to_str( - application_objects + sql_executor = get_sql_executor() + with sql_executor.use_role(self.role): + if cascade: + try: + if application_objects := self.get_objects_owned_by_application(): + application_objects_str = _application_objects_to_str( + application_objects + ) + self.console.message( + f"The following objects are owned by application {self.name} and need to be dropped:\n{application_objects_str}" + ) + except ProgrammingError as err: + if err.errno != APPLICATION_NO_LONGER_AVAILABLE: + generic_sql_error_handler(err) + self.console.warning( + "The application owns other objects but they could not be determined." ) - console.message( - f"The following objects are owned by application {self.name} and need to be dropped:\n{application_objects_str}" + user_prompt = "Do you want the Snowflake CLI to drop these objects, then drop the existing application object and recreate it?" + else: + user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?" + + if not policy.should_proceed(user_prompt): + if interactive: + self.console.message("Not upgrading the application object.") + raise typer.Exit(0) + else: + self.console.message( + "Cannot upgrade the application object non-interactively without --force." ) + raise typer.Exit(1) + try: + cascade_msg = " (cascade)" if cascade else "" + self.console.step( + f"Dropping application object {self.name}{cascade_msg}." + ) + cascade_sql = " cascade" if cascade else "" + sql_executor.execute_query(f"drop application {self.name}{cascade_sql}") except ProgrammingError as err: - if err.errno != APPLICATION_NO_LONGER_AVAILABLE: + if err.errno == APPLICATION_OWNS_EXTERNAL_OBJECTS and not cascade: + # We need to cascade the deletion, let's try again (only if we didn't try with cascade already) + return self.drop_application_before_upgrade( + policy=policy, + interactive=interactive, + cascade=True, + ) + else: generic_sql_error_handler(err) - console.warning( - "The application owns other objects but they could not be determined." - ) - user_prompt = "Do you want the Snowflake CLI to drop these objects, then drop the existing application object and recreate it?" - else: - user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?" - - if not policy.should_proceed(user_prompt): - if interactive: - console.message("Not upgrading the application object.") - raise typer.Exit(0) - else: - console.message( - "Cannot upgrade the application object non-interactively without --force." - ) - raise typer.Exit(1) - try: - cascade_msg = " (cascade)" if cascade else "" - console.step(f"Dropping application object {self.name}{cascade_msg}.") - cascade_sql = " cascade" if cascade else "" - sql_executor = get_sql_executor() - sql_executor.execute_query(f"drop application {self.name}{cascade_sql}") - except ProgrammingError as err: - if err.errno == APPLICATION_OWNS_EXTERNAL_OBJECTS and not cascade: - # We need to cascade the deletion, let's try again (only if we didn't try with cascade already) - return self.drop_application_before_upgrade( - policy=policy, - interactive=interactive, - cascade=True, - ) - else: - generic_sql_error_handler(err) def get_events( self, diff --git a/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py b/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py index 2664691f21..e4f34f0cb1 100644 --- a/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py +++ b/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Optional from snowflake.cli._plugins.nativeapp.constants import ( @@ -8,25 +9,15 @@ ApplicationCreatedExternallyError, ) from snowflake.cli._plugins.stage.manager import StageManager +from snowflake.cli.api.project.util import to_identifier +@dataclass class SameAccountInstallMethod: _requires_created_by_cli: bool - _from_release_directive: bool - version: Optional[str] - patch: Optional[int] - - def __init__( - self, - requires_created_by_cli: bool, - version: Optional[str] = None, - patch: Optional[int] = None, - from_release_directive: bool = False, - ): - self._requires_created_by_cli = requires_created_by_cli - self.version = version - self.patch = patch - self._from_release_directive = from_release_directive + version: Optional[str] = None + patch: Optional[int] = None + _from_release_directive: bool = False @classmethod def unversioned_dev(cls): @@ -39,7 +30,7 @@ def versioned_dev(cls, version: str, patch: Optional[int] = None): @classmethod def release_directive(cls): - return cls(False, from_release_directive=True) + return cls(False, _from_release_directive=True) @property def is_dev_mode(self) -> bool: @@ -53,8 +44,9 @@ def using_clause( return "" if self.version: + version_clause = f"version {to_identifier(self.version)}" patch_clause = f"patch {self.patch}" if self.patch else "" - return f"using version {self.version} {patch_clause}" + return f"using {version_clause} {patch_clause}" stage_name = StageManager.quote_stage_name(stage_fqn) return f"using {stage_name}" diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py index 1b0d7c722c..fcb2c88010 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py @@ -11,12 +11,83 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + from typing import NoReturn from click import ClickException from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType +from snowflake.cli.api.errno import ( + APPLICATION_FILE_NOT_FOUND_ON_STAGE, + APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT, + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE, + APPLICATION_NO_LONGER_AVAILABLE, + APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS, + APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, + APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND, + APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST, + CANNOT_GRANT_NON_MANIFEST_PRIVILEGE, + CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE, + CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE, + CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, + NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, + NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX, + NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY, + NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, + NO_REFERENCE_SET_FOR_DEFINITION, + NO_VERSIONS_AVAILABLE_FOR_ACCOUNT, + NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + ROLE_NOT_ASSIGNED, + SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND, + SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW, + SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL, + VIEW_EXPANSION_FAILED, +) from snowflake.connector import DatabaseError, Error, ProgrammingError +# Reasons why an `alter application ... upgrade` might fail +UPGRADE_RESTRICTION_CODES = { + CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, + ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + APPLICATION_NO_LONGER_AVAILABLE, +} + +CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES = { + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, + APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE, + # when setup script/manifest/readme isn't on the stage + APPLICATION_FILE_NOT_FOUND_ON_STAGE, + NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, + SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND, + # user tried to clone tables and it failed + VIEW_EXPANSION_FAILED, + # user tried to do something with a role that wasn't assigned to them + ROLE_NOT_ASSIGNED, + APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND, + SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL, + APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST, + APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, + SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW, + NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY, + CANNOT_GRANT_NON_MANIFEST_PRIVILEGE, + NO_REFERENCE_SET_FOR_DEFINITION, + NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX, + CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE, + APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND, + # user tried installing from release directive and there are none available + NO_VERSIONS_AVAILABLE_FOR_ACCOUNT, + APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, + APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT, + APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS, + CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE, +} + def handle_unclassified_error(err: Error | Exception, context: str) -> NoReturn: """ @@ -115,3 +186,12 @@ def __init__( if role: message += f" using role: {role}" super().__init__(message) + + +class UpgradeApplicationRestrictionError(UserInputError): + """ + Raised when an alter application ... upgrade fails due to user error. + Must be caught and handled by the caller of an upgrade_application + """ + + pass diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index b8cd77dbee..569898ed2d 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -18,20 +18,37 @@ from textwrap import dedent from typing import Any, Dict, List +from snowflake.cli._plugins.nativeapp.constants import ( + AUTHORIZE_TELEMETRY_COL, + NAME_COL, + SPECIAL_COMMENT, +) +from snowflake.cli._plugins.nativeapp.same_account_install_method import ( + SameAccountInstallMethod, +) from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( + CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES, + UPGRADE_RESTRICTION_CODES, CouldNotUseObjectError, InsufficientPrivilegesError, UnexpectedResultError, + UpgradeApplicationRestrictionError, + UserInputError, UserScriptError, handle_unclassified_error, ) +from snowflake.cli.api.cli_global_context import get_cli_context +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.errno import ( + APPLICATION_REQUIRES_TELEMETRY_SHARING, + CANNOT_DISABLE_MANDATORY_TELEMETRY, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, ) from snowflake.cli.api.identifiers import FQN +from snowflake.cli.api.metrics import CLICounterField from snowflake.cli.api.project.util import ( identifier_to_show_like_pattern, is_valid_unquoted_identifier, @@ -39,12 +56,13 @@ to_quoted_identifier, to_string_literal, ) -from snowflake.cli.api.sql_execution import BaseSqlExecutor, SqlExecutor +from snowflake.cli.api.sql_execution import BaseSqlExecutor +from snowflake.cli.api.utils.cursor import find_first_row from snowflake.connector import DictCursor, ProgrammingError class SnowflakeSQLFacade: - def __init__(self, sql_executor: SqlExecutor | None = None): + def __init__(self, sql_executor: BaseSqlExecutor | None = None): self._sql_executor = ( sql_executor if sql_executor is not None else BaseSqlExecutor() ) @@ -136,6 +154,37 @@ def _use_schema_optional(self, schema_name: str | None): """ return self._use_object_optional(UseObjectType.SCHEMA, schema_name) + def grant_privileges_to_role( + self, + privileges: list[str], + object_type: ObjectType, + object_identifier: str, + role_to_grant: str, + role_to_use: str | None = None, + ) -> None: + """ + Grants one or more access privileges on a securable object to a role + + @param privileges: List of privileges to grant to a role + @param object_type: Type of snowflake object to grant to a role + @param object_identifier: Valid identifier of the snowflake object to grant to a role + @param role_to_grant: Name of the role to grant privileges to + @param [Optional] role_to_use: Name of the role to use to grant privileges + """ + comma_separated_privileges = ", ".join(privileges) + object_type_and_name = f"{object_type.value.sf_name} {object_identifier}" + + with self._use_role_optional(role_to_use): + try: + self._sql_executor.execute_query( + f"grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}" + ) + except Exception as err: + handle_unclassified_error( + err, + f"Failed to grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}.", + ) + def execute_user_script( self, queries: str, @@ -503,6 +552,197 @@ def show_release_directives( ) return cursor.fetchall() + def get_existing_app_info(self, name: str, role: str) -> dict | None: + """ + Check for an existing application object by the same name as in project definition, in account. + It executes a 'show applications like' query and returns the result as single row, if one exists. + """ + with self._use_role_optional(role): + try: + object_type_plural = ObjectType.APPLICATION.value.sf_plural_name + show_obj_query = f"show {object_type_plural} like {identifier_to_show_like_pattern(name)}".strip() + + show_obj_cursor = self._sql_executor.execute_query( + show_obj_query, cursor_class=DictCursor + ) + + show_obj_row = find_first_row( + show_obj_cursor, lambda row: _same_identifier(row[NAME_COL], name) + ) + except Exception as err: + handle_unclassified_error( + err, f"Unable to fetch information on application {name}." + ) + return show_obj_row + + def upgrade_application( + self, + name: str, + install_method: SameAccountInstallMethod, + stage_fqn: str, + role: str, + warehouse: str, + debug_mode: bool | None, + should_authorize_event_sharing: bool | None, + ) -> list[tuple[str]]: + """ + Upgrades an application object using the provided clauses + + @param name: Name of the application object + @param install_method: Method of installing the application + @param stage_fqn: FQN of the stage housing the application artifacts + @param role: Role to use when creating the application and provider-side objects + @param warehouse: Warehouse which is required to create an application object + @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled + @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled + """ + install_method.ensure_app_usable( + app_name=name, + app_role=role, + show_app_row=self.get_existing_app_info(name, role), + ) + # If all the above checks are in order, proceed to upgrade + + with self._use_role_optional(role), self._use_warehouse_optional(warehouse): + try: + using_clause = install_method.using_clause(stage_fqn) + upgrade_cursor = self._sql_executor.execute_query( + f"alter application {name} upgrade {using_clause}", + ) + + # if debug_mode is present (controlled), ensure it is up-to-date + if install_method.is_dev_mode: + if debug_mode is not None: + self._sql_executor.execute_query( + f"alter application {name} set debug_mode = {debug_mode}" + ) + except ProgrammingError as err: + if err.errno in UPGRADE_RESTRICTION_CODES: + raise UpgradeApplicationRestrictionError(err.msg) from err + elif ( + err.errno in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES + ): + raise UserInputError( + f"Failed to upgrade application {name} with the following error message:\n" + f"{err.msg}" + ) from err + handle_unclassified_error(err, f"Failed to upgrade application {name}.") + except Exception as err: + handle_unclassified_error(err, f"Failed to upgrade application {name}.") + + try: + # Only update event sharing if the current value is different as the one we want to set + if should_authorize_event_sharing is not None: + current_authorize_event_sharing = ( + self.get_app_properties(name, role) + .get(AUTHORIZE_TELEMETRY_COL, "false") + .lower() + == "true" + ) + if ( + current_authorize_event_sharing + != should_authorize_event_sharing + ): + self._log.info( + "Setting telemetry sharing authorization to %s", + should_authorize_event_sharing, + ) + self._sql_executor.execute_query( + f"alter application {name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}" + ) + except ProgrammingError as err: + if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: + get_cli_context().metrics.set_counter( + CLICounterField.EVENT_SHARING_ERROR, 1 + ) + raise UserInputError( + "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) from err + handle_unclassified_error( + err, + f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name}.", + ) + except Exception as err: + handle_unclassified_error( + err, + f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name}.", + ) + + return upgrade_cursor.fetchall() + + def create_application( + self, + name: str, + package_name: str, + install_method: SameAccountInstallMethod, + stage_fqn: str, + role: str, + warehouse: str, + debug_mode: bool | None, + should_authorize_event_sharing: bool | None, + ) -> list[tuple[str]]: + """ + Creates a new application object using an application package, + running the setup script of the application package + + @param name: Name of the application object + @param package_name: Name of the application package to install the application from + @param install_method: Method of installing the application + @param stage_fqn: FQN of the stage housing the application artifacts + @param role: Role to use when creating the application and provider-side objects + @param warehouse: Warehouse which is required to create an application object + @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled + @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled + """ + + # by default, applications are created in debug mode when possible; + # this can be overridden in the project definition + debug_mode_clause = "" + if install_method.is_dev_mode: + initial_debug_mode = debug_mode if debug_mode is not None else True + debug_mode_clause = f"debug_mode = {initial_debug_mode}" + + authorize_telemetry_clause = "" + if should_authorize_event_sharing is not None: + self._log.info( + "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s", + should_authorize_event_sharing, + ) + authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}" + + using_clause = install_method.using_clause(stage_fqn) + with self._use_role_optional(role), self._use_warehouse_optional(warehouse): + try: + create_cursor = self._sql_executor.execute_query( + dedent( + f"""\ + create application {name} + from application package {package_name} {using_clause} {debug_mode_clause}{authorize_telemetry_clause} + comment = {SPECIAL_COMMENT} + """ + ), + ) + except ProgrammingError as err: + if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: + get_cli_context().metrics.set_counter( + CLICounterField.EVENT_SHARING_ERROR, 1 + ) + raise UserInputError( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) from err + elif ( + err.errno in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES + ): + raise UserInputError( + f"Failed to create application {name} with the following error message:\n" + f"{err.msg}" + ) from err + handle_unclassified_error(err, f"Failed to create application {name}.") + except Exception as err: + handle_unclassified_error(err, f"Failed to create application {name}.") + + return create_cursor.fetchall() + # TODO move this to src/snowflake/cli/api/project/util.py in a separate # PR since it's codeowned by the CLI team diff --git a/src/snowflake/cli/api/constants.py b/src/snowflake/cli/api/constants.py index 2e16e9abdd..d6a3e27f6e 100644 --- a/src/snowflake/cli/api/constants.py +++ b/src/snowflake/cli/api/constants.py @@ -62,6 +62,10 @@ class ObjectType(Enum): "image-repository", "image repository", "image repositories" ) GIT_REPOSITORY = ObjectNames("git-repository", "git repository", "git repositories") + APPLICATION = ObjectNames("application", "application", "applications") + APPLICATION_PACKAGE = ObjectNames( + "application-package", "application package", "application packages" + ) def __str__(self): """This makes using this Enum easier in formatted string""" @@ -69,7 +73,11 @@ def __str__(self): OBJECT_TO_NAMES = {o.value.cli_name: o.value for o in ObjectType} -SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys()) +UNSUPPORTED_OBJECTS = { + ObjectType.APPLICATION.value.cli_name, + ObjectType.APPLICATION_PACKAGE.value.cli_name, +} +SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys() - UNSUPPORTED_OBJECTS) # Scope names here must replace spaces with '-'. For example 'compute pool' is 'compute-pool'. VALID_SCOPES = ["database", "schema", "compute-pool"] diff --git a/src/snowflake/cli/api/entities/utils.py b/src/snowflake/cli/api/entities/utils.py index 4c5b8b0c78..400fe726f4 100644 --- a/src/snowflake/cli/api/entities/utils.py +++ b/src/snowflake/cli/api/entities/utils.py @@ -42,7 +42,6 @@ ) from snowflake.cli.api.secure_path import UNLIMITED, SecurePath from snowflake.connector import ProgrammingError -from snowflake.connector.cursor import SnowflakeCursor def generic_sql_error_handler(err: ProgrammingError) -> NoReturn: @@ -325,17 +324,15 @@ def drop_generic_object( console.message(f"Dropped {object_type} {object_name} successfully.") -def print_messages( - console: AbstractConsole, create_or_upgrade_cursor: Optional[SnowflakeCursor] -): +def print_messages(console: AbstractConsole, cursor_results: list[tuple[str]]): """ Shows messages in the console returned by the CREATE or UPGRADE APPLICATION command. """ - if not create_or_upgrade_cursor: + if not cursor_results: return - messages = [row[0] for row in create_or_upgrade_cursor.fetchall()] + messages = [row[0] for row in cursor_results] for message in messages: console.warning(message) console.message("") diff --git a/src/snowflake/cli/api/errno.py b/src/snowflake/cli/api/errno.py index bfe4942992..c49fd167fa 100644 --- a/src/snowflake/cli/api/errno.py +++ b/src/snowflake/cli/api/errno.py @@ -14,17 +14,59 @@ # General errors NO_WAREHOUSE_SELECTED_IN_SESSION = 606 +EMPTY_SQL_STATEMENT = 900 -DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003 -DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = 2043 -INSUFFICIENT_PRIVILEGES = 3001 +SQL_COMPILATION_ERROR = 1003 +OBJECT_ALREADY_EXISTS_IN_DOMAIN = 1998 +OBJECT_ALREADY_EXISTS = 2002 +DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003 # BASE_TABLE_OR_VIEW_NOT_FOUND +DUPLICATE_COLUMN_NAME = 2025 +VIEW_EXPANSION_FAILED = 2037 +DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = ( + 2043 # OBJECT_DOES_NOT_EXIST_OR_CANNOT_PERFORM_OPERATION +) +INSUFFICIENT_PRIVILEGES = 3001 # NOT_AUTHORIZED +INVALID_OBJECT_TYPE_FOR_SPECIFIED_PRIVILEGE = 3008 +ROLE_NOT_ASSIGNED = 3013 +NO_INDIVIDUAL_PRIVS = 3028 +OBJECT_ALREADY_EXISTS_NO_PRIVILEGES = 3041 # Native Apps +APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND = 93003 +APPLICATION_FILE_NOT_FOUND_ON_STAGE = 93009 +CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE = 93011 +CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE = 93012 +APPLICATION_PACKAGE_VERSION_ALREADY_EXISTS = 93030 +APPLICATION_PACKAGE_VERSION_NAME_TOO_LONG = 93035 +APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST = 93036 +APPLICATION_PACKAGE_MAX_VERSIONS_HIT = 93037 CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION = 93044 CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES = 93045 ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93046 +NO_VERSIONS_AVAILABLE_FOR_ACCOUNT = 93054 NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93055 APPLICATION_NO_LONGER_AVAILABLE = 93079 +APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT = 93082 +APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE = 93083 +APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT = 93084 +APPLICATION_PACKAGE_CANNOT_DROP_VERSION_IF_IT_IS_IN_USE = 93088 +APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE = 93148 +CANNOT_GRANT_NON_MANIFEST_PRIVILEGE = 93118 APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128 +APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS = 93168 +APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS = 93197 +NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD = 93301 +NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY = 93302 +NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR = 93303 +NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX = 93300 APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321 CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329 + +ERR_JAVASCRIPT_EXECUTION = 100132 + +SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL = 397007 +SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND = 397012 +SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW = 397013 + +NO_REFERENCE_SET_FOR_DEFINITION = 505019 +NO_ACTIVE_REF_DEFINITION_WITH_REF_NAME_IN_APPLICATION = 505026 diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index c26285f9b8..463e58719e 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -19,9 +19,6 @@ import pytest from click import ClickException from snowflake.cli._plugins.connection.util import UIParameter -from snowflake.cli._plugins.nativeapp.constants import ( - SPECIAL_COMMENT, -) from snowflake.cli._plugins.nativeapp.entities.application import ( ApplicationEntity, ApplicationEntityModel, @@ -39,9 +36,11 @@ from snowflake.cli._plugins.nativeapp.same_account_install_method import ( SameAccountInstallMethod, ) +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import UserInputError from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext from snowflake.cli.api.console import cli_console as cc from snowflake.cli.api.console.abc import AbstractConsole +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.errno import ( APPLICATION_REQUIRES_TELEMETRY_SHARING, CANNOT_DISABLE_MANDATORY_TELEMETRY, @@ -59,13 +58,23 @@ mock_connection, ) from tests.nativeapp.utils import ( - APP_ENTITY_GET_EXISTING_APP_INFO, GET_UI_PARAMETERS, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_CREATE_APPLICATION, + SQL_FACADE_GET_EXISTING_APP_INFO, + SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE, + SQL_FACADE_UPGRADE_APPLICATION, mock_execute_helper, + mock_side_effect_error_with_cause, ) from tests.testing_utils.fixtures import MockConnectionCtx +DEFAULT_APP_ID = "myapp" +DEFAULT_PKG_ID = "app_pkg" +DEFAULT_STAGE_FQN = "app_pkg.app_src.stage" +DEFAULT_SUCCESS_MESSAGE = "Application successfully upgraded." +DEFAULT_USER_INPUT_ERROR_MESSAGE = "User input error message." + allow_always_policy = AllowAlwaysPolicy() ask_always_policy = AskAlwaysPolicy() deny_always_policy = DenyAlwaysPolicy() @@ -187,6 +196,8 @@ def _setup_project( def _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -194,24 +205,24 @@ def _setup_mocks_for_app( expected_shared_events=None, is_prod=False, is_upgrade=False, - existing_app_flag=False, events_definitions_in_app=None, - programming_errno=None, + error_raised=None, ): if is_upgrade: return _setup_mocks_for_upgrade_app( + mock_sql_facade_upgrade_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, expected_authorize_telemetry_flag=expected_authorize_telemetry_flag, expected_shared_events=expected_shared_events, is_prod=is_prod, - existing_app_flag=existing_app_flag, events_definitions_in_app=events_definitions_in_app, - programming_errno=programming_errno, + error_raised=error_raised, ) else: return _setup_mocks_for_create_app( + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -219,11 +230,12 @@ def _setup_mocks_for_app( expected_shared_events=expected_shared_events, is_prod=is_prod, events_definitions_in_app=events_definitions_in_app, - programming_errno=programming_errno, + error_raised=error_raised, ) def _setup_mocks_for_create_app( + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -231,64 +243,16 @@ def _setup_mocks_for_create_app( expected_shared_events=None, events_definitions_in_app=None, is_prod=False, - programming_errno=None, + error_raised=None, ): mock_get_existing_app_info.return_value = None - authorize_telemetry_clause = "" - if expected_authorize_telemetry_flag is not None: - authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {expected_authorize_telemetry_flag}".upper() - install_clause = "using @app_pkg.app_src.stage debug_mode = True" - if is_prod: - install_clause = " " - calls = [ ( mock_cursor([("old_role",)], []), mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - (ProgrammingError(errno=programming_errno) if programming_errno else None), - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg {install_clause}{authorize_telemetry_clause} - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), ( mock_cursor( events_definitions_in_app or [], ["name", "type", "sharing", "status"] @@ -298,30 +262,82 @@ def _setup_mocks_for_create_app( cursor_class=DictCursor, ), ), + (None, mock.call("use role old_role")), ] if expected_shared_events is not None: - calls.append( - ( - None, - mock.call( - f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + calls.extend( + [ + ( + mock_cursor([("old_role",)], []), + mock.call("select current_role()"), ), - ), + (None, mock.call("use role app_role")), + ( + None, + mock.call( + f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + ), + ), + (None, mock.call("use role old_role")), + ] ) - calls.extend( - [ - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) side_effects, mock_execute_query_expected = mock_execute_helper(calls) mock_execute_query.side_effect = side_effects - return mock_execute_query_expected + + mock_sql_facade_create_application.side_effect = error_raised or mock_cursor( + [[(DEFAULT_SUCCESS_MESSAGE,)]], [] + ) + + mock_sql_facade_create_application_expected = [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.release_directive() + if is_prod + else SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=None, + should_authorize_event_sharing=expected_authorize_telemetry_flag, + role="app_role", + warehouse="app_warehouse", + ) + ] + + mock_sql_facade_grant_privileges_to_role_expected = [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + return [ + *mock_execute_query_expected, + *mock_sql_facade_create_application_expected, + *mock_sql_facade_grant_privileges_to_role_expected, + ] def _setup_mocks_for_upgrade_app( + mock_sql_facade_upgrade_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -329,15 +345,12 @@ def _setup_mocks_for_upgrade_app( expected_shared_events=None, events_definitions_in_app=None, is_prod=False, - existing_app_flag=False, - programming_errno=None, + error_raised=None, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "comment": "GENERATED_BY_SNOWFLAKECLI", } - install_clause = "using @app_pkg.app_src.stage" - if is_prod: - install_clause = "" + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result calls = [ ( @@ -345,16 +358,6 @@ def _setup_mocks_for_upgrade_app( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - (None, mock.call(f"alter application myapp upgrade {install_clause}")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), ( mock_cursor( events_definitions_in_app or [], ["name", "type", "sharing", "status"] @@ -364,63 +367,53 @@ def _setup_mocks_for_upgrade_app( cursor_class=DictCursor, ), ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor( - [ - { - "property": "authorize_telemetry_event_sharing", - "value": str(existing_app_flag).lower(), - } - ], - ["property", "value"], - ), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), + (None, mock.call("use role old_role")), ] - if expected_authorize_telemetry_flag is not None: - calls.append( - ( + if expected_shared_events is not None: + calls.extend( + [ ( - ProgrammingError(errno=programming_errno) - if programming_errno - else None + mock_cursor([("old_role",)], []), + mock.call("select current_role()"), ), - mock.call( - f"alter application myapp set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(expected_authorize_telemetry_flag).upper()}" - ), - ), - ) - - if expected_shared_events is not None: - calls.append( - ( - None, - mock.call( - f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + (None, mock.call("use role app_role")), + ( + None, + mock.call( + f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + ), ), - ), + (None, mock.call("use role old_role")), + ], ) - calls.extend( - [ - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) side_effects, mock_execute_query_expected = mock_execute_helper(calls) mock_execute_query.side_effect = side_effects - return mock_execute_query_expected + mock_sql_facade_upgrade_application.side_effect = error_raised or mock_cursor( + [[(DEFAULT_SUCCESS_MESSAGE,)]], [] + ) + mock_sql_facade_upgrade_application_expected = [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive() + if is_prod + else SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=None, + should_authorize_event_sharing=expected_authorize_telemetry_flag, + role="app_role", + warehouse="app_warehouse", + ) + ] + return [*mock_execute_query_expected, *mock_sql_facade_upgrade_application_expected] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -459,6 +452,9 @@ def test_event_sharing_disabled_no_change_to_current_behavior( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -467,7 +463,9 @@ def test_event_sharing_disabled_no_change_to_current_behavior( temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -487,11 +485,20 @@ def test_event_sharing_disabled_no_change_to_current_behavior( console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -521,6 +528,9 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -529,7 +539,9 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -550,13 +562,25 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_called_with( - "WARNING: Same-account event sharing is not enabled in your account, therefore, application telemetry section will be ignored." - ) + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + assert mock_console.warning.mock_calls == [ + mock.call( + "WARNING: Same-account event sharing is not enabled in your account, therefore, application telemetry section will be ignored." + ), + mock.call(DEFAULT_SUCCESS_MESSAGE), + ] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -585,6 +609,9 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -593,14 +620,15 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed expected_shared_events=[] if share_mandatory_events else None, ) mock_conn.return_value = MockConnectionCtx() @@ -616,11 +644,20 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -649,6 +686,9 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -657,14 +697,15 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, - expected_authorize_telemetry_flag=None, # make sure flag is not set again during upgrade + expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=share_mandatory_events, # existing app with same flag as target app expected_shared_events=[] if share_mandatory_events else None, ) mock_conn.return_value = MockConnectionCtx() @@ -680,11 +721,20 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -711,6 +761,9 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -719,14 +772,15 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed expected_shared_events=["ERRORS_AND_WARNINGS"], events_definitions_in_app=[ { @@ -750,11 +804,20 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert expected == [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -781,6 +844,9 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -789,16 +855,15 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, - expected_authorize_telemetry_flag=( - None if is_upgrade else share_mandatory_events - ), + expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=False, # we can't switch from True to False, so we assume False expected_shared_events=[] if share_mandatory_events else None, events_definitions_in_app=[ { @@ -822,14 +887,25 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - - mock_console.warning.assert_called_with( - "WARNING: Mandatory events are present in the application, but event sharing is not authorized in the application telemetry field. This will soon be required to set in order to deploy this application." - ) + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + assert mock_console.warning.mock_calls == [ + mock.call(DEFAULT_SUCCESS_MESSAGE), + mock.call( + "WARNING: Mandatory events are present in the application, but event sharing is not authorized in the application telemetry field. This will soon be required to set in order to deploy this application." + ), + ] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -856,6 +932,9 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -864,14 +943,15 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed expected_shared_events=[] if share_mandatory_events else None, ) mock_conn.return_value = MockConnectionCtx() @@ -887,11 +967,20 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -918,6 +1007,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -926,7 +1018,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -948,11 +1042,20 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -979,6 +1082,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -988,12 +1094,13 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused mock_cursor, ): mock_execute_query_expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed is_upgrade=is_upgrade, events_definitions_in_app=[ { @@ -1003,7 +1110,12 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused "status": "ENABLED", } ], - programming_errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + error_raised=mock_side_effect_error_with_cause( + UserInputError( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ), + ProgrammingError(errno=APPLICATION_REQUIRES_TELEMETRY_SHARING), + ), ) mock_conn.return_value = MockConnectionCtx() _setup_project( @@ -1026,7 +1138,10 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1053,6 +1168,9 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1061,13 +1179,14 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed is_upgrade=is_upgrade, events_definitions_in_app=[ { @@ -1077,7 +1196,12 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio "status": "ENABLED", } ], - programming_errno=CANNOT_DISABLE_MANDATORY_TELEMETRY, + error_raised=mock_side_effect_error_with_cause( + UserInputError( + "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ), + ProgrammingError(errno=CANNOT_DISABLE_MANDATORY_TELEMETRY), + ), ) mock_conn.return_value = MockConnectionCtx() _setup_project( @@ -1086,7 +1210,7 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio ) mock_console = MagicMock() - with pytest.raises(ClickException) as e: + with pytest.raises(UserInputError) as e: _create_or_upgrade_app( policy=MagicMock(), install_method=install_method, @@ -1100,7 +1224,10 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1126,6 +1253,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1134,7 +1264,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1156,12 +1288,23 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected expected_warning = "WARNING: Mandatory events are present in the manifest file. Automatically authorizing event sharing in dev mode. To suppress this warning, please add 'share_mandatory_events: true' in the application telemetry section." - mock_console.warning.assert_called_with(expected_warning) + assert mock_console.warning.mock_calls == [ + mock.call(expected_warning), + mock.call(DEFAULT_SUCCESS_MESSAGE), + ] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1187,6 +1330,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1195,7 +1341,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1210,7 +1358,12 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe "status": "ENABLED", } ], - programming_errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + error_raised=mock_side_effect_error_with_cause( + UserInputError( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ), + ProgrammingError(errno=APPLICATION_REQUIRES_TELEMETRY_SHARING), + ), ) mock_conn.return_value = MockConnectionCtx() _setup_project( @@ -1233,7 +1386,10 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1259,6 +1415,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1267,7 +1426,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1296,10 +1457,20 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe console=mock_console, ) - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1325,6 +1496,9 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1333,7 +1507,9 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1363,7 +1539,10 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1390,6 +1569,9 @@ def test_shared_events_with_authorization_then_success( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1399,7 +1581,9 @@ def test_shared_events_with_authorization_then_success( mock_cursor, ): shared_events = ["DEBUG_LOGS", "ERRORS_AND_WARNINGS"] - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1436,5 +1620,11 @@ def test_shared_events_with_authorization_then_success( console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) diff --git a/tests/nativeapp/test_manager.py b/tests/nativeapp/test_manager.py index 35bf4f0c10..437bede8fb 100644 --- a/tests/nativeapp/test_manager.py +++ b/tests/nativeapp/test_manager.py @@ -46,6 +46,7 @@ ObjectPropertyNotFoundError, SetupScriptFailedValidation, ) +from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade from snowflake.cli._plugins.stage.diff import ( DiffResult, StagePathType, @@ -521,7 +522,7 @@ def test_get_existing_app_info_app_exists( dm = _get_dm() app_model: ApplicationEntityModel = dm.project_definition.entities["myapp"] app = ApplicationEntity(app_model, workspace_context) - show_obj_row = app.get_existing_app_info() + show_obj_row = get_snowflake_facade().get_existing_app_info(app.name, app.role) assert show_obj_row is not None assert show_obj_row[NAME_COL] == "MYAPP" assert mock_execute.mock_calls == expected @@ -557,7 +558,7 @@ def test_get_existing_app_info_app_does_not_exist( dm = _get_dm() app_model: ApplicationEntityModel = dm.project_definition.entities["myapp"] app = ApplicationEntity(app_model, workspace_context) - show_obj_row = app.get_existing_app_info() + show_obj_row = get_snowflake_facade().get_existing_app_info(app.name, app.role) assert show_obj_row is None assert mock_execute.mock_calls == expected diff --git a/tests/nativeapp/test_run_processor.py b/tests/nativeapp/test_run_processor.py index 9a91c9f88b..dc6df30b4b 100644 --- a/tests/nativeapp/test_run_processor.py +++ b/tests/nativeapp/test_run_processor.py @@ -22,6 +22,7 @@ from click import UsageError from snowflake.cli._plugins.connection.util import UIParameter from snowflake.cli._plugins.nativeapp.constants import ( + COMMENT_COL, LOOSE_FILES_MAGIC_VERSION, SPECIAL_COMMENT, ) @@ -46,6 +47,12 @@ from snowflake.cli._plugins.nativeapp.same_account_install_method import ( SameAccountInstallMethod, ) +from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( + CouldNotUseObjectError, + UpgradeApplicationRestrictionError, + UserInputError, +) from snowflake.cli._plugins.stage.diff import DiffResult from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext from snowflake.cli._plugins.workspace.manager import WorkspaceManager @@ -57,11 +64,10 @@ APPLICATION_NO_LONGER_AVAILABLE, APPLICATION_OWNS_EXTERNAL_OBJECTS, CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, + DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, -) -from snowflake.cli.api.exceptions import ( - CouldNotUseObjectError, - NoWarehouseSelectedInSessionError, + NO_WAREHOUSE_SELECTED_IN_SESSION, ) from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.connector import ProgrammingError @@ -71,12 +77,17 @@ mock_connection, ) from tests.nativeapp.utils import ( - APP_ENTITY_GET_EXISTING_APP_INFO, APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, GET_UI_PARAMETERS, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_CREATE_APPLICATION, + SQL_FACADE_GET_EVENT_DEFINITIONS, + SQL_FACADE_GET_EXISTING_APP_INFO, + SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE, + SQL_FACADE_UPGRADE_APPLICATION, TYPER_CONFIRM, mock_execute_helper, + mock_side_effect_error_with_cause, quoted_override_yml_file_v2, ) from tests.testing_utils.files_and_dirs import create_named_file @@ -113,12 +124,27 @@ def _get_wm(): ) +DEFAULT_APP_ID = "myapp" +DEFAULT_PKG_ID = "app_pkg" +DEFAULT_STAGE_FQN = "app_pkg.app_src.stage" +DEFAULT_UPGRADE_SUCCESS_MESSAGE = "Application successfully upgraded." +DEFAULT_CREATE_SUCCESS_MESSAGE = f"Application '{DEFAULT_APP_ID}' created successfully." +DEFAULT_USER_INPUT_ERROR_MESSAGE = "User input error message." +DEFAULT_GET_EXISTING_APP_INFO_RESULT = { + "name": "myapp", + "comment": SPECIAL_COMMENT, + "owner": "app_role", +} +DEFAULT_ROLE = "app_role" +DEFAULT_WAREHOUSE = "app_warehouse" + + def _create_or_upgrade_app( policy: PolicyBase, install_method: SameAccountInstallMethod, interactive: bool = False, - package_id: str = "app_pkg", - app_id: str = "myapp", + package_id: str = DEFAULT_PKG_ID, + app_id: str = DEFAULT_APP_ID, console: AbstractConsole | None = None, ): dm = DefinitionManager() @@ -199,7 +225,9 @@ def setup_project_file(current_working_directory: str, pdf=None): # Test create_dev_app with exception thrown trying to use the warehouse -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -209,34 +237,18 @@ def setup_project_file(current_working_directory: str, pdf=None): }, ) def test_create_dev_app_w_warehouse_access_exception( - mock_param, mock_conn, mock_execute, temp_dir, mock_cursor + mock_param, + mock_conn, + mock_sql_facade_grant_privileges_to_role, + mock_get_existing_app_info, + mock_sql_facade_create_application, + temp_dir, + mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - ( - CouldNotUseObjectError( - object_type=ObjectType.WAREHOUSE, name="app_warehouse" - ), - mock.call("use warehouse app_warehouse"), - ), - ( - None, - mock.call("use warehouse old_wh"), - ), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = CouldNotUseObjectError( + object_type=UseObjectType.WAREHOUSE, name="app_warehouse" + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -249,16 +261,49 @@ def test_create_dev_app_w_warehouse_access_exception( install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected assert ( "Could not use warehouse app_warehouse. Object does not exist, or operation cannot be performed." in err.value.message ) + mock_sql_facade_create_application.assert_called_once_with( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] # Test create_dev_app with no existing application AND create succeeds AND app role == package role -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -270,52 +315,16 @@ def test_create_dev_app_w_warehouse_access_exception( def test_create_dev_app_create_new_w_no_additional_privileges( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() @@ -326,12 +335,28 @@ def test_create_dev_app_create_new_w_no_additional_privileges( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with no existing application AND create returns a warning -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -355,97 +380,21 @@ def test_create_dev_app_create_new_w_no_additional_privileges( def test_create_or_upgrade_dev_app_with_warning( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, existing_app_info, ): status_messages = ["App created/upgraded", "Warning: some warning"] - status_cursor = mock_cursor( - [(msg,) for msg in status_messages], - ["status"], - ) - create_or_upgrade_calls = ( - [ - ( - status_cursor, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ] - if existing_app_info is None - else [ - ( - status_cursor, - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("alter application myapp set debug_mode = True")), - ] - ) + status_cursor_results = [(msg,) for msg in status_messages] mock_get_existing_app_info.return_value = existing_app_info - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - *create_or_upgrade_calls, - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.return_value = status_cursor_results + mock_sql_facade_upgrade_application.return_value = status_cursor_results mock_diff_result = DiffResult() setup_project_file(os.getcwd(), test_pdf.replace("package_role", "app_role")) @@ -457,14 +406,46 @@ def test_create_or_upgrade_dev_app_with_warning( install_method=SameAccountInstallMethod.unversioned_dev(), console=mock_console, ) - assert mock_execute.mock_calls == expected + if existing_app_info is None: + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_upgrade_application.assert_not_called() + else: + mock_sql_facade_create_application.assert_not_called() + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) mock_console.warning.assert_has_calls([mock.call(msg) for msg in status_messages]) # Test create_dev_app with no existing application AND create succeeds AND app role != package role -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -476,72 +457,17 @@ def test_create_or_upgrade_dev_app_with_warning( def test_create_dev_app_create_new_with_additional_privileges( mock_param, mock_conn, - mock_execute_query, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, mock_execute_query_expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute_query.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -550,12 +476,49 @@ def test_create_dev_app_create_new_with_additional_privileges( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute_query.mock_calls == mock_execute_query_expected + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with no existing application AND create throws an exception -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -567,63 +530,46 @@ def test_create_dev_app_create_new_with_additional_privileges( def test_create_dev_app_create_new_w_missing_warehouse_exception( mock_param, mock_conn, - mock_execute, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - NoWarehouseSelectedInSessionError( - msg="No active warehouse selected in the current session" - ), - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) - mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError("No active warehouse selected in the current session"), + cause=ProgrammingError(errno=NO_WAREHOUSE_SELECTED_IN_SESSION), + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd(), test_pdf.replace("package_role", "app_role")) assert not mock_diff_result.has_changes() - with pytest.raises(NoWarehouseSelectedInSessionError) as err: + with pytest.raises(UserInputError) as err: _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert "Please provide a warehouse for the active session role" in err.value.message - assert mock_execute.mock_calls == expected + assert err.match("No active warehouse selected in the current session") + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test create_dev_app with existing application AND bad comment AND good version # Test create_dev_app with existing application AND bad comment AND bad version -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -656,24 +602,7 @@ def test_create_dev_app_incorrect_properties( "version": version, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -685,12 +614,15 @@ def test_create_dev_app_incorrect_properties( install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected + assert mock_get_existing_app_info.mock_calls == [ + mock.call(DEFAULT_APP_ID, DEFAULT_ROLE), + mock.call(DEFAULT_APP_ID, DEFAULT_ROLE), + ] # Test create_dev_app with existing application AND incorrect owner -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -702,7 +634,7 @@ def test_create_dev_app_incorrect_properties( def test_create_dev_app_incorrect_owner( mock_param, mock_conn, - mock_execute, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -713,50 +645,43 @@ def test_create_dev_app_incorrect_owner( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "wrong_owner", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - msg="Insufficient privileges to operate on database", - errno=INSUFFICIENT_PRIVILEGES, - ), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError( + msg="Insufficient privileges to operate on database", + errno=INSUFFICIENT_PRIVILEGES, + ), + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) - with pytest.raises(ProgrammingError): + with pytest.raises(UserInputError): assert not mock_diff_result.has_changes() _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test create_dev_app with existing application AND diff has no changes -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch( GET_UI_PARAMETERS, return_value={ @@ -768,7 +693,8 @@ def test_create_dev_app_incorrect_owner( def test_create_dev_app_no_diff_changes( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -779,53 +705,11 @@ def test_create_dev_app_no_diff_changes( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("alter application myapp set debug_mode = True")), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_cursor( + [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -834,12 +718,26 @@ def test_create_dev_app_no_diff_changes( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with existing application AND diff has changes -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -851,7 +749,8 @@ def test_create_dev_app_no_diff_changes( def test_create_dev_app_w_diff_changes( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -862,53 +761,11 @@ def test_create_dev_app_w_diff_changes( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("alter application myapp set debug_mode = True")), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_cursor( + [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult(different=["setup.sql"]) setup_project_file(os.getcwd()) @@ -917,12 +774,25 @@ def test_create_dev_app_w_diff_changes( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with existing application AND alter throws an error -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -934,7 +804,7 @@ def test_create_dev_app_w_diff_changes( def test_create_dev_app_recreate_w_missing_warehouse_exception( mock_param, mock_conn, - mock_execute, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -945,51 +815,30 @@ def test_create_dev_app_recreate_w_missing_warehouse_exception( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - NoWarehouseSelectedInSessionError( - msg="No active warehouse selected in the current session" - ), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError("No active warehouse selected in the current session"), + cause=ProgrammingError(errno=NO_WAREHOUSE_SELECTED_IN_SESSION), + ) mock_diff_result = DiffResult(different=["setup.sql"]) setup_project_file(os.getcwd()) assert mock_diff_result.has_changes() - with pytest.raises(NoWarehouseSelectedInSessionError) as err: + with pytest.raises(UserInputError) as err: _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected - assert "Please provide a warehouse for the active session role" in err.value.message + assert err.match("No active warehouse selected in the current session") # Test create_dev_app with no existing application AND quoted name scenario 1 -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1001,52 +850,16 @@ def test_create_dev_app_recreate_w_missing_warehouse_exception( def test_create_dev_app_create_new_quoted( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - dedent( - f"""\ - create application "My Application" - from application package "My Package" using '@"My Package".app_src.stage' debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - 'show telemetry event definitions in application "My Application"', - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() pdf_content = dedent( @@ -1084,12 +897,27 @@ def test_create_dev_app_create_new_quoted( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name='"My Application"', + package_name='"My Package"', + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn='"My Package".app_src.stage', + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + '"My Application"', DEFAULT_ROLE + ) # Test create_dev_app with no existing application AND quoted name scenario 2 -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS, return_value=[]) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1101,52 +929,16 @@ def test_create_dev_app_create_new_quoted( def test_create_dev_app_create_new_quoted_override( mock_param, mock_conn, - mock_execute, + mock_sql_facade_create_application, + mock_sql_facade_get_event_definitions, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - dedent( - f"""\ - create application "My Application" - from application package "My Package" using '@"My Package".app_src.stage' debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - 'show telemetry event definitions in application "My Application"', - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() current_working_directory = os.getcwd() @@ -1163,7 +955,19 @@ def test_create_dev_app_create_new_quoted_override( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + mock_sql_facade_create_application.assert_called_once_with( + name='"My Application"', + package_name='"My Package"', + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn='"My Package".app_src.stage', + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + mock_sql_facade_get_event_definitions.assert_called_once_with( + '"My Application"', DEFAULT_ROLE + ) # Test run existing app info @@ -1171,8 +975,12 @@ def test_create_dev_app_create_new_quoted_override( # AND user wants to drop app # AND drop succeeds # AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1186,6 +994,10 @@ def test_create_dev_app_recreate_app_when_orphaned( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1195,6 +1007,7 @@ def test_create_dev_app_recreate_app_when_orphaned( "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } + side_effects, expected = mock_execute_helper( [ ( @@ -1202,74 +1015,78 @@ def test_create_dev_app_recreate_app_when_orphaned( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + ) + + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) + setup_project_file(os.getcwd()) _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test run existing app info @@ -1278,8 +1095,12 @@ def test_create_dev_app_recreate_app_when_orphaned( # AND drop requires cascade # AND drop succeeds # AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1293,6 +1114,10 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1302,7 +1127,7 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } - # side_effects, expected = mock_execute_helper( + side_effects, expected = mock_execute_helper( [ ( @@ -1310,17 +1135,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), ( ProgrammingError(errno=APPLICATION_OWNS_EXTERNAL_OBJECTS), mock.call("drop application myapp"), @@ -1329,6 +1143,10 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock_cursor([("app_role",)], []), mock.call("select current_role()"), ), + ( + mock_cursor([("app_role",)], []), + mock.call("select current_role()"), + ), ( mock_cursor( [ @@ -1339,55 +1157,18 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock.call("show objects owned by application myapp"), ), (None, mock.call("drop application myapp cascade")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1395,6 +1176,58 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test run existing app info @@ -1404,8 +1237,12 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( # AND we can't see which objects are owned by the app # AND drop succeeds # AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1419,6 +1256,10 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1428,6 +1269,7 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } + side_effects, expected = mock_execute_helper( [ ( @@ -1435,17 +1277,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), ( ProgrammingError(errno=APPLICATION_OWNS_EXTERNAL_OBJECTS), mock.call("drop application myapp"), @@ -1454,60 +1285,27 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje mock_cursor([("app_role",)], []), mock.call("select current_role()"), ), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call("show objects owned by application myapp"), - ), - (None, mock.call("drop application myapp cascade")), ( mock_cursor([("app_role",)], []), mock.call("select current_role()"), ), - (None, mock.call("use role package_role")), ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), + ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + mock.call("show objects owned by application myapp"), ), - (None, mock.call("use warehouse old_wh")), + (None, mock.call("drop application myapp cascade")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1515,10 +1313,63 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test upgrade app method for release directives AND throws warehouse error @mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch( + SQL_FACADE_GET_EXISTING_APP_INFO, return_value={COMMENT_COL: SPECIAL_COMMENT} +) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1531,7 +1382,13 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] ) def test_upgrade_app_warehouse_error( - mock_param, mock_conn, mock_execute, policy_param, temp_dir, mock_cursor + mock_param, + mock_conn, + mock_get_existing_app_info, + mock_execute, + policy_param, + temp_dir, + mock_cursor, ): side_effects, expected = mock_execute_helper( [ @@ -1542,17 +1399,11 @@ def test_upgrade_app_warehouse_error( (None, mock.call("use role app_role")), ( mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - ( - CouldNotUseObjectError( - object_type=ObjectType.WAREHOUSE, name="app_warehouse" - ), - mock.call("use warehouse app_warehouse"), + mock.call("select current_warehouse()"), ), ( - None, - mock.call("use warehouse old_wh"), + ProgrammingError(errno=DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED), + mock.call("use warehouse app_warehouse"), ), (None, mock.call("use role old_role")), ] @@ -1572,8 +1423,9 @@ def test_upgrade_app_warehouse_error( # Test upgrade app method for release directives AND existing app info AND bad owner +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1590,6 +1442,7 @@ def test_upgrade_app_incorrect_owner( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1599,46 +1452,41 @@ def test_upgrade_app_incorrect_owner( "comment": SPECIAL_COMMENT, "owner": "wrong_owner", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - msg="Insufficient privileges to operate on database", - errno=INSUFFICIENT_PRIVILEGES, - ), - mock.call("alter application myapp upgrade "), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError("Insufficient privileges to operate on database"), + cause=ProgrammingError( + errno=INSUFFICIENT_PRIVILEGES, msg="Some error message." + ), + ) setup_project_file(os.getcwd()) - with pytest.raises(ProgrammingError): + with pytest.raises(UserInputError) as err: _create_or_upgrade_app( policy=policy_param, interactive=True, install_method=SameAccountInstallMethod.release_directive(), ) - assert mock_execute.mock_calls == expected + err.match("Insufficient privileges to operate on database") + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test upgrade app method for release directives AND existing app info AND upgrade succeeds -@mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1654,7 +1502,8 @@ def test_upgrade_app_succeeds( mock_param, mock_conn, mock_get_existing_app_info, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1664,47 +1513,11 @@ def test_upgrade_app_succeeds( "comment": SPECIAL_COMMENT, "owner": "app_role", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - (None, mock.call("alter application myapp upgrade ")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_cursor( + [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1713,12 +1526,23 @@ def test_upgrade_app_succeeds( interactive=True, install_method=SameAccountInstallMethod.release_directive(), ) - assert mock_execute.mock_calls == expected + mock_sql_facade_upgrade_application.assert_called_once_with( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test upgrade app method for release directives AND existing app info AND upgrade fails due to generic error -@mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1734,7 +1558,7 @@ def test_upgrade_app_fails_generic_error( mock_param, mock_conn, mock_get_existing_app_info, - mock_execute, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1744,47 +1568,41 @@ def test_upgrade_app_fails_generic_error( "comment": SPECIAL_COMMENT, "owner": "app_role", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=1234, - ), - mock.call("alter application myapp upgrade "), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError( + errno=1234, + ), + ) setup_project_file(os.getcwd()) - with pytest.raises(ProgrammingError): + with pytest.raises(UserInputError): _create_or_upgrade_app( policy=policy_param, interactive=True, install_method=SameAccountInstallMethod.release_directive(), ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is False AND --interactive is False # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is False AND --interactive is True AND user does not want to proceed # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is True AND user does not want to proceed +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=False ) @@ -1804,8 +1622,9 @@ def test_upgrade_app_fails_upgrade_restriction_error( mock_param, mock_conn, mock_typer_confirm, - mock_get_existing_app_info, mock_execute, + mock_get_existing_app_info, + mock_sql_facade_upgrade_application, policy_param, interactive, expected_code, @@ -1817,6 +1636,13 @@ def test_upgrade_app_fails_upgrade_restriction_error( "comment": SPECIAL_COMMENT, "owner": "app_role", } + + mock_conn.return_value = MockConnectionCtx() + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION), + ) + side_effects, expected = mock_execute_helper( [ ( @@ -1824,22 +1650,9 @@ def test_upgrade_app_fails_upgrade_restriction_error( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade "), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) - mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects setup_project_file(os.getcwd()) @@ -1851,11 +1664,27 @@ def test_upgrade_app_fails_upgrade_restriction_error( install_method=SameAccountInstallMethod.release_directive(), ) assert result.exit_code == expected_code + + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] assert mock_execute.mock_calls == expected +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS, return_value=[]) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1869,6 +1698,10 @@ def test_versioned_app_upgrade_to_unversioned( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1882,6 +1715,7 @@ def test_versioned_app_upgrade_to_unversioned( "owner": "app_role", "version": "v1", } + side_effects, expected = mock_execute_helper( [ ( @@ -1889,70 +1723,19 @@ def test_versioned_app_upgrade_to_unversioned( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - msg="Some Error Message.", - errno=93045, - ), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1963,12 +1746,60 @@ def test_versioned_app_upgrade_to_unversioned( ) assert mock_execute.mock_calls == expected + mock_sql_facade_upgrade_application.assert_called_once_with( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + + mock_sql_facade_create_application.assert_called_with( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with("myapp", DEFAULT_ROLE) + # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is True AND drop fails # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is False AND --interactive is True AND user wants to proceed AND drop fails # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is True AND user wants to proceed AND drop fails +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) @@ -1990,6 +1821,7 @@ def test_upgrade_app_fails_drop_fails( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, policy_param, interactive, temp_dir, @@ -2000,6 +1832,7 @@ def test_upgrade_app_fails_drop_fails( "comment": SPECIAL_COMMENT, "owner": "app_role", } + side_effects, expected = mock_execute_helper( [ ( @@ -2007,30 +1840,21 @@ def test_upgrade_app_fails_drop_fails( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade "), - ), ( ProgrammingError( errno=1234, ), mock.call("drop application myapp"), ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects - + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION), + ) setup_project_file(os.getcwd()) with pytest.raises(ProgrammingError): @@ -2040,11 +1864,26 @@ def test_upgrade_app_fails_drop_fails( install_method=SameAccountInstallMethod.release_directive(), ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test upgrade app method for release directives AND existing app info AND user wants to drop app AND drop succeeds AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) @@ -2063,6 +1902,10 @@ def test_upgrade_app_recreate_app( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, policy_param, temp_dir, mock_cursor, @@ -2072,6 +1915,7 @@ def test_upgrade_app_recreate_app( "comment": SPECIAL_COMMENT, "owner": "app_role", } + side_effects, expected = mock_execute_helper( [ ( @@ -2079,67 +1923,19 @@ def test_upgrade_app_recreate_app( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade "), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -2149,6 +1945,56 @@ def test_upgrade_app_recreate_app( install_method=SameAccountInstallMethod.release_directive(), ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test upgrade app method for version AND no existing version info @@ -2203,8 +2049,12 @@ def test_upgrade_app_from_version_throws_usage_error_two( APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, return_value={"key": "val"}, ) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) @@ -2223,6 +2073,10 @@ def test_upgrade_app_recreate_app_from_version( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_existing, policy_param, temp_dir, @@ -2241,67 +2095,18 @@ def test_upgrade_app_recreate_app_from_version( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade using version v1 "), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using version v1 debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = ( + UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE) + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -2321,6 +2126,57 @@ def test_upgrade_app_recreate_app_from_version( version="v1", ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.versioned_dev("v1"), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.versioned_dev("v1"), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test get_existing_version_info returns version info correctly diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index ec5c23cb50..f1d3de9bb2 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -17,6 +17,15 @@ from unittest.mock import _Call as Call import pytest +from snowflake.cli._plugins.nativeapp.constants import ( + AUTHORIZE_TELEMETRY_COL, + COMMENT_COL, + NAME_COL, + SPECIAL_COMMENT, +) +from snowflake.cli._plugins.nativeapp.same_account_install_method import ( + SameAccountInstallMethod, +) from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( CouldNotUseObjectError, @@ -24,15 +33,21 @@ InvalidSQLError, UnknownConnectorError, UnknownSQLError, + UserInputError, UserScriptError, ) from snowflake.cli._plugins.nativeapp.sf_sql_facade import ( SnowflakeSQLFacade, ) +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.errno import ( + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + APPLICATION_REQUIRES_TELEMETRY_SHARING, + CANNOT_DISABLE_MANDATORY_TELEMETRY, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, + SQL_COMPILATION_ERROR, ) from snowflake.connector import DatabaseError, DictCursor, Error from snowflake.connector.errors import ( @@ -44,10 +59,11 @@ from tests.nativeapp.utils import ( SQL_EXECUTOR_EXECUTE, SQL_EXECUTOR_EXECUTE_QUERIES, + assert_programmingerror_cause_with_errno, mock_execute_helper, ) -sql_facade = None +sql_facade = SnowflakeSQLFacade() @pytest.fixture(autouse=True) @@ -86,6 +102,22 @@ def mock_use_schema(): yield mock_use_schema +@pytest.fixture +def mock_get_app_properties(): + with mock.patch.object(sql_facade, "get_app_properties") as mock_get_app_properties: + mock_get_app_properties.return_value = {AUTHORIZE_TELEMETRY_COL: "false"} + yield mock_get_app_properties + + +@pytest.fixture +def mock_get_existing_app_info(): + with mock.patch.object( + sql_facade, "get_existing_app_info" + ) as mock_get_existing_app_info: + mock_get_existing_app_info.return_value = {COMMENT_COL: SPECIAL_COMMENT} + yield mock_get_existing_app_info + + @contextmanager def assert_in_context( mock_cms: list[tuple[mock.Mock, Call]], @@ -137,7 +169,7 @@ def reparent_mock(mock_instance, expected_call): # and add the return value's __exit__ method to the list of expected post-calls (in reverse order) expected_call = reparent_mock(mock_instance, expected_call) pre += [expected_call, expected_call.__enter__()] - post.insert(0, expected_call.__exit__(None, None, None)) + post.insert(0, expected_call.__exit__(mock.ANY, mock.ANY, mock.ANY)) for mock_instance, expected_call in inner_mocks: # Just add the modified expected_call to the list of assertions to be made within the context managers @@ -1637,3 +1669,666 @@ def test_create_stage_raises_insufficient_privileges_error( sql_facade.create_stage(stage, role=role, database=database) mock_execute_query.assert_has_calls(expected) + + +@pytest.mark.parametrize( + "args,expected_query", + [ + ( + { + "privileges": ["install", "develop"], + "object_type": ObjectType.APPLICATION_PACKAGE, + "object_identifier": "package_name", + "role_to_grant": "app_role", + "role_to_use": "package_role", + }, + "grant install, develop on application package package_name to role app_role", + ), + ( + { + "privileges": ["usage"], + "object_type": ObjectType.SCHEMA, + "object_identifier": "package_name.stage_schema", + "role_to_grant": "app_role", + "role_to_use": "package_role", + }, + "grant usage on schema package_name.stage_schema to role app_role", + ), + ( + { + "privileges": ["read"], + "object_type": ObjectType.STAGE, + "object_identifier": "stage_fqn", + "role_to_grant": "app_role", + "role_to_use": None, + }, + "grant read on stage stage_fqn to role app_role", + ), + ], +) +def test_grant_privileges_to_role( + mock_use_role, + mock_execute_query, + args, + expected_query, +): + expected_use_objects = [(mock_use_role, mock.call(args["role_to_use"]))] + expected_execute_query = [(mock_execute_query, mock.call(expected_query))] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.grant_privileges_to_role(**args) + + +@pytest.mark.parametrize( + "args,expected_query", + [ + ( + {"name": "example_app", "role": "example_role"}, + r"show applications like 'EXAMPLE\\_APP'", + ), + ( + {"name": "nounderscores", "role": None}, + r"show applications like 'NOUNDERSCORES'", + ), + ], +) +def test_get_existing_app_info( + mock_use_role, mock_execute_query, args, expected_query, mock_cursor +): + expected_use_objects = [(mock_use_role, mock.call(args["role"]))] + + mock_cursor_results = [ + { + NAME_COL: "NOT_NAME", + }, + { + NAME_COL: args["name"].upper(), + }, + ] + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor(mock_cursor_results, []), + mock.call(expected_query), + ) + ] + ) + mock_execute_query.side_effect = side_effects + expected_execute_query = [ + (mock_execute_query, mock.call(expected_query, cursor_class=DictCursor)) + ] + + with assert_in_context(expected_use_objects, expected_execute_query): + result = sql_facade.get_existing_app_info(**args) + + assert result == {NAME_COL: args["name"].upper()} + + +def test_upgrade_application_unversioned( + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + +def test_upgrade_application_version_and_patch( + mock_get_existing_app_info, + mock_use_role, + mock_use_warehouse, + mock_get_app_properties, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call( + # make sure that "3" is quoted since that was a bug we found + f'alter application {app_name} upgrade using version "3" patch 2' + ), + ), + (None, mock.call(f"alter application {app_name} set debug_mode = True")), + ( + None, + mock.call( + f"alter application {app_name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = TRUE" + ), + ), + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.versioned_dev("3", 2), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + +def test_upgrade_application_from_release_directive( + mock_get_app_properties, + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + mock_get_app_properties.return_value = { + COMMENT_COL: SPECIAL_COMMENT, + AUTHORIZE_TELEMETRY_COL: "true", + } + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call(f"alter application {app_name} upgrade "), + # not dev mode so no debug mode call + # authorize telemetry col is the same as arg, so no call + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + +def test_upgrade_application_converts_expected_programmingerrors_to_user_errors( + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + programming_error_message = "programming error message" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + msg=programming_error_message, + ), + mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno( + err, APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT + ) + assert err.match( + f"Failed to upgrade application {app_name} with the following error message:\n" + ) + assert err.match(programming_error_message) + + +def test_upgrade_application_special_message_for_event_sharing_error( + mock_get_existing_app_info, + mock_get_app_properties, + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + mock_get_app_properties.return_value = { + COMMENT_COL: SPECIAL_COMMENT, + AUTHORIZE_TELEMETRY_COL: "true", + } + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call(f"alter application {app_name} upgrade using version v1 "), + ), + (None, mock.call(f"alter application {app_name} set debug_mode = False")), + ( + ProgrammingError( + errno=CANNOT_DISABLE_MANDATORY_TELEMETRY, + ), + mock.call( + f"alter application {app_name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = FALSE" + ), + ), + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.versioned_dev("v1"), + stage_fqn=stage_fqn, + debug_mode=False, + should_authorize_event_sharing=False, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno(err, CANNOT_DISABLE_MANDATORY_TELEMETRY) + assert err.match( + "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) + + +def test_upgrade_application_converts_unexpected_programmingerrors_to_unclassified_errors( + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=SQL_COMPILATION_ERROR, + ), + mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(InvalidSQLError) as err, + ): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno(err, SQL_COMPILATION_ERROR) + + +def test_create_application_with_minimal_clauses( + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + +def test_create_application_with_all_clauses( + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} using @{stage_fqn} debug_mode = True AUTHORIZE_TELEMETRY_EVENT_SHARING = TRUE + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + +def test_create_application_converts_expected_programmingerrors_to_user_errors( + mock_use_warehouse, mock_use_role, mock_execute_query +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + programming_error_message = "programming error message" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + msg=programming_error_message, + ), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno( + err, APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT + ) + assert err.match( + f"Failed to create application {app_name} with the following error message:\n" + ) + assert err.match(programming_error_message) + + +def test_create_application_special_message_for_event_sharing_error( + mock_use_warehouse, mock_use_role, mock_execute_query +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + ), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} using version "3" patch 1 debug_mode = False AUTHORIZE_TELEMETRY_EVENT_SHARING = FALSE + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.versioned_dev("3", 1), + stage_fqn=stage_fqn, + debug_mode=False, + should_authorize_event_sharing=False, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno( + err, APPLICATION_REQUIRES_TELEMETRY_SHARING + ) + assert err.match( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) + + +def test_create_application_converts_unexpected_programmingerrors_to_unclassified_errors( + mock_use_warehouse, mock_use_role, mock_execute_query +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=SQL_COMPILATION_ERROR, + ), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(InvalidSQLError) as err, + ): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno(err, SQL_COMPILATION_ERROR) diff --git a/tests/nativeapp/test_teardown.py b/tests/nativeapp/test_teardown.py index 6566fc3ceb..a99b1052a2 100644 --- a/tests/nativeapp/test_teardown.py +++ b/tests/nativeapp/test_teardown.py @@ -50,13 +50,13 @@ from tests.nativeapp.patch_utils import mock_get_app_pkg_distribution_in_sf from tests.nativeapp.utils import ( APP_ENTITY_DROP_GENERIC_OBJECT, - APP_ENTITY_GET_EXISTING_APP_INFO, APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, APP_ENTITY_MODULE, APP_PACKAGE_ENTITY_DROP_GENERIC_OBJECT, APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO, APP_PACKAGE_ENTITY_IS_DISTRIBUTION_SAME, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_GET_EXISTING_APP_INFO, TYPER_CONFIRM, TYPER_PROMPT, mock_execute_helper, @@ -195,7 +195,7 @@ def test_drop_generic_object_failure_w_exception( # Test drop_application() when no application exists -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) @pytest.mark.parametrize( "auto_yes_param", [True, False], # This should have no effect on the test @@ -219,7 +219,7 @@ def test_drop_application_no_existing_application( # Test drop_application() when the current role is not allowed to drop it -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( APP_ENTITY_DROP_GENERIC_OBJECT, side_effect=ProgrammingError( @@ -258,7 +258,7 @@ def test_drop_application_current_role_is_not_owner( # Test drop_application() successfully when it has special comment -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, return_value=[]) @pytest.mark.parametrize( @@ -370,7 +370,7 @@ def test_drop_application_has_special_comment_and_quoted_name( # Test drop_application() without special comment AND auto_yes is False AND should_drop is False -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(f"{APP_ENTITY_MODULE}.{TYPER_CONFIRM}", return_value=False) def test_drop_application_user_prohibits_drop( @@ -408,7 +408,7 @@ def test_drop_application_user_prohibits_drop( # Test drop_application() without special comment AND auto_yes is False AND should_drop is True # Test drop_application() without special comment AND auto_yes is True -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(f"{APP_ENTITY_MODULE}.{TYPER_CONFIRM}", return_value=True) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, return_value=[]) @@ -448,7 +448,7 @@ def test_drop_application_user_allows_drop( # Test idempotent drop_application() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, return_value=[]) @pytest.mark.parametrize( @@ -496,7 +496,6 @@ def test_drop_application_idempotent( def test_drop_package_no_existing_application( mock_get_existing_app_pkg_info, auto_yes_param, temp_dir ): - current_working_directory = os.getcwd() create_named_file( file_name="snowflake.yml", @@ -1096,7 +1095,7 @@ def test_drop_package_idempotent( @mock.patch(f"{APP_ENTITY_MODULE}.{TYPER_PROMPT}") -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION) @pytest.mark.parametrize( diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index 76dd46d9ec..576fce4436 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -19,6 +19,9 @@ from textwrap import dedent from typing import List, Set +import pytest +from snowflake.connector import ProgrammingError + from tests.nativeapp.factories import ProjectV10Factory TYPER_CONFIRM = "typer.confirm" @@ -36,7 +39,6 @@ APP_ENTITY_MODULE = "snowflake.cli._plugins.nativeapp.entities.application" APP_ENTITY = f"{APP_ENTITY_MODULE}.ApplicationEntity" -APP_ENTITY_GET_EXISTING_APP_INFO = f"{APP_ENTITY}.get_existing_app_info" APP_ENTITY_DROP_GENERIC_OBJECT = f"{APP_ENTITY_MODULE}.drop_generic_object" APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION = ( f"{APP_ENTITY}.get_objects_owned_by_application" @@ -73,6 +75,11 @@ SQL_FACADE_STAGE_EXISTS = f"{SQL_FACADE}.stage_exists" SQL_FACADE_CREATE_SCHEMA = f"{SQL_FACADE}.create_schema" SQL_FACADE_CREATE_STAGE = f"{SQL_FACADE}.create_stage" +SQL_FACADE_CREATE_APPLICATION = f"{SQL_FACADE}.create_application" +SQL_FACADE_UPGRADE_APPLICATION = f"{SQL_FACADE}.upgrade_application" +SQL_FACADE_GET_EVENT_DEFINITIONS = f"{SQL_FACADE}.get_event_definitions" +SQL_FACADE_GET_EXISTING_APP_INFO = f"{SQL_FACADE}.get_existing_app_info" +SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE = f"{SQL_FACADE}.grant_privileges_to_role" mock_snowflake_yml_file = dedent( """\ @@ -308,3 +315,15 @@ def use_integration_project(): "app/manifest.yml": manifest_contents, }, ) + + +def mock_side_effect_error_with_cause(err: Exception, cause: Exception): + with pytest.raises(type(err)) as side_effect: + raise err from cause + + return side_effect.value + + +def assert_programmingerror_cause_with_errno(err: pytest.ExceptionInfo, errno: int): + assert isinstance(err.value.__cause__, ProgrammingError) + assert err.value.__cause__.errno == errno